123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- /*
- Copyright 2016 The Kubernetes 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 cp
- import (
- "archive/tar"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path"
- "path/filepath"
- "strings"
- "k8s.io/cli-runtime/pkg/genericclioptions"
- "k8s.io/client-go/kubernetes"
- restclient "k8s.io/client-go/rest"
- "k8s.io/kubernetes/pkg/kubectl/cmd/exec"
- cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
- "k8s.io/kubernetes/pkg/kubectl/util/i18n"
- "k8s.io/kubernetes/pkg/kubectl/util/templates"
- "bytes"
- "github.com/lithammer/dedent"
- "github.com/spf13/cobra"
- )
- var (
- cpExample = templates.Examples(i18n.T(`
- # !!!Important Note!!!
- # Requires that the 'tar' binary is present in your container
- # image. If 'tar' is not present, 'kubectl cp' will fail.
- # Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace
- kubectl cp /tmp/foo_dir <some-pod>:/tmp/bar_dir
- # Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container
- kubectl cp /tmp/foo <some-pod>:/tmp/bar -c <specific-container>
- # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace>
- kubectl cp /tmp/foo <some-namespace>/<some-pod>:/tmp/bar
- # Copy /tmp/foo from a remote pod to /tmp/bar locally
- kubectl cp <some-namespace>/<some-pod>:/tmp/foo /tmp/bar`))
- cpUsageStr = dedent.Dedent(`
- expected 'cp <file-spec-src> <file-spec-dest> [-c container]'.
- <file-spec> is:
- [namespace/]pod-name:/file/path for a remote file
- /file/path for a local file`)
- )
- // CopyOptions have the data required to perform the copy operation
- type CopyOptions struct {
- Container string
- Namespace string
- NoPreserve bool
- ClientConfig *restclient.Config
- Clientset kubernetes.Interface
- genericclioptions.IOStreams
- }
- // NewCopyOptions creates the options for copy
- func NewCopyOptions(ioStreams genericclioptions.IOStreams) *CopyOptions {
- return &CopyOptions{
- IOStreams: ioStreams,
- }
- }
- // NewCmdCp creates a new Copy command.
- func NewCmdCp(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
- o := NewCopyOptions(ioStreams)
- cmd := &cobra.Command{
- Use: "cp <file-spec-src> <file-spec-dest>",
- DisableFlagsInUseLine: true,
- Short: i18n.T("Copy files and directories to and from containers."),
- Long: "Copy files and directories to and from containers.",
- Example: cpExample,
- Run: func(cmd *cobra.Command, args []string) {
- cmdutil.CheckErr(o.Complete(f, cmd))
- cmdutil.CheckErr(o.Run(args))
- },
- }
- cmd.Flags().StringVarP(&o.Container, "container", "c", o.Container, "Container name. If omitted, the first container in the pod will be chosen")
- cmd.Flags().BoolVarP(&o.NoPreserve, "no-preserve", "", false, "The copied file/directory's ownership and permissions will not be preserved in the container")
- return cmd
- }
- type fileSpec struct {
- PodNamespace string
- PodName string
- File string
- }
- var (
- errFileSpecDoesntMatchFormat = errors.New("Filespec must match the canonical format: [[namespace/]pod:]file/path")
- errFileCannotBeEmpty = errors.New("Filepath can not be empty")
- )
- func extractFileSpec(arg string) (fileSpec, error) {
- if i := strings.Index(arg, ":"); i == -1 {
- return fileSpec{File: arg}, nil
- } else if i > 0 {
- file := arg[i+1:]
- pod := arg[:i]
- pieces := strings.Split(pod, "/")
- if len(pieces) == 1 {
- return fileSpec{
- PodName: pieces[0],
- File: file,
- }, nil
- }
- if len(pieces) == 2 {
- return fileSpec{
- PodNamespace: pieces[0],
- PodName: pieces[1],
- File: file,
- }, nil
- }
- }
- return fileSpec{}, errFileSpecDoesntMatchFormat
- }
- // Complete completes all the required options
- func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
- var err error
- o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
- if err != nil {
- return err
- }
- o.Clientset, err = f.KubernetesClientSet()
- if err != nil {
- return err
- }
- o.ClientConfig, err = f.ToRESTConfig()
- if err != nil {
- return err
- }
- return nil
- }
- // Validate makes sure provided values for CopyOptions are valid
- func (o *CopyOptions) Validate(cmd *cobra.Command, args []string) error {
- if len(args) != 2 {
- return cmdutil.UsageErrorf(cmd, cpUsageStr)
- }
- return nil
- }
- // Run performs the execution
- func (o *CopyOptions) Run(args []string) error {
- if len(args) < 2 {
- return fmt.Errorf("source and destination are required")
- }
- srcSpec, err := extractFileSpec(args[0])
- if err != nil {
- return err
- }
- destSpec, err := extractFileSpec(args[1])
- if err != nil {
- return err
- }
- if len(srcSpec.PodName) != 0 && len(destSpec.PodName) != 0 {
- if _, err := os.Stat(args[0]); err == nil {
- return o.copyToPod(fileSpec{File: args[0]}, destSpec, &exec.ExecOptions{})
- }
- return fmt.Errorf("src doesn't exist in local filesystem")
- }
- if len(srcSpec.PodName) != 0 {
- return o.copyFromPod(srcSpec, destSpec)
- }
- if len(destSpec.PodName) != 0 {
- return o.copyToPod(srcSpec, destSpec, &exec.ExecOptions{})
- }
- return fmt.Errorf("one of src or dest must be a remote file specification")
- }
- // checkDestinationIsDir receives a destination fileSpec and
- // determines if the provided destination path exists on the
- // pod. If the destination path does not exist or is _not_ a
- // directory, an error is returned with the exit code received.
- func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
- options := &exec.ExecOptions{
- StreamOptions: exec.StreamOptions{
- IOStreams: genericclioptions.IOStreams{
- Out: bytes.NewBuffer([]byte{}),
- ErrOut: bytes.NewBuffer([]byte{}),
- },
- Namespace: dest.PodNamespace,
- PodName: dest.PodName,
- },
- Command: []string{"test", "-d", dest.File},
- Executor: &exec.DefaultRemoteExecutor{},
- }
- return o.execute(options)
- }
- func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error {
- if len(src.File) == 0 || len(dest.File) == 0 {
- return errFileCannotBeEmpty
- }
- reader, writer := io.Pipe()
- // strip trailing slash (if any)
- if dest.File != "/" && strings.HasSuffix(string(dest.File[len(dest.File)-1]), "/") {
- dest.File = dest.File[:len(dest.File)-1]
- }
- if err := o.checkDestinationIsDir(dest); err == nil {
- // If no error, dest.File was found to be a directory.
- // Copy specified src into it
- dest.File = dest.File + "/" + path.Base(src.File)
- }
- go func() {
- defer writer.Close()
- err := makeTar(src.File, dest.File, writer)
- cmdutil.CheckErr(err)
- }()
- var cmdArr []string
- // TODO: Improve error messages by first testing if 'tar' is present in the container?
- if o.NoPreserve {
- cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xf", "-"}
- } else {
- cmdArr = []string{"tar", "-xf", "-"}
- }
- destDir := path.Dir(dest.File)
- if len(destDir) > 0 {
- cmdArr = append(cmdArr, "-C", destDir)
- }
- options.StreamOptions = exec.StreamOptions{
- IOStreams: genericclioptions.IOStreams{
- In: reader,
- Out: o.Out,
- ErrOut: o.ErrOut,
- },
- Stdin: true,
- Namespace: dest.PodNamespace,
- PodName: dest.PodName,
- }
- options.Command = cmdArr
- options.Executor = &exec.DefaultRemoteExecutor{}
- return o.execute(options)
- }
- func (o *CopyOptions) copyFromPod(src, dest fileSpec) error {
- if len(src.File) == 0 || len(dest.File) == 0 {
- return errFileCannotBeEmpty
- }
- reader, outStream := io.Pipe()
- options := &exec.ExecOptions{
- StreamOptions: exec.StreamOptions{
- IOStreams: genericclioptions.IOStreams{
- In: nil,
- Out: outStream,
- ErrOut: o.Out,
- },
- Namespace: src.PodNamespace,
- PodName: src.PodName,
- },
- // TODO: Improve error messages by first testing if 'tar' is present in the container?
- Command: []string{"tar", "cf", "-", src.File},
- Executor: &exec.DefaultRemoteExecutor{},
- }
- go func() {
- defer outStream.Close()
- err := o.execute(options)
- cmdutil.CheckErr(err)
- }()
- prefix := getPrefix(src.File)
- prefix = path.Clean(prefix)
- // remove extraneous path shortcuts - these could occur if a path contained extra "../"
- // and attempted to navigate beyond "/" in a remote filesystem
- prefix = stripPathShortcuts(prefix)
- return o.untarAll(reader, dest.File, prefix)
- }
- // stripPathShortcuts removes any leading or trailing "../" from a given path
- func stripPathShortcuts(p string) string {
- newPath := path.Clean(p)
- trimmed := strings.TrimPrefix(newPath, "../")
- for trimmed != newPath {
- newPath = trimmed
- trimmed = strings.TrimPrefix(newPath, "../")
- }
- // trim leftover {".", ".."}
- if newPath == "." || newPath == ".." {
- newPath = ""
- }
- if len(newPath) > 0 && string(newPath[0]) == "/" {
- return newPath[1:]
- }
- return newPath
- }
- func makeTar(srcPath, destPath string, writer io.Writer) error {
- // TODO: use compression here?
- tarWriter := tar.NewWriter(writer)
- defer tarWriter.Close()
- srcPath = path.Clean(srcPath)
- destPath = path.Clean(destPath)
- return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter)
- }
- func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) error {
- srcPath := path.Join(srcBase, srcFile)
- matchedPaths, err := filepath.Glob(srcPath)
- if err != nil {
- return err
- }
- for _, fpath := range matchedPaths {
- stat, err := os.Lstat(fpath)
- if err != nil {
- return err
- }
- if stat.IsDir() {
- files, err := ioutil.ReadDir(fpath)
- if err != nil {
- return err
- }
- if len(files) == 0 {
- //case empty directory
- hdr, _ := tar.FileInfoHeader(stat, fpath)
- hdr.Name = destFile
- if err := tw.WriteHeader(hdr); err != nil {
- return err
- }
- }
- for _, f := range files {
- if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tw); err != nil {
- return err
- }
- }
- return nil
- } else if stat.Mode()&os.ModeSymlink != 0 {
- //case soft link
- hdr, _ := tar.FileInfoHeader(stat, fpath)
- target, err := os.Readlink(fpath)
- if err != nil {
- return err
- }
- hdr.Linkname = target
- hdr.Name = destFile
- if err := tw.WriteHeader(hdr); err != nil {
- return err
- }
- } else {
- //case regular file or other file type like pipe
- hdr, err := tar.FileInfoHeader(stat, fpath)
- if err != nil {
- return err
- }
- hdr.Name = destFile
- if err := tw.WriteHeader(hdr); err != nil {
- return err
- }
- f, err := os.Open(fpath)
- if err != nil {
- return err
- }
- defer f.Close()
- if _, err := io.Copy(tw, f); err != nil {
- return err
- }
- return f.Close()
- }
- }
- return nil
- }
- func (o *CopyOptions) untarAll(reader io.Reader, destDir, prefix string) error {
- // TODO: use compression here?
- tarReader := tar.NewReader(reader)
- symlinks := map[string]string{} // map of link -> destination
- for {
- header, err := tarReader.Next()
- if err != nil {
- if err != io.EOF {
- return err
- }
- break
- }
- // All the files will start with the prefix, which is the directory where
- // they were located on the pod, we need to strip down that prefix, but
- // if the prefix is missing it means the tar was tempered with.
- // For the case where prefix is empty we need to ensure that the path
- // is not absolute, which also indicates the tar file was tempered with.
- if !strings.HasPrefix(header.Name, prefix) {
- return fmt.Errorf("tar contents corrupted")
- }
- // basic file information
- mode := header.FileInfo().Mode()
- destFileName := filepath.Join(destDir, header.Name[len(prefix):])
- if !isDestRelative(destDir, destFileName) {
- fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
- continue
- }
- baseName := filepath.Dir(destFileName)
- if err := os.MkdirAll(baseName, 0755); err != nil {
- return err
- }
- if header.FileInfo().IsDir() {
- if err := os.MkdirAll(destFileName, 0755); err != nil {
- return err
- }
- continue
- }
- // We need to ensure that the destination file is always within boundries
- // of the destination directory. This prevents any kind of path traversal
- // from within tar archive.
- evaledPath, err := filepath.EvalSymlinks(baseName)
- if err != nil {
- return err
- }
- // For scrutiny we verify both the actual destination as well as we follow
- // all the links that might lead outside of the destination directory.
- if !isDestRelative(destDir, filepath.Join(evaledPath, filepath.Base(destFileName))) {
- fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
- continue
- }
- if mode&os.ModeSymlink != 0 {
- if _, exists := symlinks[destFileName]; exists {
- return fmt.Errorf("duplicate symlink: %q", destFileName)
- }
- symlinks[destFileName] = header.Linkname
- } else {
- outFile, err := os.Create(destFileName)
- if err != nil {
- return err
- }
- defer outFile.Close()
- if _, err := io.Copy(outFile, tarReader); err != nil {
- return err
- }
- if err := outFile.Close(); err != nil {
- return err
- }
- }
- }
- // Create symlinks after all regular files have been written.
- // Ordering this way prevents writing data outside the destination directory through path
- // traversals.
- // Symlink chaining is prevented due to the directory tree being established (MkdirAll) before
- // creating any symlinks.
- for newname, oldname := range symlinks {
- if err := os.Symlink(oldname, newname); err != nil {
- return err
- }
- }
- return nil
- }
- // isDestRelative returns true if dest is pointing outside the base directory,
- // false otherwise.
- func isDestRelative(base, dest string) bool {
- relative, err := filepath.Rel(base, dest)
- if err != nil {
- return false
- }
- return relative == "." || relative == stripPathShortcuts(relative)
- }
- func getPrefix(file string) string {
- // tar strips the leading '/' if it's there, so we will too
- return strings.TrimLeft(file, "/")
- }
- func (o *CopyOptions) execute(options *exec.ExecOptions) error {
- if len(options.Namespace) == 0 {
- options.Namespace = o.Namespace
- }
- if len(o.Container) > 0 {
- options.ContainerName = o.Container
- }
- options.Config = o.ClientConfig
- options.PodClient = o.Clientset.CoreV1()
- if err := options.Validate(); err != nil {
- return err
- }
- if err := options.Run(); err != nil {
- return err
- }
- return nil
- }
|