419 lines
10 KiB
Go
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
|
|
}
|