integration/import-export: Add WithSkipMissing tests
Also refactor tests to use the t.Run and run each test concurrently in a separate namespace. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
parent
b9af453f0c
commit
0d3c3efe3b
@ -19,47 +19,97 @@ package client
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/containerd/containerd/v2/client"
|
. "github.com/containerd/containerd/v2/client"
|
||||||
"github.com/containerd/containerd/v2/content"
|
"github.com/containerd/containerd/v2/content"
|
||||||
|
"github.com/containerd/containerd/v2/errdefs"
|
||||||
"github.com/containerd/containerd/v2/images"
|
"github.com/containerd/containerd/v2/images"
|
||||||
"github.com/containerd/containerd/v2/images/archive"
|
"github.com/containerd/containerd/v2/images/archive"
|
||||||
|
"github.com/containerd/containerd/v2/namespaces"
|
||||||
"github.com/containerd/containerd/v2/platforms"
|
"github.com/containerd/containerd/v2/platforms"
|
||||||
|
"github.com/google/uuid"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestExport exports testImage as a tar stream
|
func TestExportAllCases(t *testing.T) {
|
||||||
func TestExport(t *testing.T) {
|
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
ctx, cancel := testContext(t)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := New(address)
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
prepare func(context.Context, *testing.T, *Client) images.Image
|
||||||
|
check func(context.Context, *testing.T, *Client, *os.File, images.Image)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "export all platforms without SkipMissing",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("skipping test on windows - the testimage index has only one platform")
|
||||||
|
}
|
||||||
|
img, err := client.Fetch(ctx, testImage, WithPlatform(platforms.DefaultString()), WithAllMetadata())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
return img
|
||||||
|
},
|
||||||
_, err = client.Fetch(ctx, testImage)
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, _ images.Image) {
|
||||||
|
err := client.Export(ctx, dstFile, archive.WithImage(client.ImageService(), testImage), archive.WithPlatform(platforms.All))
|
||||||
|
if !errdefs.IsNotFound(err) {
|
||||||
|
t.Fatal("should fail with not found error")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export all platforms with SkipMissing",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("skipping test on windows - the testimage index has only one platform")
|
||||||
|
}
|
||||||
|
img, err := client.Fetch(ctx, testImage, WithPlatform(platforms.DefaultString()), WithAllMetadata())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
dstFile, err := os.CreateTemp("", "export-import-test")
|
return img
|
||||||
|
},
|
||||||
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, img images.Image) {
|
||||||
|
defaultPlatformManifest, err := getPlatformManifest(ctx, client.ContentStore(), img.Target, platforms.Default())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
err = client.Export(ctx, dstFile, archive.WithImage(client.ImageService(), testImage), archive.WithPlatform(platforms.All), archive.WithSkipMissing(client.ContentStore()))
|
||||||
dstFile.Close()
|
if err != nil {
|
||||||
os.Remove(dstFile.Name())
|
t.Fatal(err)
|
||||||
}()
|
}
|
||||||
|
dstFile.Seek(0, 0)
|
||||||
|
assertOCITar(t, dstFile, true)
|
||||||
|
|
||||||
err = client.Export(ctx, dstFile, archive.WithPlatform(platforms.Default()), archive.WithImage(client.ImageService(), testImage))
|
// Check if archive contains only one manifest for the default platform
|
||||||
|
if !isImageInArchive(ctx, t, client, dstFile, defaultPlatformManifest) {
|
||||||
|
t.Fatal("archive does not contain manifest for the default platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isImageInArchive(ctx, t, client, dstFile, img.Target) {
|
||||||
|
t.Fatal("archive shouldn't contain all platforms")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export full image",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
img, err := client.Fetch(ctx, testImage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
},
|
||||||
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, img images.Image) {
|
||||||
|
err := client.Export(ctx, dstFile, archive.WithImage(client.ImageService(), testImage), archive.WithPlatform(platforms.All), archive.WithImage(client.ImageService(), testImage))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -67,28 +117,141 @@ func TestExport(t *testing.T) {
|
|||||||
// Seek to beginning of file before passing it to assertOCITar()
|
// Seek to beginning of file before passing it to assertOCITar()
|
||||||
dstFile.Seek(0, 0)
|
dstFile.Seek(0, 0)
|
||||||
assertOCITar(t, dstFile, true)
|
assertOCITar(t, dstFile, true)
|
||||||
|
|
||||||
|
// Archive should contain all platforms.
|
||||||
|
if !isImageInArchive(ctx, t, client, dstFile, img.Target) {
|
||||||
|
t.Fatalf("archive does not contain all platforms")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export multi-platform with SkipDockerManifest",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
img, err := client.Fetch(ctx, testImage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
},
|
||||||
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, img images.Image) {
|
||||||
|
err := client.Export(ctx, dstFile, archive.WithImage(client.ImageService(), testImage), archive.WithManifest(img.Target), archive.WithSkipDockerManifest())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExportDockerManifest exports testImage as a tar stream, using the
|
// Seek to beginning of file before passing it to assertOCITar()
|
||||||
// WithSkipDockerManifest option
|
dstFile.Seek(0, 0)
|
||||||
func TestExportDockerManifest(t *testing.T) {
|
assertOCITar(t, dstFile, false)
|
||||||
if testing.Short() {
|
|
||||||
t.Skip()
|
if !isImageInArchive(ctx, t, client, dstFile, img.Target) {
|
||||||
|
t.Fatalf("archive does not contain expected platform")
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export single-platform with SkipDockerManifest",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
img, err := client.Fetch(ctx, testImage, WithPlatform(platforms.DefaultString()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
},
|
||||||
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, img images.Image) {
|
||||||
|
result, err := getPlatformManifest(ctx, client.ContentStore(), img.Target, platforms.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Export(ctx, dstFile, archive.WithManifest(result), archive.WithSkipDockerManifest())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to beginning of file before passing it to assertOCITar()
|
||||||
|
dstFile.Seek(0, 0)
|
||||||
|
assertOCITar(t, dstFile, false)
|
||||||
|
|
||||||
|
if !isImageInArchive(ctx, t, client, dstFile, result) {
|
||||||
|
t.Fatalf("archive does not contain expected platform")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export index only",
|
||||||
|
prepare: func(ctx context.Context, t *testing.T, client *Client) images.Image {
|
||||||
|
img, err := client.Fetch(ctx, testImage, WithPlatform(platforms.DefaultString()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var all []ocispec.Descriptor
|
||||||
|
err = images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
ch, err := images.Children(ctx, client.ContentStore(), desc)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
return nil, images.ErrSkipDesc
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, ch...)
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}), img.Target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, d := range all {
|
||||||
|
if images.IsIndexType(d.MediaType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := client.ContentStore().Delete(ctx, d.Digest); err != nil && !errdefs.IsNotFound(err) {
|
||||||
|
t.Fatalf("failed to delete %v: %v", d.Digest, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return img
|
||||||
|
},
|
||||||
|
check: func(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, img images.Image) {
|
||||||
|
err := client.Export(ctx, dstFile, archive.WithImage(client.ImageService(), testImage), archive.WithSkipMissing(client.ContentStore()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to beginning of file before passing it to assertOCITar()
|
||||||
|
dstFile.Seek(0, 0)
|
||||||
|
assertOCITar(t, dstFile, false)
|
||||||
|
|
||||||
|
defaultPlatformManifest, err := getPlatformManifest(ctx, client.ContentStore(), img.Target, platforms.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if archive contains only one manifest for the default platform
|
||||||
|
if isImageInArchive(ctx, t, client, dstFile, defaultPlatformManifest) {
|
||||||
|
t.Fatal("archive shouldn't contain manifest for the default platform")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
ctx, cancel := testContext(t)
|
ctx, cancel := testContext(t)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client, err := New(address)
|
namespace := uuid.New().String()
|
||||||
|
client, err := newClient(t, address, WithDefaultNamespace(namespace))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
_, err = client.Fetch(ctx, testImage)
|
ctx = namespaces.WithNamespace(ctx, namespace)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
img := tc.prepare(ctx, t, client)
|
||||||
}
|
|
||||||
dstFile, err := os.CreateTemp("", "export-import-test")
|
dstFile, err := os.CreateTemp("", "export-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -97,66 +260,74 @@ func TestExportDockerManifest(t *testing.T) {
|
|||||||
os.Remove(dstFile.Name())
|
os.Remove(dstFile.Name())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
img, err := client.ImageService().Get(ctx, testImage)
|
tc.check(ctx, t, client, dstFile, img)
|
||||||
if err != nil {
|
})
|
||||||
t.Fatal(err)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test multi-platform export
|
func isImageInArchive(ctx context.Context, t *testing.T, client *Client, dstFile *os.File, mfst ocispec.Descriptor) bool {
|
||||||
err = client.Export(ctx, dstFile, archive.WithManifest(img.Target), archive.WithSkipDockerManifest())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
dstFile.Seek(0, 0)
|
dstFile.Seek(0, 0)
|
||||||
assertOCITar(t, dstFile, false)
|
tr := tar.NewReader(dstFile)
|
||||||
|
|
||||||
// reset to beginning
|
var blobs []string
|
||||||
dstFile.Seek(0, 0)
|
for {
|
||||||
|
h, err := tr.Next()
|
||||||
// test single-platform export
|
|
||||||
var result ocispec.Descriptor
|
|
||||||
err = images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
||||||
if images.IsManifestType(desc.MediaType) {
|
|
||||||
p, err := content.ReadBlob(ctx, client.ContentStore(), desc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if err == io.EOF {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest ocispec.Manifest
|
|
||||||
if err := json.Unmarshal(p, &manifest); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if desc.Platform == nil || platforms.Default().Match(platforms.Normalize(*desc.Platform)) {
|
|
||||||
result = desc
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
} else if images.IsIndexType(desc.MediaType) {
|
|
||||||
p, err := content.ReadBlob(ctx, client.ContentStore(), desc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx ocispec.Index
|
|
||||||
if err := json.Unmarshal(p, &idx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return idx.Manifests, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}), img.Target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = client.Export(ctx, dstFile, archive.WithManifest(result), archive.WithSkipDockerManifest())
|
|
||||||
if err != nil {
|
digest := strings.TrimPrefix(h.Name, "blobs/sha256/")
|
||||||
t.Fatal(err)
|
if digest != h.Name && digest != "" {
|
||||||
|
blobs = append(blobs, digest)
|
||||||
}
|
}
|
||||||
dstFile.Seek(0, 0)
|
}
|
||||||
assertOCITar(t, dstFile, false)
|
|
||||||
|
allPresent := true
|
||||||
|
// Check if the archive contains all blobs referenced by the manifest.
|
||||||
|
images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
for _, b := range blobs {
|
||||||
|
if desc.Digest.Hex() == b {
|
||||||
|
return images.Children(ctx, client.ContentStore(), desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allPresent = false
|
||||||
|
return nil, images.ErrStopHandler
|
||||||
|
}), mfst)
|
||||||
|
|
||||||
|
return allPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlatformManifest(ctx context.Context, cs content.Store, target ocispec.Descriptor, platform platforms.MatchComparer) (ocispec.Descriptor, error) {
|
||||||
|
mfst, err := images.LimitManifests(images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
children, err := images.Children(ctx, cs, desc)
|
||||||
|
if !images.IsManifestType(desc.MediaType) {
|
||||||
|
return children, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
return nil, images.ErrSkipDesc
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return children, nil
|
||||||
|
}), platform, 1)(ctx, target)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
if len(mfst) == 0 {
|
||||||
|
return ocispec.Descriptor{}, errdefs.ErrNotFound
|
||||||
|
}
|
||||||
|
return mfst[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertOCITar(t *testing.T, r io.Reader, docker bool) {
|
func assertOCITar(t *testing.T, r io.Reader, docker bool) {
|
||||||
|
t.Helper()
|
||||||
// TODO: add more assertion
|
// TODO: add more assertion
|
||||||
tr := tar.NewReader(r)
|
tr := tar.NewReader(r)
|
||||||
foundOCILayout := false
|
foundOCILayout := false
|
||||||
@ -168,8 +339,7 @@ func assertOCITar(t *testing.T, r io.Reader, docker bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if h.Name == ocispec.ImageLayoutFile {
|
if h.Name == ocispec.ImageLayoutFile {
|
||||||
foundOCILayout = true
|
foundOCILayout = true
|
||||||
|
@ -35,14 +35,17 @@ import (
|
|||||||
"github.com/containerd/containerd/v2/archive/compression"
|
"github.com/containerd/containerd/v2/archive/compression"
|
||||||
"github.com/containerd/containerd/v2/archive/tartest"
|
"github.com/containerd/containerd/v2/archive/tartest"
|
||||||
. "github.com/containerd/containerd/v2/client"
|
. "github.com/containerd/containerd/v2/client"
|
||||||
|
"github.com/containerd/containerd/v2/content"
|
||||||
"github.com/containerd/containerd/v2/images"
|
"github.com/containerd/containerd/v2/images"
|
||||||
"github.com/containerd/containerd/v2/images/archive"
|
"github.com/containerd/containerd/v2/images/archive"
|
||||||
"github.com/containerd/containerd/v2/leases"
|
"github.com/containerd/containerd/v2/leases"
|
||||||
|
"github.com/containerd/containerd/v2/namespaces"
|
||||||
"github.com/containerd/containerd/v2/oci"
|
"github.com/containerd/containerd/v2/oci"
|
||||||
"github.com/containerd/containerd/v2/pkg/transfer"
|
"github.com/containerd/containerd/v2/pkg/transfer"
|
||||||
tarchive "github.com/containerd/containerd/v2/pkg/transfer/archive"
|
tarchive "github.com/containerd/containerd/v2/pkg/transfer/archive"
|
||||||
"github.com/containerd/containerd/v2/pkg/transfer/image"
|
"github.com/containerd/containerd/v2/pkg/transfer/image"
|
||||||
"github.com/containerd/containerd/v2/platforms"
|
"github.com/containerd/containerd/v2/platforms"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go"
|
specs "github.com/opencontainers/image-spec/specs-go"
|
||||||
@ -159,12 +162,6 @@ func TestImport(t *testing.T) {
|
|||||||
ctx, cancel := testContext(t)
|
ctx, cancel := testContext(t)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client, err := newClient(t, address)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
tc := tartest.TarContext{}
|
tc := tartest.TarContext{}
|
||||||
|
|
||||||
b1, d1 := createContent(256, 1)
|
b1, d1 := createContent(256, 1)
|
||||||
@ -176,9 +173,12 @@ func TestImport(t *testing.T) {
|
|||||||
|
|
||||||
m1, d3, expManifest := createManifest(c1, [][]byte{b1})
|
m1, d3, expManifest := createManifest(c1, [][]byte{b1})
|
||||||
|
|
||||||
provider := client.ContentStore()
|
c2, _ := createConfig(runtime.GOOS, runtime.GOARCH, "test2")
|
||||||
|
m2, d5, _ := createManifest(c2, [][]byte{{1, 2, 3, 4, 5}})
|
||||||
|
|
||||||
checkManifest := func(ctx context.Context, t *testing.T, d ocispec.Descriptor, expManifest *ocispec.Manifest) {
|
ml1, d6 := createManifestList(m1, m2)
|
||||||
|
|
||||||
|
checkManifest := func(ctx context.Context, t *testing.T, provider content.Provider, d ocispec.Descriptor, expManifest *ocispec.Manifest) {
|
||||||
m, err := images.Manifest(ctx, provider, d, nil)
|
m, err := images.Manifest(ctx, provider, d, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to read target blob: %+v", err)
|
t.Fatalf("unable to read target blob: %+v", err)
|
||||||
@ -209,9 +209,40 @@ func TestImport(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
Name string
|
Name string
|
||||||
Writer tartest.WriterToTar
|
Writer tartest.WriterToTar
|
||||||
Check func(*testing.T, []images.Image)
|
Check func(context.Context, *testing.T, *Client, []images.Image)
|
||||||
Opts []ImportOpt
|
Opts []ImportOpt
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
Name: "OCI-IndexWithoutAnyManifest",
|
||||||
|
Writer: tartest.TarAll(
|
||||||
|
tc.Dir(ocispec.ImageBlobsDir, 0755),
|
||||||
|
tc.Dir(ocispec.ImageBlobsDir+"/sha256", 0755),
|
||||||
|
tc.File(ocispec.ImageIndexFile, createIndex(ml1, ocispec.MediaTypeImageIndex, "docker.io/library/sparse:ok"), 0644),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d6.Encoded(), ml1, 0644),
|
||||||
|
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
||||||
|
),
|
||||||
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
|
checkImages(t, d6, imgs, "docker.io/library/sparse:ok")
|
||||||
|
mfsts, err := images.Children(ctx, client.ContentStore(), imgs[0].Target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range mfsts {
|
||||||
|
exists, err := content.Exists(ctx, client.ContentStore(), m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Fatal("no manifest should be imported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Opts: []ImportOpt{
|
||||||
|
WithSkipMissing(),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "DockerV2.0",
|
Name: "DockerV2.0",
|
||||||
Writer: tartest.TarAll(
|
Writer: tartest.TarAll(
|
||||||
@ -232,7 +263,7 @@ func TestImport(t *testing.T) {
|
|||||||
tc.File("e95212f7aa2cab51d0abd765cd43.json", c1, 0644),
|
tc.File("e95212f7aa2cab51d0abd765cd43.json", c1, 0644),
|
||||||
tc.File("manifest.json", []byte(`[{"Config":"e95212f7aa2cab51d0abd765cd43.json","RepoTags":["test-import:notlatest", "another/repo:tag"],"Layers":["bd765cd43e95212f7aa2cab51d0a/layer.tar"]}]`), 0644),
|
tc.File("manifest.json", []byte(`[{"Config":"e95212f7aa2cab51d0abd765cd43.json","RepoTags":["test-import:notlatest", "another/repo:tag"],"Layers":["bd765cd43e95212f7aa2cab51d0a/layer.tar"]}]`), 0644),
|
||||||
),
|
),
|
||||||
Check: func(t *testing.T, imgs []images.Image) {
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
if len(imgs) == 0 {
|
if len(imgs) == 0 {
|
||||||
t.Fatalf("no images")
|
t.Fatalf("no images")
|
||||||
}
|
}
|
||||||
@ -243,7 +274,7 @@ func TestImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkImages(t, imgs[0].Target.Digest, imgs, names...)
|
checkImages(t, imgs[0].Target.Digest, imgs, names...)
|
||||||
checkManifest(ctx, t, imgs[0].Target, nil)
|
checkManifest(ctx, t, client.ContentStore(), imgs[0].Target, nil)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -271,17 +302,17 @@ func TestImport(t *testing.T) {
|
|||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
||||||
tc.File(ocispec.ImageIndexFile, createIndex(m1, "latest", "docker.io/lib/img:ok"), 0644),
|
tc.File(ocispec.ImageIndexFile, createIndex(m1, ocispec.MediaTypeImageManifest, "latest", "docker.io/lib/img:ok"), 0644),
|
||||||
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
||||||
),
|
),
|
||||||
Check: func(t *testing.T, imgs []images.Image) {
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
names := []string{
|
names := []string{
|
||||||
"latest",
|
"latest",
|
||||||
"docker.io/lib/img:ok",
|
"docker.io/lib/img:ok",
|
||||||
}
|
}
|
||||||
|
|
||||||
checkImages(t, d3, imgs, names...)
|
checkImages(t, d3, imgs, names...)
|
||||||
checkManifest(ctx, t, imgs[0].Target, expManifest)
|
checkManifest(ctx, t, client.ContentStore(), imgs[0].Target, expManifest)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -292,17 +323,17 @@ func TestImport(t *testing.T) {
|
|||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
||||||
tc.File(ocispec.ImageIndexFile, createIndex(m1, "latest", "docker.io/lib/img:ok"), 0644),
|
tc.File(ocispec.ImageIndexFile, createIndex(m1, ocispec.MediaTypeImageManifest, "latest", "docker.io/lib/img:ok"), 0644),
|
||||||
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
||||||
),
|
),
|
||||||
Check: func(t *testing.T, imgs []images.Image) {
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
names := []string{
|
names := []string{
|
||||||
"localhost:5000/myimage:latest",
|
"localhost:5000/myimage:latest",
|
||||||
"docker.io/lib/img:ok",
|
"docker.io/lib/img:ok",
|
||||||
}
|
}
|
||||||
|
|
||||||
checkImages(t, d3, imgs, names...)
|
checkImages(t, d3, imgs, names...)
|
||||||
checkManifest(ctx, t, imgs[0].Target, expManifest)
|
checkManifest(ctx, t, client.ContentStore(), imgs[0].Target, expManifest)
|
||||||
},
|
},
|
||||||
Opts: []ImportOpt{
|
Opts: []ImportOpt{
|
||||||
WithImageRefTranslator(archive.AddRefPrefix("localhost:5000/myimage")),
|
WithImageRefTranslator(archive.AddRefPrefix("localhost:5000/myimage")),
|
||||||
@ -316,24 +347,94 @@ func TestImport(t *testing.T) {
|
|||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
||||||
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
||||||
tc.File(ocispec.ImageIndexFile, createIndex(m1, "latest", "localhost:5000/myimage:old", "docker.io/lib/img:ok"), 0644),
|
tc.File(ocispec.ImageIndexFile, createIndex(m1, ocispec.MediaTypeImageManifest, "latest", "localhost:5000/myimage:old", "docker.io/lib/img:ok"), 0644),
|
||||||
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
||||||
),
|
),
|
||||||
Check: func(t *testing.T, imgs []images.Image) {
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
names := []string{
|
names := []string{
|
||||||
"localhost:5000/myimage:latest",
|
"localhost:5000/myimage:latest",
|
||||||
"localhost:5000/myimage:old",
|
"localhost:5000/myimage:old",
|
||||||
}
|
}
|
||||||
|
|
||||||
checkImages(t, d3, imgs, names...)
|
checkImages(t, d3, imgs, names...)
|
||||||
checkManifest(ctx, t, imgs[0].Target, expManifest)
|
checkManifest(ctx, t, client.ContentStore(), imgs[0].Target, expManifest)
|
||||||
},
|
},
|
||||||
Opts: []ImportOpt{
|
Opts: []ImportOpt{
|
||||||
WithImageRefTranslator(archive.FilterRefPrefix("localhost:5000/myimage")),
|
WithImageRefTranslator(archive.FilterRefPrefix("localhost:5000/myimage")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "OCI-IndexWithMissingManifestDescendants",
|
||||||
|
Writer: tartest.TarAll(
|
||||||
|
tc.Dir(ocispec.ImageBlobsDir, 0755),
|
||||||
|
tc.Dir(ocispec.ImageBlobsDir+"/sha256", 0755),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d1.Encoded(), b1, 0644),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d2.Encoded(), c1, 0644),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d3.Encoded(), m1, 0644),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d5.Encoded(), m2, 0644),
|
||||||
|
tc.File(ocispec.ImageIndexFile, createIndex(ml1, ocispec.MediaTypeImageIndex, "docker.io/library/sparse:ok"), 0644),
|
||||||
|
tc.File(ocispec.ImageBlobsDir+"/sha256/"+d6.Encoded(), ml1, 0644),
|
||||||
|
tc.File(ocispec.ImageLayoutFile, []byte(`{"imageLayoutVersion":"`+ocispec.ImageLayoutVersion+`"}`), 0644),
|
||||||
|
),
|
||||||
|
Check: func(ctx context.Context, t *testing.T, client *Client, imgs []images.Image) {
|
||||||
|
checkImages(t, d6, imgs, "docker.io/library/sparse:ok")
|
||||||
|
mfsts, err := images.Children(ctx, client.ContentStore(), imgs[0].Target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var importedManifest *ocispec.Descriptor
|
||||||
|
var secondManifest *ocispec.Descriptor
|
||||||
|
for _, m := range mfsts {
|
||||||
|
exists, err := content.Exists(ctx, client.ContentStore(), m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
if m.Digest == d3 {
|
||||||
|
m := m
|
||||||
|
importedManifest = &m
|
||||||
|
} else if m.Digest == d5 {
|
||||||
|
m := m
|
||||||
|
secondManifest = &m
|
||||||
|
} else {
|
||||||
|
t.Fatalf("imported manifest with unexpected digest: %v", m.Digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if importedManifest == nil {
|
||||||
|
t.Fatal("the expected manifest was not loaded")
|
||||||
|
}
|
||||||
|
checkManifest(ctx, t, client.ContentStore(), *importedManifest, expManifest)
|
||||||
|
|
||||||
|
if secondManifest == nil {
|
||||||
|
t.Fatal("the expected manifest was not loaded")
|
||||||
|
}
|
||||||
|
_, _, _, missing, err := images.Check(ctx, client.ContentStore(), *secondManifest, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) != 2 {
|
||||||
|
t.Fatalf("expected 2 missing blobs, got %+v", missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
Opts: []ImportOpt{
|
||||||
|
WithSkipMissing(),
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
client, err := newClient(t, address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
ctx := namespaces.WithNamespace(ctx, uuid.New().String())
|
||||||
|
|
||||||
images, err := client.Import(ctx, tartest.TarFromWriterTo(tc.Writer), tc.Opts...)
|
images, err := client.Import(ctx, tartest.TarFromWriterTo(tc.Writer), tc.Opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if tc.Check != nil {
|
if tc.Check != nil {
|
||||||
@ -344,7 +445,7 @@ func TestImport(t *testing.T) {
|
|||||||
t.Fatalf("expected error on import")
|
t.Fatalf("expected error on import")
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.Check(t, images)
|
tc.Check(ctx, t, client, images)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -363,7 +464,8 @@ func checkImages(t *testing.T, target digest.Digest, actual []images.Image, name
|
|||||||
}
|
}
|
||||||
|
|
||||||
if actual[i].Target.MediaType != ocispec.MediaTypeImageManifest &&
|
if actual[i].Target.MediaType != ocispec.MediaTypeImageManifest &&
|
||||||
actual[i].Target.MediaType != images.MediaTypeDockerSchema2Manifest {
|
actual[i].Target.MediaType != images.MediaTypeDockerSchema2Manifest &&
|
||||||
|
actual[i].Target.MediaType != ocispec.MediaTypeImageIndex {
|
||||||
t.Fatalf("image(%d) unexpected media type: %s", i, actual[i].Target.MediaType)
|
t.Fatalf("image(%d) unexpected media type: %s", i, actual[i].Target.MediaType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,14 +532,35 @@ func createManifest(config []byte, layers [][]byte) ([]byte, digest.Digest, *oci
|
|||||||
return b, digest.FromBytes(b), &manifest
|
return b, digest.FromBytes(b), &manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIndex(manifest []byte, tags ...string) []byte {
|
func createManifestList(manifests ...[]byte) ([]byte, digest.Digest) {
|
||||||
|
idx := ocispec.Index{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, manifest := range manifests {
|
||||||
|
d := ocispec.Descriptor{
|
||||||
|
MediaType: ocispec.MediaTypeImageManifest,
|
||||||
|
Digest: digest.FromBytes(manifest),
|
||||||
|
Size: int64(len(manifest)),
|
||||||
|
}
|
||||||
|
idx.Manifests = append(idx.Manifests, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(idx)
|
||||||
|
|
||||||
|
return b, digest.FromBytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIndex(manifest []byte, mt string, tags ...string) []byte {
|
||||||
idx := ocispec.Index{
|
idx := ocispec.Index{
|
||||||
Versioned: specs.Versioned{
|
Versioned: specs.Versioned{
|
||||||
SchemaVersion: 2,
|
SchemaVersion: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
d := ocispec.Descriptor{
|
d := ocispec.Descriptor{
|
||||||
MediaType: ocispec.MediaTypeImageManifest,
|
MediaType: mt,
|
||||||
Digest: digest.FromBytes(manifest),
|
Digest: digest.FromBytes(manifest),
|
||||||
Size: int64(len(manifest)),
|
Size: int64(len(manifest)),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user