containerd/metadata/images_test.go
Derek McGowan 89fa154efd
Update metadata image store to be initialized once
The boltdb image store now manages its own transactions when
one is not provided, but allows the caller to pass in a
transaction through the context. This makes the image store
more similar to the content and snapshot stores. Additionally,
use the reference to the metadata database to mark the content
store as dirty after an image has been deleted. The deletion
of an image means a reference to a piece of content is gone
and therefore garbage collection should be run to check if
any resources can be cleaned up as a result.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2018-01-05 13:38:45 -08:00

460 lines
11 KiB
Go

package metadata
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/filters"
"github.com/containerd/containerd/images"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func TestImagesList(t *testing.T) {
ctx, db, cancel := testEnv(t)
defer cancel()
store := NewImageStore(NewDB(db, nil, nil))
testset := map[string]*images.Image{}
for i := 0; i < 4; i++ {
id := "image-" + fmt.Sprint(i)
testset[id] = &images.Image{
Name: id,
Labels: map[string]string{
"namelabel": id,
"even": fmt.Sprint(i%2 == 0),
"odd": fmt.Sprint(i%2 != 0),
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.containerd.test",
Digest: digest.FromString(id),
},
}
now := time.Now()
result, err := store.Create(ctx, *testset[id])
if err != nil {
t.Fatal(err)
}
checkImageTimestamps(t, &result, now, true)
testset[id].UpdatedAt, testset[id].CreatedAt = result.UpdatedAt, result.CreatedAt
checkImagesEqual(t, &result, testset[id], "ensure that containers were created as expected for list")
}
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: "ByName",
filters: []string{"name==image-0"},
},
{
name: "ByNameLabelEven",
filters: []string{"labels.namelabel==image-0,labels.even==true"},
},
{
name: "ByMediaType",
filters: []string{"target.mediatype~=application/vnd.*"},
},
} {
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]*images.Image, len(testset))
for k, v := range testset {
if fs.Match(adaptImage(*v)) {
newtestset[k] = v
}
}
testset = newtestset
}
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("no 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 {
checkImagesEqual(t, &result, testset[result.Name], "list results did not match")
}
})
}
// delete everything to test it
for id := range testset {
if err := store.Delete(ctx, id); err != nil {
t.Fatal(err)
}
// try it again, get NotFound
if err := store.Delete(ctx, id); errors.Cause(err) != errdefs.ErrNotFound {
t.Fatalf("unexpected error %v", err)
}
}
}
func TestImagesCreateUpdateDelete(t *testing.T) {
ctx, db, cancel := testEnv(t)
defer cancel()
store := NewImageStore(NewDB(db, nil, nil))
for _, testcase := range []struct {
name string
original images.Image
createerr error
input images.Image
fieldpaths []string
expected images.Image
cause error
}{
{
name: "Touch",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
expected: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
},
{
name: "NoTarget",
original: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{},
},
createerr: errdefs.ErrInvalidArgument,
},
{
name: "ReplaceLabels",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"for": "bar",
"boo": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
expected: images.Image{
Labels: map[string]string{
"for": "bar",
"boo": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
},
{
name: "ReplaceLabelsFieldPath",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"for": "bar",
"boo": "boo",
},
Target: ocispec.Descriptor{
Size: 20, // ignored
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
},
},
fieldpaths: []string{"labels"},
expected: images.Image{
Labels: map[string]string{
"for": "bar",
"boo": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
},
{
name: "ReplaceLabel",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"foo": "baz",
"baz": "bunk",
},
Target: ocispec.Descriptor{
Size: 20, // ignored
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
},
},
fieldpaths: []string{"labels.foo"},
expected: images.Image{
Labels: map[string]string{
"foo": "baz",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
},
{
name: "ReplaceTarget", // target must be updated as a unit
original: imageBase(),
input: images.Image{
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab+replaced",
},
},
fieldpaths: []string{"target"},
expected: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab+replaced",
},
},
},
{
name: "EmptySize",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 0,
MediaType: "application/vnd.oci.blab",
},
},
cause: errdefs.ErrInvalidArgument,
},
{
name: "EmptySizeOnCreate",
original: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
MediaType: "application/vnd.oci.blab",
},
},
createerr: errdefs.ErrInvalidArgument,
},
{
name: "EmptyMediaType",
original: imageBase(),
input: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
},
},
cause: errdefs.ErrInvalidArgument,
},
{
name: "EmptySizeOnCreate",
original: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
},
},
createerr: errdefs.ErrInvalidArgument,
},
{
name: "TryUpdateNameFail",
original: images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
input: images.Image{
Name: "test should fail",
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
},
cause: errdefs.ErrNotFound,
},
} {
t.Run(testcase.name, func(t *testing.T) {
testcase.original.Name = testcase.name
if testcase.input.Name == "" {
testcase.input.Name = testcase.name
}
testcase.expected.Name = testcase.name
if testcase.original.Target.Digest == "" {
testcase.original.Target.Digest = digest.FromString(testcase.name)
testcase.input.Target.Digest = testcase.original.Target.Digest
testcase.expected.Target.Digest = testcase.original.Target.Digest
}
// Create
now := time.Now()
created, 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
}
checkImageTimestamps(t, &created, now, true)
testcase.original.CreatedAt = created.CreatedAt
testcase.expected.CreatedAt = created.CreatedAt
testcase.original.UpdatedAt = created.UpdatedAt
testcase.expected.UpdatedAt = created.UpdatedAt
checkImagesEqual(t, &created, &testcase.original, "unexpected image on creation")
// Update
now = time.Now()
updated, 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
}
checkImageTimestamps(t, &updated, now, false)
testcase.expected.UpdatedAt = updated.UpdatedAt
checkImagesEqual(t, &updated, &testcase.expected, "updated failed to get expected result")
// Get
result, err := store.Get(ctx, testcase.original.Name)
if err != nil {
t.Fatal(err)
}
checkImagesEqual(t, &result, &testcase.expected, "get after failed to get expected result")
})
}
}
func imageBase() images.Image {
return images.Image{
Labels: map[string]string{
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 10,
MediaType: "application/vnd.oci.blab",
},
}
}
func checkImageTimestamps(t *testing.T, im *images.Image, now time.Time, oncreate bool) {
t.Helper()
if im.UpdatedAt.IsZero() || im.CreatedAt.IsZero() {
t.Fatalf("timestamps not set")
}
if oncreate {
if !im.CreatedAt.Equal(im.UpdatedAt) {
t.Fatal("timestamps should be equal on create")
}
} else {
// ensure that updatedat is always after createdat
if !im.UpdatedAt.After(im.CreatedAt) {
t.Fatalf("timestamp for updatedat not after createdat: %v <= %v", im.UpdatedAt, im.CreatedAt)
}
}
if im.UpdatedAt.Before(now) {
t.Fatal("createdat time incorrect should be after the start of the operation")
}
}
func checkImagesEqual(t *testing.T, a, b *images.Image, format string, args ...interface{}) {
t.Helper()
if !reflect.DeepEqual(a, b) {
t.Fatalf("images not equal \n\t%v != \n\t%v: "+format, append([]interface{}{a, b}, args...)...)
}
}