Kaynağa Gözat

c8d/list: Add TestImageList

Add unit test for `Images` implementation.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Gronowski 1 yıl önce
ebeveyn
işleme
582de4bc3c

+ 243 - 0
daemon/containerd/image_list_test.go

@@ -0,0 +1,243 @@
+package containerd
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"testing"
+
+	"github.com/containerd/containerd"
+	"github.com/containerd/containerd/content"
+	"github.com/containerd/containerd/images"
+	"github.com/containerd/containerd/metadata"
+	"github.com/containerd/containerd/namespaces"
+	"github.com/containerd/containerd/snapshots"
+	"github.com/containerd/log/logtest"
+	imagetypes "github.com/docker/docker/api/types/image"
+	daemonevents "github.com/docker/docker/daemon/events"
+	"github.com/docker/docker/internal/testutils/specialimage"
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"gotest.tools/v3/assert"
+	is "gotest.tools/v3/assert/cmp"
+)
+
+func imagesFromIndex(index ...*ocispec.Index) []images.Image {
+	var imgs []images.Image
+	for _, idx := range index {
+		for _, desc := range idx.Manifests {
+			imgs = append(imgs, images.Image{
+				Name:   desc.Annotations["io.containerd.image.name"],
+				Target: desc,
+			})
+		}
+	}
+	return imgs
+}
+
+func TestImageList(t *testing.T) {
+	ctx := namespaces.WithNamespace(context.TODO(), "testing")
+
+	blobsDir := t.TempDir()
+
+	multilayer, err := specialimage.MultiLayer(blobsDir)
+	assert.NilError(t, err)
+
+	twoplatform, err := specialimage.TwoPlatform(blobsDir)
+	assert.NilError(t, err)
+
+	cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")}
+
+	snapshotter := &testSnapshotterService{}
+
+	for _, tc := range []struct {
+		name   string
+		images []images.Image
+		opts   imagetypes.ListOptions
+
+		check func(*testing.T, []*imagetypes.Summary) // Change the type of the check function
+	}{
+		{
+			name:   "one multi-layer image",
+			images: imagesFromIndex(multilayer),
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
+				assert.Check(t, is.Len(all, 1))
+
+				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
+			},
+		},
+		{
+			name:   "one image with two platforms is still one entry",
+			images: imagesFromIndex(twoplatform),
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
+				assert.Check(t, is.Len(all, 1))
+
+				assert.Check(t, is.Equal(all[0].ID, twoplatform.Manifests[0].Digest.String()))
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"twoplatform:latest"}))
+			},
+		},
+		{
+			name:   "two images are two entries",
+			images: imagesFromIndex(multilayer, twoplatform),
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
+				assert.Check(t, is.Len(all, 2))
+
+				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
+
+				assert.Check(t, is.Equal(all[1].ID, twoplatform.Manifests[0].Digest.String()))
+				assert.Check(t, is.DeepEqual(all[1].RepoTags, []string{"twoplatform:latest"}))
+			},
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := logtest.WithT(ctx, t)
+			mdb := newTestDB(ctx, t)
+
+			snapshotters := map[string]snapshots.Snapshotter{
+				containerd.DefaultSnapshotter: snapshotter,
+			}
+
+			service := &ImageService{
+				images:              metadata.NewImageStore(mdb),
+				containers:          emptyTestContainerStore(),
+				content:             cs,
+				eventsService:       daemonevents.New(),
+				snapshotterServices: snapshotters,
+				snapshotter:         containerd.DefaultSnapshotter,
+			}
+
+			// containerd.Image gets the services directly from containerd.Client
+			// so we need to create a "fake" containerd.Client with the test services.
+			c8dCli, err := containerd.New("", containerd.WithServices(
+				containerd.WithImageStore(service.images),
+				containerd.WithContentStore(cs),
+				containerd.WithSnapshotters(snapshotters),
+			))
+			assert.NilError(t, err)
+
+			service.client = c8dCli
+
+			for _, img := range tc.images {
+				_, err := service.images.Create(ctx, img)
+				assert.NilError(t, err)
+			}
+
+			all, err := service.Images(ctx, tc.opts)
+			assert.NilError(t, err)
+
+			sort.Slice(all, func(i, j int) bool {
+				firstTag := func(idx int) string {
+					if len(all[idx].RepoTags) > 0 {
+						return all[idx].RepoTags[0]
+					}
+					return ""
+				}
+				return firstTag(i) < firstTag(j)
+			})
+
+			tc.check(t, all)
+		})
+	}
+
+}
+
+type blobsDirContentStore struct {
+	blobs string
+}
+
+type fileReaderAt struct {
+	*os.File
+}
+
+func (f *fileReaderAt) Size() int64 {
+	fi, err := f.Stat()
+	if err != nil {
+		return -1
+	}
+	return fi.Size()
+}
+
+func (s *blobsDirContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
+	p := filepath.Join(s.blobs, desc.Digest.Encoded())
+	r, err := os.Open(p)
+	if err != nil {
+		return nil, err
+	}
+	return &fileReaderAt{r}, nil
+}
+
+func (s *blobsDirContentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
+	return nil, fmt.Errorf("read-only")
+}
+
+func (s *blobsDirContentStore) Status(ctx context.Context, _ string) (content.Status, error) {
+	return content.Status{}, fmt.Errorf("not implemented")
+}
+
+func (s *blobsDirContentStore) Delete(ctx context.Context, dgst digest.Digest) error {
+	return fmt.Errorf("read-only")
+}
+
+func (s *blobsDirContentStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
+	return nil, nil
+}
+
+func (s *blobsDirContentStore) Abort(ctx context.Context, ref string) error {
+	return fmt.Errorf("not implemented")
+}
+
+func (s *blobsDirContentStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
+	entries, err := os.ReadDir(s.blobs)
+	if err != nil {
+		return err
+	}
+
+	for _, e := range entries {
+		if e.IsDir() {
+			continue
+		}
+
+		d := digest.FromString(e.Name())
+		if d == "" {
+			continue
+		}
+
+		stat, err := e.Info()
+		if err != nil {
+			return err
+		}
+
+		if err := fn(content.Info{Digest: d, Size: stat.Size()}); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *blobsDirContentStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
+	f, err := os.Open(filepath.Join(s.blobs, dgst.Encoded()))
+	if err != nil {
+		return content.Info{}, err
+	}
+	defer f.Close()
+
+	stat, err := f.Stat()
+	if err != nil {
+		return content.Info{}, err
+	}
+
+	return content.Info{
+		Digest: dgst,
+		Size:   stat.Size(),
+	}, nil
+}
+
+func (s *blobsDirContentStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
+	return content.Info{}, fmt.Errorf("read-only")
+}

+ 13 - 0
daemon/containerd/image_test.go

@@ -10,6 +10,7 @@ import (
 	"github.com/containerd/containerd/images"
 	"github.com/containerd/containerd/images"
 	"github.com/containerd/containerd/metadata"
 	"github.com/containerd/containerd/metadata"
 	"github.com/containerd/containerd/namespaces"
 	"github.com/containerd/containerd/namespaces"
+	"github.com/containerd/containerd/snapshots"
 	"github.com/containerd/log/logtest"
 	"github.com/containerd/log/logtest"
 	"github.com/distribution/reference"
 	"github.com/distribution/reference"
 	dockerimages "github.com/docker/docker/daemon/images"
 	dockerimages "github.com/docker/docker/daemon/images"
@@ -296,3 +297,15 @@ func newTestDB(ctx context.Context, t *testing.T) *metadata.DB {
 
 
 	return mdb
 	return mdb
 }
 }
+
+type testSnapshotterService struct {
+	snapshots.Snapshotter
+}
+
+func (s *testSnapshotterService) Stat(ctx context.Context, key string) (snapshots.Info, error) {
+	return snapshots.Info{}, nil
+}
+
+func (s *testSnapshotterService) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
+	return snapshots.Usage{}, nil
+}

