
To allow for updating extensions without collisions, we have moved to using a map type that can be explicitly selected via the field path for updates. This ensures that multiple parties can operate on their extensions without stepping on each other's toes or incurring an inordinate number of round trips. Signed-off-by: Stephen J Day <stephen.day@docker.com>
722 lines
17 KiB
Go
722 lines
17 KiB
Go
package metadata
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/boltdb/bolt"
|
|
"github.com/containerd/containerd/containers"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/filters"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/containerd/typeurl"
|
|
"github.com/gogo/protobuf/types"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func init() {
|
|
typeurl.Register(&specs.Spec{}, "opencontainers/runtime-spec", "v1", "Spec")
|
|
}
|
|
|
|
func TestContainersList(t *testing.T) {
|
|
ctx, db, cancel := testEnv(t)
|
|
defer cancel()
|
|
|
|
spec := &specs.Spec{}
|
|
encoded, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testset := map[string]*containers.Container{}
|
|
for i := 0; i < 4; i++ {
|
|
id := "container-" + fmt.Sprint(i)
|
|
testset[id] = &containers.Container{
|
|
ID: id,
|
|
Labels: map[string]string{
|
|
"idlabel": id,
|
|
"even": fmt.Sprint(i%2 == 0),
|
|
"odd": fmt.Sprint(i%2 != 0),
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
}
|
|
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
store := NewContainerStore(tx)
|
|
now := time.Now()
|
|
result, err := store.Create(ctx, *testset[id])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, true)
|
|
testset[id].UpdatedAt, testset[id].CreatedAt = result.UpdatedAt, result.CreatedAt
|
|
checkContainersEqual(t, &result, testset[id], "ensure that containers were created as expected for list")
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
for _, testcase := range []struct {
|
|
name string
|
|
filters []string
|
|
}{
|
|
{
|
|
name: "FullSet",
|
|
},
|
|
{
|
|
name: "FullSetFiltered", // full set, but because we have OR filter
|
|
filters: []string{"labels.even==true", "labels.odd==true"},
|
|
},
|
|
{
|
|
name: "Even",
|
|
filters: []string{"labels.even==true"},
|
|
},
|
|
{
|
|
name: "Odd",
|
|
filters: []string{"labels.odd==true"},
|
|
},
|
|
{
|
|
name: "ByID",
|
|
filters: []string{"id==container-0"},
|
|
},
|
|
{
|
|
name: "ByIDLabelEven",
|
|
filters: []string{"labels.idlabel==container-0,labels.even==true"},
|
|
},
|
|
{
|
|
name: "ByRuntime",
|
|
filters: []string{"runtime.name==testruntime"},
|
|
},
|
|
} {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
testset := testset
|
|
if len(testcase.filters) > 0 {
|
|
fs, err := filters.ParseAll(testcase.filters...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newtestset := make(map[string]*containers.Container, len(testset))
|
|
for k, v := range testset {
|
|
if fs.Match(adaptContainer(*v)) {
|
|
newtestset[k] = v
|
|
}
|
|
}
|
|
testset = newtestset
|
|
}
|
|
|
|
if err := db.View(func(tx *bolt.Tx) error {
|
|
store := NewContainerStore(tx)
|
|
results, err := store.List(ctx, testcase.filters...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(results) == 0 { // all tests return a non-empty result set
|
|
t.Fatalf("not results returned")
|
|
}
|
|
|
|
if len(results) != len(testset) {
|
|
t.Fatalf("length of result does not match testset: %v != %v", len(results), len(testset))
|
|
}
|
|
|
|
for _, result := range results {
|
|
checkContainersEqual(t, &result, testset[result.ID], "list results did not match")
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// delete everything to test it
|
|
for id := range testset {
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
store := NewContainerStore(tx)
|
|
return store.Delete(ctx, id)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// try it again, get NotFound
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
store := NewContainerStore(tx)
|
|
return store.Delete(ctx, id)
|
|
}); errors.Cause(err) != errdefs.ErrNotFound {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestContainersUpdate ensures that updates are taken in an expected manner.
|
|
func TestContainersCreateUpdateDelete(t *testing.T) {
|
|
ctx, db, cancel := testEnv(t)
|
|
defer cancel()
|
|
|
|
spec := &specs.Spec{}
|
|
encoded, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
spec.Annotations = map[string]string{"updated": "true"}
|
|
encodedUpdated, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, testcase := range []struct {
|
|
name string
|
|
original containers.Container
|
|
createerr error
|
|
input containers.Container
|
|
fieldpaths []string
|
|
expected containers.Container
|
|
cause error
|
|
}{
|
|
{
|
|
name: "UpdateIDFail",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
ID: "newid",
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
fieldpaths: []string{"id"},
|
|
cause: errdefs.ErrNotFound,
|
|
},
|
|
{
|
|
name: "UpdateRuntimeFail",
|
|
original: containers.Container{
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntimedifferent",
|
|
},
|
|
},
|
|
fieldpaths: []string{"runtime"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateRuntimeClearFail",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
},
|
|
fieldpaths: []string{"runtime"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateFail",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
// try to clear image field
|
|
},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateSpec",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Spec: encodedUpdated,
|
|
},
|
|
fieldpaths: []string{"spec"},
|
|
expected: containers.Container{
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Spec: encodedUpdated,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateLabel",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"bar": "baz",
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels.bar"},
|
|
expected: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "baz",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "DeleteAllLabels",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: nil,
|
|
},
|
|
fieldpaths: []string{"labels"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "DeleteLabel",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"bar": "",
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels.bar"},
|
|
expected: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateSnapshotKeyImmutable",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "",
|
|
Snapshotter: "",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
SnapshotKey: "something",
|
|
Snapshotter: "something",
|
|
},
|
|
fieldpaths: []string{"snapshotkey", "snapshotter"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "SnapshotKeyWithoutSnapshot",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "/nosnapshot",
|
|
Snapshotter: "",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
createerr: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFull",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsNotInFieldpath",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFieldPath",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"extensions"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFieldPathIsolated",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
// leaves hello in place.
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("universe"), // this will be ignored
|
|
},
|
|
"bar": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("foo"), // this will be added
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"extensions.bar"}, //
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"), // remains as world
|
|
},
|
|
"bar": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("foo"), // this will be added
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
testcase.original.ID = testcase.name
|
|
if testcase.input.ID == "" {
|
|
testcase.input.ID = testcase.name
|
|
}
|
|
testcase.expected.ID = testcase.name
|
|
|
|
done := errors.New("test complete")
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
var (
|
|
now = time.Now().UTC()
|
|
store = NewContainerStore(tx)
|
|
)
|
|
|
|
result, err := store.Create(ctx, testcase.original)
|
|
if errors.Cause(err) != testcase.createerr {
|
|
if testcase.createerr == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.createerr)
|
|
}
|
|
} else if testcase.createerr != nil {
|
|
return done
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, true)
|
|
|
|
// ensure that createdat is never tampered with
|
|
testcase.original.CreatedAt = result.CreatedAt
|
|
testcase.expected.CreatedAt = result.CreatedAt
|
|
testcase.original.UpdatedAt = result.UpdatedAt
|
|
testcase.expected.UpdatedAt = result.UpdatedAt
|
|
|
|
checkContainersEqual(t, &result, &testcase.original, "unexpected result on container update")
|
|
return nil
|
|
}); err != nil {
|
|
if err == done {
|
|
return
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
now := time.Now()
|
|
store := NewContainerStore(tx)
|
|
result, err := store.Update(ctx, testcase.input, testcase.fieldpaths...)
|
|
if errors.Cause(err) != testcase.cause {
|
|
if testcase.cause == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.cause)
|
|
}
|
|
} else if testcase.cause != nil {
|
|
return done
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, false)
|
|
testcase.expected.UpdatedAt = result.UpdatedAt
|
|
checkContainersEqual(t, &result, &testcase.expected, "updated failed to get expected result")
|
|
return nil
|
|
}); err != nil {
|
|
if err == done {
|
|
return
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := db.View(func(tx *bolt.Tx) error {
|
|
store := NewContainerStore(tx)
|
|
result, err := store.Get(ctx, testcase.original.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
checkContainersEqual(t, &result, &testcase.expected, "get after failed to get expected result")
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func checkContainerTimestamps(t *testing.T, c *containers.Container, now time.Time, oncreate bool) {
|
|
if c.UpdatedAt.IsZero() || c.CreatedAt.IsZero() {
|
|
t.Fatalf("timestamps not set")
|
|
}
|
|
|
|
if oncreate {
|
|
if !c.CreatedAt.Equal(c.UpdatedAt) {
|
|
t.Fatal("timestamps should be equal on create")
|
|
}
|
|
|
|
} else {
|
|
// ensure that updatedat is always after createdat
|
|
if !c.UpdatedAt.After(c.CreatedAt) {
|
|
t.Fatalf("timestamp for updatedat not after createdat: %v <= %v", c.UpdatedAt, c.CreatedAt)
|
|
}
|
|
}
|
|
|
|
if c.UpdatedAt.Before(now) {
|
|
t.Fatal("createdat time incorrect should be after the start of the operation")
|
|
}
|
|
}
|
|
|
|
func checkContainersEqual(t *testing.T, a, b *containers.Container, format string, args ...interface{}) {
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Fatalf("containers not equal \n\t%v != \n\t%v: "+format, append([]interface{}{a, b}, args...)...)
|
|
}
|
|
}
|
|
|
|
func testEnv(t *testing.T) (context.Context, *bolt.DB, func()) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ctx = namespaces.WithNamespace(ctx, "testing")
|
|
|
|
dirname, err := ioutil.TempDir("", t.Name()+"-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db, err := bolt.Open(filepath.Join(dirname, "meta.db"), 0644, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return ctx, db, func() {
|
|
db.Close()
|
|
if err := os.RemoveAll(dirname); err != nil {
|
|
t.Log("failed removing temp dir", err)
|
|
}
|
|
cancel()
|
|
}
|
|
}
|