diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index f8afa6a3c..43b9100e4 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -16,9 +16,12 @@ limitations under the License. package root import ( + "bytes" + "encoding/json" "errors" "strings" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "oras.land/oras-go/v2" @@ -41,6 +44,7 @@ import ( type pushOptions struct { option.Common option.Packer + option.Platform option.ImageSpec option.Target option.Format @@ -97,6 +101,9 @@ Example - Push repository with manifest annotations: Example - Push repository with manifest annotation file: oras push --annotation-file annotation.json localhost:5000/hello:v1 +Example - Push repository with platform: + oras push --platform linux/arm/v5 localhost:5000/hello:v1 + Example - Push file "hi.txt" with multiple tags: oras push localhost:5000/hello:tag1,tag2,tag3 hi.txt @@ -116,7 +123,7 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t return err } - if opts.manifestConfigRef != "" && opts.artifactType == "" { + if (opts.manifestConfigRef != "" || opts.Platform.Platform != nil) && opts.artifactType == "" { if !cmd.Flags().Changed("image-spec") { // switch to v1.0 manifest since artifact type is suggested // by OCI v1.1 artifact guidance but is not presented @@ -136,6 +143,9 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t if opts.manifestConfigRef != "" && opts.artifactType != "" { return errors.New("--artifact-type and --config cannot both be provided for 1.0 OCI image") } + if opts.Platform.Platform != nil && opts.artifactType != "" { + return errors.New("--artifact-type and --platform cannot both be provided for 1.0 OCI image") + } case oras.PackManifestVersion1_1: if opts.manifestConfigRef == "" && opts.artifactType == "" { opts.artifactType = oras.MediaTypeUnknownArtifact @@ -152,6 +162,7 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level") opts.SetTypes(option.FormatTypeText, option.FormatTypeJSON, option.FormatTypeGoTemplate) option.ApplyFlags(&opts, cmd.Flags()) + cmd.MarkFlagsMutuallyExclusive("config", "platform") return oerrors.Command(cmd, &opts.Target) } @@ -183,6 +194,22 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { } desc.Annotations = packOpts.ConfigAnnotations packOpts.ConfigDescriptor = &desc + } else if opts.Platform.Platform != nil { + blob, err := json.Marshal(opts.Platform.Platform) + if err != nil { + return err + } + desc := ocispec.Descriptor{ + MediaType: oras.MediaTypeUnknownConfig, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + err = store.Push(ctx, desc, bytes.NewReader(blob)) + if err != nil { + return err + } + desc.Annotations = packOpts.ConfigAnnotations + packOpts.ConfigDescriptor = &desc } memoryStore := memory.New() union := contentutil.MultiReadOnlyTarget(memoryStore, store) diff --git a/test/e2e/internal/testdata/foobar/const.go b/test/e2e/internal/testdata/foobar/const.go index dc3de4a89..defe57785 100644 --- a/test/e2e/internal/testdata/foobar/const.go +++ b/test/e2e/internal/testdata/foobar/const.go @@ -38,6 +38,12 @@ var ( Digest: "46b68ac1696c", Name: "application/vnd.unknown.config.v1+json", } + PlatformConfigSize = 38 + PlatformConfigDigest = digest.Digest("sha256:e94c0ba80a11") + PlatformConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + } + FileBarName = "foobar/bar" FileBarStateKey = match.StateKey{Digest: "fcde2b2edba5", Name: FileLayerNames[2]} FileStateKeys = []match.StateKey{ diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 5cb644a66..8aa65d6ae 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -77,6 +77,22 @@ var _ = Describe("ORAS beginners:", func() { ORAS("push", ref, "--config", foobar.FileConfigName, "--artifact-type", "test/artifact+json", "--image-spec", "v1.0").ExpectFailure().WithWorkDir(tempDir).Exec() }) + It("should fail to use --platform and --artifact-type at the same time for OCI spec v1.0 registry", func() { + tempDir := PrepareTempFiles() + repo := pushTestRepo("no-mediatype") + ref := RegistryRef(ZOTHost, repo, "") + + ORAS("push", ref, "--platform", "linux/amd64", "--artifact-type", "test/artifact+json", "--image-spec", "v1.0").ExpectFailure().WithWorkDir(tempDir).Exec() + }) + + It("should fail to use --platform and --config at the same time", func() { + tempDir := PrepareTempFiles() + repo := pushTestRepo("no-mediatype") + ref := RegistryRef(ZOTHost, repo, "") + + ORAS("push", ref, "--platform", "linux/amd64", "--config", foobar.FileConfigName).ExpectFailure().WithWorkDir(tempDir).Exec() + }) + It("should fail if image spec is not valid", func() { testRepo := attachTestRepo("invalid-image-spec") subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) @@ -116,6 +132,16 @@ var _ = Describe("ORAS beginners:", func() { MatchErrKeyWords("missing artifact type for OCI image-spec v1.1 artifacts"). Exec() }) + + It("should fail if image spec v1.1 is used, with --platform and without --artifactType", func() { + testRepo := pushTestRepo("v1-1/no-artifact-type") + subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) + imageSpecFlag := "v1.1" + ORAS("push", subjectRef, "--platform", "linux/amd64", Flags.ImageSpec, imageSpecFlag). + ExpectFailure(). + MatchErrKeyWords("missing artifact type for OCI image-spec v1.1 artifacts"). + Exec() + }) }) }) @@ -612,6 +638,26 @@ var _ = Describe("OCI image layout users:", func() { })) }) + It("should push files with platform", func() { + tempDir := PrepareTempFiles() + ref := LayoutRef(tempDir, tag) + ORAS("push", Flags.Layout, ref, "--platform", "darwin/arm64", foobar.FileBarName, "-v"). + MatchStatus([]match.StateKey{ + foobar.PlatformConfigStateKey, + foobar.FileBarStateKey, + }, true, 2). + WithWorkDir(tempDir).Exec() + // validate + fetched := ORAS("manifest", "fetch", Flags.Layout, ref).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ + MediaType: foobar.PlatformConfigStateKey.Name, + Size: int64(foobar.PlatformConfigSize), + Digest: foobar.PlatformConfigDigest, + })) + }) + It("should push files with customized manifest annotation", func() { tempDir := PrepareTempFiles() ref := LayoutRef(tempDir, tag)