Skip to content

Commit

Permalink
metadata: ensure correct updates on Container
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
stevvooe committed Aug 18, 2017
1 parent 8095244 commit 783ed05
Show file tree
Hide file tree
Showing 4 changed files with 688 additions and 48 deletions.
57 changes: 49 additions & 8 deletions containers/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,51 @@ import (
//
// The resources specified in this object are used to create tasks from the container.
type Container struct {
ID string
Labels map[string]string
Image string
Runtime RuntimeInfo
Spec *types.Any
RootFS string
// ID uniquely identifies the container in a nameapace.
//
// This property is required and cannot be changed after creation.
ID string

// Labels provide metadata extension for a contaienr.
//
// These are optional and fully mutable.
Labels map[string]string

// Image specifies the image reference used for a container.
//
// This property is optional but immutable.
Image string

// Runtime specifies which runtime should be used when launching container
// tasks.
//
// This property is required and immutable.
Runtime RuntimeInfo

// Spec should carry the the runtime specification used to implement the
// container.
//
// This field is required but mutable.
Spec *types.Any

// RootFS specifies the snapshot key to use for the container's root
// filesystem. When starting a task from this container, a caller should
// look up the mounts from the snapshot service and include those on the
// task create request.
//
// This field is not required but immutable.
RootFS string

// Snapshotter specifies the snapshotter name used for rootfs
//
// This field is not required but immutable.
Snapshotter string
CreatedAt time.Time
UpdatedAt time.Time

// CreatedAt is the time at which the container was created.
CreatedAt time.Time

// UpdatedAt is the time at which the container was updated.
UpdatedAt time.Time
}

type RuntimeInfo struct {
Expand All @@ -34,6 +70,7 @@ type Store interface {
// List returns containers that match one or more of the provided filters.
List(ctx context.Context, filters ...string) ([]Container, error)

// Create a container in the store from the provided container.
Create(ctx context.Context, container Container) (Container, error)

// Update the container with the provided container object. ID must be set.
Expand All @@ -42,5 +79,9 @@ type Store interface {
// the fieldpaths will be mutated.
Update(ctx context.Context, container Container, fieldpaths ...string) (Container, error)

// Delete a container using the id.
//
// nil will be returned on success. If the container is not known to the
// store, ErrNotFound will be returned.
Delete(ctx context.Context, id string) error
}
23 changes: 12 additions & 11 deletions metadata/buckets.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ var (
bucketKeyObjectBlob = []byte("blob") // stores content links
bucketKeyObjectIngest = []byte("ingest") // stores ingest links

bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
bucketKeySize = []byte("size")
bucketKeyImage = []byte("image")
bucketKeyRuntime = []byte("runtime")
bucketKeyName = []byte("name")
bucketKeyParent = []byte("parent")
bucketKeyOptions = []byte("options")
bucketKeySpec = []byte("spec")
bucketKeyRootFS = []byte("rootfs")
bucketKeyTarget = []byte("target")
bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
bucketKeySize = []byte("size")
bucketKeyImage = []byte("image")
bucketKeyRuntime = []byte("runtime")
bucketKeyName = []byte("name")
bucketKeyParent = []byte("parent")
bucketKeyOptions = []byte("options")
bucketKeySpec = []byte("spec")
bucketKeyRootFS = []byte("rootfs")
bucketKeySnapshotter = []byte("snapshotter")
bucketKeyTarget = []byte("target")
)

func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
Expand Down
99 changes: 70 additions & 29 deletions metadata/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ func (s *containerStore) Create(ctx context.Context, container containers.Contai
return containers.Container{}, err
}

if err := identifiers.Validate(container.ID); 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)
Expand Down Expand Up @@ -144,37 +144,55 @@ func (s *containerStore) Update(ctx context.Context, container containers.Contai
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.
if len(fieldpaths) > 0 {
// TODO(stevvooe): Move this logic into the store itself.
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
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 "image":
updated.Image = container.Image
case "spec":
updated.Spec = container.Spec
case "rootfs":
updated.RootFS = container.RootFS
default:
return containers.Container{}, errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on %q", path, container.ID)
}
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)
}
} else {
// no field mask present, just replace everything
updated = container
}

if err := validateContainer(&updated); err != nil {
return containers.Container{}, errors.Wrap(err, "update failed validation")
}

updated.CreatedAt = createdat
Expand All @@ -183,7 +201,7 @@ func (s *containerStore) Update(ctx context.Context, container containers.Contai
return containers.Container{}, errors.Wrap(err, "failed to write container")
}

return container, nil
return updated, nil
}

func (s *containerStore) Delete(ctx context.Context, id string) error {
Expand All @@ -203,6 +221,27 @@ func (s *containerStore) Delete(ctx context.Context, id string) error {
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 {
Expand Down Expand Up @@ -247,7 +286,8 @@ func readContainer(container *containers.Container, bkt *bolt.Bucket) error {
container.Spec = &any
case string(bucketKeyRootFS):
container.RootFS = string(v)

case string(bucketKeySnapshotter):
container.Snapshotter = string(v)
}

return nil
Expand All @@ -272,6 +312,7 @@ func writeContainer(bkt *bolt.Bucket, container *containers.Container) error {

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 {
Expand Down
Loading

0 comments on commit 783ed05

Please sign in to comment.