Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"oras.land/oras/cmd/oras/internal/command"
"oras.land/oras/cmd/oras/internal/display"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/fileref"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/graph"
"oras.land/oras/internal/registryutil"
Expand All @@ -43,8 +44,9 @@ type attachOptions struct {
option.Platform
option.Terminal

artifactType string
concurrency int
artifactType string
manifestConfigRef string
concurrency int
// Deprecated: verbose is deprecated and will be removed in the future.
verbose bool
}
Expand Down Expand Up @@ -92,6 +94,9 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder

Example - Attach file to the manifest tagged 'example.com:v1' in an OCI image layout folder 'layout-dir':
oras attach --artifact-type doc/example --oci-layout-path layout-dir example.com:v1 hi.txt

Example - Attach file 'hi.txt' with the custom manifest config "config.json" of the custom media type "application/vnd.me.config":
oras attach --artifact-type doc/example --config config.json:application/vnd.me.config localhost:5000/hello:v1 hi.txt
`,
Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination artifact for attaching."),
PreRunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -100,6 +105,9 @@ Example - Attach file to the manifest tagged 'example.com:v1' in an OCI image la
err := option.Parse(cmd, &opts)
if err == nil {
opts.DisableTTY(opts.Debug, false)
if err = oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "config", "platform"); err != nil {
return err
}
if err = opts.EnsureReferenceNotEmpty(cmd, true); err == nil {
return nil
}
Expand All @@ -121,6 +129,7 @@ Example - Attach file to the manifest tagged 'example.com:v1' in an OCI image la
}

cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type")
cmd.Flags().StringVarP(&opts.manifestConfigRef, "config", "", "", "`path` of image config file")
Comment thread
TerryHowe marked this conversation as resolved.
cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level")
cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", true, "print status output for unnamed blobs")
opts.FlagDescription = "attach to an arch-specific subject"
Expand Down Expand Up @@ -187,6 +196,18 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error {
ManifestAnnotations: opts.Annotations[option.AnnotationManifest],
Layers: descs,
}
if opts.manifestConfigRef != "" {
path, cfgMediaType, err := fileref.Parse(opts.manifestConfigRef, oras.MediaTypeUnknownConfig)
if err != nil {
return err
}
desc, err := addFile(ctx, store, option.AnnotationConfig, cfgMediaType, path)
if err != nil {
return err
}
desc.Annotations = opts.Annotations[option.AnnotationConfig]
packOpts.ConfigDescriptor = &desc
}
pack := func() (ocispec.Descriptor, error) {
return oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, opts.artifactType, packOpts)
}
Expand Down
27 changes: 27 additions & 0 deletions cmd/oras/root/attach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,40 @@ package root
import (
"context"
"errors"
"strings"
"testing"

"github.com/spf13/cobra"

"oras.land/oras/cmd/oras/internal/option"
)

func Test_attachCmd_configAndPlatformMutuallyExclusive(t *testing.T) {
cmd := attachCmd()
cmd.SetArgs([]string{
"--artifact-type", "doc/example",
"--config", "config.json",
"--platform", "linux/amd64",
"localhost:5000/hello:v1",
"hi.txt",
})
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), "cannot be used at the same time") {
t.Fatalf("expected mutual exclusion error, got %v", err)
}
}

func Test_attachCmd_configFlagRegistered(t *testing.T) {
cmd := attachCmd()
f := cmd.Flags().Lookup("config")
if f == nil {
t.Fatal("expected --config flag to be registered")
}
if f.Usage != "`path` of image config file" {
t.Fatalf("unexpected usage: %q", f.Usage)
}
}

func Test_runAttach_errType(t *testing.T) {
// prepare
cmd := &cobra.Command{}
Expand Down
67 changes: 67 additions & 0 deletions test/e2e/suite/command/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
package command

import (
"encoding/json"
"fmt"
"path/filepath"
"regexp"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/onsi/gomega"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/test/e2e/internal/testdata/feature"
"oras.land/oras/test/e2e/internal/testdata/foobar"
"oras.land/oras/test/e2e/internal/testdata/multi_arch"
Expand Down Expand Up @@ -271,6 +273,71 @@ var _ = Describe("1.1 registry users:", func() {
ExpectFailure().
Exec()
})

It("should attach a file to a subject with customized config file", func() {
// prepare
testRepo := attachTestRepo("config")
tempDir := PrepareTempFiles()
subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag)
CopyZOTRepo(ImageRepo, testRepo)
// test
ref := ORAS("attach", "--artifact-type", "test/attach", "--config", foobar.FileConfigName, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}").
WithWorkDir(tempDir).Exec().Out.Contents()
// validate
fetched := ORAS("manifest", "fetch", string(ref)).Exec().Out.Contents()
var manifest ocispec.Manifest
Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred())
Expect(manifest.Config).Should(Equal(ocispec.Descriptor{
MediaType: "application/vnd.unknown.config.v1+json",
Size: int64(foobar.FileConfigSize),
Digest: foobar.FileConfigDigest,
}))
})

It("should attach a file to a subject with customized config file and media type", func() {
// prepare
testRepo := attachTestRepo("config/mediatype")
configType := "config/type"
tempDir := PrepareTempFiles()
subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag)
CopyZOTRepo(ImageRepo, testRepo)
// test
ref := ORAS("attach", "--artifact-type", "test/attach", "--config", fmt.Sprintf("%s:%s", foobar.FileConfigName, configType), subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}").
WithWorkDir(tempDir).Exec().Out.Contents()
// validate
fetched := ORAS("manifest", "fetch", string(ref)).Exec().Out.Contents()
var manifest ocispec.Manifest
Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred())
Expect(manifest.Config).Should(Equal(ocispec.Descriptor{
MediaType: configType,
Size: int64(foobar.FileConfigSize),
Digest: foobar.FileConfigDigest,
}))
})

It("should fail to use --config and --platform at the same time", func() {
ref := RegistryRef(ZOTHost, ImageRepo, foobar.Tag)
tempDir := PrepareTempFiles()
ORAS("attach", "--artifact-type", "test/attach", "--config", foobar.FileConfigName, "--platform", "linux/amd64", ref, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)).
ExpectFailure().WithWorkDir(tempDir).Exec()
})

It("should fail when config file path is empty", func() {
testRepo := attachTestRepo("config/empty-path")
subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag)
CopyZOTRepo(ImageRepo, testRepo)
tempDir := PrepareTempFiles()
ORAS("attach", "--artifact-type", "test/attach", "--config", ":", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)).
ExpectFailure().WithWorkDir(tempDir).Exec()
})

It("should fail when config file does not exist", func() {
testRepo := attachTestRepo("config/notfound")
subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag)
CopyZOTRepo(ImageRepo, testRepo)
ORAS("attach", "--artifact-type", "test/attach", "--config", "nonexistent-config.json", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)).
ExpectFailure().Exec()
})
})
})

Expand Down