Files
moby/daemon/containerd/image_test.go
Sebastiaan van Stijn c1c9087404 daemon/containerd: use t.Context() in tests
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-25 21:15:02 +02:00

310 lines
8.2 KiB
Go

package containerd
import (
"context"
"io"
"math/rand"
"path/filepath"
"testing"
c8dimages "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/log/logtest"
"github.com/distribution/reference"
dockerimages "github.com/moby/moby/v2/daemon/images"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
bolt "go.etcd.io/bbolt"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestLookup(t *testing.T) {
ctx := namespaces.WithNamespace(t.Context(), "testing")
ctx = logtest.WithT(ctx, t)
mdb := newTestDB(ctx, t)
service := &ImageService{
images: metadata.NewImageStore(mdb),
}
ubuntuLatest := c8dimages.Image{
Name: "docker.io/library/ubuntu:latest",
Target: desc(10),
}
ubuntuLatestWithDigest := c8dimages.Image{
Name: "docker.io/library/ubuntu:latest@" + digestFor(10).String(),
Target: desc(10),
}
ubuntuLatestWithOldDigest := c8dimages.Image{
Name: "docker.io/library/ubuntu:latest@" + digestFor(11).String(),
Target: desc(11),
}
ambiguousShortName := c8dimages.Image{
Name: "docker.io/library/abcdef:latest",
Target: desc(12),
}
ambiguousShortNameWithDigest := c8dimages.Image{
Name: "docker.io/library/abcdef:latest@" + digestFor(12).String(),
Target: desc(12),
}
shortNameIsHashAlgorithm := c8dimages.Image{
Name: "docker.io/library/sha256:defcab",
Target: desc(13),
}
testImages := []c8dimages.Image{
ubuntuLatest,
ubuntuLatestWithDigest,
ubuntuLatestWithOldDigest,
ambiguousShortName,
ambiguousShortNameWithDigest,
shortNameIsHashAlgorithm,
{
Name: "docker.io/test/volatile:retried",
Target: desc(14),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(15),
},
}
for _, img := range testImages {
if _, err := service.images.Create(ctx, img); err != nil {
t.Fatalf("failed to create image %q: %v", img.Name, err)
}
}
for _, tc := range []struct {
lookup string
img *c8dimages.Image
all []c8dimages.Image
err error
}{
{
// Get ubuntu images with default "latest" tag
lookup: "ubuntu",
img: &ubuntuLatest,
all: []c8dimages.Image{ubuntuLatest, ubuntuLatestWithDigest},
},
{
// Get all images by image id
lookup: ubuntuLatest.Target.Digest.String(),
img: nil,
all: []c8dimages.Image{ubuntuLatest, ubuntuLatestWithDigest},
},
{
// Fail to lookup reference with no tag, reference has both tag and digest
lookup: "ubuntu@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
img: nil,
all: []c8dimages.Image{ubuntuLatestWithOldDigest},
},
{
// Get all image with both tag and digest
lookup: "ubuntu:latest@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
img: &ubuntuLatestWithOldDigest,
all: []c8dimages.Image{ubuntuLatestWithOldDigest},
},
{
// Fail to lookup reference with no tag for digest that doesn't exist
lookup: "ubuntu@" + digestFor(20).String(),
err: dockerimages.ErrImageDoesNotExist{Ref: nameDigest("ubuntu", digestFor(20))},
},
{
// Fail to lookup reference with nonexistent tag
lookup: "ubuntu:nonexistent",
err: dockerimages.ErrImageDoesNotExist{Ref: nameTag("ubuntu", "nonexistent")},
},
{
// Get abcdef image which also matches short image id
lookup: "abcdef",
img: &ambiguousShortName,
all: []c8dimages.Image{ambiguousShortName, ambiguousShortNameWithDigest},
},
{
// Fail to lookup image named "sha256" with tag that doesn't exist
lookup: "sha256:abcdef",
err: dockerimages.ErrImageDoesNotExist{Ref: nameTag("sha256", "abcdef")},
},
{
// Lookup with shortened image id
lookup: ambiguousShortName.Target.Digest.Encoded()[:8],
img: nil,
all: []c8dimages.Image{ambiguousShortName, ambiguousShortNameWithDigest},
},
{
// Lookup an actual image named "sha256" in the default namespace
lookup: "sha256:defcab",
img: &shortNameIsHashAlgorithm,
all: []c8dimages.Image{shortNameIsHashAlgorithm},
},
} {
t.Run(tc.lookup, func(t *testing.T) {
t.Parallel()
img, all, err := service.resolveAllReferences(ctx, tc.lookup)
if tc.err == nil {
assert.NilError(t, err)
} else {
assert.Error(t, err, tc.err.Error())
}
if tc.img == nil {
assert.Assert(t, is.Nil(img))
} else {
assert.Assert(t, img != nil)
assert.Check(t, is.Equal(img.Name, tc.img.Name))
assert.Check(t, is.Equal(img.Target.Digest, tc.img.Target.Digest))
}
assert.Assert(t, is.Len(tc.all, len(all)))
// Order should match
for i := range all {
assert.Check(t, is.Equal(all[i].Name, tc.all[i].Name), "image[%d]", i)
assert.Check(t, is.Equal(all[i].Target.Digest, tc.all[i].Target.Digest), "image[%d]", i)
}
})
}
t.Run("fail-inconsistency", func(t *testing.T) {
svc := &ImageService{
images: &mutateOnGetImageStore{
Store: service.images,
getMutations: []c8dimages.Image{
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(18),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(19),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(20),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(21),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(22),
},
},
t: t,
},
}
_, _, err := svc.resolveAllReferences(ctx, "test/volatile:inconsistent")
assert.ErrorIs(t, err, errInconsistentData)
})
t.Run("retry-inconsistency", func(t *testing.T) {
svc := &ImageService{
images: &mutateOnGetImageStore{
Store: service.images,
getMutations: []c8dimages.Image{
{
Name: "docker.io/test/volatile:retried",
Target: desc(16),
},
{
Name: "docker.io/test/volatile:retried",
Target: desc(17),
},
},
t: t,
},
}
img, all, err := svc.resolveAllReferences(ctx, "test/volatile:retried")
assert.NilError(t, err)
assert.Assert(t, img != nil)
assert.Check(t, is.Equal(img.Name, "docker.io/test/volatile:retried"))
assert.Check(t, is.Equal(img.Target.Digest, digestFor(17)))
assert.Assert(t, is.Len(all, 1))
assert.Check(t, is.Equal(all[0].Name, "docker.io/test/volatile:retried"))
assert.Check(t, is.Equal(all[0].Target.Digest, digestFor(17)))
})
}
type mutateOnGetImageStore struct {
c8dimages.Store
getMutations []c8dimages.Image
t *testing.T
}
func (m *mutateOnGetImageStore) Get(ctx context.Context, name string) (c8dimages.Image, error) {
img, err := m.Store.Get(ctx, name)
if len(m.getMutations) > 0 {
m.Store.Update(ctx, m.getMutations[0])
m.getMutations = m.getMutations[1:]
m.t.Logf("Get %s", name)
}
return img, err
}
func nameDigest(name string, dgst digest.Digest) reference.Reference {
named, _ := reference.WithName(name)
digested, _ := reference.WithDigest(named, dgst)
return digested
}
func nameTag(name, tag string) reference.Reference {
named, _ := reference.WithName(name)
tagged, _ := reference.WithTag(named, tag)
return tagged
}
func desc(size int64) ocispec.Descriptor {
return ocispec.Descriptor{
Digest: digestFor(size),
Size: size,
MediaType: ocispec.MediaTypeImageIndex,
}
}
func digestFor(i int64) digest.Digest {
r := rand.New(rand.NewSource(i))
dgstr := digest.SHA256.Digester()
_, err := io.Copy(dgstr.Hash(), io.LimitReader(r, i))
if err != nil {
panic(err)
}
return dgstr.Digest()
}
func newTestDB(ctx context.Context, t testing.TB) *metadata.DB {
t.Helper()
p := filepath.Join(t.TempDir(), "metadata")
bdb, err := bolt.Open(p, 0o600, &bolt.Options{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { bdb.Close() })
mdb := metadata.NewDB(bdb, nil, nil)
if err := mdb.Init(ctx); err != nil {
t.Fatal(err)
}
return mdb
}
type testSnapshotterService struct {
snapshots.Snapshotter
}
func (s *testSnapshotterService) Stat(ctx context.Context, key string) (snapshots.Info, error) {
return snapshots.Info{}, nil
}
func (s *testSnapshotterService) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
return snapshots.Usage{}, nil
}