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 }