mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
749 lines
24 KiB
Go
749 lines
24 KiB
Go
//go:build !containers_image_storage_stub
|
||
|
||
package storage
|
||
|
||
import (
|
||
"archive/tar"
|
||
"bytes"
|
||
"context"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"encoding/json"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
imanifest "github.com/containers/image/v5/internal/manifest"
|
||
"github.com/containers/image/v5/internal/private"
|
||
"github.com/containers/image/v5/manifest"
|
||
"github.com/containers/image/v5/pkg/blobinfocache/memory"
|
||
"github.com/containers/image/v5/types"
|
||
"github.com/containers/storage"
|
||
"github.com/containers/storage/pkg/archive"
|
||
"github.com/containers/storage/pkg/idtools"
|
||
"github.com/containers/storage/pkg/ioutils"
|
||
"github.com/containers/storage/pkg/reexec"
|
||
"github.com/opencontainers/go-digest"
|
||
"github.com/sirupsen/logrus"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
var (
|
||
_ types.ImageDestination = &storageImageDestination{}
|
||
_ private.ImageDestination = (*storageImageDestination)(nil)
|
||
_ types.ImageSource = &storageImageSource{}
|
||
_ private.ImageSource = (*storageImageSource)(nil)
|
||
_ types.ImageReference = &storageReference{}
|
||
_ types.ImageTransport = &storageTransport{}
|
||
)
|
||
|
||
const (
|
||
layerSize = 12345
|
||
)
|
||
|
||
func TestMain(m *testing.M) {
|
||
if reexec.Init() {
|
||
return
|
||
}
|
||
debug := false
|
||
flag.BoolVar(&debug, "debug", false, "print debug statements")
|
||
flag.Parse()
|
||
if debug {
|
||
logrus.SetLevel(logrus.DebugLevel)
|
||
}
|
||
os.Exit(m.Run())
|
||
}
|
||
|
||
func newStoreWithGraphDriverOptions(t *testing.T, options []string) storage.Store {
|
||
wd := t.TempDir()
|
||
run := filepath.Join(wd, "run")
|
||
root := filepath.Join(wd, "root")
|
||
Transport.SetDefaultUIDMap([]idtools.IDMap{{
|
||
ContainerID: 0,
|
||
HostID: os.Getuid(),
|
||
Size: 1,
|
||
}})
|
||
Transport.SetDefaultGIDMap([]idtools.IDMap{{
|
||
ContainerID: 0,
|
||
HostID: os.Getgid(),
|
||
Size: 1,
|
||
}})
|
||
store, err := storage.GetStore(storage.StoreOptions{
|
||
RunRoot: run,
|
||
GraphRoot: root,
|
||
GraphDriverName: "vfs",
|
||
GraphDriverOptions: options,
|
||
UIDMap: Transport.DefaultUIDMap(),
|
||
GIDMap: Transport.DefaultGIDMap(),
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
Transport.SetStore(store)
|
||
return store
|
||
}
|
||
|
||
func newStore(t *testing.T) storage.Store {
|
||
return newStoreWithGraphDriverOptions(t, []string{})
|
||
}
|
||
|
||
func TestParse(t *testing.T) {
|
||
store := newStore(t)
|
||
|
||
ref, err := Transport.ParseReference("test")
|
||
if err != nil {
|
||
t.Fatalf("ParseReference(%q) returned error %v", "test", err)
|
||
}
|
||
if ref == nil {
|
||
t.Fatalf("ParseReference returned nil reference")
|
||
}
|
||
|
||
ref, err = Transport.ParseStoreReference(store, "test")
|
||
if err != nil {
|
||
t.Fatalf("ParseStoreReference(%q) returned error %v", "test", err)
|
||
}
|
||
|
||
strRef := ref.StringWithinTransport()
|
||
ref, err = Transport.ParseReference(strRef)
|
||
if err != nil {
|
||
t.Fatalf("ParseReference(%q) returned error: %v", strRef, err)
|
||
}
|
||
if ref == nil {
|
||
t.Fatalf("ParseReference(%q) returned nil reference", strRef)
|
||
}
|
||
|
||
transport := storageTransport{
|
||
store: store,
|
||
defaultUIDMap: Transport.(*storageTransport).defaultUIDMap,
|
||
defaultGIDMap: Transport.(*storageTransport).defaultGIDMap,
|
||
}
|
||
_references := []storageReference{
|
||
{
|
||
named: ref.(*storageReference).named,
|
||
id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||
transport: transport,
|
||
},
|
||
{
|
||
named: ref.(*storageReference).named,
|
||
transport: transport,
|
||
},
|
||
{
|
||
id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||
transport: transport,
|
||
},
|
||
{
|
||
named: ref.DockerReference(),
|
||
transport: transport,
|
||
},
|
||
}
|
||
for _, reference := range _references {
|
||
s := reference.StringWithinTransport()
|
||
ref, err := Transport.ParseStoreReference(store, s)
|
||
if err != nil {
|
||
t.Fatalf("ParseReference(%q) returned error: %v", strRef, err)
|
||
}
|
||
if ref.id != reference.id {
|
||
t.Fatalf("ParseReference(%q) failed to extract ID", s)
|
||
}
|
||
if reference.named == nil {
|
||
if ref.named != nil {
|
||
t.Fatalf("ParseReference(%q) set non-nil named", s)
|
||
}
|
||
} else {
|
||
if ref.named.String() != reference.named.String() {
|
||
t.Fatalf("ParseReference(%q) failed to extract reference (%q!=%q)", s, ref.named.String(), reference.named.String())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseWithGraphDriverOptions(t *testing.T) {
|
||
optionLists := [][]string{
|
||
{},
|
||
{"vfs.ignore_chown_errors=true"},
|
||
{"vfs.ignore_chown_errors=false"},
|
||
}
|
||
for _, optionList := range optionLists {
|
||
store := newStoreWithGraphDriverOptions(t, optionList)
|
||
ref, err := Transport.ParseStoreReference(store, "test")
|
||
require.NoError(t, err, optionList)
|
||
require.NotNil(t, ref)
|
||
spec := ref.StringWithinTransport()
|
||
ref2, err := Transport.ParseReference(spec)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, ref)
|
||
sref, ok := ref2.(*storageReference)
|
||
require.True(t, ok, "transport %s", ref2.Transport().Name())
|
||
parsedOptions := sref.transport.store.GraphOptions()
|
||
assert.Equal(t, optionList, parsedOptions)
|
||
}
|
||
}
|
||
|
||
// makeLayerGoroutine writes to dest, and on success, updates uncompressedCount and uncompressedDigest
|
||
// before it terminates.
|
||
func makeLayerGoroutine(dest io.Writer, uncompressedCount *int64, uncompressedDigest *digest.Digest, compression archive.Compression) (retErr error) {
|
||
if compression != archive.Uncompressed {
|
||
compressor, err := archive.CompressStream(dest, compression)
|
||
if err != nil {
|
||
return fmt.Errorf("compressing layer: %w", err)
|
||
}
|
||
defer func() {
|
||
if err := compressor.Close(); err != nil && retErr == nil {
|
||
retErr = err
|
||
}
|
||
}()
|
||
dest = compressor
|
||
}
|
||
|
||
uncompressedCounter := ioutils.NewWriteCounter(dest)
|
||
uncompressedDigester := digest.Canonical.Digester()
|
||
twriter := tar.NewWriter(io.MultiWriter(uncompressedCounter, uncompressedDigester.Hash()))
|
||
// defer twriter.Close()
|
||
// should be called here to correctly terminate the archive.
|
||
// We do not do that, to workaround https://github.com/containers/storage/issues/1729 :
|
||
// tar-split runs a goroutine that consumes/forwards tar content and might access
|
||
// concurrently-freed objects if it sees a valid EOF marker.
|
||
// Instead, really on raw EOF to terminate the goroutine.
|
||
// This depends on implementation details of tar.Writer (that it does not do any
|
||
// internal buffering).
|
||
|
||
buf := make([]byte, layerSize)
|
||
n, err := rand.Read(buf)
|
||
if err != nil {
|
||
return fmt.Errorf("reading tar data: %w", err)
|
||
}
|
||
if n != len(buf) {
|
||
return fmt.Errorf("short read reading tar data: %d < %d", n, len(buf))
|
||
}
|
||
for i := 1024; i < 2048; i++ {
|
||
buf[i] = 0
|
||
}
|
||
|
||
if err := twriter.WriteHeader(&tar.Header{
|
||
Name: "/random-single-file",
|
||
Mode: 0600,
|
||
Size: int64(len(buf)),
|
||
ModTime: time.Now(),
|
||
AccessTime: time.Now(),
|
||
ChangeTime: time.Now(),
|
||
Typeflag: tar.TypeReg,
|
||
}); err != nil {
|
||
return fmt.Errorf("Error writing tar header: %w", err)
|
||
}
|
||
n, err = twriter.Write(buf)
|
||
if err != nil {
|
||
return fmt.Errorf("Error writing tar header: %w", err)
|
||
}
|
||
if n != len(buf) {
|
||
return fmt.Errorf("Short write writing tar header: %d < %d", n, len(buf))
|
||
}
|
||
if err := twriter.Flush(); err != nil {
|
||
return fmt.Errorf("Error flushing output to tar archive: %w", err)
|
||
}
|
||
*uncompressedCount = uncompressedCounter.Count
|
||
*uncompressedDigest = uncompressedDigester.Digest()
|
||
return nil
|
||
}
|
||
|
||
type testBlob struct {
|
||
uncompressedDigest digest.Digest
|
||
compressedDigest digest.Digest
|
||
uncompressedSize int64
|
||
compressedSize int64
|
||
data []byte
|
||
}
|
||
|
||
func makeLayer(t *testing.T, compression archive.Compression) testBlob {
|
||
preader, pwriter := io.Pipe()
|
||
var uncompressedCount int64
|
||
var uncompressedDigest digest.Digest
|
||
go func() {
|
||
err := errors.New("Internal error: unexpected panic in makeLayer")
|
||
defer func() { // Note that this is not the same as {defer pipeWriter.CloseWithError(err)}; we need err to be evaluated lazily.
|
||
_ = pwriter.CloseWithError(err)
|
||
}()
|
||
err = makeLayerGoroutine(pwriter, &uncompressedCount, &uncompressedDigest, compression)
|
||
}()
|
||
|
||
tbuffer := bytes.Buffer{}
|
||
_, err := io.Copy(&tbuffer, preader)
|
||
require.NoError(t, err)
|
||
return testBlob{
|
||
uncompressedDigest: uncompressedDigest,
|
||
compressedDigest: digest.SHA256.FromBytes(tbuffer.Bytes()),
|
||
uncompressedSize: uncompressedCount,
|
||
compressedSize: int64(tbuffer.Len()),
|
||
data: tbuffer.Bytes(),
|
||
}
|
||
}
|
||
|
||
func (l testBlob) storeBlob(t *testing.T, dest types.ImageDestination, cache types.BlobInfoCache, mimeType string, isConfig bool) manifest.Schema2Descriptor {
|
||
_, err := dest.PutBlob(context.Background(), bytes.NewReader(l.data), types.BlobInfo{
|
||
Size: l.compressedSize,
|
||
Digest: l.compressedDigest,
|
||
}, cache, isConfig)
|
||
require.NoError(t, err)
|
||
return manifest.Schema2Descriptor{
|
||
MediaType: mimeType,
|
||
Size: l.compressedSize,
|
||
Digest: l.compressedDigest,
|
||
}
|
||
}
|
||
|
||
// ensureTestCanCreateImages skips the current test if it is not possible to create layers and images in a private store.
|
||
func ensureTestCanCreateImages(t *testing.T) {
|
||
t.Helper()
|
||
switch runtime.GOOS {
|
||
case "darwin":
|
||
return // Due to https://github.com/containers/storage/pull/811 , c/storage can be used on macOS unprivileged.
|
||
case "linux":
|
||
if os.Geteuid() != 0 {
|
||
t.Skip("test requires root privileges on Linux")
|
||
}
|
||
default:
|
||
// Unknown, let’s leave the tests enabled so that this can be investigated when working on that architecture.
|
||
}
|
||
}
|
||
|
||
// configForLayers returns a minimally-plausible config for layers
|
||
func configForLayers(t *testing.T, layers []testBlob) testBlob {
|
||
rootFS := manifest.Schema2RootFS{
|
||
Type: "layers",
|
||
DiffIDs: []digest.Digest{},
|
||
}
|
||
for _, l := range layers {
|
||
rootFS.DiffIDs = append(rootFS.DiffIDs, l.uncompressedDigest)
|
||
}
|
||
// Add a unique label so that different calls to configForLayers don’t try to use the same image ID.
|
||
randomBytes := make([]byte, digest.Canonical.Size())
|
||
_, err := rand.Read(randomBytes)
|
||
require.NoError(t, err)
|
||
config := manifest.Schema2Image{
|
||
Schema2V1Image: manifest.Schema2V1Image{
|
||
Config: &manifest.Schema2Config{
|
||
Labels: map[string]string{"unique": fmt.Sprintf("%x", randomBytes)},
|
||
},
|
||
Created: time.Now(),
|
||
},
|
||
RootFS: &rootFS,
|
||
}
|
||
configBytes, err := json.Marshal(config)
|
||
require.NoError(t, err)
|
||
configDigest := digest.Canonical.FromBytes(configBytes)
|
||
return testBlob{
|
||
uncompressedDigest: configDigest,
|
||
compressedDigest: configDigest,
|
||
uncompressedSize: int64(len(configBytes)),
|
||
compressedSize: int64(len(configBytes)),
|
||
data: configBytes,
|
||
}
|
||
}
|
||
|
||
func createUncommittedImageDest(t *testing.T, ref types.ImageReference, cache types.BlobInfoCache,
|
||
layers []testBlob, config *testBlob) (types.ImageDestination, types.UnparsedImage) {
|
||
dest, err := ref.NewImageDestination(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
|
||
layerDescriptors := []manifest.Schema2Descriptor{}
|
||
for _, layer := range layers {
|
||
desc := layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType, false)
|
||
layerDescriptors = append(layerDescriptors, desc)
|
||
}
|
||
|
||
if config == nil {
|
||
cd := configForLayers(t, layers)
|
||
config = &cd
|
||
}
|
||
configDescriptor := config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType, true)
|
||
|
||
manifest := manifest.Schema2FromComponents(configDescriptor, layerDescriptors)
|
||
manifestBytes, err := manifest.Serialize()
|
||
require.NoError(t, err)
|
||
err = dest.PutManifest(context.Background(), manifestBytes, nil)
|
||
require.NoError(t, err)
|
||
unparsedToplevel := unparsedImage{
|
||
imageReference: nil,
|
||
manifestBytes: manifestBytes,
|
||
manifestType: manifest.MediaType,
|
||
signatures: nil,
|
||
}
|
||
return dest, &unparsedToplevel
|
||
}
|
||
|
||
func createImage(t *testing.T, ref types.ImageReference, cache types.BlobInfoCache,
|
||
layers []testBlob, config *testBlob) {
|
||
dest, unparsedToplevel := createUncommittedImageDest(t, ref, cache, layers, config)
|
||
err := dest.Commit(context.Background(), unparsedToplevel)
|
||
require.NoError(t, err)
|
||
err = dest.Close()
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestWriteRead(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
manifests := []string{
|
||
//`{
|
||
// "schemaVersion": 2,
|
||
// "mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||
// "config": {
|
||
// "mediaType": "application/vnd.oci.image.serialization.config.v1+json",
|
||
// "size": %cs,
|
||
// "digest": "%ch"
|
||
// },
|
||
// "layers": [
|
||
// {
|
||
// "mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip",
|
||
// "digest": "%lh",
|
||
// "size": %ls
|
||
// }
|
||
// ]
|
||
//}`,
|
||
`{
|
||
"schemaVersion": 1,
|
||
"name": "test",
|
||
"tag": "latest",
|
||
"architecture": "amd64",
|
||
"fsLayers": [
|
||
{
|
||
"blobSum": "%lh"
|
||
}
|
||
],
|
||
"history": [
|
||
{
|
||
"v1Compatibility": "{\"id\":\"%li\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":%ls}"
|
||
}
|
||
]
|
||
}`,
|
||
`{
|
||
"schemaVersion": 2,
|
||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||
"config": {
|
||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||
"size": %cs,
|
||
"digest": "%ch"
|
||
},
|
||
"layers": [
|
||
{
|
||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||
"digest": "%lh",
|
||
"size": %ls
|
||
}
|
||
]
|
||
}`,
|
||
}
|
||
// Start signatures with 0xA0 to fool internal/signature.FromBlob into thinking it is valid GPG
|
||
signatures := [][]byte{
|
||
[]byte("\xA0Signature A"),
|
||
[]byte("\xA0Signature B"),
|
||
}
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
ref, err := Transport.ParseReference("test")
|
||
require.NoError(t, err)
|
||
|
||
for _, manifestFmt := range manifests {
|
||
dest, err := ref.NewImageDestination(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, ref.StringWithinTransport(), dest.Reference().StringWithinTransport())
|
||
t.Logf("supported manifest MIME types: %v", dest.SupportedManifestMIMETypes())
|
||
err = dest.SupportsSignatures(context.Background())
|
||
require.NoError(t, err)
|
||
t.Logf("compress layers: %v", dest.DesiredLayerCompression())
|
||
compression := archive.Uncompressed
|
||
if dest.DesiredLayerCompression() == types.Compress {
|
||
compression = archive.Gzip
|
||
}
|
||
layer := makeLayer(t, compression)
|
||
_ = layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType, false)
|
||
t.Logf("Wrote randomly-generated layer %q (%d/%d bytes) to destination", layer.compressedDigest, layer.compressedSize, layer.uncompressedSize)
|
||
config := configForLayers(t, []testBlob{layer})
|
||
_ = config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType, true)
|
||
|
||
manifest := strings.ReplaceAll(manifestFmt, "%lh", layer.compressedDigest.String())
|
||
manifest = strings.ReplaceAll(manifest, "%ch", config.compressedDigest.String())
|
||
manifest = strings.ReplaceAll(manifest, "%ls", fmt.Sprintf("%d", layer.compressedSize))
|
||
manifest = strings.ReplaceAll(manifest, "%cs", fmt.Sprintf("%d", config.compressedSize))
|
||
manifest = strings.ReplaceAll(manifest, "%li", layer.compressedDigest.Encoded())
|
||
manifest = strings.ReplaceAll(manifest, "%ci", config.compressedDigest.Encoded())
|
||
t.Logf("this manifest is %q", manifest)
|
||
err = dest.PutManifest(context.Background(), []byte(manifest), nil)
|
||
require.NoError(t, err)
|
||
err = dest.PutSignatures(context.Background(), signatures, nil)
|
||
require.NoError(t, err)
|
||
unparsedToplevel := unparsedImage{
|
||
imageReference: nil,
|
||
manifestBytes: []byte(manifest),
|
||
manifestType: imanifest.GuessMIMEType([]byte(manifest)),
|
||
signatures: signatures,
|
||
}
|
||
err = dest.Commit(context.Background(), &unparsedToplevel)
|
||
require.NoError(t, err)
|
||
err = dest.Close()
|
||
require.NoError(t, err)
|
||
|
||
img, err := ref.NewImage(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
imageConfigInfo := img.ConfigInfo()
|
||
if imageConfigInfo.Digest != "" {
|
||
blob, err := img.ConfigBlob(context.Background())
|
||
require.NoError(t, err)
|
||
sum := digest.SHA256.FromBytes(blob)
|
||
assert.Equal(t, config.compressedDigest, sum)
|
||
assert.Len(t, blob, int(config.compressedSize))
|
||
}
|
||
layerInfos := img.LayerInfos()
|
||
assert.NotNil(t, layerInfos)
|
||
imageInfo, err := img.Inspect(context.Background())
|
||
require.NoError(t, err)
|
||
assert.False(t, imageInfo.Created.IsZero())
|
||
|
||
src, err := ref.NewImageSource(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
if src.Reference().StringWithinTransport() != ref.StringWithinTransport() {
|
||
// As long as it's only the addition of an ID suffix, that's okay.
|
||
assert.True(t, strings.HasPrefix(src.Reference().StringWithinTransport(), ref.StringWithinTransport()+"@"))
|
||
}
|
||
_, manifestType, err := src.GetManifest(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
t.Logf("this manifest's type appears to be %q", manifestType)
|
||
instanceDigest, err := imanifest.Digest([]byte(manifest))
|
||
require.NoError(t, err)
|
||
retrieved, _, err := src.GetManifest(context.Background(), &instanceDigest)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, manifest, string(retrieved))
|
||
sigs, err := src.GetSignatures(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, signatures, sigs)
|
||
sigs2, err := src.GetSignatures(context.Background(), &instanceDigest)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, sigs, sigs2)
|
||
for _, layerInfo := range layerInfos {
|
||
buf := bytes.Buffer{}
|
||
layer, size, err := src.GetBlob(context.Background(), layerInfo, cache)
|
||
require.NoError(t, err)
|
||
t.Logf("Decompressing blob %q, blob size = %d, layerInfo.Size = %d bytes", layerInfo.Digest, size, layerInfo.Size)
|
||
hasher := sha256.New()
|
||
compressed := ioutils.NewWriteCounter(hasher)
|
||
countedLayer := io.TeeReader(layer, compressed)
|
||
decompressed, err := archive.DecompressStream(countedLayer)
|
||
require.NoError(t, err)
|
||
n, err := io.Copy(&buf, decompressed)
|
||
require.NoError(t, err)
|
||
layer.Close()
|
||
if layerInfo.Size >= 0 {
|
||
assert.Equal(t, layerInfo.Size, compressed.Count)
|
||
assert.Equal(t, layerInfo.Size, n)
|
||
}
|
||
if size >= 0 {
|
||
assert.Equal(t, size, compressed.Count)
|
||
}
|
||
sum := hasher.Sum(nil)
|
||
assert.Equal(t, layerInfo.Digest, digest.NewDigestFromBytes(digest.SHA256, sum))
|
||
}
|
||
err = src.Close()
|
||
require.NoError(t, err)
|
||
err = img.Close()
|
||
require.NoError(t, err)
|
||
err = ref.DeleteImage(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
}
|
||
}
|
||
|
||
func TestDuplicateName(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
ref, err := Transport.ParseReference("test")
|
||
require.NoError(t, err)
|
||
|
||
createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Uncompressed)}, nil)
|
||
createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Gzip)}, nil)
|
||
}
|
||
|
||
func TestDuplicateID(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
ref, err := Transport.ParseReference("@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||
require.NoError(t, err)
|
||
|
||
createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Gzip)}, nil)
|
||
|
||
dest, unparsedToplevel := createUncommittedImageDest(t, ref, cache,
|
||
[]testBlob{makeLayer(t, archive.Gzip)}, nil)
|
||
err = dest.Commit(context.Background(), unparsedToplevel)
|
||
require.Error(t, err)
|
||
assert.ErrorIs(t, err, storage.ErrDuplicateID)
|
||
err = dest.Close()
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestDuplicateNameID(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
ref, err := Transport.ParseReference("test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||
require.NoError(t, err)
|
||
|
||
createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Gzip)}, nil)
|
||
|
||
dest, unparsedToplevel := createUncommittedImageDest(t, ref, cache,
|
||
[]testBlob{makeLayer(t, archive.Gzip)}, nil)
|
||
err = dest.Commit(context.Background(), unparsedToplevel)
|
||
require.Error(t, err)
|
||
assert.ErrorIs(t, err, storage.ErrDuplicateID)
|
||
err = dest.Close()
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestNamespaces(t *testing.T) {
|
||
newStore(t)
|
||
|
||
ref, err := Transport.ParseReference("test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||
if err != nil {
|
||
t.Fatalf("ParseReference(%q) returned error %v", "test", err)
|
||
}
|
||
if ref == nil {
|
||
t.Fatalf("ParseReference returned nil reference")
|
||
}
|
||
|
||
namespaces := ref.PolicyConfigurationNamespaces()
|
||
for _, namespace := range namespaces {
|
||
t.Logf("namespace: %q", namespace)
|
||
err = Transport.ValidatePolicyConfigurationScope(namespace)
|
||
if ref == nil {
|
||
t.Fatalf("ValidatePolicyConfigurationScope(%q) returned error: %v", namespace, err)
|
||
}
|
||
}
|
||
namespace := ref.StringWithinTransport()
|
||
t.Logf("ref: %q", namespace)
|
||
err = Transport.ValidatePolicyConfigurationScope(namespace)
|
||
if err != nil {
|
||
t.Fatalf("ValidatePolicyConfigurationScope(%q) returned error: %v", namespace, err)
|
||
}
|
||
for _, namespace := range []string{
|
||
"@beefee",
|
||
":miracle",
|
||
":miracle@beefee",
|
||
"@beefee:miracle",
|
||
} {
|
||
t.Logf("invalid ref: %q", namespace)
|
||
err = Transport.ValidatePolicyConfigurationScope(namespace)
|
||
if err == nil {
|
||
t.Fatalf("ValidatePolicyConfigurationScope(%q) should have failed", namespace)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSize(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
layer1 := makeLayer(t, archive.Gzip)
|
||
layer2 := makeLayer(t, archive.Gzip)
|
||
layerBlobs := []testBlob{layer1, layer2}
|
||
config := configForLayers(t, layerBlobs)
|
||
|
||
ref, err := Transport.ParseReference("test")
|
||
require.NoError(t, err)
|
||
|
||
createImage(t, ref, cache, layerBlobs, &config)
|
||
|
||
img, err := ref.NewImage(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
manifest, _, err := img.Manifest(context.Background())
|
||
require.NoError(t, err)
|
||
|
||
usize, err := img.Size()
|
||
require.NoError(t, err)
|
||
require.NotEqual(t, -1, usize)
|
||
|
||
assert.Equal(t, config.compressedSize+layer1.uncompressedSize+layer2.uncompressedSize+2*int64(len(manifest)), usize)
|
||
err = img.Close()
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestDuplicateBlob(t *testing.T) {
|
||
ensureTestCanCreateImages(t)
|
||
|
||
newStore(t)
|
||
cache := memory.New()
|
||
|
||
ref, err := Transport.ParseReference("test")
|
||
require.NoError(t, err)
|
||
|
||
layer1 := makeLayer(t, archive.Gzip)
|
||
layer2 := makeLayer(t, archive.Gzip)
|
||
layerBlobs := []testBlob{layer1, layer2, layer1, layer2}
|
||
config := configForLayers(t, layerBlobs)
|
||
createImage(t, ref, cache, layerBlobs, &config)
|
||
|
||
img, err := ref.NewImage(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
src, err := ref.NewImageSource(context.Background(), nil)
|
||
require.NoError(t, err)
|
||
source, ok := src.(*storageImageSource)
|
||
require.True(t, ok)
|
||
|
||
layers := []string{}
|
||
layersInfo, err := img.LayerInfosForCopy(context.Background())
|
||
require.NoError(t, err)
|
||
for _, layerInfo := range layersInfo {
|
||
digestLayers, _ := source.imageRef.transport.store.LayersByUncompressedDigest(layerInfo.Digest)
|
||
rc, _, layerID, err := source.getBlobAndLayerID(layerInfo.Digest, digestLayers)
|
||
require.NoError(t, err)
|
||
_, err = io.Copy(io.Discard, rc)
|
||
require.NoError(t, err)
|
||
rc.Close()
|
||
layers = append(layers, layerID)
|
||
}
|
||
assert.Len(t, layers, 4)
|
||
for i, layerID := range layers {
|
||
for j, otherID := range layers {
|
||
if i != j && layerID == otherID {
|
||
t.Fatalf("Layer IDs are not unique: %v", layers)
|
||
}
|
||
}
|
||
}
|
||
err = src.Close()
|
||
require.NoError(t, err)
|
||
err = img.Close()
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
type unparsedImage struct {
|
||
imageReference types.ImageReference
|
||
manifestBytes []byte
|
||
manifestType string
|
||
signatures [][]byte
|
||
}
|
||
|
||
func (u *unparsedImage) Reference() types.ImageReference {
|
||
return u.imageReference
|
||
}
|
||
func (u *unparsedImage) Manifest(context.Context) ([]byte, string, error) {
|
||
return u.manifestBytes, u.manifestType, nil
|
||
}
|
||
func (u *unparsedImage) Signatures(context.Context) ([][]byte, error) {
|
||
return u.signatures, nil
|
||
}
|