Skip to content

Commit

Permalink
feat(oci-layout): support in oras cp (#748)
Browse files Browse the repository at this point in the history
Related to #378

Signed-off-by: Billy Zha <[email protected]>
  • Loading branch information
qweeah authored Jan 17, 2023
1 parent 9013115 commit 6447e8e
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 57 deletions.
71 changes: 36 additions & 35 deletions cmd/oras/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,12 @@ import (
)

type copyOptions struct {
src option.Remote
dst option.Remote
option.Common
option.Platform
recursive bool
option.BinaryTarget

recursive bool
concurrency int
srcRef string
dstRef string
extraRefs []string
}

Expand All @@ -52,54 +49,62 @@ func copyCmd() *cobra.Command {
** This command is in preview and under development. **
Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net-monitor' to repository 'localhost:5000/net-monitor-copy'
oras cp localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1
Example - Copy an artifact between registries:
oras cp localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Example - Copy the artifact tagged with 'v1' and its referrers from repository 'localhost:5000/net-monitor' to 'localhost:5000/net-monitor-copy'
oras cp -r localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1
Example - Download an artifact into an OCI layout folder:
oras cp --to-oci-layout localhost:5000/net-monitor:v1 ./downloaded:v1
Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net-monitor' to 'localhost:5000/net-monitor-copy' with certain platform
oras cp --platform linux/arm/v5 localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1
Example - Upload an artifact from an OCI layout folder:
oras cp --from-oci-layout ./to-upload:v1 localhost:5000/net-monitor:v1
Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net-monitor' to 'localhost:5000/net-monitor-copy' with multiple tags
oras cp localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1,tag2,tag3
Example - Upload an artifact from an OCI layout tar archive:
oras cp --from-oci-layout ./to-upload.tar:v1 localhost:5000/net-monitor:v1
Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net-monitor' to 'localhost:5000/net-monitor-copy' with multiple tags and concurrency level tuned
oras cp --concurrency 6 localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1,tag2,tag3
Example - Copy an artifact and its referrers:
oras cp -r localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Example - Copy certain platform of an artifact:
oras cp --platform linux/arm/v5 localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Example - Copy an artifact with multiple tags:
oras cp localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:tag1,tag2,tag3
Example - Copy an artifact with multiple tags with concurrency tuned:
oras cp --concurrency 10 localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:tag1,tag2,tag3
`,
Args: cobra.ExactArgs(2),
PreRunE: func(cmd *cobra.Command, args []string) error {
opts.From.RawReference = args[0]
refs := strings.Split(args[1], ",")
opts.To.RawReference = refs[0]
opts.extraRefs = refs[1:]
return option.Parse(&opts)
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.srcRef = args[0]
refs := strings.Split(args[1], ",")
opts.dstRef = refs[0]
opts.extraRefs = refs[1:]
return runCopy(opts)
},
}

cmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "recursively copy the artifact and its referrer artifacts")
opts.src.ApplyFlagsWithPrefix(cmd.Flags(), "from", "source")
opts.dst.ApplyFlagsWithPrefix(cmd.Flags(), "to", "destination")
cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 3, "concurrency level")
option.ApplyFlags(&opts, cmd.Flags())

return cmd
}

func runCopy(opts copyOptions) error {
ctx, _ := opts.SetLoggerLevel()

// Prepare source
src, err := opts.src.NewRepository(opts.srcRef, opts.Common)
src, err := opts.From.NewReadonlyTarget(ctx, opts.Common)
if err != nil {
return err
}
if opts.From.Reference == "" {
return errors.NewErrInvalidReferenceStr(opts.From.RawReference)
}

// Prepare destination
dst, err := opts.dst.NewRepository(opts.dstRef, opts.Common)
dst, err := opts.To.NewTarget(opts.Common)
if err != nil {
return err
}
Expand All @@ -121,14 +126,10 @@ func runCopy(opts copyOptions) error {
return display.PrintStatus(desc, "Exists ", opts.Verbose)
}

if src.Reference.Reference == "" {
return errors.NewErrInvalidReference(src.Reference)
}

var desc ocispec.Descriptor
if ref := dst.Reference.Reference; ref == "" {
if ref := opts.To.Reference; ref == "" {
// push to the destination with digest only if no tag specified
desc, err = src.Resolve(ctx, src.Reference.Reference)
desc, err = src.Resolve(ctx, opts.From.Reference)
if err != nil {
return err
}
Expand All @@ -139,27 +140,27 @@ func runCopy(opts copyOptions) error {
}
} else {
if opts.recursive {
desc, err = oras.ExtendedCopy(ctx, src, opts.srcRef, dst, opts.dstRef, extendedCopyOptions)
desc, err = oras.ExtendedCopy(ctx, src, opts.From.Reference, dst, opts.To.Reference, extendedCopyOptions)
} else {
copyOptions := oras.CopyOptions{
CopyGraphOptions: extendedCopyOptions.CopyGraphOptions,
}
if opts.Platform.Platform != nil {
copyOptions.WithTargetPlatform(opts.Platform.Platform)
}
desc, err = oras.Copy(ctx, src, opts.srcRef, dst, opts.dstRef, copyOptions)
desc, err = oras.Copy(ctx, src, opts.From.Reference, dst, opts.To.Reference, copyOptions)
}
}
if err != nil {
return err
}

fmt.Println("Copied", opts.srcRef, "=>", opts.dstRef)
fmt.Println("Copied", opts.From.AnnotatedReference(), "=>", opts.To.AnnotatedReference())

if len(opts.extraRefs) != 0 {
tagNOpts := oras.DefaultTagNOptions
tagNOpts.Concurrency = opts.concurrency
if err = oras.TagN(ctx, &display.TagManifestStatusPrinter{Repository: dst}, opts.dstRef, opts.extraRefs, tagNOpts); err != nil {
if _, err = oras.TagN(ctx, display.NewTagManifestStatusPrinter(dst), opts.To.Reference, opts.extraRefs, tagNOpts); err != nil {
return err
}
}
Expand Down
33 changes: 29 additions & 4 deletions cmd/oras/internal/display/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import (
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry"
)

var printLock sync.Mutex
Expand Down Expand Up @@ -74,14 +75,38 @@ func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, status s
return nil
}

type TagManifestStatusPrinter struct {
*remote.Repository
// NewTagManifestStatusPrinter creates a wrapper type for printing tag status.
func NewTagManifestStatusPrinter(target oras.Target) oras.Target {
if repo, ok := target.(registry.Repository); ok {
return &tagManifestStatusForRepo{
Repository: repo,
}
}
return &tagManifestStatusForTarget{
Target: target,
}
}

type tagManifestStatusForRepo struct {
registry.Repository
}

// PushReference overrides Repository.PushReference method to print off which tag(s) were added successfully.
func (p *TagManifestStatusPrinter) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
func (p *tagManifestStatusForRepo) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
if err := p.Repository.PushReference(ctx, expected, content, reference); err != nil {
return err
}
return Print("Tagged", reference)
}

type tagManifestStatusForTarget struct {
oras.Target
}

// Tag tags a descriptor with a reference string.
func (p *tagManifestStatusForTarget) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
if err := p.Target.Tag(ctx, desc, reference); err != nil {
return err
}
return Print("Tagged", reference)
}
5 changes: 5 additions & 0 deletions cmd/oras/internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ import (

// NewErrInvalidReference creates a new error based on the reference string.
func NewErrInvalidReference(ref registry.Reference) error {
return NewErrInvalidReferenceStr(ref.String())
}

// NewErrInvalidReferenceStr creates a new error based on the reference string.
func NewErrInvalidReferenceStr(ref string) error {
return fmt.Errorf("%s: invalid image reference, expecting <name:tag|name@digest>", ref)
}
8 changes: 4 additions & 4 deletions cmd/oras/internal/fileref/unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ import (
)

// Parse parses file reference on unix.
func Parse(reference string, defaultMediaType string) (filePath, mediaType string, err error) {
func Parse(reference string, defaultMetadata string) (filePath, metadata string, err error) {
i := strings.LastIndex(reference, ":")
if i < 0 {
filePath, mediaType = reference, defaultMediaType
filePath, metadata = reference, defaultMetadata
} else {
filePath, mediaType = reference[:i], reference[i+1:]
filePath, metadata = reference[:i], reference[i+1:]
}
if filePath == "" {
return "", "", fmt.Errorf("found empty file path in %q", reference)
}
return filePath, mediaType, nil
return filePath, metadata, nil
}
14 changes: 7 additions & 7 deletions cmd/oras/internal/fileref/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,24 @@ import (
"unicode"
)

// Parse parses file reference on windows.
func Parse(reference string, defaultMediaType string) (filePath, mediaType string, err error) {
filePath, mediaType = doParse(reference, defaultMediaType)
// Parse parses file reference into filePath and metadata.
func Parse(reference string, defaultMetadata string) (filePath, metadata string, err error) {
filePath, metadata = doParse(reference, defaultMetadata)
if filePath == "" {
return "", "", fmt.Errorf("found empty file path in %q", reference)
}
if strings.ContainsAny(filePath, `<>:"|?*`) {
if strings.ContainsAny(filePath, `<>"|?*`) {
// Reference: https://learn.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions
return "", "", fmt.Errorf("reserved characters found in the file path: %s", filePath)
}
return filePath, mediaType, nil
return filePath, metadata, nil
}

func doParse(reference string, mediaType string) (filePath, mediatype string) {
func doParse(reference string, defaultMetadata string) (filePath, metadata string) {
i := strings.LastIndex(reference, ":")
if i < 0 || (i == 1 && len(reference) > 2 && unicode.IsLetter(rune(reference[0])) && reference[2] == '\\') {
// Relative file path with disk prefix is NOT supported, e.g. `c:file1`
return reference, mediaType
return reference, defaultMetadata
}
return reference[:i], reference[i+1:]
}
3 changes: 2 additions & 1 deletion cmd/oras/internal/fileref/windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func TestParse(t *testing.T) {
wantMediatype string
wantErr bool
}{
{"valid file name", args{`c:\some-folder\test`, ""}, `c:\some-folder\test`, "", false},
{"valid file name and media type", args{`c:\some-folder\test`, "type"}, `c:\some-folder\test`, "type", false},
{"no input", args{"", ""}, "", "", true},
{"empty file name", args{":", ""}, "", "", true},
{"reserved character1 in file name", args{"<", "a"}, "", "", true},
Expand All @@ -97,7 +99,6 @@ func TestParse(t *testing.T) {
{"reserved character4 in file name, with media type", args{`":`, "a"}, "", "", true},
{"reserved character5 in file name, with media type", args{"|:", "a"}, "", "", true},
{"reserved character6 in file name, with media type", args{"?:", "a"}, "", "", true},
{"reserved character7 in file name, with media type", args{"::", "a"}, "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Loading

0 comments on commit 6447e8e

Please sign in to comment.