//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 != "" { 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 "": 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{ "", "", "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) } } } } } } } }