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 }