diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 2f7428585..ccad542aa 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -174,6 +174,11 @@ func NewManifestPushHandler(printer *output.Printer) metadata.ManifestPushHandle return text.NewManifestPushHandler(printer) } +// NewManifestIndexCreateHandler returns an index create handler. +func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestIndexCreateHandler { + return text.NewManifestIndexCreateHandler(printer) +} + // NewCopyHandler returns copy handlers. func NewCopyHandler(printer *output.Printer, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) { return status.NewTextCopyHandler(printer, fetcher), text.NewCopyHandler(printer) diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index aacc91309..12c10b87b 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -78,6 +78,11 @@ type ManifestPushHandler interface { TaggedHandler } +// ManifestIndexCreateHandler handles metadata output for index create events. +type ManifestIndexCreateHandler interface { + TaggedHandler +} + // CopyHandler handles metadata output for cp events. type CopyHandler interface { TaggedHandler diff --git a/cmd/oras/internal/display/metadata/text/manifest_index_create.go b/cmd/oras/internal/display/metadata/text/manifest_index_create.go new file mode 100644 index 000000000..960f676c2 --- /dev/null +++ b/cmd/oras/internal/display/metadata/text/manifest_index_create.go @@ -0,0 +1,39 @@ +/* +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 text + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/output" +) + +// ManifestIndexCreateHandler handles text metadata output for index create events. +type ManifestIndexCreateHandler struct { + printer *output.Printer +} + +// NewManifestIndexCreateHandler returns a new handler for index create events. +func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestIndexCreateHandler { + return &ManifestIndexCreateHandler{ + printer: printer, + } +} + +// OnTagged implements metadata.TaggedHandler. +func (h *ManifestIndexCreateHandler) OnTagged(_ ocispec.Descriptor, tag string) error { + return h.printer.Println("Tagged", tag) +} diff --git a/cmd/oras/internal/display/status/utils.go b/cmd/oras/internal/display/status/utils.go index 453a1acc6..860156067 100644 --- a/cmd/oras/internal/display/status/utils.go +++ b/cmd/oras/internal/display/status/utils.go @@ -48,6 +48,14 @@ const ( copyPromptMounted = "Mounted" ) +// Prompts for index events. +const ( + IndexPromptFetching = "Fetching" + IndexPromptFetched = "Fetched " + IndexPromptPacked = "Packed " + IndexPromptPushed = "Pushed " +) + // DeduplicatedFilter filters out deduplicated descriptors. func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool { return func(desc ocispec.Descriptor) bool { diff --git a/cmd/oras/root/manifest/cmd.go b/cmd/oras/root/manifest/cmd.go index c68668614..2a84ab048 100644 --- a/cmd/oras/root/manifest/cmd.go +++ b/cmd/oras/root/manifest/cmd.go @@ -17,6 +17,7 @@ package manifest import ( "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/root/manifest/index" ) func Cmd() *cobra.Command { @@ -30,6 +31,7 @@ func Cmd() *cobra.Command { fetchCmd(), fetchConfigCmd(), pushCmd(), + index.Cmd(), ) return cmd } diff --git a/cmd/oras/root/manifest/index/cmd.go b/cmd/oras/root/manifest/index/cmd.go new file mode 100644 index 000000000..54391c123 --- /dev/null +++ b/cmd/oras/root/manifest/index/cmd.go @@ -0,0 +1,30 @@ +/* +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 index + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "index [command]", + Short: "[Experimental] Index operations", + } + + cmd.AddCommand( + createCmd(), + ) + return cmd +} diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go new file mode 100644 index 000000000..3e5a2754f --- /dev/null +++ b/cmd/oras/root/manifest/index/create.go @@ -0,0 +1,181 @@ +/* +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 index + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras/cmd/oras/internal/argument" + "oras.land/oras/cmd/oras/internal/command" + "oras.land/oras/cmd/oras/internal/display" + "oras.land/oras/cmd/oras/internal/display/status" + oerrors "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/cmd/oras/internal/output" + "oras.land/oras/internal/descriptor" + "oras.land/oras/internal/listener" +) + +var maxConfigSize int64 = 4 * 1024 * 1024 // 4 MiB + +type createOptions struct { + option.Common + option.Target + + sources []string + extraRefs []string +} + +func createCmd() *cobra.Command { + var opts createOptions + cmd := &cobra.Command{ + Use: "create [flags] [:][...]] [{|}...]", + Short: "[Experimental] Create and push an index from provided manifests", + Long: `[Experimental] Create and push an index from provided manifests. All manifests should be in the same repository + +Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push without tagging: + oras manifest index create localhost:5000/hello linux-amd64 linux-arm64 + +Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push with the tag 'v1': + oras manifest index create localhost:5000/hello:v1 linux-amd64 linux-arm64 + +Example - create an index from source manifests using both tags and digests, and push with tag 'v1': + oras manifest index create localhost:5000/hello:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - create an index and push it with multiple tags: + oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': + oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 +`, + Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."), + PreRunE: func(cmd *cobra.Command, args []string) error { + refs := strings.Split(args[0], ",") + opts.RawReference = refs[0] + opts.extraRefs = refs[1:] + opts.sources = args[1:] + return option.Parse(cmd, &opts) + }, + Aliases: []string{"pack"}, + RunE: func(cmd *cobra.Command, args []string) error { + return createIndex(cmd, opts) + }, + } + option.ApplyFlags(&opts, cmd.Flags()) + return oerrors.Command(cmd, &opts.Target) +} + +func createIndex(cmd *cobra.Command, opts createOptions) error { + ctx, logger := command.GetLogger(cmd, &opts.Common) + target, err := opts.NewTarget(opts.Common, logger) + if err != nil { + return err + } + manifests, err := fetchSourceManifests(ctx, target, opts) + if err != nil { + return err + } + index := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + } + indexBytes, _ := json.Marshal(index) + desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) + opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) + return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) +} + +func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { + resolved := []ocispec.Descriptor{} + for _, source := range opts.sources { + opts.Println(status.IndexPromptFetching, source) + desc, content, err := oras.FetchBytes(ctx, target, source, oras.DefaultFetchBytesOptions) + if err != nil { + return nil, fmt.Errorf("could not find the manifest %s: %w", source, err) + } + if !descriptor.IsManifest(desc) { + return nil, fmt.Errorf("%s is not a manifest", source) + } + opts.Println(status.IndexPromptFetched, source) + desc = descriptor.Plain(desc) + if descriptor.IsImageManifest(desc) { + desc.Platform, err = getPlatform(ctx, target, content) + if err != nil { + return nil, err + } + } + resolved = append(resolved, desc) + } + return resolved, nil +} + +func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes []byte) (*ocispec.Platform, error) { + // extract config descriptor + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return nil, err + } + // if config size is larger than 4 MiB, discontinue the fetch + if manifest.Config.Size > maxConfigSize { + return nil, fmt.Errorf("config size %v exceeds MaxBytes %v: %w", manifest.Config.Size, maxConfigSize, errdef.ErrSizeExceedsLimit) + } + // fetch config content + contentBytes, err := content.FetchAll(ctx, target, manifest.Config) + if err != nil { + return nil, err + } + var platform ocispec.Platform + if err := json.Unmarshal(contentBytes, &platform); err != nil || (platform.Architecture == "" && platform.OS == "") { + // ignore if the manifest does not have platform information + return nil, nil + } + return &platform, nil +} + +func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string, printer *output.Printer) error { + // push the index + var err error + if ref == "" { + err = target.Push(ctx, desc, bytes.NewReader(content)) + } else { + _, err = oras.TagBytes(ctx, target, desc.MediaType, content, ref) + } + if err != nil { + return err + } + printer.Println(status.IndexPromptPushed, path) + if len(extraRefs) != 0 { + handler := display.NewManifestIndexCreateHandler(printer) + tagListener := listener.NewTaggedListener(target, handler.OnTagged) + if _, err = oras.TagBytesN(ctx, tagListener, desc.MediaType, content, extraRefs, oras.DefaultTagBytesNOptions); err != nil { + return err + } + } + return printer.Println("Digest:", desc.Digest) +} diff --git a/internal/descriptor/descriptor.go b/internal/descriptor/descriptor.go index 84806fed2..f884edd99 100644 --- a/internal/descriptor/descriptor.go +++ b/internal/descriptor/descriptor.go @@ -21,6 +21,20 @@ import ( "oras.land/oras/internal/docker" ) +// IsManifest checks if a descriptor describes a manifest. +// Adapted from `oras-go`: https://github.com/oras-project/oras-go/blob/d6c837e439f4c567f8003eab6e423c22900452a8/internal/descriptor/descriptor.go#L67 +func IsManifest(desc ocispec.Descriptor) bool { + switch desc.MediaType { + case docker.MediaTypeManifest, + docker.MediaTypeManifestList, + ocispec.MediaTypeImageManifest, + ocispec.MediaTypeImageIndex: + return true + default: + return false + } +} + // IsImageManifest checks whether a manifest is an image manifest. func IsImageManifest(desc ocispec.Descriptor) bool { return desc.MediaType == docker.MediaTypeManifest || desc.MediaType == ocispec.MediaTypeImageManifest @@ -37,6 +51,16 @@ func ShortDigest(desc ocispec.Descriptor) (digestString string) { return digestString } +// Plain returns a plain descriptor that contains only MediaType, Digest and Size. +// Copied from `oras-go`: https://github.com/oras-project/oras-go/blob/d6c837e439f4c567f8003eab6e423c22900452a8/internal/descriptor/descriptor.go#L81 +func Plain(desc ocispec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + // GetTitleOrMediaType gets a descriptor name using either title or media type. func GetTitleOrMediaType(desc ocispec.Descriptor) (name string, isTitle bool) { name, ok := desc.Annotations[ocispec.AnnotationTitle] diff --git a/internal/descriptor/descriptor_test.go b/internal/descriptor/descriptor_test.go index 4d01e77a4..392bfa671 100644 --- a/internal/descriptor/descriptor_test.go +++ b/internal/descriptor/descriptor_test.go @@ -56,6 +56,18 @@ func TestDescriptor_IsImageManifest(t *testing.T) { } } +func TestDescriptor_IsManifest(t *testing.T) { + got := descriptor.IsManifest(imageDesc) + if !got { + t.Fatalf("IsManifest() got %v, want %v", got, true) + } + + got = descriptor.IsManifest(artifactDesc) + if got { + t.Fatalf("IsManifest() got %v, want %v", got, false) + } +} + func TestDescriptor_ShortDigest(t *testing.T) { expected := "2e0e0fe1fb3e" got := descriptor.ShortDigest(titledDesc) diff --git a/test/e2e/README.md b/test/e2e/README.md index 2306b256c..ea7431160 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -163,11 +163,18 @@ graph TD; A2-- hello.tar -->A8(blob) A3-- hello.tar -->A8(blob) A4-- hello.tar -->A8(blob) + A9>tag: linux-amd64]-..->A2 + A10>tag: linux-arm64]-..->A3 + A11>tag: linux-armv7]-..->A4 B0>tag: foobar]-..->B1[oci image] B1-- foo1 -->B2(blob1) B1-- foo2 -->B2(blob1) B1-- bar -->B3(blob2) + + C0>tag: nonjson-config]-..->C1[oci image] + C1-->C2(config4) + C1-->C3(blob4) end ``` diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index d4a3ae380..c1062a42c 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -27,6 +27,7 @@ var ( Digest = "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f" Manifest = `{"mediaType":"application/vnd.oci.image.index.v1+json","schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c","size":458,"platform":{"architecture":"arm64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255","size":458,"platform":{"architecture":"arm","os":"linux","variant":"v7"}}]}` Descriptor = `{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f","size":706}` + DescriptorObject = ocispec.Descriptor{MediaType: "application/vnd.oci.image.index.v1+json", Digest: "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", Size: 706} AnnotatedDescriptor = `{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f","size":706,"annotations":{"org.opencontainers.image.ref.name":"multi"}}` IndexReferrerDigest = "sha256:d37baf66300b9006b0f4c7102075d56b970fbf910be5c6bca07fdbb000dfa383" IndexReferrerStateKey = match.StateKey{Digest: "d3cf790759b0", Name: "application/vnd.oci.image.manifest.v1+json"} @@ -48,6 +49,10 @@ var ( MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.Digest("sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1"), Size: 458, + Platform: &ocispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, } LinuxAMD64DescStr = `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458}` LinuxAMD64IndexDesc = `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}` @@ -72,4 +77,23 @@ var ( {Digest: "fe9dbc99451d", Name: "application/vnd.oci.image.config.v1+json"}, {Digest: "2ef548696ac7", Name: "hello.tar"}, } + LinuxARM64 = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest.Digest("sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c"), + Size: 458, + Platform: &ocispec.Platform{ + Architecture: "arm64", + OS: "linux", + }, + } + LinuxARMV7 = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest.Digest("sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255"), + Size: 458, + Platform: &ocispec.Platform{ + Architecture: "arm", + OS: "linux", + Variant: "v7", + }, + } ) diff --git a/test/e2e/internal/testdata/nonjson_config/const.go b/test/e2e/internal/testdata/nonjson_config/const.go new file mode 100644 index 000000000..576848327 --- /dev/null +++ b/test/e2e/internal/testdata/nonjson_config/const.go @@ -0,0 +1,28 @@ +/* +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 nonjson_config + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +var ( + Descriptor = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", + Size: 529, + } +) diff --git a/test/e2e/scripts/prepare.sh b/test/e2e/scripts/prepare.sh index a0691eda2..13b119574 100755 --- a/test/e2e/scripts/prepare.sh +++ b/test/e2e/scripts/prepare.sh @@ -62,5 +62,5 @@ docker run --pull always -dp $ZOT_REGISTRY_PORT:5000 \ --name $ZOT_CTR_NAME \ -u $(id -u $(whoami)) \ --mount type=bind,source="${e2e_root}/testdata/zot/",target=/etc/zot \ - --rm ghcr.io/project-zot/zot-linux-amd64:v2.0.1 + --rm ghcr.io/project-zot/zot-linux-amd64:v2.1.1 echo " <<< prepared : zot <<< " diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go new file mode 100644 index 000000000..cfd23e8ac --- /dev/null +++ b/test/e2e/suite/command/manifest_index.go @@ -0,0 +1,228 @@ +/* +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 command + +import ( + "encoding/json" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/test/e2e/internal/testdata/multi_arch" + "oras.land/oras/test/e2e/internal/testdata/nonjson_config" + . "oras.land/oras/test/e2e/internal/utils" +) + +var _ = Describe("ORAS beginners:", func() { + When("running manifest index command", func() { + When("running `manifest index create`", func() { + It("should show help doc with alias", func() { + ORAS("manifest", "index", "create", "--help").MatchKeyWords("Aliases", "pack").Exec() + }) + }) + }) +}) + +func indexTestRepo(subcommand string, text string) string { + return fmt.Sprintf("command/index/%d/%s/%s", GinkgoRandomSeed(), subcommand, text) +} + +func ValidateIndex(content []byte, manifests []ocispec.Descriptor) { + var index ocispec.Index + Expect(json.Unmarshal(content, &index)).ShouldNot(HaveOccurred()) + Expect(index.Manifests).To(Equal(manifests)) +} + +var _ = Describe("1.1 registry users:", func() { + When("running `manifest index create`", func() { + It("should create index by using source manifest digests", func() { + testRepo := indexTestRepo("create", "by-digest") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "latest"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)). + MatchKeyWords("Fetched", "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", + "Fetched", "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", + "Pushed", "sha256:cce9590b1193d8bcb70467e2381dc81e77869be4801c09abe9bc274b6a1d2001").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "latest")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64} + ValidateIndex(content, expectedManifests) + }) + + It("should create index by using source manifest tags", func() { + testRepo := indexTestRepo("create", "by-tag") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "latest"), + "linux-arm64", "linux-amd64"). + MatchKeyWords("Fetched", "linux-arm64", + "Fetched", "linux-amd64", + "Pushed", "sha256:5c98cfc90e390c575679370a5dc5e37b52e854bbb7b9cb80cc1f30b56b8d183e").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "latest")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARM64, multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create index without tagging it", func() { + testRepo := indexTestRepo("create", "no-tag") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + "linux-arm64", "linux-amd64", "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255"). + MatchKeyWords("Pushed", "sha256:820503ae4fecfdb841b5b6acc8718c8c5b298cf6b8f2259010f370052341cec8").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "sha256:820503ae4fecfdb841b5b6acc8718c8c5b298cf6b8f2259010f370052341cec8")). + Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARM64, multi_arch.LinuxAMD64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should create index with multiple tags", func() { + testRepo := indexTestRepo("create", "multiple-tags") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", fmt.Sprintf("%s,t1,t2,t3", RegistryRef(ZOTHost, testRepo, "t0")), + "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", "linux-arm64", "linux-amd64"). + MatchKeyWords("Fetched", "Pushed", "Tagged", + "sha256:bfa1728d6292d5fa7689f8f4daa145ee6f067b5779528c6e059d1132745ef508").Exec() + // verify + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARMV7, multi_arch.LinuxARM64, multi_arch.LinuxAMD64} + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t0")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t1")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t2")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t3")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + }) + + It("should create nested indexes", func() { + testRepo := indexTestRepo("create", "nested-index") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "nested"), "multi").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "nested")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.DescriptorObject} + ValidateIndex(content, expectedManifests) + }) + + It("should create index from image with non-json config", func() { + testRepo := indexTestRepo("create", "nonjson-config") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "unusual-config"), + "nonjson-config").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "unusual-config")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{nonjson_config.Descriptor} + ValidateIndex(content, expectedManifests) + }) + + It("should fail if given a reference that does not exist in the repo", func() { + testRepo := indexTestRepo("create", "nonexist-ref") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + }) +}) + +var _ = Describe("OCI image layout users:", func() { + When("running `manifest index create`", func() { + It("should create an index with source manifest digest", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest)). + WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index with source manifest tag", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64"). + WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index without tagging it", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64"). + WithWorkDir(root).MatchKeyWords("Digest: sha256:c543059818cb70e6442597a33454ec1e3d3a2bdb526c17875578d33c2ddcf72e").Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "sha256:c543059818cb70e6442597a33454ec1e3d3a2bdb526c17875578d33c2ddcf72e")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index with multiple tags", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := fmt.Sprintf("%s,t1,t2,t3", LayoutRef(root, "t0")) + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64").WithWorkDir(root).Exec() + // verify + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t0")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t1")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t2")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t3")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + }) + + It("should create nested indexes", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "nested-index") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "multi").WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.DescriptorObject} + ValidateIndex(content, expectedManifests) + }) + + It("should create index from image with non-json config", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "unusual-config") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "nonjson-config").WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{nonjson_config.Descriptor} + ValidateIndex(content, expectedManifests) + }) + + It("should fail if given a reference that does not exist in the repo", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + + It("should fail if given a digest that is not a manifest", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "sha256:02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2").ExpectFailure(). + MatchErrKeyWords("is not a manifest").Exec() + }) + }) +}) diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 b/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 new file mode 100644 index 000000000..5236c8555 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 @@ -0,0 +1 @@ +sometext diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a b/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a new file mode 100644 index 000000000..f3147bc70 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a @@ -0,0 +1 @@ +my artifact diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 b/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 new file mode 100644 index 000000000..44f74ea02 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.unknown.config.v1+json","digest":"sha256:02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2","size":9},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a","size":12,"annotations":{"org.opencontainers.image.title":"artifactfile"}}],"annotations":{"org.opencontainers.image.created":"2024-08-22T08:22:13Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/zot/command/images/index.json b/test/e2e/testdata/zot/command/images/index.json index 2162bc883..9de2694d3 100644 --- a/test/e2e/testdata/zot/command/images/index.json +++ b/test/e2e/testdata/zot/command/images/index.json @@ -1,57 +1,63 @@ { - "schemaVersion": 2, - "manifests": [ - { - "mediaType": "application/vnd.oci.image.index.v1+json", - "digest": "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", - "size": 706, - "annotations": { - "org.opencontainers.image.ref.name": "multi" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", - "size": 851, - "annotations": { - "org.opencontainers.image.ref.name": "foobar" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", - "size": 458, - "platform": { - "architecture": "arm", - "os": "linux", - "variant": "v7" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", - "size": 458, - "platform": { - "architecture": "arm64", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", - "size": 458, - "platform": { - "architecture": "amd64", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.index.v1+json", - "digest": "sha256:b2a5fcfb112ccde647a5a3dc0215c2c9e7d0ce598924a5ec48aa85beca048286", - "size": 89, - "annotations": { - "org.opencontainers.image.ref.name": "empty_index" - } - } - ] + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-armv7" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", + "size": 529, + "annotations": { + "org.opencontainers.image.created": "2024-08-22T08:22:13Z", + "org.opencontainers.image.ref.name": "nonjson-config" + }, + "artifactType": "application/vnd.unknown.config.v1+json" + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", + "size": 851, + "annotations": { + "org.opencontainers.image.ref.name": "foobar" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-arm64" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:b2a5fcfb112ccde647a5a3dc0215c2c9e7d0ce598924a5ec48aa85beca048286", + "size": 89, + "annotations": { + "org.opencontainers.image.ref.name": "empty_index" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", + "size": 706, + "annotations": { + "org.opencontainers.image.ref.name": "multi" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-amd64" + } + } + ] } \ No newline at end of file