c8d/list: Add TestImageList
Add unit test for `Images` implementation. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
parent
a6e7e67d3a
commit
582de4bc3c
3 changed files with 373 additions and 0 deletions
243
daemon/containerd/image_list_test.go
Normal file
243
daemon/containerd/image_list_test.go
Normal 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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
117
internal/testutils/specialimage/twoplatform.go
Normal file
117
internal/testutils/specialimage/twoplatform.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue