508 lines
18 KiB
Go
508 lines
18 KiB
Go
//go:build !remote
|
|
|
|
package libimage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/containers/common/pkg/config"
|
|
cp "github.com/containers/image/v5/copy"
|
|
"github.com/containers/image/v5/image"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/pkg/compression"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/storage"
|
|
"github.com/containers/storage/pkg/ioutils"
|
|
"github.com/opencontainers/go-digest"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateManifestList(t *testing.T) {
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
list, err := runtime.CreateManifestList("mylist")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
initialID := list.ID()
|
|
|
|
list, err = runtime.LookupManifestList("mylist")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
require.Equal(t, initialID, list.ID())
|
|
|
|
_, rmErrors := runtime.RemoveImages(ctx, []string{"mylist"}, nil)
|
|
require.Nil(t, rmErrors)
|
|
|
|
_, err = runtime.LookupManifestList("nosuchthing")
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, storage.ErrImageUnknown))
|
|
|
|
_, err = runtime.Pull(ctx, "busybox", config.PullPolicyMissing, nil)
|
|
require.NoError(t, err)
|
|
_, err = runtime.LookupManifestList("busybox")
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, ErrNotAManifestList))
|
|
}
|
|
|
|
func TestConvertManifestList(t *testing.T) {
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
images, err := runtime.Pull(ctx, "busybox", config.PullPolicyMissing, nil)
|
|
require.NoError(t, err)
|
|
_, err = runtime.LookupManifestList("busybox")
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, ErrNotAManifestList)
|
|
|
|
require.NotEmpty(t, images)
|
|
_, err = images[0].ToManifestList()
|
|
require.ErrorIs(t, err, ErrNotAManifestList)
|
|
isList, err := images[0].IsManifestList(ctx)
|
|
require.NoError(t, err)
|
|
require.False(t, isList, "non-list thinks it's a list")
|
|
|
|
list, err := images[0].ConvertToManifestList(ctx)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
isList, err = images[0].IsManifestList(ctx)
|
|
require.NoError(t, err)
|
|
require.True(t, isList, "list thinks it's not a list")
|
|
}
|
|
|
|
// Inspect must contain both formats i.e OCIv1 and docker
|
|
func TestInspectManifestListWithAnnotations(t *testing.T) {
|
|
listName := "testinspect"
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
list, err := runtime.CreateManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
manifestListOpts := &ManifestListAddOptions{All: true}
|
|
_, err = list.Add(ctx, "docker://busybox", manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
list, err = runtime.LookupManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
inspectReport, err := list.Inspect()
|
|
// get digest of the first instance
|
|
digest := inspectReport.Manifests[0].Digest
|
|
require.NoError(t, err)
|
|
|
|
annotateOptions := ManifestListAnnotateOptions{}
|
|
annotations := map[string]string{"hello": "world"}
|
|
annotateOptions.Annotations = annotations
|
|
indexAnnotations := map[string]string{"goodbye": "globe"}
|
|
annotateOptions.IndexAnnotations = indexAnnotations
|
|
|
|
subjectPath, err := filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "blobs-only"))
|
|
require.NoError(t, err)
|
|
annotateOptions.Subject = "oci:" + subjectPath
|
|
|
|
err = list.AnnotateInstance(digest, &annotateOptions)
|
|
require.NoError(t, err)
|
|
// Inspect list again
|
|
inspectReport, err = list.Inspect()
|
|
require.NoError(t, err)
|
|
// verify annotation
|
|
require.Contains(t, inspectReport.Manifests[0].Annotations, "hello")
|
|
require.Equal(t, inspectReport.Manifests[0].Annotations["hello"], annotations["hello"])
|
|
require.Equal(t, inspectReport.Annotations, indexAnnotations)
|
|
require.Equal(t, inspectReport.Subject.MediaType, imgspecv1.MediaTypeImageManifest)
|
|
|
|
// verify that we can clear the variant field by not setting it when we set the arch
|
|
annotateOptions = ManifestListAnnotateOptions{
|
|
Architecture: "arm64",
|
|
Variant: "v8",
|
|
}
|
|
err = list.AnnotateInstance(digest, &annotateOptions)
|
|
require.NoError(t, err)
|
|
inspectReport, err = list.Inspect()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "arm64", inspectReport.Manifests[0].Platform.Architecture)
|
|
require.Equal(t, "v8", inspectReport.Manifests[0].Platform.Variant)
|
|
|
|
annotateOptions = ManifestListAnnotateOptions{
|
|
Architecture: "arm64",
|
|
}
|
|
err = list.AnnotateInstance(digest, &annotateOptions)
|
|
require.NoError(t, err)
|
|
inspectReport, err = list.Inspect()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "arm64", inspectReport.Manifests[0].Platform.Architecture)
|
|
require.Equal(t, "", inspectReport.Manifests[0].Platform.Variant)
|
|
}
|
|
|
|
// Following test ensure that `Tag` tags the manifest list instead of resolved image.
|
|
// Both the tags should point to same image id
|
|
func TestCreateAndTagManifestList(t *testing.T) {
|
|
tagName := "testlisttagged"
|
|
listName := "testlist"
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
list, err := runtime.CreateManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
_, err = runtime.Load(ctx, "testdata/oci-unnamed.tar.gz", nil)
|
|
require.NoError(t, err)
|
|
|
|
// add a remote reference
|
|
manifestListOpts := &ManifestListAddOptions{All: true}
|
|
_, err = list.Add(ctx, "docker://busybox", manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
// add a remote reference where we have to figure out that it's remote
|
|
_, err = list.Add(ctx, "busybox", manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
// add using a local image's ID
|
|
_, err = list.Add(ctx, "5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
list, err = runtime.LookupManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
lookupOptions := &LookupImageOptions{ManifestList: true}
|
|
image, _, err := runtime.LookupImage(listName, lookupOptions)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, image)
|
|
err = image.Tag(tagName)
|
|
require.NoError(t, err, "tag should have succeeded: %s", tagName)
|
|
|
|
taggedImage, _, err := runtime.LookupImage(tagName, lookupOptions)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, taggedImage)
|
|
|
|
// Both origin list and newly tagged list should point to same image id
|
|
require.Equal(t, image.ID(), taggedImage.ID())
|
|
}
|
|
|
|
// Following test ensure that we test Removing a manifestList
|
|
// Test tags two manifestlist and deletes one of them and
|
|
// confirms if other one is not deleted.
|
|
func TestCreateAndRemoveManifestList(t *testing.T) {
|
|
tagName := "manifestlisttagged"
|
|
listName := "manifestlist"
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
list, err := runtime.CreateManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
manifestListOpts := &ManifestListAddOptions{All: true}
|
|
_, err = list.Add(ctx, "docker://busybox", manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
lookupOptions := &LookupImageOptions{ManifestList: true}
|
|
image, _, err := runtime.LookupImage(listName, lookupOptions)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, image)
|
|
err = image.Tag(tagName)
|
|
require.NoError(t, err, "tag should have succeeded: %s", tagName)
|
|
|
|
// Try deleting the manifestList with tag
|
|
rmReports, rmErrors := runtime.RemoveImages(ctx, []string{tagName}, &RemoveImagesOptions{Force: true, LookupManifest: true})
|
|
require.Nil(t, rmErrors)
|
|
require.Equal(t, []string{"localhost/manifestlisttagged:latest"}, rmReports[0].Untagged)
|
|
|
|
// Remove original listname as well
|
|
rmReports, rmErrors = runtime.RemoveImages(ctx, []string{listName}, &RemoveImagesOptions{Force: true, LookupManifest: true})
|
|
require.Nil(t, rmErrors)
|
|
// output should contain log of untagging the original manifestlist
|
|
require.True(t, rmReports[0].Removed)
|
|
require.Equal(t, []string{"localhost/manifestlist:latest"}, rmReports[0].Untagged)
|
|
}
|
|
|
|
// TestAddSomeArtifacts ensures that we don't fail to add artifact manifests to
|
|
// a manifest list, even (or especially) when their config blobs aren't valid
|
|
// OCI or Docker config blobs.
|
|
func TestAddSomeArtifacts(t *testing.T) {
|
|
listName := "manifestlist"
|
|
runtime := testNewRuntime(t)
|
|
ctx := context.Background()
|
|
|
|
list, err := runtime.CreateManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
manifestListOpts := &ManifestListAddOptions{All: true}
|
|
absPath, err := filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "blobs-only"))
|
|
require.NoError(t, err)
|
|
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
absPath, err = filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "config-only"))
|
|
require.NoError(t, err)
|
|
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
|
|
require.NoError(t, err)
|
|
|
|
absPath, err = filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "no-blobs"))
|
|
require.NoError(t, err)
|
|
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestAddArtifacts ensures that we don't fail to add artifact manifests to
|
|
// a manifest list, even (or especially) when their config blobs aren't valid
|
|
// OCI or Docker config blobs.
|
|
func TestAddArtifacts(t *testing.T) {
|
|
listName := "manifestlist"
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
annotations := map[string]string{
|
|
"a": "b",
|
|
}
|
|
indexAnnotations := map[string]string{
|
|
"c": "d",
|
|
}
|
|
files := []struct {
|
|
path string
|
|
size int
|
|
data []byte
|
|
noCompress bool
|
|
guessedMediaType string // what we expect, might be wrong
|
|
}{
|
|
{path: "first.txt", size: mathrand.Intn(256), guessedMediaType: "text/plain"},
|
|
{path: "second.qcow2", size: 512 + mathrand.Intn(256), guessedMediaType: "application/x-qemu-disk"},
|
|
{path: "third", size: 1024 + mathrand.Intn(256), guessedMediaType: "application/x-gzip"},
|
|
{path: "fourth", size: 2048 + mathrand.Intn(256), noCompress: true, guessedMediaType: "application/octet-stream"},
|
|
}
|
|
artifacts := make([]string, 0, len(files))
|
|
for n := range files {
|
|
file := filepath.Join(dir, files[n].path)
|
|
abs, err := filepath.Abs(file)
|
|
require.NoError(t, err)
|
|
files[n].path = abs
|
|
if files[n].data == nil {
|
|
buf := bytes.Buffer{}
|
|
wc := ioutils.NopWriteCloser(&buf)
|
|
if !files[n].noCompress {
|
|
wc, err = compression.CompressStream(&buf, compression.Gzip, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = io.CopyN(wc, rand.Reader, int64(files[n].size))
|
|
require.NoError(t, err)
|
|
wc.Close()
|
|
files[n].size = buf.Len()
|
|
files[n].data = buf.Bytes()
|
|
}
|
|
err = os.WriteFile(abs, files[n].data, 0o600)
|
|
require.NoError(t, err)
|
|
artifacts = append(artifacts, abs)
|
|
}
|
|
artifactSubjectPath, err := filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "blobs-only"))
|
|
require.NoError(t, err)
|
|
artifactSubject := "oci:" + artifactSubjectPath
|
|
indexSubjectPath, err := filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "config-only"))
|
|
require.NoError(t, err)
|
|
indexSubject := "oci:" + indexSubjectPath
|
|
runtime := testNewRuntime(t)
|
|
descriptorForSubject := func(t *testing.T, refName string) imgspecv1.Descriptor {
|
|
if refName == "" {
|
|
return imgspecv1.Descriptor{}
|
|
}
|
|
ref, err := alltransports.ParseImageName(refName)
|
|
require.NoError(t, err)
|
|
src, err := ref.NewImageSource(ctx, nil)
|
|
require.NoError(t, err)
|
|
defer src.Close()
|
|
manifestBytes, manifestType, err := image.UnparsedInstance(src, nil).Manifest(ctx)
|
|
require.NoError(t, err)
|
|
manifestDigest, err := manifest.Digest(manifestBytes)
|
|
require.NoError(t, err)
|
|
artifactType := ""
|
|
if !manifest.MIMETypeIsMultiImage(manifestType) {
|
|
var manifestContents imgspecv1.Manifest
|
|
require.NoError(t, json.Unmarshal(manifestBytes, &manifestContents))
|
|
artifactType = manifestContents.ArtifactType
|
|
}
|
|
return imgspecv1.Descriptor{
|
|
MediaType: manifestType,
|
|
ArtifactType: artifactType,
|
|
Digest: manifestDigest,
|
|
Size: int64(len(manifestBytes)),
|
|
}
|
|
}
|
|
listIndex := 0
|
|
testWith := func(t *testing.T, testName string, artifactTypeSpec string, configType string, configData string, layerType string, excludeTitles bool, artifactSubject string, artifactSubjectDescriptor imgspecv1.Descriptor, indexSubject string, indexSubjectDescriptor imgspecv1.Descriptor) {
|
|
listIndex++
|
|
listName := listName + strconv.Itoa(listIndex)
|
|
t.Run(testName, func(t *testing.T) {
|
|
var artifactType *string
|
|
if artifactTypeSpec != "<nil>" {
|
|
artifactType = &artifactTypeSpec
|
|
}
|
|
options := ManifestListAddArtifactOptions{
|
|
Type: artifactType,
|
|
ConfigType: configType,
|
|
Config: configData,
|
|
LayerType: layerType,
|
|
ExcludeTitles: excludeTitles,
|
|
Annotations: annotations,
|
|
Subject: artifactSubject,
|
|
}
|
|
list, err := runtime.CreateManifestList(listName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, list)
|
|
|
|
d, err := list.AddArtifact(ctx, &options, artifacts...)
|
|
require.NoError(t, err)
|
|
|
|
aoptions := ManifestListAnnotateOptions{
|
|
IndexAnnotations: indexAnnotations,
|
|
Subject: indexSubject,
|
|
}
|
|
err = list.AnnotateInstance(d, &aoptions)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:usetesting // Test fails when using t.TempDir() because the resulting file name is to long.
|
|
destination, err := os.MkdirTemp(dir, "pushed")
|
|
require.NoError(t, err)
|
|
|
|
_, err = list.Push(ctx, "oci:"+destination+":tag", &ManifestListPushOptions{ImageListSelection: cp.CopyAllImages})
|
|
require.NoError(t, err)
|
|
|
|
ref, err := alltransports.ParseImageName("oci:" + destination + ":tag")
|
|
require.NoError(t, err)
|
|
|
|
src, err := ref.NewImageSource(ctx, list.image.runtime.systemContextCopy())
|
|
require.NoError(t, err)
|
|
indexManifest, indexType, err := image.UnparsedInstance(src, nil).Manifest(ctx)
|
|
require.NoError(t, err)
|
|
require.True(t, manifest.MIMETypeIsMultiImage(indexType))
|
|
var index imgspecv1.Index
|
|
require.NoError(t, json.Unmarshal(indexManifest, &index))
|
|
// check some things in the image index
|
|
assert.Equal(t, index.Annotations, indexAnnotations)
|
|
if index.Subject != nil {
|
|
assert.Equal(t, indexSubjectDescriptor, *index.Subject, "subject in index was not preserved")
|
|
}
|
|
for _, descriptor := range index.Manifests {
|
|
artifactManifest, artifactManifestType, err := image.UnparsedInstance(src, &descriptor.Digest).Manifest(ctx)
|
|
require.NoError(t, err)
|
|
require.False(t, manifest.MIMETypeIsMultiImage(artifactManifestType))
|
|
var artifact imgspecv1.Manifest
|
|
require.NoError(t, json.Unmarshal(artifactManifest, &artifact))
|
|
// check some things in the artifact manifest
|
|
switch artifactTypeSpec {
|
|
case "<nil>":
|
|
assert.Equal(t, "application/vnd.unknown.artifact.v1", artifact.ArtifactType)
|
|
default:
|
|
assert.Equal(t, *artifactType, artifact.ArtifactType)
|
|
}
|
|
// FIXME: require.Equal(t, artifact.ArtifactType, descriptor.ArtifactType, "artifact type in index descriptor not preserved during push")
|
|
switch configType {
|
|
case "":
|
|
if len(configData) > 0 {
|
|
assert.Equal(t, imgspecv1.MediaTypeImageConfig, artifact.Config.MediaType)
|
|
} else {
|
|
assert.Equal(t, imgspecv1.DescriptorEmptyJSON.MediaType, artifact.Config.MediaType)
|
|
}
|
|
default:
|
|
assert.Equal(t, configType, artifact.Config.MediaType)
|
|
}
|
|
for i, layer := range artifact.Layers {
|
|
switch layerType {
|
|
case "":
|
|
var rawMediaType string
|
|
baseName := filepath.Base(files[i].path)
|
|
if dotIndex := strings.LastIndex(filepath.Base(files[i].path), "."); dotIndex != -1 {
|
|
rawMediaType = mime.TypeByExtension(baseName[dotIndex:])
|
|
} else {
|
|
rawMediaType = http.DetectContentType(files[i].data)
|
|
}
|
|
parsedMediaType, _, err := mime.ParseMediaType(rawMediaType)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, files[i].guessedMediaType, parsedMediaType)
|
|
default:
|
|
assert.Equal(t, layerType, layer.MediaType)
|
|
}
|
|
if excludeTitles {
|
|
assert.NotContains(t, layer.Annotations, imgspecv1.AnnotationTitle)
|
|
// FIXME: } else {
|
|
// FIXME: require.Contains(t, layer.Annotations, imgspecv1.AnnotationTitle, "layer annotations lost during push")
|
|
// FIXME: assert.Equal(t, filepath.Base(files[i].path), layer.Annotations[imgspecv1.AnnotationTitle], "layer annotations lost during push")
|
|
}
|
|
if layer.MediaType != imgspecv1.MediaTypeImageLayerGzip { // might have been (re)compressed
|
|
assert.Equal(t, digest.FromBytes(files[i].data), layer.Digest, "layer content digest changed during push")
|
|
assert.Equal(t, int64(len(files[i].data)), layer.Size, "layer content size changed during push")
|
|
}
|
|
if artifact.Subject != nil {
|
|
assert.Equal(t, artifactSubjectDescriptor, *artifact.Subject)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
for _, artifactTypeSpec := range []string{
|
|
"<nil>",
|
|
"",
|
|
"application/vnd.unknown.artifact.v1",
|
|
"application/x-something-else",
|
|
} {
|
|
testName := "artifactType=" + artifactTypeSpec
|
|
for _, configType := range []string{
|
|
"",
|
|
imgspecv1.MediaTypeImageConfig,
|
|
imgspecv1.DescriptorEmptyJSON.MediaType,
|
|
} {
|
|
testName := testName + ",configType=" + configType
|
|
for _, configData := range []string{
|
|
"",
|
|
`{"a":"b"}`,
|
|
} {
|
|
testName := testName + ",configLength=" + strconv.Itoa(len(configData))
|
|
for _, layerType := range []string{
|
|
"",
|
|
"application/octet-stream",
|
|
imgspecv1.MediaTypeImageLayerGzip,
|
|
} {
|
|
testName := testName + ",layerType=" + layerType
|
|
for _, excludeTitles := range []bool{false, true} {
|
|
testName := testName + ",excludeTitles=" + fmt.Sprintf("%v", excludeTitles)
|
|
for _, artifactSubject := range []string{"", artifactSubject} {
|
|
testName := testName + ",artifactSubject="
|
|
if artifactSubject != "" {
|
|
testName += filepath.Base(artifactSubjectPath)
|
|
}
|
|
artifactSubjectDescriptor := descriptorForSubject(t, artifactSubject)
|
|
for _, indexSubject := range []string{"", indexSubject} {
|
|
testName := testName + ",indexSubject="
|
|
if indexSubject != "" {
|
|
testName += filepath.Base(indexSubjectPath)
|
|
}
|
|
indexSubjectDescriptor := descriptorForSubject(t, indexSubject)
|
|
testWith(t, testName, artifactTypeSpec, configType, configData, layerType, excludeTitles, artifactSubject, artifactSubjectDescriptor, indexSubject, indexSubjectDescriptor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|