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 31 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-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},
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Loading