containerd/pkg/transfer/image/imagestore_test.go
Derek McGowan 13bf5565eb
[transfer] update export to use image store references
Signed-off-by: Derek McGowan <derek@mcg.dev>
2023-03-02 11:14:32 -08:00

419 lines
10 KiB
Go

/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image
import (
"context"
"errors"
"sort"
"sync"
"testing"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestStore(t *testing.T) {
for _, testCase := range []struct {
Name string
ImageStore *Store
// Annotations are the different references annotations to run the test with,
// the possible values:
// - "OCI": Uses the OCI defined annotation "org.opencontainers.image.ref.name"
// This annotation may be a full reference or tag only
// - "containerd": Uses the containerd defined annotation "io.containerd.image.name"
// This annotation is always a full reference as used by containerd
// - "Annotation": Sets the annotation flag but does not set a reference annotation
// Use this case to test the default where no reference is provided
// - "NoAnnotation": Does not set the annotation flag
// Use this case to test storing of the index images by reference
Annotations []string
ImageName string
Images []string
Err error
}{
{
Name: "Prefix",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/image:latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "Overwrite",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "placeholder",
IsPrefix: true,
AllowOverwrite: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/image:latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "TagOnly",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "AddDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
},
},
},
Annotations: []string{"Annotation"},
Images: []string{"registry.test/base@"},
},
{
Name: "NameAndDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/base:latest", "registry.test/base@"},
},
{
Name: "NameSkipDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
SkipNamedDigest: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/base:latest"},
},
{
Name: "OverwriteNameDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "base-name",
IsPrefix: true,
AllowOverwrite: true,
AddDigest: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Images: []string{"registry.test/base:latest", "base-name@"},
},
{
Name: "OverwriteNameSkipDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "base-name",
IsPrefix: true,
AllowOverwrite: true,
AddDigest: true,
SkipNamedDigest: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Images: []string{"registry.test/base:latest"},
},
{
Name: "ReferenceNotFound",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Err: errdefs.ErrNotFound,
},
{
Name: "NoReference",
ImageStore: &Store{},
Annotations: []string{"Annotation", "NoAnnotation"},
Err: errdefs.ErrNotFound,
},
{
Name: "ImageName",
ImageStore: &Store{
imageName: "registry.test/index:latest",
},
Annotations: []string{"NoAnnotation"},
Images: []string{"registry.test/index:latest"},
},
} {
testCase := testCase
for _, a := range testCase.Annotations {
name := testCase.Name + "_" + a
dgst := digest.Canonical.FromString(name)
desc := ocispec.Descriptor{
Digest: dgst,
Annotations: map[string]string{},
}
expected := make([]string, len(testCase.Images))
for i, img := range testCase.Images {
if img[len(img)-1] == '@' {
img = img + dgst.String()
}
expected[i] = img
}
switch a {
case "containerd":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
desc.Annotations[images.AnnotationImageName] = testCase.ImageName
case "OCI":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
desc.Annotations[ocispec.AnnotationRefName] = testCase.ImageName
case "Annotation":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
}
t.Run(name, func(t *testing.T) {
imgs, err := testCase.ImageStore.Store(context.Background(), desc, newSimpleImageStore())
if err != nil {
if testCase.Err == nil {
t.Fatal(err)
}
if !errors.Is(err, testCase.Err) {
t.Fatalf("unexpected error %v: expeceted %v", err, testCase.Err)
}
return
} else if testCase.Err != nil {
t.Fatalf("succeeded but expected error: %v", testCase.Err)
}
if len(imgs) != len(expected) {
t.Fatalf("mismatched array length\nexpected:\n\t%v\nactual\n\t%v", expected, imgs)
}
for i, name := range expected {
if imgs[i].Name != name {
t.Fatalf("wrong image name %q, expected %q", imgs[i].Name, name)
}
if imgs[i].Target.Digest != dgst {
t.Fatalf("wrong image digest %s, expected %s", imgs[i].Target.Digest, dgst)
}
}
})
}
}
}
func TestLookup(t *testing.T) {
ctx := context.Background()
is := newSimpleImageStore()
for _, name := range []string{
"registry.io/test1:latest",
"registry.io/test1:v1",
} {
is.Create(ctx, images.Image{
Name: name,
})
}
for _, testCase := range []struct {
Name string
ImageStore *Store
Expected []string
Err error
}{
{
Name: "SingleImage",
ImageStore: &Store{
imageName: "registry.io/test1:latest",
},
Expected: []string{"registry.io/test1:latest"},
},
{
Name: "MultipleReferences",
ImageStore: &Store{
imageName: "registry.io/test1:latest",
extraReferences: []Reference{
{
Name: "registry.io/test1:v1",
},
},
},
Expected: []string{"registry.io/test1:latest", "registry.io/test1:v1"},
},
{
Name: "OnlyReferences",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.io/test1:latest",
},
{
Name: "registry.io/test1:v1",
},
},
},
Expected: []string{"registry.io/test1:latest", "registry.io/test1:v1"},
},
{
Name: "UnsupportedPrefix",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.io/test1:latest",
IsPrefix: true,
},
},
},
Err: errdefs.ErrNotImplemented,
},
} {
t.Run(testCase.Name, func(t *testing.T) {
images, err := testCase.ImageStore.Lookup(ctx, is)
if err != nil {
if !errors.Is(err, testCase.Err) {
t.Errorf("unexpected error %v, expected %v", err, testCase.Err)
}
return
} else if testCase.Err != nil {
t.Fatal("expected error")
}
imageNames := make([]string, len(images))
for i, img := range images {
imageNames[i] = img.Name
}
sort.Strings(imageNames)
sort.Strings(testCase.Expected)
if len(images) != len(testCase.Expected) {
t.Fatalf("unexpected images:\n\t%v\nexpected:\n\t%v", imageNames, testCase.Expected)
}
for i := range imageNames {
if imageNames[i] != testCase.Expected[i] {
t.Fatalf("unexpected images:\n\t%v\nexpected:\n\t%v", imageNames, testCase.Expected)
}
}
})
}
}
// simpleImageStore is for testing images in memory,
// no filter support
type simpleImageStore struct {
l sync.Mutex
images map[string]images.Image
}
func newSimpleImageStore() images.Store {
return &simpleImageStore{
images: make(map[string]images.Image),
}
}
func (is *simpleImageStore) Get(ctx context.Context, name string) (images.Image, error) {
is.l.Lock()
defer is.l.Unlock()
img, ok := is.images[name]
if !ok {
return images.Image{}, errdefs.ErrNotFound
}
return img, nil
}
func (is *simpleImageStore) List(ctx context.Context, filters ...string) ([]images.Image, error) {
is.l.Lock()
defer is.l.Unlock()
var imgs []images.Image
// filters not supported, return all
for _, img := range is.images {
imgs = append(imgs, img)
}
return imgs, nil
}
func (is *simpleImageStore) Create(ctx context.Context, image images.Image) (images.Image, error) {
is.l.Lock()
defer is.l.Unlock()
if _, ok := is.images[image.Name]; ok {
return images.Image{}, errdefs.ErrAlreadyExists
}
is.images[image.Name] = image
return image, nil
}
func (is *simpleImageStore) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
is.l.Lock()
defer is.l.Unlock()
if _, ok := is.images[image.Name]; !ok {
return images.Image{}, errdefs.ErrNotFound
}
// fieldpaths no supported, update entire image
is.images[image.Name] = image
return image, nil
}
func (is *simpleImageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
is.l.Lock()
defer is.l.Unlock()
if _, ok := is.images[name]; !ok {
return errdefs.ErrNotFound
}
delete(is.images, name)
return nil
}