Add more thorough dirty checking across all types which may be deleted and hold references. Signed-off-by: Derek McGowan <derek@mcgstyle.net>
		
			
				
	
	
		
			387 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			387 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package metadata
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/binary"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"sync/atomic"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/containerd/containerd/errdefs"
 | 
						|
	"github.com/containerd/containerd/filters"
 | 
						|
	"github.com/containerd/containerd/images"
 | 
						|
	"github.com/containerd/containerd/labels"
 | 
						|
	"github.com/containerd/containerd/metadata/boltutil"
 | 
						|
	"github.com/containerd/containerd/namespaces"
 | 
						|
	digest "github.com/opencontainers/go-digest"
 | 
						|
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	bolt "go.etcd.io/bbolt"
 | 
						|
)
 | 
						|
 | 
						|
type imageStore struct {
 | 
						|
	db *DB
 | 
						|
}
 | 
						|
 | 
						|
// NewImageStore returns a store backed by a bolt DB
 | 
						|
func NewImageStore(db *DB) images.Store {
 | 
						|
	return &imageStore{db: db}
 | 
						|
}
 | 
						|
 | 
						|
func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error) {
 | 
						|
	var image images.Image
 | 
						|
 | 
						|
	namespace, err := namespaces.NamespaceRequired(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := view(ctx, s.db, func(tx *bolt.Tx) error {
 | 
						|
		bkt := getImagesBucket(tx, namespace)
 | 
						|
		if bkt == nil {
 | 
						|
			return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
 | 
						|
		}
 | 
						|
 | 
						|
		ibkt := bkt.Bucket([]byte(name))
 | 
						|
		if ibkt == nil {
 | 
						|
			return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
 | 
						|
		}
 | 
						|
 | 
						|
		image.Name = name
 | 
						|
		if err := readImage(&image, ibkt); err != nil {
 | 
						|
			return errors.Wrapf(err, "image %q", name)
 | 
						|
		}
 | 
						|
 | 
						|
		return nil
 | 
						|
	}); err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	return image, nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *imageStore) List(ctx context.Context, fs ...string) ([]images.Image, error) {
 | 
						|
	namespace, err := namespaces.NamespaceRequired(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	filter, err := filters.ParseAll(fs...)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(errdefs.ErrInvalidArgument, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	var m []images.Image
 | 
						|
	if err := view(ctx, s.db, func(tx *bolt.Tx) error {
 | 
						|
		bkt := getImagesBucket(tx, namespace)
 | 
						|
		if bkt == nil {
 | 
						|
			return nil // empty store
 | 
						|
		}
 | 
						|
 | 
						|
		return bkt.ForEach(func(k, v []byte) error {
 | 
						|
			var (
 | 
						|
				image = images.Image{
 | 
						|
					Name: string(k),
 | 
						|
				}
 | 
						|
				kbkt = bkt.Bucket(k)
 | 
						|
			)
 | 
						|
 | 
						|
			if err := readImage(&image, kbkt); err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			if filter.Match(adaptImage(image)) {
 | 
						|
				m = append(m, image)
 | 
						|
			}
 | 
						|
			return nil
 | 
						|
		})
 | 
						|
	}); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return m, nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *imageStore) Create(ctx context.Context, image images.Image) (images.Image, error) {
 | 
						|
	namespace, err := namespaces.NamespaceRequired(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := update(ctx, s.db, func(tx *bolt.Tx) error {
 | 
						|
		if err := validateImage(&image); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		bkt, err := createImagesBucket(tx, namespace)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		ibkt, err := bkt.CreateBucket([]byte(image.Name))
 | 
						|
		if err != nil {
 | 
						|
			if err != bolt.ErrBucketExists {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			return errors.Wrapf(errdefs.ErrAlreadyExists, "image %q", image.Name)
 | 
						|
		}
 | 
						|
 | 
						|
		image.CreatedAt = time.Now().UTC()
 | 
						|
		image.UpdatedAt = image.CreatedAt
 | 
						|
		return writeImage(ibkt, &image)
 | 
						|
	}); err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	return image, nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *imageStore) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
 | 
						|
	namespace, err := namespaces.NamespaceRequired(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	if image.Name == "" {
 | 
						|
		return images.Image{}, errors.Wrapf(errdefs.ErrInvalidArgument, "image name is required for update")
 | 
						|
	}
 | 
						|
 | 
						|
	var updated images.Image
 | 
						|
 | 
						|
	if err := update(ctx, s.db, func(tx *bolt.Tx) error {
 | 
						|
		bkt, err := createImagesBucket(tx, namespace)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		ibkt := bkt.Bucket([]byte(image.Name))
 | 
						|
		if ibkt == nil {
 | 
						|
			return errors.Wrapf(errdefs.ErrNotFound, "image %q", image.Name)
 | 
						|
		}
 | 
						|
 | 
						|
		if err := readImage(&updated, ibkt); err != nil {
 | 
						|
			return errors.Wrapf(err, "image %q", image.Name)
 | 
						|
		}
 | 
						|
		createdat := updated.CreatedAt
 | 
						|
		updated.Name = image.Name
 | 
						|
 | 
						|
		if len(fieldpaths) > 0 {
 | 
						|
			for _, path := range fieldpaths {
 | 
						|
				if strings.HasPrefix(path, "labels.") {
 | 
						|
					if updated.Labels == nil {
 | 
						|
						updated.Labels = map[string]string{}
 | 
						|
					}
 | 
						|
 | 
						|
					key := strings.TrimPrefix(path, "labels.")
 | 
						|
					updated.Labels[key] = image.Labels[key]
 | 
						|
					continue
 | 
						|
				} else if strings.HasPrefix(path, "annotations.") {
 | 
						|
					if updated.Target.Annotations == nil {
 | 
						|
						updated.Target.Annotations = map[string]string{}
 | 
						|
					}
 | 
						|
 | 
						|
					key := strings.TrimPrefix(path, "annotations.")
 | 
						|
					updated.Target.Annotations[key] = image.Target.Annotations[key]
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				switch path {
 | 
						|
				case "labels":
 | 
						|
					updated.Labels = image.Labels
 | 
						|
				case "target":
 | 
						|
					// NOTE(stevvooe): While we allow setting individual labels, we
 | 
						|
					// only support replacing the target as a unit, since that is
 | 
						|
					// commonly pulled as a unit from other sources. It often doesn't
 | 
						|
					// make sense to modify the size or digest without touching the
 | 
						|
					// mediatype, as well, for example.
 | 
						|
					updated.Target = image.Target
 | 
						|
				case "annotations":
 | 
						|
					updated.Target.Annotations = image.Target.Annotations
 | 
						|
				default:
 | 
						|
					return errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on image %q", path, image.Name)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			updated = image
 | 
						|
		}
 | 
						|
 | 
						|
		if err := validateImage(&updated); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		updated.CreatedAt = createdat
 | 
						|
		updated.UpdatedAt = time.Now().UTC()
 | 
						|
		return writeImage(ibkt, &updated)
 | 
						|
	}); err != nil {
 | 
						|
		return images.Image{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	return updated, nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (s *imageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
 | 
						|
	namespace, err := namespaces.NamespaceRequired(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return update(ctx, s.db, func(tx *bolt.Tx) error {
 | 
						|
		bkt := getImagesBucket(tx, namespace)
 | 
						|
		if bkt == nil {
 | 
						|
			return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
 | 
						|
		}
 | 
						|
 | 
						|
		if err = bkt.DeleteBucket([]byte(name)); err != nil {
 | 
						|
			if err == bolt.ErrBucketNotFound {
 | 
						|
				err = errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
 | 
						|
			}
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		atomic.AddUint32(&s.db.dirty, 1)
 | 
						|
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func validateImage(image *images.Image) error {
 | 
						|
	if image.Name == "" {
 | 
						|
		return errors.Wrapf(errdefs.ErrInvalidArgument, "image name must not be empty")
 | 
						|
	}
 | 
						|
 | 
						|
	for k, v := range image.Labels {
 | 
						|
		if err := labels.Validate(k, v); err != nil {
 | 
						|
			return errors.Wrapf(err, "image.Labels")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return validateTarget(&image.Target)
 | 
						|
}
 | 
						|
 | 
						|
func validateTarget(target *ocispec.Descriptor) error {
 | 
						|
	// NOTE(stevvooe): Only validate fields we actually store.
 | 
						|
 | 
						|
	if err := target.Digest.Validate(); err != nil {
 | 
						|
		return errors.Wrapf(errdefs.ErrInvalidArgument, "Target.Digest %q invalid: %v", target.Digest, err)
 | 
						|
	}
 | 
						|
 | 
						|
	if target.Size <= 0 {
 | 
						|
		return errors.Wrapf(errdefs.ErrInvalidArgument, "Target.Size must be greater than zero")
 | 
						|
	}
 | 
						|
 | 
						|
	if target.MediaType == "" {
 | 
						|
		return errors.Wrapf(errdefs.ErrInvalidArgument, "Target.MediaType must be set")
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func readImage(image *images.Image, bkt *bolt.Bucket) error {
 | 
						|
	if err := boltutil.ReadTimestamps(bkt, &image.CreatedAt, &image.UpdatedAt); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	labels, err := boltutil.ReadLabels(bkt)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	image.Labels = labels
 | 
						|
 | 
						|
	image.Target.Annotations, err = boltutil.ReadAnnotations(bkt)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	tbkt := bkt.Bucket(bucketKeyTarget)
 | 
						|
	if tbkt == nil {
 | 
						|
		return errors.New("unable to read target bucket")
 | 
						|
	}
 | 
						|
	return tbkt.ForEach(func(k, v []byte) error {
 | 
						|
		if v == nil {
 | 
						|
			return nil // skip it? a bkt maybe?
 | 
						|
		}
 | 
						|
 | 
						|
		// TODO(stevvooe): This is why we need to use byte values for
 | 
						|
		// keys, rather than full arrays.
 | 
						|
		switch string(k) {
 | 
						|
		case string(bucketKeyDigest):
 | 
						|
			image.Target.Digest = digest.Digest(v)
 | 
						|
		case string(bucketKeyMediaType):
 | 
						|
			image.Target.MediaType = string(v)
 | 
						|
		case string(bucketKeySize):
 | 
						|
			image.Target.Size, _ = binary.Varint(v)
 | 
						|
		}
 | 
						|
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func writeImage(bkt *bolt.Bucket, image *images.Image) error {
 | 
						|
	if err := boltutil.WriteTimestamps(bkt, image.CreatedAt, image.UpdatedAt); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := boltutil.WriteLabels(bkt, image.Labels); err != nil {
 | 
						|
		return errors.Wrapf(err, "writing labels for image %v", image.Name)
 | 
						|
	}
 | 
						|
 | 
						|
	if err := boltutil.WriteAnnotations(bkt, image.Target.Annotations); err != nil {
 | 
						|
		return errors.Wrapf(err, "writing Annotations for image %v", image.Name)
 | 
						|
	}
 | 
						|
 | 
						|
	// write the target bucket
 | 
						|
	tbkt, err := bkt.CreateBucketIfNotExists(bucketKeyTarget)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	sizeEncoded, err := encodeInt(image.Target.Size)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, v := range [][2][]byte{
 | 
						|
		{bucketKeyDigest, []byte(image.Target.Digest)},
 | 
						|
		{bucketKeyMediaType, []byte(image.Target.MediaType)},
 | 
						|
		{bucketKeySize, sizeEncoded},
 | 
						|
	} {
 | 
						|
		if err := tbkt.Put(v[0], v[1]); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func encodeInt(i int64) ([]byte, error) {
 | 
						|
	var (
 | 
						|
		buf      [binary.MaxVarintLen64]byte
 | 
						|
		iEncoded = buf[:]
 | 
						|
	)
 | 
						|
	iEncoded = iEncoded[:binary.PutVarint(iEncoded, i)]
 | 
						|
 | 
						|
	if len(iEncoded) == 0 {
 | 
						|
		return nil, fmt.Errorf("failed encoding integer = %v", i)
 | 
						|
	}
 | 
						|
	return iEncoded, nil
 | 
						|
}
 |