| /* |
| 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/renstrom/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`) |
| ) |
| |
| type CopyOptions struct { |
| Container string |
| Namespace string |
| NoPreserve bool |
| |
| ClientConfig *restclient.Config |
| Clientset kubernetes.Interface |
| |
| genericclioptions.IOStreams |
| } |
| |
| 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 |
| } |
| |
| 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 |
| } |
| |
| func (o *CopyOptions) Validate(cmd *cobra.Command, args []string) error { |
| if len(args) != 2 { |
| return cmdutil.UsageErrorf(cmd, cpUsageStr) |
| } |
| return nil |
| } |
| |
| 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() |
| o.execute(options) |
| }() |
| 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 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 = "" |
| } |
| |
| 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 { |
| filepath := path.Join(srcBase, srcFile) |
| stat, err := os.Lstat(filepath) |
| if err != nil { |
| return err |
| } |
| if stat.IsDir() { |
| files, err := ioutil.ReadDir(filepath) |
| if err != nil { |
| return err |
| } |
| if len(files) == 0 { |
| //case empty directory |
| hdr, _ := tar.FileInfoHeader(stat, filepath) |
| 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, filepath) |
| target, err := os.Readlink(filepath) |
| 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, filepath) |
| if err != nil { |
| return err |
| } |
| hdr.Name = destFile |
| |
| if err := tw.WriteHeader(hdr); err != nil { |
| return err |
| } |
| |
| f, err := os.Open(filepath) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| if _, err := io.Copy(tw, f); err != nil { |
| return err |
| } |
| return f.Close() |
| } |
| return nil |
| } |
| |
| // clean prevents path traversals by stripping them out. |
| // This is adapted from https://golang.org/src/net/http/fs.go#L74 |
| func clean(fileName string) string { |
| return path.Clean(string(os.PathSeparator) + fileName) |
| } |
| |
| func untarAll(reader io.Reader, destFile, prefix string) error { |
| entrySeq := -1 |
| |
| // TODO: use compression here? |
| tarReader := tar.NewReader(reader) |
| for { |
| header, err := tarReader.Next() |
| if err != nil { |
| if err != io.EOF { |
| return err |
| } |
| break |
| } |
| entrySeq++ |
| mode := header.FileInfo().Mode() |
| outFileName := path.Join(destFile, clean(header.Name[len(prefix):])) |
| baseName := path.Dir(outFileName) |
| if err := os.MkdirAll(baseName, 0755); err != nil { |
| return err |
| } |
| if header.FileInfo().IsDir() { |
| if err := os.MkdirAll(outFileName, 0755); err != nil { |
| return err |
| } |
| continue |
| } |
| |
| // handle coping remote file into local directory |
| if entrySeq == 0 && !header.FileInfo().IsDir() { |
| exists, err := dirExists(outFileName) |
| if err != nil { |
| return err |
| } |
| if exists { |
| outFileName = filepath.Join(outFileName, path.Base(clean(header.Name))) |
| } |
| } |
| |
| if mode&os.ModeSymlink != 0 { |
| err := os.Symlink(header.Linkname, outFileName) |
| if err != nil { |
| return err |
| } |
| } else { |
| outFile, err := os.Create(outFileName) |
| 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 |
| } |
| } |
| } |
| |
| if entrySeq == -1 { |
| //if no file was copied |
| errInfo := fmt.Sprintf("error: %s no such file or directory", prefix) |
| return errors.New(errInfo) |
| } |
| return nil |
| } |
| |
| 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.Core() |
| |
| if err := options.Validate(); err != nil { |
| return err |
| } |
| |
| if err := options.Run(); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // dirExists checks if a path exists and is a directory. |
| func dirExists(path string) (bool, error) { |
| fi, err := os.Stat(path) |
| if err == nil && fi.IsDir() { |
| return true, nil |
| } |
| if os.IsNotExist(err) { |
| return false, nil |
| } |
| return false, err |
| } |