github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_test.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"math/rand"
     7  	"path/filepath"
     8  	"testing"
     9  
    10  	"github.com/containerd/containerd/images"
    11  	"github.com/containerd/containerd/metadata"
    12  	"github.com/containerd/containerd/namespaces"
    13  	"github.com/containerd/containerd/snapshots"
    14  	"github.com/containerd/log/logtest"
    15  	"github.com/distribution/reference"
    16  	dockerimages "github.com/docker/docker/daemon/images"
    17  	"github.com/opencontainers/go-digest"
    18  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    19  
    20  	"go.etcd.io/bbolt"
    21  
    22  	"gotest.tools/v3/assert"
    23  	is "gotest.tools/v3/assert/cmp"
    24  )
    25  
    26  func TestLookup(t *testing.T) {
    27  	ctx := namespaces.WithNamespace(context.TODO(), "testing")
    28  	ctx = logtest.WithT(ctx, t)
    29  	mdb := newTestDB(ctx, t)
    30  	service := &ImageService{
    31  		images: metadata.NewImageStore(mdb),
    32  	}
    33  
    34  	ubuntuLatest := images.Image{
    35  		Name:   "docker.io/library/ubuntu:latest",
    36  		Target: desc(10),
    37  	}
    38  	ubuntuLatestWithDigest := images.Image{
    39  		Name:   "docker.io/library/ubuntu:latest@" + digestFor(10).String(),
    40  		Target: desc(10),
    41  	}
    42  	ubuntuLatestWithOldDigest := images.Image{
    43  		Name:   "docker.io/library/ubuntu:latest@" + digestFor(11).String(),
    44  		Target: desc(11),
    45  	}
    46  	ambiguousShortName := images.Image{
    47  		Name:   "docker.io/library/abcdef:latest",
    48  		Target: desc(12),
    49  	}
    50  	ambiguousShortNameWithDigest := images.Image{
    51  		Name:   "docker.io/library/abcdef:latest@" + digestFor(12).String(),
    52  		Target: desc(12),
    53  	}
    54  	shortNameIsHashAlgorithm := images.Image{
    55  		Name:   "docker.io/library/sha256:defcab",
    56  		Target: desc(13),
    57  	}
    58  
    59  	testImages := []images.Image{
    60  		ubuntuLatest,
    61  		ubuntuLatestWithDigest,
    62  		ubuntuLatestWithOldDigest,
    63  		ambiguousShortName,
    64  		ambiguousShortNameWithDigest,
    65  		shortNameIsHashAlgorithm,
    66  		{
    67  			Name:   "docker.io/test/volatile:retried",
    68  			Target: desc(14),
    69  		},
    70  		{
    71  			Name:   "docker.io/test/volatile:inconsistent",
    72  			Target: desc(15),
    73  		},
    74  	}
    75  	for _, img := range testImages {
    76  		if _, err := service.images.Create(ctx, img); err != nil {
    77  			t.Fatalf("failed to create image %q: %v", img.Name, err)
    78  		}
    79  	}
    80  
    81  	for _, tc := range []struct {
    82  		lookup string
    83  		img    *images.Image
    84  		all    []images.Image
    85  		err    error
    86  	}{
    87  		{
    88  			// Get ubuntu images with default "latest" tag
    89  			lookup: "ubuntu",
    90  			img:    &ubuntuLatest,
    91  			all:    []images.Image{ubuntuLatest, ubuntuLatestWithDigest},
    92  		},
    93  		{
    94  			// Get all images by image id
    95  			lookup: ubuntuLatest.Target.Digest.String(),
    96  			img:    nil,
    97  			all:    []images.Image{ubuntuLatest, ubuntuLatestWithDigest},
    98  		},
    99  		{
   100  			// Fail to lookup reference with no tag, reference has both tag and digest
   101  			lookup: "ubuntu@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
   102  			img:    nil,
   103  			all:    []images.Image{ubuntuLatestWithOldDigest},
   104  		},
   105  		{
   106  			// Get all image with both tag and digest
   107  			lookup: "ubuntu:latest@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
   108  			img:    &ubuntuLatestWithOldDigest,
   109  			all:    []images.Image{ubuntuLatestWithOldDigest},
   110  		},
   111  		{
   112  			// Fail to lookup reference with no tag for digest that doesn't exist
   113  			lookup: "ubuntu@" + digestFor(20).String(),
   114  			err:    dockerimages.ErrImageDoesNotExist{Ref: nameDigest("ubuntu", digestFor(20))},
   115  		},
   116  		{
   117  			// Fail to lookup reference with nonexistent tag
   118  			lookup: "ubuntu:nonexistent",
   119  			err:    dockerimages.ErrImageDoesNotExist{Ref: nameTag("ubuntu", "nonexistent")},
   120  		},
   121  		{
   122  			// Get abcdef image which also matches short image id
   123  			lookup: "abcdef",
   124  			img:    &ambiguousShortName,
   125  			all:    []images.Image{ambiguousShortName, ambiguousShortNameWithDigest},
   126  		},
   127  		{
   128  			// Fail to lookup image named "sha256" with tag that doesn't exist
   129  			lookup: "sha256:abcdef",
   130  			err:    dockerimages.ErrImageDoesNotExist{Ref: nameTag("sha256", "abcdef")},
   131  		},
   132  		{
   133  			// Lookup with shortened image id
   134  			lookup: ambiguousShortName.Target.Digest.Encoded()[:8],
   135  			img:    nil,
   136  			all:    []images.Image{ambiguousShortName, ambiguousShortNameWithDigest},
   137  		},
   138  		{
   139  			// Lookup an actual image named "sha256" in the default namespace
   140  			lookup: "sha256:defcab",
   141  			img:    &shortNameIsHashAlgorithm,
   142  			all:    []images.Image{shortNameIsHashAlgorithm},
   143  		},
   144  	} {
   145  		tc := tc
   146  		t.Run(tc.lookup, func(t *testing.T) {
   147  			t.Parallel()
   148  			img, all, err := service.resolveAllReferences(ctx, tc.lookup)
   149  			if tc.err == nil {
   150  				assert.NilError(t, err)
   151  			} else {
   152  				assert.Error(t, err, tc.err.Error())
   153  			}
   154  			if tc.img == nil {
   155  				assert.Assert(t, is.Nil(img))
   156  			} else {
   157  				assert.Assert(t, img != nil)
   158  				assert.Check(t, is.Equal(img.Name, tc.img.Name))
   159  				assert.Check(t, is.Equal(img.Target.Digest, tc.img.Target.Digest))
   160  			}
   161  
   162  			assert.Assert(t, is.Len(tc.all, len(all)))
   163  
   164  			// Order should match
   165  			for i := range all {
   166  				assert.Check(t, is.Equal(all[i].Name, tc.all[i].Name), "image[%d]", i)
   167  				assert.Check(t, is.Equal(all[i].Target.Digest, tc.all[i].Target.Digest), "image[%d]", i)
   168  			}
   169  		})
   170  	}
   171  
   172  	t.Run("fail-inconsistency", func(t *testing.T) {
   173  		service := &ImageService{
   174  			images: &mutateOnGetImageStore{
   175  				Store: service.images,
   176  				getMutations: []images.Image{
   177  					{
   178  						Name:   "docker.io/test/volatile:inconsistent",
   179  						Target: desc(18),
   180  					},
   181  					{
   182  						Name:   "docker.io/test/volatile:inconsistent",
   183  						Target: desc(19),
   184  					},
   185  					{
   186  						Name:   "docker.io/test/volatile:inconsistent",
   187  						Target: desc(20),
   188  					},
   189  					{
   190  						Name:   "docker.io/test/volatile:inconsistent",
   191  						Target: desc(21),
   192  					},
   193  					{
   194  						Name:   "docker.io/test/volatile:inconsistent",
   195  						Target: desc(22),
   196  					},
   197  				},
   198  				t: t,
   199  			},
   200  		}
   201  
   202  		_, _, err := service.resolveAllReferences(ctx, "test/volatile:inconsistent")
   203  		assert.ErrorIs(t, err, errInconsistentData)
   204  	})
   205  
   206  	t.Run("retry-inconsistency", func(t *testing.T) {
   207  		service := &ImageService{
   208  			images: &mutateOnGetImageStore{
   209  				Store: service.images,
   210  				getMutations: []images.Image{
   211  					{
   212  						Name:   "docker.io/test/volatile:retried",
   213  						Target: desc(16),
   214  					},
   215  					{
   216  						Name:   "docker.io/test/volatile:retried",
   217  						Target: desc(17),
   218  					},
   219  				},
   220  				t: t,
   221  			},
   222  		}
   223  
   224  		img, all, err := service.resolveAllReferences(ctx, "test/volatile:retried")
   225  		assert.NilError(t, err)
   226  
   227  		assert.Assert(t, img != nil)
   228  		assert.Check(t, is.Equal(img.Name, "docker.io/test/volatile:retried"))
   229  		assert.Check(t, is.Equal(img.Target.Digest, digestFor(17)))
   230  		assert.Assert(t, is.Len(all, 1))
   231  		assert.Check(t, is.Equal(all[0].Name, "docker.io/test/volatile:retried"))
   232  		assert.Check(t, is.Equal(all[0].Target.Digest, digestFor(17)))
   233  	})
   234  }
   235  
   236  type mutateOnGetImageStore struct {
   237  	images.Store
   238  	getMutations []images.Image
   239  	t            *testing.T
   240  }
   241  
   242  func (m *mutateOnGetImageStore) Get(ctx context.Context, name string) (images.Image, error) {
   243  	img, err := m.Store.Get(ctx, name)
   244  	if len(m.getMutations) > 0 {
   245  		m.Store.Update(ctx, m.getMutations[0])
   246  		m.getMutations = m.getMutations[1:]
   247  		m.t.Logf("Get %s", name)
   248  	}
   249  	return img, err
   250  }
   251  
   252  func nameDigest(name string, dgst digest.Digest) reference.Reference {
   253  	named, _ := reference.WithName(name)
   254  	digested, _ := reference.WithDigest(named, dgst)
   255  	return digested
   256  }
   257  
   258  func nameTag(name, tag string) reference.Reference {
   259  	named, _ := reference.WithName(name)
   260  	tagged, _ := reference.WithTag(named, tag)
   261  	return tagged
   262  }
   263  
   264  func desc(size int64) ocispec.Descriptor {
   265  	return ocispec.Descriptor{
   266  		Digest:    digestFor(size),
   267  		Size:      size,
   268  		MediaType: ocispec.MediaTypeImageIndex,
   269  	}
   270  
   271  }
   272  
   273  func digestFor(i int64) digest.Digest {
   274  	r := rand.New(rand.NewSource(i))
   275  	dgstr := digest.SHA256.Digester()
   276  	_, err := io.Copy(dgstr.Hash(), io.LimitReader(r, i))
   277  	if err != nil {
   278  		panic(err)
   279  	}
   280  	return dgstr.Digest()
   281  }
   282  
   283  func newTestDB(ctx context.Context, t testing.TB) *metadata.DB {
   284  	t.Helper()
   285  
   286  	p := filepath.Join(t.TempDir(), "metadata")
   287  	bdb, err := bbolt.Open(p, 0600, &bbolt.Options{})
   288  	if err != nil {
   289  		t.Fatal(err)
   290  	}
   291  	t.Cleanup(func() { bdb.Close() })
   292  
   293  	mdb := metadata.NewDB(bdb, nil, nil)
   294  	if err := mdb.Init(ctx); err != nil {
   295  		t.Fatal(err)
   296  	}
   297  
   298  	return mdb
   299  }
   300  
   301  type testSnapshotterService struct {
   302  	snapshots.Snapshotter
   303  }
   304  
   305  func (s *testSnapshotterService) Stat(ctx context.Context, key string) (snapshots.Info, error) {
   306  	return snapshots.Info{}, nil
   307  }
   308  
   309  func (s *testSnapshotterService) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
   310  	return snapshots.Usage{}, nil
   311  }