diff --git a/content/graph.go b/content/graph.go index fa2f9efe..9ae83728 100644 --- a/content/graph.go +++ b/content/graph.go @@ -75,18 +75,33 @@ func Successors(ctx context.Context, fetcher Fetcher, node ocispec.Descriptor) ( } nodes = append(nodes, manifest.Config) return append(nodes, manifest.Layers...), nil - case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: + case docker.MediaTypeManifestList: content, err := FetchAll(ctx, fetcher, node) if err != nil { return nil, err } - // docker manifest list and oci index are equivalent for successors. + // OCI manifest index schema can be used to marshal docker manifest list var index ocispec.Index if err := json.Unmarshal(content, &index); err != nil { return nil, err } return index.Manifests, nil + case ocispec.MediaTypeImageIndex: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + var nodes []ocispec.Descriptor + if index.Subject != nil { + nodes = append(nodes, *index.Subject) + } + return append(nodes, index.Manifests...), nil case spec.MediaTypeArtifactManifest: content, err := FetchAll(ctx, fetcher, node) if err != nil { diff --git a/content/graph_test.go b/content/graph_test.go new file mode 100644 index 00000000..d90dc81a --- /dev/null +++ b/content/graph_test.go @@ -0,0 +1,391 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content_test + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" +) + +func TestSuccessors_dockerManifest(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(docker.MediaTypeManifest, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest(descs[0], descs[1:4]...) // Blob 4 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors + manifestDesc := descs[4] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[0:4]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } +} + +func TestSuccessors_imageManifest(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Subject: subject, + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest(nil, descs[0], descs[1:4]...) // Blob 4 + appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 5 + appendBlob("test/sig", []byte("sig")) // Blob 6 + generateManifest(&descs[4], descs[5], descs[6]) // Blob 7 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors: image manifest without a subject + manifestDesc := descs[4] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[0:4]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } + + // test Successors: image manifest with a subject + manifestDesc = descs[7] + got, err = content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[4:7]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } +} + +func TestSuccessors_dockerManifestList(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(docker.MediaTypeManifest, manifestJSON) + } + generateIndex := func(manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(docker.MediaTypeManifestList, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest(descs[0], descs[1:3]...) // Blob 4 + generateManifest(descs[0], descs[3]) // Blob 5 + generateIndex(descs[4:6]...) // Blob 6 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors + manifestDesc := descs[6] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[4:6]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } +} + +func TestSuccessors_imageIndex(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Subject: subject, + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Subject: subject, + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageIndex, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest(nil, descs[0], descs[1:3]...) // Blob 4 + generateManifest(nil, descs[0], descs[3]) // Blob 5 + appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 6 + appendBlob("test/sig", []byte("sig")) // Blob 7 + generateManifest(&descs[4], descs[5], descs[6]) // Blob 8 + generateIndex(&descs[8], descs[4:6]...) // Blob 9 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors + manifestDesc := descs[9] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := append([]ocispec.Descriptor{descs[8]}, descs[4:6]...); !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } +} + +func TestSuccessors_artifactManifest(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateArtifactManifest := func(subject *ocispec.Descriptor, blobs ...ocispec.Descriptor) { + manifest := spec.Artifact{ + Subject: subject, + Blobs: blobs, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(spec.MediaTypeArtifactManifest, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 2 + generateArtifactManifest(nil, descs[0:3]...) // Blob 3 + appendBlob("test/sig", []byte("sig")) // Blob 4 + generateArtifactManifest(&descs[3], descs[4]) // Blob 5 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors: image manifest without a subject + manifestDesc := descs[3] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[0:3]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } + + // test Successors: image manifest with a subject + manifestDesc = descs[5] + got, err = content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if want := descs[3:5]; !reflect.DeepEqual(got, want) { + t.Errorf("Successors() = %v, want %v", got, want) + } +} + +func TestSuccessors_otherMediaType(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(mediaType, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest("whatever", descs[0], descs[1:4]...) // Blob 4 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test Successors: other media type + manifestDesc := descs[4] + got, err := content.Successors(ctx, storage, manifestDesc) + if err != nil { + t.Fatal("Successors() error =", err) + } + if got != nil { + t.Errorf("Successors() = %v, want nil", got) + } +} diff --git a/extendedcopy_test.go b/extendedcopy_test.go index 6c9d7f3a..08a0a8c3 100644 --- a/extendedcopy_test.go +++ b/extendedcopy_test.go @@ -344,6 +344,124 @@ func TestExtendedCopyGraph_PartialCopy(t *testing.T) { } } +func TestExtendedCopyGraph_artifactIndex(t *testing.T) { + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Subject: subject, + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Subject: subject, + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageIndex, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config_1")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("layer_1")) // Blob 1 + generateManifest(nil, descs[0], descs[1]) // Blob 2 + appendBlob(ocispec.MediaTypeImageConfig, []byte("config_2")) // Blob 3 + appendBlob(ocispec.MediaTypeImageLayer, []byte("layer_2")) // Blob 4 + generateManifest(nil, descs[3], descs[4]) // Blob 5 + appendBlob(ocispec.MediaTypeImageLayer, []byte("{}")) // Blob 6 + appendBlob(ocispec.MediaTypeImageLayer, []byte("sbom_1")) // Blob 7 + generateManifest(&descs[2], descs[6], descs[7]) // Blob 8 + appendBlob(ocispec.MediaTypeImageLayer, []byte("sbom_2")) // Blob 9 + generateManifest(&descs[5], descs[6], descs[9]) // Blob 10 + generateIndex(nil, []ocispec.Descriptor{descs[2], descs[5]}...) // Blob 11 (root) + generateIndex(&descs[11], []ocispec.Descriptor{descs[8], descs[10]}...) // Blob 12 (root) + + ctx := context.Background() + verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { + for _, i := range copiedIndice { + got, err := content.FetchAll(ctx, dst, descs[i]) + if err != nil { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, false) + continue + } + if want := blobs[i]; !bytes.Equal(got, want) { + t.Errorf("content[%d] = %v, want %v", i, got, want) + } + } + for _, i := range uncopiedIndice { + if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound) + } + } + } + + src := memory.New() + for i := range blobs { + err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + // test extended copy by descs[0] + dst := memory.New() + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], oras.ExtendedCopyGraphOptions{}); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + // all blobs should be copied + copiedIndice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + uncopiedIndice := []int{} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[2] + dst = memory.New() + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[2], oras.ExtendedCopyGraphOptions{}); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + // all blobs should be copied + copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + uncopiedIndice = []int{} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[8] + dst = memory.New() + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[8], oras.ExtendedCopyGraphOptions{}); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + // all blobs should be copied + copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + uncopiedIndice = []int{} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[11] + dst = memory.New() + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[11], oras.ExtendedCopyGraphOptions{}); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + // all blobs should be copied + copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + uncopiedIndice = []int{} + verifyCopy(dst, copiedIndice, uncopiedIndice) +} + func TestExtendedCopyGraph_WithDepthOption(t *testing.T) { // generate test content var blobs [][]byte diff --git a/internal/manifestutil/parser.go b/internal/manifestutil/parser.go new file mode 100644 index 00000000..c904dc69 --- /dev/null +++ b/internal/manifestutil/parser.go @@ -0,0 +1,63 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifestutil + +import ( + "context" + "encoding/json" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/docker" +) + +// Config returns the config of desc, if present. +func Config(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) { + switch desc.MediaType { + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + // OCI manifest schema can be used to marshal docker manifest + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return &manifest.Config, nil + default: + return nil, nil + } +} + +// Manifest returns the manifests of desc, if present. +func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + switch desc.MediaType { + case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + // OCI manifest index schema can be used to marshal docker manifest list + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + return index.Manifests, nil + default: + return nil, nil + } +} diff --git a/internal/manifestutil/parser_test.go b/internal/manifestutil/parser_test.go new file mode 100644 index 00000000..44c5e43e --- /dev/null +++ b/internal/manifestutil/parser_test.go @@ -0,0 +1,202 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifestutil + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/docker" +) + +func TestConfig(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(mediaType, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + generateManifest(ocispec.MediaTypeImageManifest, descs[0], descs[1]) // Blob 2 + generateManifest(docker.MediaTypeManifest, descs[0], descs[1]) // Blob 3 + generateManifest("whatever", descs[0], descs[1]) // Blob 4 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + tests := []struct { + name string + desc ocispec.Descriptor + want *ocispec.Descriptor + wantErr bool + }{ + { + name: "OCI Image Manifest", + desc: descs[2], + want: &descs[0], + }, + { + name: "Docker Manifest", + desc: descs[3], + want: &descs[0], + wantErr: false, + }, + { + name: "Other media type", + desc: descs[4], + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Config(ctx, storage, tt.desc) + if (err != nil) != tt.wantErr { + t.Errorf("Config() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Config() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestManifests(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Subject: subject, + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + generateIndex := func(mediaType string, subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Subject: subject, + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(mediaType, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 + generateManifest(nil, descs[0], descs[1:3]...) // Blob 4 + generateManifest(nil, descs[0], descs[3]) // Blob 5 + appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 6 + appendBlob("test/sig", []byte("sig")) // Blob 7 + generateManifest(&descs[4], descs[5], descs[6]) // Blob 8 + generateIndex(ocispec.MediaTypeImageIndex, &descs[8], descs[4:6]...) // Blob 9 + generateIndex(docker.MediaTypeManifestList, nil, descs[4:6]...) // Blob 10 + generateIndex("whatever", &descs[8], descs[4:6]...) // Blob 11 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + tests := []struct { + name string + desc ocispec.Descriptor + want []ocispec.Descriptor + wantErr bool + }{ + { + name: "OCI Image Index", + desc: descs[9], + want: descs[4:6], + }, + { + name: "Docker Manifest List", + desc: descs[10], + want: descs[4:6], + wantErr: false, + }, + { + name: "Other media type", + desc: descs[11], + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Manifests(ctx, storage, tt.desc) + if (err != nil) != tt.wantErr { + t.Errorf("Manifests() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Manifests() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 38d8d47f..e903fe3d 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -25,6 +25,7 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/manifestutil" ) // Match checks whether the current platform matches the target platform. @@ -35,7 +36,7 @@ import ( // array of the current platform. // // Note: Variant, OSVersion and OSFeatures are optional fields, will skip -// the comparison if the target platform does not provide specfic value. +// the comparison if the target platform does not provide specific value. func Match(got *ocispec.Platform, want *ocispec.Platform) bool { if got.Architecture != want.Architecture || got.OS != want.OS { return false @@ -77,7 +78,7 @@ func isSubset(a, b []string) bool { func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { switch root.MediaType { case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: - manifests, err := content.Successors(ctx, src, root) + manifests, err := manifestutil.Manifests(ctx, src, root) if err != nil { return ocispec.Descriptor{}, err } @@ -90,7 +91,8 @@ func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocisp } return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound) case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: - descs, err := content.Successors(ctx, src, root) + // config will be non-nil for docker manifest and OCI image manifest + config, err := manifestutil.Config(ctx, src, root) if err != nil { return ocispec.Descriptor{}, err } @@ -99,8 +101,7 @@ func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocisp if root.MediaType == ocispec.MediaTypeImageManifest { configMediaType = ocispec.MediaTypeImageConfig } - - cfgPlatform, err := getPlatformFromConfig(ctx, src, descs[0], configMediaType) + cfgPlatform, err := getPlatformFromConfig(ctx, src, *config, configMediaType) if err != nil { return ocispec.Descriptor{}, err } diff --git a/internal/platform/platform_test.go b/internal/platform/platform_test.go index a19e0b37..621ce959 100644 --- a/internal/platform/platform_test.go +++ b/internal/platform/platform_test.go @@ -144,10 +144,11 @@ func TestSelectManifest(t *testing.T) { }, }) } - generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + generateManifest := func(arc, os, variant string, subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) { manifest := ocispec.Manifest{ - Config: config, - Layers: layers, + Subject: subject, + Config: config, + Layers: layers, } manifestJSON, err := json.Marshal(manifest) if err != nil { @@ -155,8 +156,9 @@ func TestSelectManifest(t *testing.T) { } appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON) } - generateIndex := func(manifests ...ocispec.Descriptor) { + generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) { index := ocispec.Index{ + Subject: subject, Manifests: manifests, } indexJSON, err := json.Marshal(index) @@ -166,20 +168,21 @@ func TestSelectManifest(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } + appendBlob("test/subject", []byte("dummy subject")) // Blob 0 appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json", "created":"2022-07-29T08:13:55Z", "author":"test author", "architecture":"test-arc-1", "os":"test-os-1", -"variant":"v1"}`)) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 - appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 - generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3 - appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 4 - generateManifest(arc_2, os_2, variant_1, descs[0], descs[4]) // Blob 5 - appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 6 - generateManifest(arc_1, os_1, variant_2, descs[0], descs[6]) // Blob 7 - generateIndex(descs[3], descs[5], descs[7]) // Blob 8 +"variant":"v1"}`)) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 3 + generateManifest(arc_1, os_1, variant_1, &descs[0], descs[1], descs[2:4]...) // Blob 4 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 5 + generateManifest(arc_2, os_2, variant_1, nil, descs[1], descs[5]) // Blob 6 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 7 + generateManifest(arc_1, os_1, variant_2, nil, descs[1], descs[7]) // Blob 8 + generateIndex(&descs[0], descs[4], descs[6], descs[8]) // Blob 9 ctx := context.Background() for i := range blobs { @@ -190,12 +193,12 @@ func TestSelectManifest(t *testing.T) { } // test SelectManifest on image index, only one matching manifest found - root := descs[8] + root := descs[9] targetPlatform := ocispec.Platform{ Architecture: arc_2, OS: os_2, } - wantDesc := descs[5] + wantDesc := descs[6] gotDesc, err := SelectManifest(ctx, storage, root, &targetPlatform) if err != nil { t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false) @@ -211,7 +214,7 @@ func TestSelectManifest(t *testing.T) { Architecture: arc_1, OS: os_1, } - wantDesc = descs[3] + wantDesc = descs[4] gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform) if err != nil { t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false) @@ -221,12 +224,12 @@ func TestSelectManifest(t *testing.T) { } // test SelectManifest on manifest - root = descs[7] + root = descs[8] targetPlatform = ocispec.Platform{ Architecture: arc_1, OS: os_1, } - wantDesc = descs[7] + wantDesc = descs[8] gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform) if err != nil { t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false) @@ -237,7 +240,7 @@ func TestSelectManifest(t *testing.T) { // test SelectManifest on manifest, but there is no matching node. // Should return not found error. - root = descs[7] + root = descs[8] targetPlatform = ocispec.Platform{ Architecture: arc_1, OS: os_1, @@ -255,24 +258,26 @@ func TestSelectManifest(t *testing.T) { Architecture: arc_1, OS: os_1, } - root = descs[1] + root = descs[2] _, err = SelectManifest(ctx, storage, root, &targetPlatform) if !errors.Is(err, errdef.ErrUnsupported) { t.Fatalf("SelectManifest() error = %v, wantErr %v", err, errdef.ErrUnsupported) } // generate incorrect test content + storage = cas.NewMemory() blobs = nil descs = nil + appendBlob("test/subject", []byte("dummy subject")) // Blob 0 appendBlob(docker.MediaTypeConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json", -"created":"2022-07-29T08:13:55Z", -"author":"test author 1", -"architecture":"test-arc-1", -"os":"test-os-1", -"variant":"v1"}`)) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo1")) // Blob 1 - generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2 - generateIndex(descs[2]) // Blob 3 + "created":"2022-07-29T08:13:55Z", + "author":"test author 1", + "architecture":"test-arc-1", + "os":"test-os-1", + "variant":"v1"}`)) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo1")) // Blob 2 + generateManifest(arc_1, os_1, variant_1, &descs[0], descs[1], descs[2]) // Blob 3 + generateIndex(&descs[0], descs[3]) // Blob 4 ctx = context.Background() for i := range blobs { @@ -285,7 +290,7 @@ func TestSelectManifest(t *testing.T) { // test SelectManifest on manifest, but the manifest is // invalid by having docker mediaType config in the manifest and oci // mediaType in the image config. Should return error. - root = descs[2] + root = descs[3] targetPlatform = ocispec.Platform{ Architecture: arc_1, OS: os_1, @@ -297,12 +302,14 @@ func TestSelectManifest(t *testing.T) { } // generate test content with null config blob + storage = cas.NewMemory() blobs = nil descs = nil - appendBlob(ocispec.MediaTypeImageConfig, []byte("null")) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo2")) // Blob 1 - generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2 - generateIndex(descs[2]) // Blob 3 + appendBlob("test/subject", []byte("dummy subject")) // Blob 0 + appendBlob(ocispec.MediaTypeImageConfig, []byte("null")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo2")) // Blob 2 + generateManifest(arc_1, os_1, variant_1, &descs[0], descs[1], descs[2]) // Blob 3 + generateIndex(nil, descs[3]) // Blob 4 ctx = context.Background() for i := range blobs { @@ -314,7 +321,7 @@ func TestSelectManifest(t *testing.T) { // test SelectManifest on manifest with null config blob, // should return not found error. - root = descs[2] + root = descs[3] targetPlatform = ocispec.Platform{ Architecture: arc_1, OS: os_1, @@ -326,12 +333,14 @@ func TestSelectManifest(t *testing.T) { } // generate test content with empty config blob + storage = cas.NewMemory() blobs = nil descs = nil - appendBlob(ocispec.MediaTypeImageConfig, []byte("")) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo3")) // Blob 1 - generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2 - generateIndex(descs[2]) // Blob 3 + appendBlob("test/subject", []byte("dummy subject")) // Blob 0 + appendBlob(ocispec.MediaTypeImageConfig, []byte("")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo3")) // Blob 2 + generateManifest(arc_1, os_1, variant_1, nil, descs[1], descs[2]) // Blob 3 + generateIndex(&descs[0], descs[3]) // Blob 4 ctx = context.Background() for i := range blobs { @@ -343,13 +352,16 @@ func TestSelectManifest(t *testing.T) { // test SelectManifest on manifest with empty config blob // should return not found error - root = descs[2] + root = descs[3] + targetPlatform = ocispec.Platform{ Architecture: arc_1, OS: os_1, } + _, err = SelectManifest(ctx, storage, root, &targetPlatform) expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound) + if err.Error() != expected { t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected) }