...

Source file src/k8s.io/kubectl/pkg/cmd/cp/cp.go

Documentation: k8s.io/kubectl/pkg/cmd/cp

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  // CopyOptions have the data required to perform the copy operation
    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  // NewCopyOptions creates the options for copy
    85  func NewCopyOptions(ioStreams genericiooptions.IOStreams) *CopyOptions {
    86  	return &CopyOptions{
    87  		IOStreams: ioStreams,
    88  	}
    89  }
    90  
    91  // NewCmdCp creates a new Copy command.
    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  					// Looks like a path, do nothing
   106  				} else if strings.Contains(toComplete, ":") {
   107  					// TODO: complete remote files in the pod
   108  				} else if idx := strings.Index(toComplete, "/"); idx > 0 {
   109  					// complete <namespace>/<pod>
   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  					// Complete namespaces followed by a /
   115  					for _, ns := range completion.CompGetResource(f, "namespace", toComplete) {
   116  						comps = append(comps, fmt.Sprintf("%s/", ns))
   117  					}
   118  					// Complete pod names followed by a :
   119  					for _, pod := range completion.CompGetResource(f, "pod", toComplete) {
   120  						comps = append(comps, fmt.Sprintf("%s:", pod))
   121  					}
   122  
   123  					// Finally, provide file completion if we need to.
   124  					// We only do this if:
   125  					// 1- There are other completions found (if there are no completions,
   126  					//    the shell will do file completion itself)
   127  					// 2- If there is some input from the user (or else we will end up
   128  					//    listing the entire content of the current directory which could
   129  					//    be too many choices for the user)
   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  									// We are completing a file prefix
   139  									comps = append(comps, filename)
   140  								}
   141  							}
   142  						}
   143  					} else if len(toComplete) == 0 {
   144  						// If the user didn't provide any input to complete,
   145  						// we provide a hint that a path can also be used
   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  	// filespec starting with a semicolon is invalid
   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  // Complete completes all the required options
   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  // Validate makes sure provided values for CopyOptions are valid
   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  // Run performs the execution
   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  // checkDestinationIsDir receives a destination fileSpec and
   263  // determines if the provided destination path exists on the
   264  // pod. If the destination path does not exist or is _not_ a
   265  // directory, an error is returned with the exit code received.
   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  		// If no error, dest.File was found to be a directory.
   296  		// Copy specified src into it
   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  	// remove extraneous path shortcuts - these could occur if a path contained extra "../"
   338  	// and attempted to navigate beyond "/" in a remote filesystem
   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  	// TODO: use compression here?
   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  				//case empty directory
   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  			//case soft link
   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  			//case regular file or other file type like pipe
   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  	// TODO: use compression here?
   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  		// All the files will start with the prefix, which is the directory where
   498  		// they were located on the pod, we need to strip down that prefix, but
   499  		// if the prefix is missing it means the tar was tempered with.
   500  		// For the case where prefix is empty we need to ensure that the path
   501  		// is not absolute, which also indicates the tar file was tempered with.
   502  		if !strings.HasPrefix(header.Name, prefix) {
   503  			return fmt.Errorf("tar contents corrupted")
   504  		}
   505  
   506  		// basic file information
   507  		mode := header.FileInfo().Mode()
   508  		// header.Name is a name of the REMOTE file, so we need to create
   509  		// a remotePath so that it goes through appropriate processing related
   510  		// with cleaning remote paths
   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