Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oci-layout): support in oras cp #748

Merged
merged 32 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
83c4af1
feat: support oci image layout in `oras cp`
qweeah Jan 13, 2023
f8d4a27
add example
qweeah Jan 13, 2023
2026d9d
add example
qweeah Jan 13, 2023
071c792
add go mod
qweeah Jan 13, 2023
c244cd5
rename
qweeah Jan 13, 2023
a5fd875
doc clean
qweeah Jan 13, 2023
d4f96d3
update license header
qweeah Jan 13, 2023
b0e8e28
add interface for printer
qweeah Jan 13, 2023
3df7915
doc clean
qweeah Jan 13, 2023
ec2f556
rename
qweeah Jan 13, 2023
07dba15
Merge remote-tracking branch 'origin_src/main' into oci-copy
qweeah Jan 13, 2023
f5bb892
update examples
qweeah Jan 13, 2023
6dbd0a6
update doc
qweeah Jan 13, 2023
d71860c
doc clean
qweeah Jan 13, 2023
6d11cef
resolve comments
qweeah Jan 13, 2023
01c5674
add wrapper type for tag status printer
qweeah Jan 13, 2023
c20e948
remove long oci image layout flag
qweeah Jan 13, 2023
3962a55
doc clean
qweeah Jan 13, 2023
33f6561
resolve comments
qweeah Jan 14, 2023
fca9db1
Merge remote-tracking branch 'origin_src/main' into oci-copy
qweeah Jan 14, 2023
2a70519
fix unit test
qweeah Jan 14, 2023
6036d0d
rename target type
qweeah Jan 16, 2023
1e463d4
simplify examples
qweeah Jan 16, 2023
c83b54b
fix bug for windows path parsing and add tests
qweeah Jan 16, 2023
b4b2a6c
fix unit test
qweeah Jan 16, 2023
1585f5a
resolve comments
qweeah Jan 16, 2023
12fc8ea
doc clean
qweeah Jan 16, 2023
87771b6
doc clean
qweeah Jan 16, 2023
6af8f50
rename parsing arguments
qweeah Jan 16, 2023
56e29f9
doc clean
qweeah Jan 16, 2023
b9f5df3
resolve comments
qweeah Jan 16, 2023
440731e
fix doc
qweeah Jan 17, 2023
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
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 localhost:5000/net-monitor:v1 ./downloaded:v1
qweeah marked this conversation as resolved.
Show resolved Hide resolved

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 ./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 ./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
qweeah marked this conversation as resolved.
Show resolved Hide resolved

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.TagOrDigest == "" {
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.TagOrDigest; 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.TagOrDigest)
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.TagOrDigest, dst, opts.To.TagOrDigest, 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.TagOrDigest, dst, opts.To.TagOrDigest, 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.TagOrDigest, 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)
}
171 changes: 171 additions & 0 deletions cmd/oras/internal/option/target.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
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 option

import (
"context"
"fmt"
"os"
"strings"

"github.com/spf13/pflag"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras-go/v2/registry"
)

const (
qweeah marked this conversation as resolved.
Show resolved Hide resolved
TargetTypeRemote = "registry"
TargetTypeOCILayout = "oci"
)

// Unary target option struct.
qweeah marked this conversation as resolved.
Show resolved Hide resolved
type Target struct {
Remote
RawReference string
Type string
TagOrDigest string

isOCI bool
qweeah marked this conversation as resolved.
Show resolved Hide resolved
}

// ApplyFlags applies flags to a command flag set for unary target
func (opts *Target) ApplyFlags(fs *pflag.FlagSet) {
opts.applyFlagsWithPrefix(fs, "", "")
opts.Remote.ApplyFlags(fs)
}

// AnnotatedReference returns full printable reference.
func (opts *Target) AnnotatedReference() string {
return fmt.Sprintf("[%s] %s", opts.Type, opts.RawReference)
}

// applyFlagsWithPrefix applies flags to a command flag set with a prefix string.
// Commonly used for non-unary remote targets.
func (opts *Target) applyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
var (
flagPrefix string
noteSuffix string
)
if prefix != "" {
flagPrefix = prefix + "-"
noteSuffix = description + " "
}
fs.BoolVarP(&opts.isOCI, flagPrefix+"oci", "", false, "Set "+noteSuffix+"target as an OCI-layout.")
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
}

// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
// Commonly used for non-unary remote targets.
func (opts *Target) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
opts.applyFlagsWithPrefix(fs, prefix, description)
opts.Remote.ApplyFlagsWithPrefix(fs, prefix, description)
}

// Parse gets target options from user input.
func (opts *Target) Parse() error {
switch {
case opts.isOCI:
opts.Type = TargetTypeOCILayout
default:
opts.Type = TargetTypeRemote
}
return nil
}

qweeah marked this conversation as resolved.
Show resolved Hide resolved
func parseOCILayoutReference(raw string) (string, string) {
var path, ref string
if idx := strings.LastIndex(raw, "@"); idx != -1 {
// `digest` found
path = raw[:idx]
ref = raw[idx+1:]
} else if idx = strings.LastIndex(raw, ":"); idx != -1 {
// `tag` found
path = raw[:idx]
ref = raw[idx+1:]
}
return path, ref
}

// NewTarget generates a new target based on opts.
func (opts *Target) NewTarget(common Common) (graphTarget oras.GraphTarget, err error) {
switch opts.Type {
case TargetTypeOCILayout:
var path string
path, opts.TagOrDigest = parseOCILayoutReference(opts.RawReference)
graphTarget, err = oci.New(path)
return
case TargetTypeRemote:
repo, err := opts.NewRepository(opts.RawReference, common)
if err != nil {
return nil, err
}
opts.TagOrDigest = repo.Reference.Reference
return repo, nil
}
return nil, fmt.Errorf("unknown target type: %q", opts.Type)
}

// Read-only graph target with tag lister.
qweeah marked this conversation as resolved.
Show resolved Hide resolved
type ReadOnlyGraphTagFinderTarget interface {
oras.ReadOnlyGraphTarget
registry.TagLister
}

// NewReadonlyTargets generates a new read only target based on opts.
func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common) (ReadOnlyGraphTagFinderTarget, error) {
switch opts.Type {
case TargetTypeOCILayout:
var path string
path, opts.TagOrDigest = parseOCILayoutReference(opts.RawReference)
info, err := os.Stat(path)
if err != nil {
return nil, err
}
if info.IsDir() {
return oci.NewFromFS(ctx, os.DirFS(path))
}
return oci.NewFromTar(ctx, path)
case TargetTypeRemote:
repo, err := opts.NewRepository(opts.RawReference, common)
if err != nil {
return nil, err
}
opts.TagOrDigest = repo.Reference.Reference
return repo, nil
}
return nil, fmt.Errorf("unknown target type: %q", opts.Type)
}

// Binary target option struct.
qweeah marked this conversation as resolved.
Show resolved Hide resolved
type BinaryTarget struct {
From Target
To Target
}

// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
qweeah marked this conversation as resolved.
Show resolved Hide resolved
// Commonly used for non-unary remote targets.
func (opts *BinaryTarget) ApplyFlags(fs *pflag.FlagSet) {
opts.From.ApplyFlagsWithPrefix(fs, "from", "source")
opts.To.ApplyFlagsWithPrefix(fs, "to", "destination")
}

// Parse parses user-provided flags and arguments into option struct.
func (opts *BinaryTarget) Parse() error {
if err := opts.From.Parse(); err != nil {
return err
}
return opts.To.Parse()
}
Loading