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
|
|
}
|