
This fixes a few bugs in the container store related to reading and writing fields. Specifically, on update, the full field set wasn't being returned to the caller, making it appear that the store was corrupted. We now return the correctly updated field and store the missing field that was omitted in the original implementation. In course, we also have defined the update semantics of each field, as well as whether or not they are required. The big addition here is really the container metadata testsuite. It covers listing, filtering, creates, updates and deletes in a vareity of scenarios. Signed-off-by: Stephen J Day <stephen.day@docker.com>
356 lines
9.4 KiB
Go
356 lines
9.4 KiB
Go
package metadata
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/boltdb/bolt"
|
|
"github.com/containerd/containerd/containers"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/filters"
|
|
"github.com/containerd/containerd/identifiers"
|
|
"github.com/containerd/containerd/metadata/boltutil"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/gogo/protobuf/types"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type containerStore struct {
|
|
tx *bolt.Tx
|
|
}
|
|
|
|
func NewContainerStore(tx *bolt.Tx) containers.Store {
|
|
return &containerStore{
|
|
tx: tx,
|
|
}
|
|
}
|
|
|
|
func (s *containerStore) Get(ctx context.Context, id string) (containers.Container, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return containers.Container{}, err
|
|
}
|
|
|
|
bkt := getContainerBucket(s.tx, namespace, id)
|
|
if bkt == nil {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrNotFound, "bucket name %q:%q", namespace, id)
|
|
}
|
|
|
|
container := containers.Container{ID: id}
|
|
if err := readContainer(&container, bkt); err != nil {
|
|
return containers.Container{}, errors.Wrapf(err, "failed to read container %v", id)
|
|
}
|
|
|
|
return container, nil
|
|
}
|
|
|
|
func (s *containerStore) List(ctx context.Context, fs ...string) ([]containers.Container, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filter, err := filters.ParseAll(fs...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, err.Error())
|
|
}
|
|
|
|
bkt := getContainersBucket(s.tx, namespace)
|
|
if bkt == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var m []containers.Container
|
|
if err := bkt.ForEach(func(k, v []byte) error {
|
|
cbkt := bkt.Bucket(k)
|
|
if cbkt == nil {
|
|
return nil
|
|
}
|
|
container := containers.Container{ID: string(k)}
|
|
|
|
if err := readContainer(&container, cbkt); err != nil {
|
|
return errors.Wrap(err, "failed to read container")
|
|
}
|
|
|
|
if filter.Match(adaptContainer(container)) {
|
|
m = append(m, container)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *containerStore) Create(ctx context.Context, container containers.Container) (containers.Container, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return containers.Container{}, err
|
|
}
|
|
|
|
if err := validateContainer(&container); err != nil {
|
|
return containers.Container{}, errors.Wrap(err, "create container failed validation")
|
|
}
|
|
|
|
bkt, err := createContainersBucket(s.tx, namespace)
|
|
if err != nil {
|
|
return containers.Container{}, err
|
|
}
|
|
|
|
cbkt, err := bkt.CreateBucket([]byte(container.ID))
|
|
if err != nil {
|
|
if err == bolt.ErrBucketExists {
|
|
err = errors.Wrapf(errdefs.ErrAlreadyExists, "content %q", container.ID)
|
|
}
|
|
return containers.Container{}, err
|
|
}
|
|
|
|
container.CreatedAt = time.Now().UTC()
|
|
container.UpdatedAt = container.CreatedAt
|
|
if err := writeContainer(cbkt, &container); err != nil {
|
|
return containers.Container{}, errors.Wrap(err, "failed to write container")
|
|
}
|
|
|
|
return container, nil
|
|
}
|
|
|
|
func (s *containerStore) Update(ctx context.Context, container containers.Container, fieldpaths ...string) (containers.Container, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return containers.Container{}, err
|
|
}
|
|
|
|
if container.ID == "" {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "must specify a container id")
|
|
}
|
|
|
|
bkt := getContainersBucket(s.tx, namespace)
|
|
if bkt == nil {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrNotFound, "container %q", container.ID)
|
|
}
|
|
|
|
cbkt := bkt.Bucket([]byte(container.ID))
|
|
if cbkt == nil {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrNotFound, "container %q", container.ID)
|
|
}
|
|
|
|
var updated containers.Container
|
|
if err := readContainer(&updated, cbkt); err != nil {
|
|
return updated, errors.Wrapf(err, "failed to read container from bucket")
|
|
}
|
|
createdat := updated.CreatedAt
|
|
updated.ID = container.ID
|
|
|
|
if len(fieldpaths) == 0 {
|
|
// only allow updates to these field on full replace.
|
|
fieldpaths = []string{"labels", "spec"}
|
|
|
|
// Fields that are immutable must cause an error when no field paths
|
|
// are provided. This allows these fields to become mutable in the
|
|
// future.
|
|
if updated.Image != container.Image {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "container.Image field is immutable")
|
|
}
|
|
|
|
if updated.RootFS != container.RootFS {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "container.RootFS field is immutable")
|
|
}
|
|
|
|
if updated.Snapshotter != container.Snapshotter {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "container.Snapshotter field is immutable")
|
|
}
|
|
|
|
if updated.Runtime.Name != container.Runtime.Name {
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "container.Runtime.Name field is immutable")
|
|
}
|
|
}
|
|
|
|
// apply the field mask. If you update this code, you better follow the
|
|
// field mask rules in field_mask.proto. If you don't know what this
|
|
// is, do not update this code.
|
|
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] = container.Labels[key]
|
|
continue
|
|
}
|
|
|
|
switch path {
|
|
case "labels":
|
|
updated.Labels = container.Labels
|
|
case "spec":
|
|
updated.Spec = container.Spec
|
|
default:
|
|
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on %q", path, container.ID)
|
|
}
|
|
}
|
|
|
|
if err := validateContainer(&updated); err != nil {
|
|
return containers.Container{}, errors.Wrap(err, "update failed validation")
|
|
}
|
|
|
|
updated.CreatedAt = createdat
|
|
updated.UpdatedAt = time.Now().UTC()
|
|
if err := writeContainer(cbkt, &updated); err != nil {
|
|
return containers.Container{}, errors.Wrap(err, "failed to write container")
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
func (s *containerStore) Delete(ctx context.Context, id string) error {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bkt := getContainersBucket(s.tx, namespace)
|
|
if bkt == nil {
|
|
return errors.Wrapf(errdefs.ErrNotFound, "cannot delete container %v, bucket not present", id)
|
|
}
|
|
|
|
if err := bkt.DeleteBucket([]byte(id)); err == bolt.ErrBucketNotFound {
|
|
return errors.Wrapf(errdefs.ErrNotFound, "container %v", id)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func validateContainer(container *containers.Container) error {
|
|
if err := identifiers.Validate(container.ID); err != nil {
|
|
return errors.Wrapf(err, "container.ID validation error")
|
|
}
|
|
|
|
// labels and image have no validation
|
|
if container.Runtime.Name == "" {
|
|
return errors.Wrapf(errdefs.ErrInvalidArgument, "container.Runtime.Name must be set")
|
|
}
|
|
|
|
if container.Spec == nil {
|
|
return errors.Wrapf(errdefs.ErrInvalidArgument, "container.Spec must be set")
|
|
}
|
|
|
|
if container.RootFS != "" && container.Snapshotter == "" {
|
|
return errors.Wrapf(errdefs.ErrInvalidArgument, "container.Snapshotter must be set if container.RootFS is set")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readContainer(container *containers.Container, bkt *bolt.Bucket) error {
|
|
labels, err := boltutil.ReadLabels(bkt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
container.Labels = labels
|
|
|
|
if err := boltutil.ReadTimestamps(bkt, &container.CreatedAt, &container.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
return bkt.ForEach(func(k, v []byte) error {
|
|
switch string(k) {
|
|
case string(bucketKeyImage):
|
|
container.Image = string(v)
|
|
case string(bucketKeyRuntime):
|
|
rbkt := bkt.Bucket(bucketKeyRuntime)
|
|
if rbkt == nil {
|
|
return nil // skip runtime. should be an error?
|
|
}
|
|
|
|
n := rbkt.Get(bucketKeyName)
|
|
if n != nil {
|
|
container.Runtime.Name = string(n)
|
|
}
|
|
|
|
obkt := rbkt.Get(bucketKeyOptions)
|
|
if obkt == nil {
|
|
return nil
|
|
}
|
|
|
|
var any types.Any
|
|
if err := proto.Unmarshal(obkt, &any); err != nil {
|
|
return err
|
|
}
|
|
container.Runtime.Options = &any
|
|
case string(bucketKeySpec):
|
|
var any types.Any
|
|
if err := proto.Unmarshal(v, &any); err != nil {
|
|
return err
|
|
}
|
|
container.Spec = &any
|
|
case string(bucketKeyRootFS):
|
|
container.RootFS = string(v)
|
|
case string(bucketKeySnapshotter):
|
|
container.Snapshotter = string(v)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func writeContainer(bkt *bolt.Bucket, container *containers.Container) error {
|
|
if err := boltutil.WriteTimestamps(bkt, container.CreatedAt, container.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if container.Spec != nil {
|
|
spec, err := container.Spec.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := bkt.Put(bucketKeySpec, spec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, v := range [][2][]byte{
|
|
{bucketKeyImage, []byte(container.Image)},
|
|
{bucketKeySnapshotter, []byte(container.Snapshotter)},
|
|
{bucketKeyRootFS, []byte(container.RootFS)},
|
|
} {
|
|
if err := bkt.Put(v[0], v[1]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if rbkt := bkt.Bucket(bucketKeyRuntime); rbkt != nil {
|
|
if err := bkt.DeleteBucket(bucketKeyRuntime); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
rbkt, err := bkt.CreateBucket(bucketKeyRuntime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := rbkt.Put(bucketKeyName, []byte(container.Runtime.Name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
obkt, err := rbkt.CreateBucket(bucketKeyOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if container.Runtime.Options != nil {
|
|
data, err := proto.Marshal(container.Runtime.Options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := obkt.Put(bucketKeyOptions, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return boltutil.WriteLabels(bkt, container.Labels)
|
|
}
|