1
15 package main
16
17 import (
18 "context"
19 "fmt"
20 "os"
21 "path/filepath"
22 "strings"
23
24 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
25 log "github.com/sirupsen/logrus"
26 "github.com/spf13/cobra"
27
28 "oras.land/oras-go/pkg/artifact"
29 "oras.land/oras-go/pkg/content"
30 "oras.land/oras-go/pkg/oras"
31 "oras.land/oras-go/pkg/target"
32 )
33
34 func main() {
35 var verbose int
36 cmd := &cobra.Command{
37 Use: fmt.Sprintf("%s [command]", os.Args[0]),
38 SilenceUsage: true,
39 PersistentPreRun: func(cmd *cobra.Command, args []string) {
40 log.SetLevel(log.InfoLevel)
41 if verbose > 1 {
42 log.SetLevel(log.DebugLevel)
43 }
44 },
45 }
46 cmd.AddCommand(copyCmd())
47 cmd.PersistentFlags().IntVarP(&verbose, "verbose", "v", 1, "set log level")
48 if err := cmd.Execute(); err != nil {
49 os.Exit(1)
50 }
51 }
52
53 func copyCmd() *cobra.Command {
54 var (
55 fromStr, toStr string
56 manifestConfig string
57 manifestAnnotations map[string]string
58 configAnnotations map[string]string
59 showRootManifest, showLayers bool
60 opts content.RegistryOptions
61 )
62 cmd := &cobra.Command{
63 Use: "copy <name:tag|name@digest>",
64 Short: "Copy artifacts from one location to another",
65 Long: `Copy artifacts from one location to another
66 Example - Copy artifacts from local files to local files:
67 oras copy foo/bar:v1 --from files --to files:path/to/save file1 file2 ... filen
68 Example - Copy artifacts from registry to local files:
69 oras copy foo/bar:v1 --from registry --to files:path/to/save
70 Example - Copy artifacts from registry to oci:
71 oras copy foo/bar:v1 --from registry --to oci:path/to/oci
72 Example - Copy artifacts from local files to registry:
73 oras copy foo/bar:v1 --from files --to registry file1 file2 ... filen
74
75 When the source (--from) is "files", the config by default will be "{}" and of media type
76 application/vnd.unknown.config.v1+json. You can override it by setting the path, for example:
77
78 oras copy foo/bar:v1 --from files --manifest-config path/to/config:application/vnd.oci.image.config.v1+json --to files:path/to/save file1 file2 ... filen
79
80
81 `,
82 Args: cobra.MinimumNArgs(1),
83 RunE: func(cmd *cobra.Command, args []string) error {
84 var (
85 ref = args[0]
86 err error
87 from, to target.Target
88 configDesc ocispec.Descriptor
89 )
90
91 fromParts := strings.SplitN(fromStr, ":", 2)
92 toParts := strings.SplitN(toStr, ":", 2)
93 switch fromParts[0] {
94 case "files":
95 fromFile := content.NewFile("")
96 descs, err := loadFiles(fromFile, args[1:]...)
97 if err != nil {
98 return fmt.Errorf("unable to load files: %w", err)
99 }
100
101 if manifestConfig != "" {
102 manifestConfigPath, manifestConfigMediaType := parseFileRef(manifestConfig, artifact.UnknownConfigMediaType)
103 configDesc, err = fromFile.Add("", manifestConfigMediaType, manifestConfigPath)
104 if err != nil {
105 return fmt.Errorf("unable to load manifest config: %w", err)
106 }
107 } else {
108 var config []byte
109 config, configDesc, err = content.GenerateConfig(configAnnotations)
110 if err != nil {
111 return fmt.Errorf("unable to create new manifest config: %w", err)
112 }
113 if err := fromFile.Load(configDesc, config); err != nil {
114 return fmt.Errorf("unable to load new manifest config: %w", err)
115 }
116 }
117 manifest, manifestDesc, err := content.GenerateManifest(&configDesc, manifestAnnotations, descs...)
118 if err != nil {
119 return fmt.Errorf("unable to create manifest: %w", err)
120 }
121 if err := fromFile.StoreManifest(ref, manifestDesc, manifest); err != nil {
122 return fmt.Errorf("unable to generate root manifest: %w", err)
123 }
124 rootDesc, rootManifest, err := fromFile.Ref(ref)
125 if err != nil {
126 return err
127 }
128 log.Debugf("root manifest: %s %v %s", ref, rootDesc, rootManifest)
129 from = fromFile
130 case "registry":
131 from, err = content.NewRegistry(opts)
132 if err != nil {
133 return fmt.Errorf("could not create registry target: %w", err)
134 }
135 case "oci":
136 from, err = content.NewOCI(fromParts[1])
137 if err != nil {
138 return fmt.Errorf("could not read OCI layout at %s: %w", fromParts[1], err)
139 }
140 default:
141 return fmt.Errorf("unknown from argyment: %s", from)
142 }
143
144 switch toParts[0] {
145 case "files":
146 to = content.NewFile(toParts[1])
147 case "registry":
148 to, err = content.NewRegistry(opts)
149 if err != nil {
150 return fmt.Errorf("could not create registry target: %w", err)
151 }
152 case "oci":
153 to, err = content.NewOCI(toParts[1])
154 if err != nil {
155 return fmt.Errorf("could not read OCI layout at %s: %v", toParts[1], err)
156 }
157 default:
158 return fmt.Errorf("unknown from argyment: %s", from)
159 }
160
161 if manifestConfig != "" && fromParts[0] != "files" {
162 return fmt.Errorf("only specify --manifest-config when using --from files")
163 }
164 var copyOpts []oras.CopyOpt
165 if showRootManifest {
166 copyOpts = append(copyOpts, oras.WithRootManifest(func(b []byte) {
167 fmt.Printf("root: %s\n", b)
168 }))
169 }
170 if showLayers {
171 copyOpts = append(copyOpts, oras.WithLayerDescriptors(func(layers []ocispec.Descriptor) {
172 fmt.Printf("%#v\n", layers)
173 }))
174 }
175 return runCopy(ref, from, to, copyOpts...)
176 },
177 }
178 cmd.Flags().StringVar(&fromStr, "from", "", "source type and possible options")
179 cmd.MarkFlagRequired("from")
180 cmd.Flags().StringVar(&toStr, "to", "", "destination type and possible options")
181 cmd.MarkFlagRequired("to")
182 cmd.Flags().StringArrayVarP(&opts.Configs, "config", "c", nil, "auth config path")
183 cmd.Flags().StringVarP(&opts.Username, "username", "u", "", "registry username")
184 cmd.Flags().StringVarP(&opts.Password, "password", "p", "", "registry password")
185 cmd.Flags().BoolVarP(&opts.Insecure, "insecure", "", false, "allow connections to SSL registry without certs")
186 cmd.Flags().BoolVarP(&opts.PlainHTTP, "plain-http", "", false, "use plain http and not https")
187 cmd.Flags().StringVar(&manifestConfig, "manifest-config", "", "path to manifest config and its media type, e.g. path/to/file.json:application/vnd.oci.image.config.v1+json")
188 cmd.Flags().StringToStringVar(&manifestAnnotations, "manifest-annotations", nil, "key-value pairs of annotations to set on the manifest, e.g. 'annotation=foo,other=bar'")
189 cmd.Flags().StringToStringVar(&configAnnotations, "config-annotations", nil, "key-value pairs of annotations to set on the config, only if config is not passed explicitly, e.g. 'annotation=foo,other=bar'")
190 cmd.Flags().BoolVarP(&showRootManifest, "show-manifest", "", false, "when copying, show the root manifest")
191 cmd.Flags().BoolVarP(&showLayers, "show-layers", "", false, "when copying, show the descriptors for the layers")
192 return cmd
193 }
194
195 func runCopy(ref string, from, to target.Target, copyOpts ...oras.CopyOpt) error {
196 desc, err := oras.Copy(context.Background(), from, ref, to, "", copyOpts...)
197 if err != nil {
198 fmt.Fprintf(os.Stderr, "error: %v", err)
199 os.Exit(1)
200 }
201 fmt.Printf("%#v\n", desc)
202 return nil
203 }
204
205 func loadFiles(store *content.File, files ...string) ([]ocispec.Descriptor, error) {
206 var descs []ocispec.Descriptor
207 for _, fileRef := range files {
208 filename, mediaType := parseFileRef(fileRef, "")
209 name := filepath.Clean(filename)
210 if !filepath.IsAbs(name) {
211
212 name = filepath.ToSlash(name)
213 }
214 desc, err := store.Add(name, mediaType, filename)
215 if err != nil {
216 return nil, err
217 }
218 descs = append(descs, desc)
219 }
220 return descs, nil
221 }
222
View as plain text