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

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/containerd/containerd"
    15  	"github.com/containerd/containerd/content"
    16  	"github.com/containerd/containerd/images"
    17  	"github.com/containerd/containerd/metadata"
    18  	"github.com/containerd/containerd/namespaces"
    19  	"github.com/containerd/containerd/platforms"
    20  	"github.com/containerd/containerd/snapshots"
    21  	"github.com/containerd/log/logtest"
    22  	imagetypes "github.com/docker/docker/api/types/image"
    23  	daemonevents "github.com/docker/docker/daemon/events"
    24  	"github.com/docker/docker/internal/testutils/specialimage"
    25  	"github.com/opencontainers/go-digest"
    26  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"gotest.tools/v3/assert"
    28  	is "gotest.tools/v3/assert/cmp"
    29  )
    30  
    31  func imagesFromIndex(index ...*ocispec.Index) []images.Image {
    32  	var imgs []images.Image
    33  	for _, idx := range index {
    34  		for _, desc := range idx.Manifests {
    35  			imgs = append(imgs, images.Image{
    36  				Name:   desc.Annotations["io.containerd.image.name"],
    37  				Target: desc,
    38  			})
    39  		}
    40  	}
    41  	return imgs
    42  }
    43  
    44  func BenchmarkImageList(b *testing.B) {
    45  	populateStore := func(ctx context.Context, is *ImageService, dir string, count int) {
    46  		// Use constant seed for reproducibility
    47  		src := rand.NewSource(1982731263716)
    48  
    49  		for i := 0; i < count; i++ {
    50  			platform := platforms.DefaultSpec()
    51  
    52  			// 20% is other architecture than the host
    53  			if i%5 == 0 {
    54  				platform.Architecture = "other"
    55  			}
    56  
    57  			idx, err := specialimage.RandomSinglePlatform(dir, platform, src)
    58  			assert.NilError(b, err)
    59  
    60  			imgs := imagesFromIndex(idx)
    61  			for _, desc := range imgs {
    62  				_, err := is.images.Create(ctx, desc)
    63  				assert.NilError(b, err)
    64  			}
    65  		}
    66  	}
    67  
    68  	for _, count := range []int{10, 100, 1000} {
    69  		csDir := b.TempDir()
    70  
    71  		ctx := namespaces.WithNamespace(context.TODO(), "testing-"+strconv.Itoa(count))
    72  
    73  		cs := &delayedStore{
    74  			store:    &blobsDirContentStore{blobs: filepath.Join(csDir, "blobs/sha256")},
    75  			overhead: 500 * time.Microsecond,
    76  		}
    77  
    78  		is := fakeImageService(b, ctx, cs)
    79  		populateStore(ctx, is, csDir, count)
    80  
    81  		b.Run(strconv.Itoa(count)+"-images", func(b *testing.B) {
    82  			for i := 0; i < b.N; i++ {
    83  				_, err := is.Images(ctx, imagetypes.ListOptions{All: true})
    84  				assert.NilError(b, err)
    85  			}
    86  		})
    87  	}
    88  }
    89  
    90  func TestImageList(t *testing.T) {
    91  	ctx := namespaces.WithNamespace(context.TODO(), "testing")
    92  
    93  	blobsDir := t.TempDir()
    94  
    95  	multilayer, err := specialimage.MultiLayer(blobsDir)
    96  	assert.NilError(t, err)
    97  
    98  	twoplatform, err := specialimage.TwoPlatform(blobsDir)
    99  	assert.NilError(t, err)
   100  
   101  	emptyIndex, err := specialimage.EmptyIndex(blobsDir)
   102  	assert.NilError(t, err)
   103  
   104  	configTarget, err := specialimage.ConfigTarget(blobsDir)
   105  	assert.NilError(t, err)
   106  
   107  	cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")}
   108  
   109  	for _, tc := range []struct {
   110  		name   string
   111  		images []images.Image
   112  		opts   imagetypes.ListOptions
   113  
   114  		check func(*testing.T, []*imagetypes.Summary)
   115  	}{
   116  		{
   117  			name:   "one multi-layer image",
   118  			images: imagesFromIndex(multilayer),
   119  			check: func(t *testing.T, all []*imagetypes.Summary) {
   120  				assert.Check(t, is.Len(all, 1))
   121  
   122  				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
   123  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
   124  			},
   125  		},
   126  		{
   127  			name:   "one image with two platforms is still one entry",
   128  			images: imagesFromIndex(twoplatform),
   129  			check: func(t *testing.T, all []*imagetypes.Summary) {
   130  				assert.Check(t, is.Len(all, 1))
   131  
   132  				assert.Check(t, is.Equal(all[0].ID, twoplatform.Manifests[0].Digest.String()))
   133  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"twoplatform:latest"}))
   134  			},
   135  		},
   136  		{
   137  			name:   "two images are two entries",
   138  			images: imagesFromIndex(multilayer, twoplatform),
   139  			check: func(t *testing.T, all []*imagetypes.Summary) {
   140  				assert.Check(t, is.Len(all, 2))
   141  
   142  				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
   143  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
   144  
   145  				assert.Check(t, is.Equal(all[1].ID, twoplatform.Manifests[0].Digest.String()))
   146  				assert.Check(t, is.DeepEqual(all[1].RepoTags, []string{"twoplatform:latest"}))
   147  			},
   148  		},
   149  		{
   150  			name:   "three images, one is an empty index",
   151  			images: imagesFromIndex(multilayer, emptyIndex, twoplatform),
   152  			check: func(t *testing.T, all []*imagetypes.Summary) {
   153  				assert.Check(t, is.Len(all, 2))
   154  			},
   155  		},
   156  		{
   157  			// Make sure an invalid image target doesn't break the whole operation
   158  			name:   "one good image, second has config as a target",
   159  			images: imagesFromIndex(multilayer, configTarget),
   160  			check: func(t *testing.T, all []*imagetypes.Summary) {
   161  				assert.Check(t, is.Len(all, 1))
   162  
   163  				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
   164  			},
   165  		},
   166  	} {
   167  		tc := tc
   168  		t.Run(tc.name, func(t *testing.T) {
   169  			ctx := logtest.WithT(ctx, t)
   170  			service := fakeImageService(t, ctx, cs)
   171  
   172  			for _, img := range tc.images {
   173  				_, err := service.images.Create(ctx, img)
   174  				assert.NilError(t, err)
   175  			}
   176  
   177  			all, err := service.Images(ctx, tc.opts)
   178  			assert.NilError(t, err)
   179  
   180  			sort.Slice(all, func(i, j int) bool {
   181  				firstTag := func(idx int) string {
   182  					if len(all[idx].RepoTags) > 0 {
   183  						return all[idx].RepoTags[0]
   184  					}
   185  					return ""
   186  				}
   187  				return firstTag(i) < firstTag(j)
   188  			})
   189  
   190  			tc.check(t, all)
   191  		})
   192  	}
   193  
   194  }
   195  
   196  func fakeImageService(t testing.TB, ctx context.Context, cs content.Store) *ImageService {
   197  	snapshotter := &testSnapshotterService{}
   198  
   199  	mdb := newTestDB(ctx, t)
   200  
   201  	snapshotters := map[string]snapshots.Snapshotter{
   202  		containerd.DefaultSnapshotter: snapshotter,
   203  	}
   204  
   205  	service := &ImageService{
   206  		images:              metadata.NewImageStore(mdb),
   207  		containers:          emptyTestContainerStore(),
   208  		content:             cs,
   209  		eventsService:       daemonevents.New(),
   210  		snapshotterServices: snapshotters,
   211  		snapshotter:         containerd.DefaultSnapshotter,
   212  	}
   213  
   214  	// containerd.Image gets the services directly from containerd.Client
   215  	// so we need to create a "fake" containerd.Client with the test services.
   216  	c8dCli, err := containerd.New("", containerd.WithServices(
   217  		containerd.WithImageStore(service.images),
   218  		containerd.WithContentStore(cs),
   219  		containerd.WithSnapshotters(snapshotters),
   220  	))
   221  	assert.NilError(t, err)
   222  
   223  	service.client = c8dCli
   224  	return service
   225  }
   226  
   227  type blobsDirContentStore struct {
   228  	blobs string
   229  }
   230  
   231  type fileReaderAt struct {
   232  	*os.File
   233  }
   234  
   235  func (f *fileReaderAt) Size() int64 {
   236  	fi, err := f.Stat()
   237  	if err != nil {
   238  		return -1
   239  	}
   240  	return fi.Size()
   241  }
   242  
   243  func (s *blobsDirContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   244  	p := filepath.Join(s.blobs, desc.Digest.Encoded())
   245  	r, err := os.Open(p)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	return &fileReaderAt{r}, nil
   250  }
   251  
   252  func (s *blobsDirContentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   253  	return nil, fmt.Errorf("read-only")
   254  }
   255  
   256  func (s *blobsDirContentStore) Status(ctx context.Context, _ string) (content.Status, error) {
   257  	return content.Status{}, fmt.Errorf("not implemented")
   258  }
   259  
   260  func (s *blobsDirContentStore) Delete(ctx context.Context, dgst digest.Digest) error {
   261  	return fmt.Errorf("read-only")
   262  }
   263  
   264  func (s *blobsDirContentStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   265  	return nil, nil
   266  }
   267  
   268  func (s *blobsDirContentStore) Abort(ctx context.Context, ref string) error {
   269  	return fmt.Errorf("not implemented")
   270  }
   271  
   272  func (s *blobsDirContentStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   273  	entries, err := os.ReadDir(s.blobs)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	for _, e := range entries {
   279  		if e.IsDir() {
   280  			continue
   281  		}
   282  
   283  		d := digest.FromString(e.Name())
   284  		if d == "" {
   285  			continue
   286  		}
   287  
   288  		stat, err := e.Info()
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		if err := fn(content.Info{Digest: d, Size: stat.Size()}); err != nil {
   294  			return err
   295  		}
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  func (s *blobsDirContentStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   302  	f, err := os.Open(filepath.Join(s.blobs, dgst.Encoded()))
   303  	if err != nil {
   304  		return content.Info{}, err
   305  	}
   306  	defer f.Close()
   307  
   308  	stat, err := f.Stat()
   309  	if err != nil {
   310  		return content.Info{}, err
   311  	}
   312  
   313  	return content.Info{
   314  		Digest: dgst,
   315  		Size:   stat.Size(),
   316  	}, nil
   317  }
   318  
   319  func (s *blobsDirContentStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   320  	return content.Info{}, fmt.Errorf("read-only")
   321  }
   322  
   323  // delayedStore is a content store wrapper that adds a constant delay to all
   324  // operations in order to imitate gRPC overhead.
   325  //
   326  // The delay is constant to make the benchmark results more reproducible
   327  // Since content store may be accessed concurrently random delay would be
   328  // order-dependent.
   329  type delayedStore struct {
   330  	store    content.Store
   331  	overhead time.Duration
   332  }
   333  
   334  func (s *delayedStore) delay() {
   335  	time.Sleep(s.overhead)
   336  }
   337  
   338  func (s *delayedStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   339  	s.delay()
   340  	return s.store.ReaderAt(ctx, desc)
   341  }
   342  
   343  func (s *delayedStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   344  	s.delay()
   345  	return s.store.Writer(ctx, opts...)
   346  }
   347  
   348  func (s *delayedStore) Status(ctx context.Context, st string) (content.Status, error) {
   349  	s.delay()
   350  	return s.store.Status(ctx, st)
   351  }
   352  
   353  func (s *delayedStore) Delete(ctx context.Context, dgst digest.Digest) error {
   354  	s.delay()
   355  	return s.store.Delete(ctx, dgst)
   356  }
   357  
   358  func (s *delayedStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   359  	s.delay()
   360  	return s.store.ListStatuses(ctx, filters...)
   361  }
   362  
   363  func (s *delayedStore) Abort(ctx context.Context, ref string) error {
   364  	s.delay()
   365  	return s.store.Abort(ctx, ref)
   366  }
   367  
   368  func (s *delayedStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   369  	s.delay()
   370  	return s.store.Walk(ctx, fn, filters...)
   371  }
   372  
   373  func (s *delayedStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   374  	s.delay()
   375  	return s.store.Info(ctx, dgst)
   376  }
   377  
   378  func (s *delayedStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   379  	s.delay()
   380  	return s.store.Update(ctx, info, fieldpaths...)
   381  }