c8d/list: Add TestImageList

Add unit test for `Images` implementation.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2024-02-26 17:43:41 +01:00
parent a6e7e67d3a
commit 582de4bc3c
No known key found for this signature in database
GPG key ID: B85EFCFE26DEF92A
3 changed files with 373 additions and 0 deletions

View file

@ -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")
}

View file

@ -10,6 +10,7 @@ import (
"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"
"github.com/distribution/reference"
dockerimages "github.com/docker/docker/daemon/images"
@ -296,3 +297,15 @@ func newTestDB(ctx context.Context, t *testing.T) *metadata.DB {
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
}

View file

@ -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
}