+ 117 - 0
internal/testutils/specialimage/twoplatform.go

@@ -0,0 +1,117 @@
+package specialimage
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/containerd/containerd/platforms"
+	"github.com/distribution/reference"
+	"github.com/opencontainers/go-digest"
+	"github.com/opencontainers/image-spec/specs-go"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+func TwoPlatform(dir string) (*ocispec.Index, error) {
+	const imageRef = "twoplatform:latest"
+
+	layer1Desc, err := writeLayerWithOneFile(dir, "bash", []byte("layer1"))
+	if err != nil {
+		return nil, err
+	}
+	layer2Desc, err := writeLayerWithOneFile(dir, "bash", []byte("layer2"))
+	if err != nil {
+		return nil, err
+	}
+
+	config1Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
+		Platform: platforms.MustParse("linux/amd64"),
+		Config: ocispec.ImageConfig{
+			Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
+		},
+		RootFS: ocispec.RootFS{
+			Type:    "layers",
+			DiffIDs: []digest.Digest{layer1Desc.Digest},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	manifest1Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{
+		MediaType: ocispec.MediaTypeImageManifest,
+		Config:    config1Desc,
+		Layers:    []ocispec.Descriptor{layer1Desc},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	config2Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
+		Platform: platforms.MustParse("linux/arm64"),
+		Config: ocispec.ImageConfig{
+			Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
+		},
+		RootFS: ocispec.RootFS{
+			Type:    "layers",
+			DiffIDs: []digest.Digest{layer1Desc.Digest},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	manifest2Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{
+		MediaType: ocispec.MediaTypeImageManifest,
+		Config:    config2Desc,
+		Layers:    []ocispec.Descriptor{layer2Desc},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	index := ocispec.Index{
+		Versioned: specs.Versioned{SchemaVersion: 2},
+		MediaType: ocispec.MediaTypeImageIndex,
+		Manifests: []ocispec.Descriptor{manifest1Desc, manifest2Desc},
+	}
+
+	ref, err := reference.ParseNormalizedNamed(imageRef)
+	if err != nil {
+		return nil, err
+	}
+	return multiPlatformImage(dir, ref, index)
+}
+
+func multiPlatformImage(dir string, ref reference.Named, target ocispec.Index) (*ocispec.Index, error) {
+	targetDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageIndex, target)
+	if err != nil {
+		return nil, err
+	}
+
+	if ref != nil {
+		targetDesc.Annotations = map[string]string{
+			"io.containerd.image.name": ref.String(),
+		}
+
+		if tagged, ok := ref.(reference.Tagged); ok {
+			targetDesc.Annotations[ocispec.AnnotationRefName] = tagged.Tag()
+		}
+	}
+
+	index := ocispec.Index{
+		Versioned: specs.Versioned{SchemaVersion: 2},
+		MediaType: ocispec.MediaTypeImageIndex,
+		Manifests: []ocispec.Descriptor{targetDesc},
+	}
+
+	if err := writeJson(index, filepath.Join(dir, "index.json")); err != nil {
+		return nil, err
+	}
+
+	err = os.WriteFile(filepath.Join(dir, "oci-layout"), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0o644)
+	if err != nil {
+		return nil, err
+	}
+
+	return &index, nil
+}