mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
TestImagesCreateUpdateDelete asserts that an image's updatedat is strictly after its createdat. Both timestamps are stamped via time.Now().UTC(), which strips the monotonic reading, so the comparison falls back to the wall clock. On platforms with coarse timer resolution (e.g. Windows, which advances system time at the ~15.6ms tick), the Create and Update calls can land in the same tick and produce identical timestamps, making the strict After() check fail intermittently. Wait for the wall clock to advance past the creation timestamp before updating so the assertion stays meaningful without depending on clock resolution. On fine-resolution clocks the loop runs zero iterations. Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
667 lines
16 KiB
Go
667 lines
16 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 (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/pkg/filters"
|
|
"github.com/containerd/errdefs"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
func TestImagesList(t *testing.T) {
|
|
ctx, db := testEnv(t)
|
|
store := NewImageStore(NewDB(db, nil, nil))
|
|
|
|
testset := map[string]*images.Image{}
|
|
for i := range 4 {
|
|
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),
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
}
|
|
|
|
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); !errdefs.IsNotFound(err) {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
}
|
|
func TestImagesCreateUpdateDelete(t *testing.T) {
|
|
ctx, db := testEnv(t)
|
|
store := NewImageStore(NewDB(db, nil, nil))
|
|
|
|
for _, testcase := range []struct {
|
|
name string
|
|
original images.Image
|
|
createerr error
|
|
input images.Image // Input target size determines target digest, base image uses 10
|
|
fieldpaths []string
|
|
expected images.Image
|
|
cause error
|
|
deleteerr 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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
expected: images.Image{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
expected: images.Image{
|
|
Labels: map[string]string{
|
|
"for": "bar",
|
|
"boo": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceLabelsFieldPath",
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Labels: map[string]string{
|
|
"for": "bar",
|
|
"boo": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
|
|
Annotations: map[string]string{
|
|
"not": "bar",
|
|
"new": "boo",
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels"},
|
|
expected: images.Image{
|
|
Labels: map[string]string{
|
|
"for": "bar",
|
|
"boo": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceLabelsAnnotationsFieldPath",
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Labels: map[string]string{
|
|
"for": "bar",
|
|
"boo": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
|
|
Annotations: map[string]string{
|
|
"foo": "boo",
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"annotations", "labels"},
|
|
expected: images.Image{
|
|
Labels: map[string]string{
|
|
"for": "bar",
|
|
"boo": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "boo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceLabel",
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Labels: map[string]string{
|
|
"foo": "baz",
|
|
"baz": "bunk",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceAnnotation",
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Labels: map[string]string{
|
|
"foo": "baz",
|
|
"baz": "bunk",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
|
|
Annotations: map[string]string{
|
|
"foo": "baz",
|
|
"baz": "bunk",
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"annotations.foo"},
|
|
expected: images.Image{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "baz",
|
|
"baz": "boo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceTargetTypeAndAnnotations", // target must be updated as a unit
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Target: ocispec.Descriptor{
|
|
Size: 10,
|
|
MediaType: "application/vnd.oci.blab+replaced",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ReplaceTargetFieldpath", // target must be updated as a unit
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Target: ocispec.Descriptor{
|
|
Size: 20,
|
|
MediaType: "application/vnd.oci.blab+replaced",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"target"},
|
|
expected: images.Image{
|
|
Labels: map[string]string{ // Labels not updated
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
Size: 20,
|
|
MediaType: "application/vnd.oci.blab+replaced",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
deleteerr: errdefs.ErrNotFound,
|
|
},
|
|
{
|
|
name: "ReplaceTarget", // target must be updated as a unit
|
|
original: imageBase(),
|
|
input: images.Image{
|
|
Target: ocispec.Descriptor{
|
|
Size: 20,
|
|
MediaType: "application/vnd.oci.blab+replaced",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
expected: images.Image{
|
|
Target: ocispec.Descriptor{
|
|
Size: 20,
|
|
MediaType: "application/vnd.oci.blab+replaced",
|
|
Annotations: map[string]string{
|
|
"fox": "dog",
|
|
},
|
|
},
|
|
},
|
|
deleteerr: errdefs.ErrNotFound,
|
|
},
|
|
{
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "EmptySizeOnCreate",
|
|
original: images.Image{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
Target: ocispec.Descriptor{
|
|
MediaType: "application/vnd.oci.blab",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
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
|
|
|
|
testcase.original.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.original.Target.Size))
|
|
testcase.input.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.input.Target.Size))
|
|
testcase.expected.Target.Digest = testcase.input.Target.Digest
|
|
|
|
// Create
|
|
now := time.Now()
|
|
created, err := store.Create(ctx, testcase.original)
|
|
if !errors.Is(err, testcase.createerr) {
|
|
if testcase.createerr == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Unwrap(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")
|
|
|
|
// Wait for the wall clock to advance past the creation timestamp
|
|
// before updating. Create and Update both stamp time.Now().UTC()
|
|
// (which strips the monotonic reading), and on platforms with a
|
|
// coarse timer resolution (e.g. Windows) two back-to-back stamps
|
|
// can be identical, making updatedat == createdat. Spinning here
|
|
// keeps the strict "updatedat after createdat" assertion below
|
|
// meaningful without depending on clock resolution.
|
|
for !time.Now().After(created.UpdatedAt) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
// Update
|
|
now = time.Now()
|
|
updated, err := store.Update(ctx, testcase.input, testcase.fieldpaths...)
|
|
if !errors.Is(err, testcase.cause) {
|
|
if testcase.cause == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Unwrap(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")
|
|
|
|
if testcase.original.Target.Digest != testcase.expected.Target.Digest {
|
|
t.Log("Delete should fail")
|
|
}
|
|
// Delete
|
|
err = store.Delete(ctx, testcase.original.Name, images.DeleteTarget(&testcase.original.Target))
|
|
if err != nil {
|
|
if testcase.deleteerr == nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !errors.Is(err, testcase.deleteerr) {
|
|
t.Fatal("unexpected error", err, ", expected", testcase.deleteerr)
|
|
}
|
|
} else if testcase.deleteerr != nil {
|
|
t.Fatal("no error on deleted, expected", testcase.deleteerr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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",
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
"baz": "boo",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
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 ...any) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Fatalf("images not equal \n\t%v != \n\t%v: "+format, append([]any{a, b}, args...)...)
|
|
}
|
|
}
|