1
16
17 package cp
18
19 import (
20 "archive/tar"
21 "bytes"
22 "errors"
23 "fmt"
24 "io"
25 "os"
26 "strings"
27
28 "github.com/spf13/cobra"
29
30 "k8s.io/cli-runtime/pkg/genericiooptions"
31 "k8s.io/client-go/kubernetes"
32 restclient "k8s.io/client-go/rest"
33 "k8s.io/kubectl/pkg/cmd/exec"
34 cmdutil "k8s.io/kubectl/pkg/cmd/util"
35 "k8s.io/kubectl/pkg/util/completion"
36 "k8s.io/kubectl/pkg/util/i18n"
37 "k8s.io/kubectl/pkg/util/templates"
38 )
39
40 var (
41 cpExample = templates.Examples(i18n.T(`
42 # !!!Important Note!!!
43 # Requires that the 'tar' binary is present in your container
44 # image. If 'tar' is not present, 'kubectl cp' will fail.
45 #
46 # For advanced use cases, such as symlinks, wildcard expansion or
47 # file mode preservation, consider using 'kubectl exec'.
48
49 # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace>
50 tar cf - /tmp/foo | kubectl exec -i -n <some-namespace> <some-pod> -- tar xf - -C /tmp/bar
51
52 # Copy /tmp/foo from a remote pod to /tmp/bar locally
53 kubectl exec -n <some-namespace> <some-pod> -- tar cf - /tmp/foo | tar xf - -C /tmp/bar
54
55 # Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace
56 kubectl cp /tmp/foo_dir <some-pod>:/tmp/bar_dir
57
58 # Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container
59 kubectl cp /tmp/foo <some-pod>:/tmp/bar -c <specific-container>
60
61 # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace>
62 kubectl cp /tmp/foo <some-namespace>/<some-pod>:/tmp/bar
63
64 # Copy /tmp/foo from a remote pod to /tmp/bar locally
65 kubectl cp <some-namespace>/<some-pod>:/tmp/foo /tmp/bar`))
66 )
67
68
69 type CopyOptions struct {
70 Container string
71 Namespace string
72 NoPreserve bool
73 MaxTries int
74
75 ClientConfig *restclient.Config
76 Clientset kubernetes.Interface
77 ExecParentCmdName string
78
79 args []string
80
81 genericiooptions.IOStreams
82 }
83
84
85 func NewCopyOptions(ioStreams genericiooptions.IOStreams) *CopyOptions {
86 return &CopyOptions{
87 IOStreams: ioStreams,
88 }
89 }
90
91
92 func NewCmdCp(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
93 o := NewCopyOptions(ioStreams)
94
95 cmd := &cobra.Command{
96 Use: "cp <file-spec-src> <file-spec-dest>",
97 DisableFlagsInUseLine: true,
98 Short: i18n.T("Copy files and directories to and from containers"),
99 Long: i18n.T("Copy files and directories to and from containers."),
100 Example: cpExample,
101 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
102 var comps []string
103 if len(args) == 0 {
104 if strings.IndexAny(toComplete, "/.~") == 0 {
105
106 } else if strings.Contains(toComplete, ":") {
107
108 } else if idx := strings.Index(toComplete, "/"); idx > 0 {
109
110 namespace := toComplete[:idx]
111 template := "{{ range .items }}{{ .metadata.namespace }}/{{ .metadata.name }}: {{ end }}"
112 comps = completion.CompGetFromTemplate(&template, f, namespace, []string{"pod"}, toComplete)
113 } else {
114
115 for _, ns := range completion.CompGetResource(f, "namespace", toComplete) {
116 comps = append(comps, fmt.Sprintf("%s/", ns))
117 }
118
119 for _, pod := range completion.CompGetResource(f, "pod", toComplete) {
120 comps = append(comps, fmt.Sprintf("%s:", pod))
121 }
122
123
124
125
126
127
128
129
130 if len(comps) > 0 && len(toComplete) > 0 {
131 if files, err := os.ReadDir("."); err == nil {
132 for _, file := range files {
133 filename := file.Name()
134 if strings.HasPrefix(filename, toComplete) {
135 if file.IsDir() {
136 filename = fmt.Sprintf("%s/", filename)
137 }
138
139 comps = append(comps, filename)
140 }
141 }
142 }
143 } else if len(toComplete) == 0 {
144
145
146 comps = append(comps, "./", "/")
147 }
148 }
149 }
150 return comps, cobra.ShellCompDirectiveNoSpace
151 },
152 Run: func(cmd *cobra.Command, args []string) {
153 cmdutil.CheckErr(o.Complete(f, cmd, args))
154 cmdutil.CheckErr(o.Validate())
155 cmdutil.CheckErr(o.Run())
156 },
157 }
158 cmdutil.AddContainerVarFlags(cmd, &o.Container, o.Container)
159 cmd.Flags().BoolVarP(&o.NoPreserve, "no-preserve", "", false, "The copied file/directory's ownership and permissions will not be preserved in the container")
160 cmd.Flags().IntVarP(&o.MaxTries, "retries", "", 0, "Set number of retries to complete a copy operation from a container. Specify 0 to disable or any negative value for infinite retrying. The default is 0 (no retry).")
161
162 return cmd
163 }
164
165 var (
166 errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [[namespace/]pod:]file/path")
167 )
168
169 func extractFileSpec(arg string) (fileSpec, error) {
170 i := strings.Index(arg, ":")
171
172
173 if i == 0 {
174 return fileSpec{}, errFileSpecDoesntMatchFormat
175 }
176 if i == -1 {
177 return fileSpec{
178 File: newLocalPath(arg),
179 }, nil
180 }
181
182 pod, file := arg[:i], arg[i+1:]
183 pieces := strings.Split(pod, "/")
184 switch len(pieces) {
185 case 1:
186 return fileSpec{
187 PodName: pieces[0],
188 File: newRemotePath(file),
189 }, nil
190 case 2:
191 return fileSpec{
192 PodNamespace: pieces[0],
193 PodName: pieces[1],
194 File: newRemotePath(file),
195 }, nil
196 default:
197 return fileSpec{}, errFileSpecDoesntMatchFormat
198 }
199 }
200
201
202 func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
203 if cmd.Parent() != nil {
204 o.ExecParentCmdName = cmd.Parent().CommandPath()
205 }
206
207 var err error
208 o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
209 if err != nil {
210 return err
211 }
212
213 o.Clientset, err = f.KubernetesClientSet()
214 if err != nil {
215 return err
216 }
217
218 o.ClientConfig, err = f.ToRESTConfig()
219 if err != nil {
220 return err
221 }
222
223 o.args = args
224 return nil
225 }
226
227
228 func (o *CopyOptions) Validate() error {
229 if len(o.args) != 2 {
230 return fmt.Errorf("source and destination are required")
231 }
232 return nil
233 }
234
235
236 func (o *CopyOptions) Run() error {
237 srcSpec, err := extractFileSpec(o.args[0])
238 if err != nil {
239 return err
240 }
241 destSpec, err := extractFileSpec(o.args[1])
242 if err != nil {
243 return err
244 }
245
246 if len(srcSpec.PodName) != 0 && len(destSpec.PodName) != 0 {
247 return fmt.Errorf("one of src or dest must be a local file specification")
248 }
249 if len(srcSpec.File.String()) == 0 || len(destSpec.File.String()) == 0 {
250 return errors.New("filepath can not be empty")
251 }
252
253 if len(srcSpec.PodName) != 0 {
254 return o.copyFromPod(srcSpec, destSpec)
255 }
256 if len(destSpec.PodName) != 0 {
257 return o.copyToPod(srcSpec, destSpec, &exec.ExecOptions{})
258 }
259 return fmt.Errorf("one of src or dest must be a remote file specification")
260 }
261
262
263
264
265
266 func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
267 options := &exec.ExecOptions{
268 StreamOptions: exec.StreamOptions{
269 IOStreams: genericiooptions.IOStreams{
270 Out: bytes.NewBuffer([]byte{}),
271 ErrOut: bytes.NewBuffer([]byte{}),
272 },
273
274 Namespace: dest.PodNamespace,
275 PodName: dest.PodName,
276 },
277
278 Command: []string{"test", "-d", dest.File.String()},
279 Executor: &exec.DefaultRemoteExecutor{},
280 }
281
282 return o.execute(options)
283 }
284
285 func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error {
286 if _, err := os.Stat(src.File.String()); err != nil {
287 return fmt.Errorf("%s doesn't exist in local filesystem", src.File)
288 }
289 reader, writer := io.Pipe()
290
291 srcFile := src.File.(localPath)
292 destFile := dest.File.(remotePath)
293
294 if err := o.checkDestinationIsDir(dest); err == nil {
295
296
297 destFile = destFile.Join(srcFile.Base())
298 }
299
300 go func(src localPath, dest remotePath, writer io.WriteCloser) {
301 defer writer.Close()
302 cmdutil.CheckErr(makeTar(src, dest, writer))
303 }(srcFile, destFile, writer)
304 var cmdArr []string
305
306 if o.NoPreserve {
307 cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-"}
308 } else {
309 cmdArr = []string{"tar", "-xmf", "-"}
310 }
311 destFileDir := destFile.Dir().String()
312 if len(destFileDir) > 0 {
313 cmdArr = append(cmdArr, "-C", destFileDir)
314 }
315
316 options.StreamOptions = exec.StreamOptions{
317 IOStreams: genericiooptions.IOStreams{
318 In: reader,
319 Out: o.Out,
320 ErrOut: o.ErrOut,
321 },
322 Stdin: true,
323
324 Namespace: dest.PodNamespace,
325 PodName: dest.PodName,
326 }
327
328 options.Command = cmdArr
329 options.Executor = &exec.DefaultRemoteExecutor{}
330 return o.execute(options)
331 }
332
333 func (o *CopyOptions) copyFromPod(src, dest fileSpec) error {
334 reader := newTarPipe(src, o)
335 srcFile := src.File.(remotePath)
336 destFile := dest.File.(localPath)
337
338
339 prefix := stripPathShortcuts(srcFile.StripSlashes().Clean().String())
340 return o.untarAll(src.PodNamespace, src.PodName, prefix, srcFile, destFile, reader)
341 }
342
343 type TarPipe struct {
344 src fileSpec
345 o *CopyOptions
346 reader *io.PipeReader
347 outStream *io.PipeWriter
348 bytesRead uint64
349 retries int
350 }
351
352 func newTarPipe(src fileSpec, o *CopyOptions) *TarPipe {
353 t := new(TarPipe)
354 t.src = src
355 t.o = o
356 t.initReadFrom(0)
357 return t
358 }
359
360 func (t *TarPipe) initReadFrom(n uint64) {
361 t.reader, t.outStream = io.Pipe()
362 options := &exec.ExecOptions{
363 StreamOptions: exec.StreamOptions{
364 IOStreams: genericiooptions.IOStreams{
365 In: nil,
366 Out: t.outStream,
367 ErrOut: t.o.Out,
368 },
369
370 Namespace: t.src.PodNamespace,
371 PodName: t.src.PodName,
372 },
373
374 Command: []string{"tar", "cf", "-", t.src.File.String()},
375 Executor: &exec.DefaultRemoteExecutor{},
376 }
377 if t.o.MaxTries != 0 {
378 options.Command = []string{"sh", "-c", fmt.Sprintf("tar cf - %s | tail -c+%d", t.src.File, n)}
379 }
380
381 go func() {
382 defer t.outStream.Close()
383 cmdutil.CheckErr(t.o.execute(options))
384 }()
385 }
386
387 func (t *TarPipe) Read(p []byte) (n int, err error) {
388 n, err = t.reader.Read(p)
389 if err != nil {
390 if t.o.MaxTries < 0 || t.retries < t.o.MaxTries {
391 t.retries++
392 fmt.Printf("Resuming copy at %d bytes, retry %d/%d\n", t.bytesRead, t.retries, t.o.MaxTries)
393 t.initReadFrom(t.bytesRead + 1)
394 err = nil
395 } else {
396 fmt.Printf("Dropping out copy after %d retries\n", t.retries)
397 }
398 } else {
399 t.bytesRead += uint64(n)
400 }
401 return
402 }
403
404 func makeTar(src localPath, dest remotePath, writer io.Writer) error {
405
406 tarWriter := tar.NewWriter(writer)
407 defer tarWriter.Close()
408
409 srcPath := src.Clean()
410 destPath := dest.Clean()
411 return recursiveTar(srcPath.Dir(), srcPath.Base(), destPath.Dir(), destPath.Base(), tarWriter)
412 }
413
414 func recursiveTar(srcDir, srcFile localPath, destDir, destFile remotePath, tw *tar.Writer) error {
415 matchedPaths, err := srcDir.Join(srcFile).Glob()
416 if err != nil {
417 return err
418 }
419 for _, fpath := range matchedPaths {
420 stat, err := os.Lstat(fpath)
421 if err != nil {
422 return err
423 }
424 if stat.IsDir() {
425 files, err := os.ReadDir(fpath)
426 if err != nil {
427 return err
428 }
429 if len(files) == 0 {
430
431 hdr, _ := tar.FileInfoHeader(stat, fpath)
432 hdr.Name = destFile.String()
433 if err := tw.WriteHeader(hdr); err != nil {
434 return err
435 }
436 }
437 for _, f := range files {
438 if err := recursiveTar(srcDir, srcFile.Join(newLocalPath(f.Name())),
439 destDir, destFile.Join(newRemotePath(f.Name())), tw); err != nil {
440 return err
441 }
442 }
443 return nil
444 } else if stat.Mode()&os.ModeSymlink != 0 {
445
446 hdr, _ := tar.FileInfoHeader(stat, fpath)
447 target, err := os.Readlink(fpath)
448 if err != nil {
449 return err
450 }
451
452 hdr.Linkname = target
453 hdr.Name = destFile.String()
454 if err := tw.WriteHeader(hdr); err != nil {
455 return err
456 }
457 } else {
458
459 hdr, err := tar.FileInfoHeader(stat, fpath)
460 if err != nil {
461 return err
462 }
463 hdr.Name = destFile.String()
464
465 if err := tw.WriteHeader(hdr); err != nil {
466 return err
467 }
468
469 f, err := os.Open(fpath)
470 if err != nil {
471 return err
472 }
473 defer f.Close()
474
475 if _, err := io.Copy(tw, f); err != nil {
476 return err
477 }
478 return f.Close()
479 }
480 }
481 return nil
482 }
483
484 func (o *CopyOptions) untarAll(ns, pod string, prefix string, src remotePath, dest localPath, reader io.Reader) error {
485 symlinkWarningPrinted := false
486
487 tarReader := tar.NewReader(reader)
488 for {
489 header, err := tarReader.Next()
490 if err != nil {
491 if err != io.EOF {
492 return err
493 }
494 break
495 }
496
497
498
499
500
501
502 if !strings.HasPrefix(header.Name, prefix) {
503 return fmt.Errorf("tar contents corrupted")
504 }
505
506
507 mode := header.FileInfo().Mode()
508
509
510
511 destFileName := dest.Join(newRemotePath(header.Name[len(prefix):]))
512
513 if !isRelative(dest, destFileName) {
514 fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
515 continue
516 }
517
518 if err := os.MkdirAll(destFileName.Dir().String(), 0755); err != nil {
519 return err
520 }
521 if header.FileInfo().IsDir() {
522 if err := os.MkdirAll(destFileName.String(), 0755); err != nil {
523 return err
524 }
525 continue
526 }
527
528 if mode&os.ModeSymlink != 0 {
529 if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 {
530 fmt.Fprintf(o.IOStreams.ErrOut,
531 "warning: skipping symlink: %q -> %q (consider using \"%s exec -n %q %q -- tar cf - %q | tar xf -\")\n",
532 destFileName, header.Linkname, o.ExecParentCmdName, ns, pod, src)
533 symlinkWarningPrinted = true
534 continue
535 }
536 fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname)
537 continue
538 }
539 outFile, err := os.Create(destFileName.String())
540 if err != nil {
541 return err
542 }
543 defer outFile.Close()
544 if _, err := io.Copy(outFile, tarReader); err != nil {
545 return err
546 }
547 if err := outFile.Close(); err != nil {
548 return err
549 }
550 }
551
552 return nil
553 }
554
555 func (o *CopyOptions) execute(options *exec.ExecOptions) error {
556 if len(options.Namespace) == 0 {
557 options.Namespace = o.Namespace
558 }
559
560 if len(o.Container) > 0 {
561 options.ContainerName = o.Container
562 }
563
564 options.Config = o.ClientConfig
565 options.PodClient = o.Clientset.CoreV1()
566
567 if err := options.Validate(); err != nil {
568 return err
569 }
570
571 return options.Run()
572 }
573
View as plain text