diff --git a/cmd/oras/internal/option/platform.go b/cmd/oras/internal/option/platform.go index 37cbea1d9..e8181e035 100644 --- a/cmd/oras/internal/option/platform.go +++ b/cmd/oras/internal/option/platform.go @@ -40,7 +40,7 @@ func (opts *Platform) ApplyFlags(fs *pflag.FlagSet) { fs.StringVarP(&opts.platform, "platform", "", "", opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") } -// parse parses the input platform flag to an oci platform type. +// Parse parses the input platform flag to an oci platform type. func (opts *Platform) Parse(*cobra.Command) error { if opts.platform == "" { return nil @@ -73,3 +73,19 @@ func (opts *Platform) Parse(*cobra.Command) error { opts.Platform = &p return nil } + +// ArtifactPlatform option struct. +type ArtifactPlatform struct { + Platform +} + +// ApplyFlags applies flags to a command flag set. +func (opts *ArtifactPlatform) ApplyFlags(fs *pflag.FlagSet) { + opts.FlagDescription = "set artifact platform" + fs.StringVarP(&opts.platform, "artifact-platform", "", "", opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") +} + +// Parse parses the input platform flag to an oci platform type. +func (opts *ArtifactPlatform) Parse(cmd *cobra.Command) error { + return opts.Platform.Parse(cmd) +} diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 4c7ff4d22..4e942de40 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -16,6 +16,8 @@ limitations under the License. package root import ( + "bytes" + "encoding/json" "errors" "strings" @@ -41,6 +43,7 @@ import ( type pushOptions struct { option.Common option.Packer + option.ArtifactPlatform option.ImageSpec option.Target option.Format @@ -97,6 +100,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 artifact to repository with platform: + oras push --artifact-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 +122,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 @@ -130,12 +136,19 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t } } } + configAndPlatform := []string{"config", "artifact-platform"} + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), configAndPlatform...); err != nil { + return err + } switch opts.PackVersion { case oras.PackManifestVersion1_0: 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-platform and --artifact-type cannot both be provided for 1.0 OCI image") + } case oras.PackManifestVersion1_1: if opts.manifestConfigRef == "" && opts.artifactType == "" { opts.artifactType = oras.MediaTypeUnknownArtifact @@ -179,6 +192,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 + } + mediaType := oras.MediaTypeUnknownConfig + if opts.Flag == option.ImageSpecV1_0 && opts.artifactType != "" { + mediaType = opts.artifactType + } + desc := content.NewDescriptorFromBytes(mediaType, 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..b4c1265f6 100644 --- a/test/e2e/internal/testdata/foobar/const.go +++ b/test/e2e/internal/testdata/foobar/const.go @@ -38,6 +38,18 @@ var ( Digest: "46b68ac1696c", Name: "application/vnd.unknown.config.v1+json", } + PlatformConfigSize = 38 + PlatformConfigDigest = digest.Digest("sha256:e94c0ba80a1157ffab5b5c6656fffc089c6446c7ed0604f3382910d1ef7dd40d") + PlatformConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + } + + PlatformV10ConfigSize = 38 + PlatformV10ConfigDigest = digest.Digest("sha256:e94c0ba80a1157ffab5b5c6656fffc089c6446c7ed0604f3382910d1ef7dd40d") + PlatformV10ConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "test/artifact+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..12d73a57f 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -77,6 +77,14 @@ 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 --artifact-platform and --config at the same time", func() { + tempDir := PrepareTempFiles() + repo := pushTestRepo("no-mediatype") + ref := RegistryRef(ZOTHost, repo, "") + + ORAS("push", ref, "--artifact-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 +124,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 --artifact-platform and without --artifactType", func() { + testRepo := pushTestRepo("v1-1/no-artifact-type") + subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) + imageSpecFlag := "v1.1" + ORAS("push", subjectRef, "--artifact-platform", "linux/amd64", Flags.ImageSpec, imageSpecFlag). + ExpectFailure(). + MatchErrKeyWords("missing artifact type for OCI image-spec v1.1 artifacts"). + Exec() + }) }) }) @@ -612,6 +630,46 @@ 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, "--artifact-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 platform with mediaType as artifactType for v1.0", func() { + tempDir := PrepareTempFiles() + ref := LayoutRef(tempDir, tag) + ORAS("push", Flags.Layout, ref, "--image-spec", "v1.0", "--artifact-type", "test/artifact+json", "--artifact-platform", "darwin/arm64", foobar.FileBarName, "-v"). + MatchStatus([]match.StateKey{ + foobar.PlatformV10ConfigStateKey, + 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: "test/artifact+json", + Size: int64(foobar.PlatformV10ConfigSize), + Digest: foobar.PlatformV10ConfigDigest, + })) + }) + It("should push files with customized manifest annotation", func() { tempDir := PrepareTempFiles() ref := LayoutRef(tempDir, tag)