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: add copyCmd() #432

Merged
merged 18 commits into from
Jul 8, 2022
Merged
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
138 changes: 138 additions & 0 deletions cmd/oras/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
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 main

import (
"context"
"fmt"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
"oras.land/oras-go/v2"
"oras.land/oras/cmd/oras/internal/display"
"oras.land/oras/cmd/oras/internal/option"
)

type copyOptions struct {
src option.Remote
dst option.Remote
option.Common
rescursive bool

srcRef string
dstRef string
}

func copyCmd() *cobra.Command {
var opts copyOptions
cmd := &cobra.Command{
Use: "copy <from-ref> <to-ref>",
Aliases: []string{"cp"},
Short: "[Preview] Copy artifacts from one target to another",
Long: `[Preview] Copy artifacts from one target to another

** This command is in preview and under development. **

Examples - Copy the artifact tagged '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

Examples - Copy the artifact tagged '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
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.srcRef = args[0]
opts.dstRef = args[1]
return runCopy(opts)
},
}

cmd.Flags().BoolVarP(&opts.rescursive, "recursive", "r", false, "recursively copy artifacts and its referrer artifacts")
opts.src.ApplyFlagsWithPrefix(cmd.Flags(), "from", "source")
opts.dst.ApplyFlagsWithPrefix(cmd.Flags(), "to", "destination")
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)
if err != nil {
return err
}

// Prepare destination
dst, err := opts.dst.NewRepository(opts.dstRef, opts.Common)
if err != nil {
return err
}

// Prepare copy options
extendedCopyOptions := oras.DefaultExtendedCopyOptions
outputStatus := func(status string) func(context.Context, ocispec.Descriptor) error {
return func(ctx context.Context, desc ocispec.Descriptor) error {
name, ok := desc.Annotations[ocispec.AnnotationTitle]
if !ok {
if !opts.Verbose {
return nil
}
name = desc.MediaType
}
return display.Print(status, display.ShortDigest(desc), name)
}
}
extendedCopyOptions.PreCopy = outputStatus("Copying")
extendedCopyOptions.PostCopy = outputStatus("Copied ")
extendedCopyOptions.OnCopySkipped = outputStatus("Exists ")

if src.Reference.Reference == "" {
return newErrInvalidReference(src.Reference)
}

// push to the destination with digest only if no tag specified
var desc ocispec.Descriptor
if ref := dst.Reference.Reference; ref == "" {
desc, err = src.Resolve(ctx, src.Reference.Reference)
if err != nil {
return err
}
if opts.rescursive {
err = oras.ExtendedCopyGraph(ctx, src, dst, desc, extendedCopyOptions.ExtendedCopyGraphOptions)
} else {
err = oras.CopyGraph(ctx, src, dst, desc, extendedCopyOptions.CopyGraphOptions)
}
} else {
if opts.rescursive {
desc, err = oras.ExtendedCopy(ctx, src, opts.srcRef, dst, opts.dstRef, extendedCopyOptions)
} else {
copyOptions := oras.CopyOptions{
CopyGraphOptions: extendedCopyOptions.CopyGraphOptions,
}
desc, err = oras.Copy(ctx, src, opts.srcRef, dst, opts.dstRef, copyOptions)
}
}
if err != nil {
return err
}

fmt.Println("Copied", opts.srcRef, "=>", opts.dstRef)
fmt.Println("Digest:", desc.Digest)

return nil
}
33 changes: 27 additions & 6 deletions cmd/oras/internal/option/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,34 @@ type Remote struct {

// ApplyFlags applies flags to a command flag set.
func (opts *Remote) ApplyFlags(fs *pflag.FlagSet) {
fs.StringArrayVarP(&opts.Configs, "config", "c", nil, "auth config path")
fs.StringVarP(&opts.Username, "username", "u", "", "registry username")
fs.StringVarP(&opts.Password, "password", "p", "", "registry password or identity token")
opts.ApplyFlagsWithPrefix(fs, "", "")
fs.BoolVarP(&opts.PasswordFromStdin, "password-stdin", "", false, "read password or identity token from stdin")
fs.BoolVarP(&opts.Insecure, "insecure", "", false, "allow connections to SSL registry without certs")
fs.StringVarP(&opts.CACertFilePath, "ca-file", "", "", "server certificate authority file for the remote registry")
fs.BoolVarP(&opts.PlainHTTP, "plain-http", "", false, "allow insecure connections to registry without SSL")
}

// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
// Commonly used for non-unary remote targets.
func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
var (
shortUser string
shortPassword string
flagPrefix string
notePrefix string
)
if prefix == "" {
shortUser, shortPassword = "u", "p"
} else {
flagPrefix = prefix + "-"
notePrefix = description + " "
}
fs.StringVarP(&opts.Username, flagPrefix+"username", shortUser, "", notePrefix+"registry username")
fs.StringVarP(&opts.Password, flagPrefix+"password", shortPassword, "", notePrefix+"registry password or identity token")
fs.BoolVarP(&opts.Insecure, flagPrefix+"insecure", "", false, "allow connections to "+notePrefix+"SSL registry without certs")
fs.BoolVarP(&opts.PlainHTTP, flagPrefix+"plain-http", "", false, "allow insecure connections to "+notePrefix+"registry without SSL check")
fs.StringVarP(&opts.CACertFilePath, flagPrefix+"ca-file", "", "", "server certificate authority file for the remote "+notePrefix+"registry")

if fs.Lookup("config") != nil {
fs.StringArrayVarP(&opts.Configs, "config", "c", nil, "auth config path")
}
}

// ReadPassword tries to read password with optional cmd prompt.
Expand Down
10 changes: 9 additions & 1 deletion cmd/oras/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ func main() {
Use: "oras [command]",
SilenceUsage: true,
}
cmd.AddCommand(pullCmd(), pushCmd(), loginCmd(), logoutCmd(), versionCmd(), discoverCmd())
cmd.AddCommand(
pullCmd(),
pushCmd(),
loginCmd(),
logoutCmd(),
versionCmd(),
discoverCmd(),
copyCmd(),
)
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
Expand Down