package containerd import ( "context" "io" "math/rand" "path/filepath" "testing" "github.com/containerd/containerd/images" "github.com/containerd/containerd/metadata" "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/snapshots" "github.com/containerd/log/logtest" "github.com/distribution/reference" dockerimages "github.com/docker/docker/daemon/images" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "go.etcd.io/bbolt" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestLookup(t *testing.T) { ctx := namespaces.WithNamespace(context.TODO(), "testing") ctx = logtest.WithT(ctx, t) mdb := newTestDB(ctx, t) service := &ImageService{ images: metadata.NewImageStore(mdb), } ubuntuLatest := images.Image{ Name: "docker.io/library/ubuntu:latest", Target: desc(10), } ubuntuLatestWithDigest := images.Image{ Name: "docker.io/library/ubuntu:latest@" + digestFor(10).String(), Target: desc(10), } ubuntuLatestWithOldDigest := images.Image{ Name: "docker.io/library/ubuntu:latest@" + digestFor(11).String(), Target: desc(11), } ambiguousShortName := images.Image{ Name: "docker.io/library/abcdef:latest", Target: desc(12), } ambiguousShortNameWithDigest := images.Image{ Name: "docker.io/library/abcdef:latest@" + digestFor(12).String(), Target: desc(12), } shortNameIsHashAlgorithm := images.Image{ Name: "docker.io/library/sha256:defcab", Target: desc(13), } testImages := []images.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 *images.Image all []images.Image err error }{ { // Get ubuntu images with default "latest" tag lookup: "ubuntu", img: &ubuntuLatest, all: []images.Image{ubuntuLatest, ubuntuLatestWithDigest}, }, { // Get all images by image id lookup: ubuntuLatest.Target.Digest.String(), img: nil, all: []images.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: []images.Image{ubuntuLatestWithOldDigest}, }, { // Get all image with both tag and digest lookup: "ubuntu:latest@" + ubuntuLatestWithOldDigest.Target.Digest.String(), img: &ubuntuLatestWithOldDigest, all: []images.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: []images.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: []images.Image{ambiguousShortName, ambiguousShortNameWithDigest}, }, { // Lookup an actual image named "sha256" in the default namespace lookup: "sha256:defcab", img: &shortNameIsHashAlgorithm, all: []images.Image{shortNameIsHashAlgorithm}, }, } { tc := tc 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) { service := &ImageService{ images: &mutateOnGetImageStore{ Store: service.images, getMutations: []images.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 := service.resolveAllReferences(ctx, "test/volatile:inconsistent") assert.ErrorIs(t, err, errInconsistentData) }) t.Run("retry-inconsistency", func(t *testing.T) { service := &ImageService{ images: &mutateOnGetImageStore{ Store: service.images, getMutations: []images.Image{ { Name: "docker.io/test/volatile:retried", Target: desc(16), }, { Name: "docker.io/test/volatile:retried", Target: desc(17), }, }, t: t, }, } img, all, err := service.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 { images.Store getMutations []images.Image t *testing.T } func (m *mutateOnGetImageStore) Get(ctx context.Context, name string) (images.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 := bbolt.Open(p, 0600, &bbolt.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 }