github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+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  	cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")}
   105  
   106  	for _, tc := range []struct {
   107  		name   string
   108  		images []images.Image
   109  		opts   imagetypes.ListOptions
   110  
   111  		check func(*testing.T, []*imagetypes.Summary)
   112  	}{
   113  		{
   114  			name:   "one multi-layer image",
   115  			images: imagesFromIndex(multilayer),
   116  			check: func(t *testing.T, all []*imagetypes.Summary) {
   117  				assert.Check(t, is.Len(all, 1))
   118  
   119  				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
   120  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
   121  			},
   122  		},
   123  		{
   124  			name:   "one image with two platforms is still one entry",
   125  			images: imagesFromIndex(twoplatform),
   126  			check: func(t *testing.T, all []*imagetypes.Summary) {
   127  				assert.Check(t, is.Len(all, 1))
   128  
   129  				assert.Check(t, is.Equal(all[0].ID, twoplatform.Manifests[0].Digest.String()))
   130  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"twoplatform:latest"}))
   131  			},
   132  		},
   133  		{
   134  			name:   "two images are two entries",
   135  			images: imagesFromIndex(multilayer, twoplatform),
   136  			check: func(t *testing.T, all []*imagetypes.Summary) {
   137  				assert.Check(t, is.Len(all, 2))
   138  
   139  				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
   140  				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
   141  
   142  				assert.Check(t, is.Equal(all[1].ID, twoplatform.Manifests[0].Digest.String()))
   143  				assert.Check(t, is.DeepEqual(all[1].RepoTags, []string{"twoplatform:latest"}))
   144  			},
   145  		},
   146  		{
   147  			name:   "three images, one is an empty index",
   148  			images: imagesFromIndex(multilayer, emptyIndex, twoplatform),
   149  			check: func(t *testing.T, all []*imagetypes.Summary) {
   150  				assert.Check(t, is.Len(all, 2))
   151  			},
   152  		},
   153  	} {
   154  		tc := tc
   155  		t.Run(tc.name, func(t *testing.T) {
   156  			ctx := logtest.WithT(ctx, t)
   157  			service := fakeImageService(t, ctx, cs)
   158  
   159  			for _, img := range tc.images {
   160  				_, err := service.images.Create(ctx, img)
   161  				assert.NilError(t, err)
   162  			}
   163  
   164  			all, err := service.Images(ctx, tc.opts)
   165  			assert.NilError(t, err)
   166  
   167  			sort.Slice(all, func(i, j int) bool {
   168  				firstTag := func(idx int) string {
   169  					if len(all[idx].RepoTags) > 0 {
   170  						return all[idx].RepoTags[0]
   171  					}
   172  					return ""
   173  				}
   174  				return firstTag(i) < firstTag(j)
   175  			})
   176  
   177  			tc.check(t, all)
   178  		})
   179  	}
   180  
   181  }
   182  
   183  func fakeImageService(t testing.TB, ctx context.Context, cs content.Store) *ImageService {
   184  	snapshotter := &testSnapshotterService{}
   185  
   186  	mdb := newTestDB(ctx, t)
   187  
   188  	snapshotters := map[string]snapshots.Snapshotter{
   189  		containerd.DefaultSnapshotter: snapshotter,
   190  	}
   191  
   192  	service := &ImageService{
   193  		images:              metadata.NewImageStore(mdb),
   194  		containers:          emptyTestContainerStore(),
   195  		content:             cs,
   196  		eventsService:       daemonevents.New(),
   197  		snapshotterServices: snapshotters,
   198  		snapshotter:         containerd.DefaultSnapshotter,
   199  	}
   200  
   201  	// containerd.Image gets the services directly from containerd.Client
   202  	// so we need to create a "fake" containerd.Client with the test services.
   203  	c8dCli, err := containerd.New("", containerd.WithServices(
   204  		containerd.WithImageStore(service.images),
   205  		containerd.WithContentStore(cs),
   206  		containerd.WithSnapshotters(snapshotters),
   207  	))
   208  	assert.NilError(t, err)
   209  
   210  	service.client = c8dCli
   211  	return service
   212  }
   213  
   214  type blobsDirContentStore struct {
   215  	blobs string
   216  }
   217  
   218  type fileReaderAt struct {
   219  	*os.File
   220  }
   221  
   222  func (f *fileReaderAt) Size() int64 {
   223  	fi, err := f.Stat()
   224  	if err != nil {
   225  		return -1
   226  	}
   227  	return fi.Size()
   228  }
   229  
   230  func (s *blobsDirContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   231  	p := filepath.Join(s.blobs, desc.Digest.Encoded())
   232  	r, err := os.Open(p)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	return &fileReaderAt{r}, nil
   237  }
   238  
   239  func (s *blobsDirContentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   240  	return nil, fmt.Errorf("read-only")
   241  }
   242  
   243  func (s *blobsDirContentStore) Status(ctx context.Context, _ string) (content.Status, error) {
   244  	return content.Status{}, fmt.Errorf("not implemented")
   245  }
   246  
   247  func (s *blobsDirContentStore) Delete(ctx context.Context, dgst digest.Digest) error {
   248  	return fmt.Errorf("read-only")
   249  }
   250  
   251  func (s *blobsDirContentStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   252  	return nil, nil
   253  }
   254  
   255  func (s *blobsDirContentStore) Abort(ctx context.Context, ref string) error {
   256  	return fmt.Errorf("not implemented")
   257  }
   258  
   259  func (s *blobsDirContentStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   260  	entries, err := os.ReadDir(s.blobs)
   261  	if err != nil {
   262  		return err
   263  	}
   264  
   265  	for _, e := range entries {
   266  		if e.IsDir() {
   267  			continue
   268  		}
   269  
   270  		d := digest.FromString(e.Name())
   271  		if d == "" {
   272  			continue
   273  		}
   274  
   275  		stat, err := e.Info()
   276  		if err != nil {
   277  			return err
   278  		}
   279  
   280  		if err := fn(content.Info{Digest: d, Size: stat.Size()}); err != nil {
   281  			return err
   282  		}
   283  	}
   284  
   285  	return nil
   286  }
   287  
   288  func (s *blobsDirContentStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   289  	f, err := os.Open(filepath.Join(s.blobs, dgst.Encoded()))
   290  	if err != nil {
   291  		return content.Info{}, err
   292  	}
   293  	defer f.Close()
   294  
   295  	stat, err := f.Stat()
   296  	if err != nil {
   297  		return content.Info{}, err
   298  	}
   299  
   300  	return content.Info{
   301  		Digest: dgst,
   302  		Size:   stat.Size(),
   303  	}, nil
   304  }
   305  
   306  func (s *blobsDirContentStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   307  	return content.Info{}, fmt.Errorf("read-only")
   308  }
   309  
   310  // delayedStore is a content store wrapper that adds a constant delay to all
   311  // operations in order to imitate gRPC overhead.
   312  //
   313  // The delay is constant to make the benchmark results more reproducible
   314  // Since content store may be accessed concurrently random delay would be
   315  // order-dependent.
   316  type delayedStore struct {
   317  	store    content.Store
   318  	overhead time.Duration
   319  }
   320  
   321  func (s *delayedStore) delay() {
   322  	time.Sleep(s.overhead)
   323  }
   324  
   325  func (s *delayedStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   326  	s.delay()
   327  	return s.store.ReaderAt(ctx, desc)
   328  }
   329  
   330  func (s *delayedStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   331  	s.delay()
   332  	return s.store.Writer(ctx, opts...)
   333  }
   334  
   335  func (s *delayedStore) Status(ctx context.Context, st string) (content.Status, error) {
   336  	s.delay()
   337  	return s.store.Status(ctx, st)
   338  }
   339  
   340  func (s *delayedStore) Delete(ctx context.Context, dgst digest.Digest) error {
   341  	s.delay()
   342  	return s.store.Delete(ctx, dgst)
   343  }
   344  
   345  func (s *delayedStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   346  	s.delay()
   347  	return s.store.ListStatuses(ctx, filters...)
   348  }
   349  
   350  func (s *delayedStore) Abort(ctx context.Context, ref string) error {
   351  	s.delay()
   352  	return s.store.Abort(ctx, ref)
   353  }
   354  
   355  func (s *delayedStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   356  	s.delay()
   357  	return s.store.Walk(ctx, fn, filters...)
   358  }
   359  
   360  func (s *delayedStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   361  	s.delay()
   362  	return s.store.Info(ctx, dgst)
   363  }
   364  
   365  func (s *delayedStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   366  	s.delay()
   367  	return s.store.Update(ctx, info, fieldpaths...)
   368  }