diff --git a/pkg/cmd/cp/cp.go b/pkg/cmd/cp/cp.go deleted file mode 100644 index 1c7b0bf6..00000000 --- a/pkg/cmd/cp/cp.go +++ /dev/null @@ -1,540 +0,0 @@ -/* -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/kubectl/pkg/cmd/exec" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/i18n" - "k8s.io/kubectl/pkg/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 :/tmp/bar_dir - - # Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container - kubectl cp /tmp/foo :/tmp/bar -c - - # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace - kubectl cp /tmp/foo /:/tmp/bar - - # Copy /tmp/foo from a remote pod to /tmp/bar locally - kubectl cp /:/tmp/foo /tmp/bar`)) - - cpUsageStr = dedent.Dedent(` - expected 'cp [-c container]'. - 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 ", - 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", "-xmf", "-"} - } else { - cmdArr = []string{"tar", "-xmf", "-"} - } - 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) - 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 { - linkname := header.Linkname - // We need to ensure that the link destination is always within boundries - // of the destination directory. This prevents any kind of path traversal - // from within tar archive. - linkTarget := linkname - if !filepath.IsAbs(linkname) { - linkTarget = filepath.Join(evaledPath, linkname) - } - if !isDestRelative(destDir, linkTarget) { - fmt.Fprintf(o.IOStreams.ErrOut, "warning: link %q is pointing to %q which is outside target destination, skipping\n", destFileName, header.Linkname) - continue - } - if err := os.Symlink(linkname, destFileName); err != nil { - return err - } - } 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 - } - } - } - - 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 -} diff --git a/pkg/cmd/cp/cp_test.go b/pkg/cmd/cp/cp_test.go deleted file mode 100644 index 5490a893..00000000 --- a/pkg/cmd/cp/cp_test.go +++ /dev/null @@ -1,975 +0,0 @@ -/* -Copyright 2014 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" - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest/fake" - kexec "k8s.io/kubectl/pkg/cmd/exec" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" -) - -type FileType int - -const ( - RegularFile FileType = 0 - SymLink FileType = 1 - RegexFile FileType = 2 -) - -func TestExtractFileSpec(t *testing.T) { - tests := []struct { - spec string - expectedPod string - expectedNamespace string - expectedFile string - expectErr bool - }{ - { - spec: "namespace/pod:/some/file", - expectedPod: "pod", - expectedNamespace: "namespace", - expectedFile: "/some/file", - }, - { - spec: "pod:/some/file", - expectedPod: "pod", - expectedFile: "/some/file", - }, - { - spec: "/some/file", - expectedFile: "/some/file", - }, - { - spec: ":file:not:exist:in:local:filesystem", - expectErr: true, - }, - { - spec: "namespace/pod/invalid:/some/file", - expectErr: true, - }, - { - spec: "pod:/some/filenamewith:in", - expectedPod: "pod", - expectedFile: "/some/filenamewith:in", - }, - } - for _, test := range tests { - spec, err := extractFileSpec(test.spec) - if test.expectErr && err == nil { - t.Errorf("unexpected non-error") - continue - } - if err != nil && !test.expectErr { - t.Errorf("unexpected error: %v", err) - continue - } - if spec.PodName != test.expectedPod { - t.Errorf("expected: %s, saw: %s", test.expectedPod, spec.PodName) - } - if spec.PodNamespace != test.expectedNamespace { - t.Errorf("expected: %s, saw: %s", test.expectedNamespace, spec.PodNamespace) - } - if spec.File != test.expectedFile { - t.Errorf("expected: %s, saw: %s", test.expectedFile, spec.File) - } - } -} - -func TestGetPrefix(t *testing.T) { - tests := []struct { - input string - expected string - }{ - { - input: "/foo/bar", - expected: "foo/bar", - }, - { - input: "foo/bar", - expected: "foo/bar", - }, - } - for _, test := range tests { - out := getPrefix(test.input) - if out != test.expected { - t.Errorf("expected: %s, saw: %s", test.expected, out) - } - } -} - -func TestStripPathShortcuts(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "test single path shortcut prefix", - input: "../foo/bar", - expected: "foo/bar", - }, - { - name: "test multiple path shortcuts", - input: "../../foo/bar", - expected: "foo/bar", - }, - { - name: "test multiple path shortcuts with absolute path", - input: "/tmp/one/two/../../foo/bar", - expected: "tmp/foo/bar", - }, - { - name: "test multiple path shortcuts with no named directory", - input: "../../", - expected: "", - }, - { - name: "test multiple path shortcuts with no named directory and no trailing slash", - input: "../..", - expected: "", - }, - { - name: "test multiple path shortcuts with absolute path and filename containing leading dots", - input: "/tmp/one/two/../../foo/..bar", - expected: "tmp/foo/..bar", - }, - { - name: "test multiple path shortcuts with no named directory and filename containing leading dots", - input: "../...foo", - expected: "...foo", - }, - { - name: "test filename containing leading dots", - input: "...foo", - expected: "...foo", - }, - { - name: "test root directory", - input: "/", - expected: "", - }, - } - - for _, test := range tests { - out := stripPathShortcuts(test.input) - if out != test.expected { - t.Errorf("expected: %s, saw: %s", test.expected, out) - } - } -} -func TestIsDestRelative(t *testing.T) { - tests := []struct { - base string - dest string - relative bool - }{ - { - base: "/dir", - dest: "/dir/../link", - relative: false, - }, - { - base: "/dir", - dest: "/dir/../../link", - relative: false, - }, - { - base: "/dir", - dest: "/link", - relative: false, - }, - { - base: "/dir", - dest: "/dir/link", - relative: true, - }, - { - base: "/dir", - dest: "/dir/int/../link", - relative: true, - }, - { - base: "dir", - dest: "dir/link", - relative: true, - }, - { - base: "dir", - dest: "dir/int/../link", - relative: true, - }, - { - base: "dir", - dest: "dir/../../link", - relative: false, - }, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - if test.relative != isDestRelative(test.base, test.dest) { - t.Errorf("unexpected result for: base %q, dest %q", test.base, test.dest) - } - }) - } -} - -func checkErr(t *testing.T, err error) { - if err != nil { - t.Errorf("unexpected error: %v", err) - t.FailNow() - } -} - -func TestTarUntar(t *testing.T) { - dir, err := ioutil.TempDir("", "input") - checkErr(t, err) - dir2, err := ioutil.TempDir("", "output") - checkErr(t, err) - dir3, err := ioutil.TempDir("", "dir") - checkErr(t, err) - - dir = dir + "/" - defer func() { - os.RemoveAll(dir) - os.RemoveAll(dir2) - os.RemoveAll(dir3) - }() - - files := []struct { - name string - nameList []string - data string - omitted bool - fileType FileType - }{ - { - name: "foo", - data: "foobarbaz", - fileType: RegularFile, - }, - { - name: "dir/blah", - data: "bazblahfoo", - fileType: RegularFile, - }, - { - name: "some/other/directory/", - data: "with more data here", - fileType: RegularFile, - }, - { - name: "blah", - data: "same file name different data", - fileType: RegularFile, - }, - { - name: "gakki", - data: "tmp/gakki", - fileType: SymLink, - }, - { - name: "relative_to_dest", - data: path.Join(dir2, "foo"), - fileType: SymLink, - }, - { - name: "tricky_relative", - data: path.Join(dir3, "xyz"), - omitted: true, - fileType: SymLink, - }, - { - name: "absolute_path", - data: "/tmp/gakki", - omitted: true, - fileType: SymLink, - }, - { - name: "blah*", - nameList: []string{"blah1", "blah2"}, - data: "regexp file name", - fileType: RegexFile, - }, - } - - for _, file := range files { - filepath := path.Join(dir, file.name) - if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if file.fileType == RegularFile { - createTmpFile(t, filepath, file.data) - } else if file.fileType == SymLink { - err := os.Symlink(file.data, filepath) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - } else if file.fileType == RegexFile { - for _, fileName := range file.nameList { - createTmpFile(t, path.Join(dir, fileName), file.data) - } - } else { - t.Fatalf("unexpected file type: %v", file) - } - } - - opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard()) - - writer := &bytes.Buffer{} - if err := makeTar(dir, dir, writer); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - reader := bytes.NewBuffer(writer.Bytes()) - if err := opts.untarAll(reader, dir2, ""); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - for _, file := range files { - absPath := filepath.Join(dir2, strings.TrimPrefix(dir, os.TempDir())) - filePath := filepath.Join(absPath, file.name) - - if file.fileType == RegularFile { - cmpFileData(t, filePath, file.data) - } else if file.fileType == SymLink { - dest, err := os.Readlink(filePath) - if file.omitted { - if err != nil && strings.Contains(err.Error(), "no such file or directory") { - continue - } - t.Fatalf("expected to omit symlink for %s", filePath) - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if file.data != dest { - t.Fatalf("expected: %s, saw: %s", file.data, dest) - } - } else if file.fileType == RegexFile { - for _, fileName := range file.nameList { - cmpFileData(t, path.Join(dir, fileName), file.data) - } - } else { - t.Fatalf("unexpected file type: %v", file) - } - } -} - -func TestTarUntarWrongPrefix(t *testing.T) { - dir, err := ioutil.TempDir("", "input") - checkErr(t, err) - dir2, err := ioutil.TempDir("", "output") - checkErr(t, err) - - dir = dir + "/" - defer func() { - os.RemoveAll(dir) - os.RemoveAll(dir2) - }() - - filepath := path.Join(dir, "foo") - if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil { - t.Fatalf("unexpected error: %v", err) - } - createTmpFile(t, filepath, "sample data") - - opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard()) - - writer := &bytes.Buffer{} - if err := makeTar(dir, dir, writer); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - reader := bytes.NewBuffer(writer.Bytes()) - err = opts.untarAll(reader, dir2, "verylongprefix-showing-the-tar-was-tempered-with") - if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestTarDestinationName(t *testing.T) { - dir, err := ioutil.TempDir(os.TempDir(), "input") - dir2, err2 := ioutil.TempDir(os.TempDir(), "output") - if err != nil || err2 != nil { - t.Errorf("unexpected error: %v | %v", err, err2) - t.FailNow() - } - defer func() { - if err := os.RemoveAll(dir); err != nil { - t.Errorf("Unexpected error cleaning up: %v", err) - } - if err := os.RemoveAll(dir2); err != nil { - t.Errorf("Unexpected error cleaning up: %v", err) - } - }() - - files := []struct { - name string - data string - }{ - { - name: "foo", - data: "foobarbaz", - }, - { - name: "dir/blah", - data: "bazblahfoo", - }, - { - name: "some/other/directory", - data: "with more data here", - }, - { - name: "blah", - data: "same file name different data", - }, - } - - // ensure files exist on disk - for _, file := range files { - filepath := path.Join(dir, file.name) - if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil { - t.Errorf("unexpected error: %v", err) - t.FailNow() - } - createTmpFile(t, filepath, file.data) - } - - reader, writer := io.Pipe() - go func() { - if err := makeTar(dir, dir2, writer); err != nil { - t.Errorf("unexpected error: %v", err) - } - }() - - tarReader := tar.NewReader(reader) - for { - hdr, err := tarReader.Next() - if err == io.EOF { - break - } else if err != nil { - t.Errorf("unexpected error: %v", err) - t.FailNow() - } - - if !strings.HasPrefix(hdr.Name, path.Base(dir2)) { - t.Errorf("expected %q as destination filename prefix, saw: %q", path.Base(dir2), hdr.Name) - } - } -} - -func TestBadTar(t *testing.T) { - dir, err := ioutil.TempDir(os.TempDir(), "dest") - if err != nil { - t.Errorf("unexpected error: %v ", err) - t.FailNow() - } - defer os.RemoveAll(dir) - - // More or less cribbed from https://golang.org/pkg/archive/tar/#example__minimal - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - var files = []struct { - name string - body string - }{ - {"/prefix/foo/bar/../../home/bburns/names.txt", "Down and back"}, - } - for _, file := range files { - hdr := &tar.Header{ - Name: file.name, - Mode: 0600, - Size: int64(len(file.body)), - } - if err := tw.WriteHeader(hdr); err != nil { - t.Errorf("unexpected error: %v ", err) - t.FailNow() - } - if _, err := tw.Write([]byte(file.body)); err != nil { - t.Errorf("unexpected error: %v ", err) - t.FailNow() - } - } - if err := tw.Close(); err != nil { - t.Errorf("unexpected error: %v ", err) - t.FailNow() - } - - opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard()) - if err := opts.untarAll(&buf, dir, "/prefix"); err != nil { - t.Errorf("unexpected error: %v ", err) - t.FailNow() - } - - for _, file := range files { - _, err := os.Stat(path.Join(dir, path.Clean(file.name[len("/prefix"):]))) - if err != nil { - t.Errorf("Error finding file: %v", err) - } - } -} - -// 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 TestClean(t *testing.T) { - tests := []struct { - input string - cleaned string - }{ - { - "../../../tmp/foo", - "/tmp/foo", - }, - { - "/../../../tmp/foo", - "/tmp/foo", - }, - } - - for _, test := range tests { - out := clean(test.input) - if out != test.cleaned { - t.Errorf("Expected: %s, saw %s", test.cleaned, out) - } - } -} - -func TestCopyToPod(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - ns := scheme.Codecs - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, - NegotiatedSerializer: ns, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - responsePod := &v1.Pod{} - return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil - }), - } - - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdCp(tf, ioStreams) - - srcFile, err := ioutil.TempDir("", "test") - if err != nil { - t.Errorf("unexpected error: %v", err) - t.FailNow() - } - defer os.RemoveAll(srcFile) - - tests := map[string]struct { - dest string - expectedErr bool - }{ - "copy to directory": { - dest: "/tmp/", - expectedErr: false, - }, - "copy to root": { - dest: "/", - expectedErr: false, - }, - "copy to empty file name": { - dest: "", - expectedErr: true, - }, - } - - for name, test := range tests { - opts := NewCopyOptions(ioStreams) - src := fileSpec{ - File: srcFile, - } - dest := fileSpec{ - PodNamespace: "pod-ns", - PodName: "pod-name", - File: test.dest, - } - opts.Complete(tf, cmd) - t.Run(name, func(t *testing.T) { - err = opts.copyToPod(src, dest, &kexec.ExecOptions{}) - //If error is NotFound error , it indicates that the - //request has been sent correctly. - //Treat this as no error. - if test.expectedErr && errors.IsNotFound(err) { - t.Errorf("expected error but didn't get one") - } - if !test.expectedErr && !errors.IsNotFound(err) { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -func TestCopyToPodNoPreserve(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - ns := scheme.Codecs - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, - NegotiatedSerializer: ns, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - responsePod := &v1.Pod{} - return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil - }), - } - - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdCp(tf, ioStreams) - - srcFile, err := ioutil.TempDir("", "test") - if err != nil { - t.Errorf("unexpected error: %v", err) - t.FailNow() - } - defer os.RemoveAll(srcFile) - - tests := map[string]struct { - expectedCmd []string - nopreserve bool - }{ - "copy to pod no preserve user and permissions": { - expectedCmd: []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-", "-C", "."}, - nopreserve: true, - }, - "copy to pod preserve user and permissions": { - expectedCmd: []string{"tar", "-xmf", "-", "-C", "."}, - nopreserve: false, - }, - } - opts := NewCopyOptions(ioStreams) - src := fileSpec{ - File: srcFile, - } - dest := fileSpec{ - PodNamespace: "pod-ns", - PodName: "pod-name", - File: "foo", - } - opts.Complete(tf, cmd) - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - options := &kexec.ExecOptions{} - opts.NoPreserve = test.nopreserve - err = opts.copyToPod(src, dest, options) - if !(reflect.DeepEqual(test.expectedCmd, options.Command)) { - t.Errorf("expected cmd: %v, got: %v", test.expectedCmd, options.Command) - } - }) - } -} - -func TestValidate(t *testing.T) { - tests := []struct { - name string - args []string - expectedErr bool - }{ - { - name: "Validate Succeed", - args: []string{"1", "2"}, - expectedErr: false, - }, - { - name: "Validate Fail", - args: []string{"1", "2", "3"}, - expectedErr: true, - }, - } - tf := cmdtesting.NewTestFactory() - ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() - opts := NewCopyOptions(ioStreams) - cmd := NewCmdCp(tf, ioStreams) - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := opts.Validate(cmd, test.args) - if (err != nil) != test.expectedErr { - t.Errorf("expected error: %v, saw: %v, error: %v", test.expectedErr, err != nil, err) - } - }) - } -} - -func TestUntar(t *testing.T) { - testdir, err := ioutil.TempDir("", "test-untar") - require.NoError(t, err) - defer os.RemoveAll(testdir) - t.Logf("Test base: %s", testdir) - - basedir := filepath.Join(testdir, "base") - - type file struct { - path string - linkTarget string // For link types - expected string // Expect to find the file here (or not, if empty) - } - files := []file{{ - // Absolute file within dest - path: filepath.Join(basedir, "abs"), - expected: filepath.Join(basedir, basedir, "abs"), - }, { // Absolute file outside dest - path: filepath.Join(testdir, "abs-out"), - expected: filepath.Join(basedir, testdir, "abs-out"), - }, { // Absolute nested file within dest - path: filepath.Join(basedir, "nested/nest-abs"), - expected: filepath.Join(basedir, basedir, "nested/nest-abs"), - }, { // Absolute nested file outside dest - path: filepath.Join(basedir, "nested/../../nest-abs-out"), - expected: filepath.Join(basedir, testdir, "nest-abs-out"), - }, { // Relative file inside dest - path: "relative", - expected: filepath.Join(basedir, "relative"), - }, { // Relative file outside dest - path: "../unrelative", - expected: "", - }, { // Nested relative file inside dest - path: "nested/nest-rel", - expected: filepath.Join(basedir, "nested/nest-rel"), - }, { // Nested relative file outside dest - path: "nested/../../nest-unrelative", - expected: "", - }} - - mkExpectation := func(expected, suffix string) string { - if expected == "" { - return "" - } - return expected + suffix - } - mkBacklinkExpectation := func(expected, suffix string) string { - // "resolve" the back link relative to the expectation - targetDir := filepath.Dir(filepath.Dir(expected)) - // If the "resolved" target is not nested in basedir, it is escaping. - if !filepath.HasPrefix(targetDir, basedir) { - return "" - } - return expected + suffix - } - links := []file{} - for _, f := range files { - links = append(links, file{ - path: f.path + "-innerlink", - linkTarget: "link-target", - expected: mkExpectation(f.expected, "-innerlink"), - }, file{ - path: f.path + "-innerlink-abs", - linkTarget: filepath.Join(basedir, "link-target"), - expected: mkExpectation(f.expected, "-innerlink-abs"), - }, file{ - path: f.path + "-backlink", - linkTarget: filepath.Join("..", "link-target"), - expected: mkBacklinkExpectation(f.expected, "-backlink"), - }, file{ - path: f.path + "-outerlink-abs", - linkTarget: filepath.Join(testdir, "link-target"), - expected: "", - }) - - if f.expected != "" { - // outerlink is the number of backticks to escape to testdir - outerlink, _ := filepath.Rel(f.expected, testdir) - links = append(links, file{ - path: f.path + "outerlink", - linkTarget: filepath.Join(outerlink, "link-target"), - expected: "", - }) - } - } - files = append(files, links...) - - // Test back-tick escaping through a symlink. - files = append(files, - file{ - path: "nested/again/back-link", - linkTarget: "../../nested", - expected: filepath.Join(basedir, "nested/again/back-link"), - }, - file{ - path: "nested/again/back-link/../../../back-link-file", - expected: filepath.Join(basedir, "back-link-file"), - }) - - // Test chaining back-tick symlinks. - files = append(files, - file{ - path: "nested/back-link-first", - linkTarget: "../", - expected: filepath.Join(basedir, "nested/back-link-first"), - }, - file{ - path: "nested/back-link-first/back-link-second", - linkTarget: "../", - expected: "", - }) - - files = append(files, - file{ // Relative directory path with terminating / - path: "direct/dir/", - expected: "", - }) - - buf := &bytes.Buffer{} - tw := tar.NewWriter(buf) - expectations := map[string]bool{} - for _, f := range files { - if f.expected != "" { - expectations[f.expected] = false - } - if f.linkTarget == "" { - hdr := &tar.Header{ - Name: f.path, - Mode: 0666, - Size: int64(len(f.path)), - } - require.NoError(t, tw.WriteHeader(hdr), f.path) - if !strings.HasSuffix(f.path, "/") { - _, err := tw.Write([]byte(f.path)) - require.NoError(t, err, f.path) - } - } else { - hdr := &tar.Header{ - Name: f.path, - Mode: int64(0777 | os.ModeSymlink), - Typeflag: tar.TypeSymlink, - Linkname: f.linkTarget, - } - require.NoError(t, tw.WriteHeader(hdr), f.path) - } - } - tw.Close() - - // Capture warnings to stderr for debugging. - output := (*testWriter)(t) - opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output}) - - require.NoError(t, opts.untarAll(buf, filepath.Join(basedir), "")) - - filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil // Ignore directories. - } - if _, ok := expectations[path]; !ok { - t.Errorf("Unexpected file at %s", path) - } else { - expectations[path] = true - } - return nil - }) - for path, found := range expectations { - if !found { - t.Errorf("Missing expected file %s", path) - } - } -} - -func TestUntar_SingleFile(t *testing.T) { - testdir, err := ioutil.TempDir("", "test-untar") - require.NoError(t, err) - defer os.RemoveAll(testdir) - - dest := filepath.Join(testdir, "target") - - buf := &bytes.Buffer{} - tw := tar.NewWriter(buf) - - const ( - srcName = "source" - content = "file contents" - ) - hdr := &tar.Header{ - Name: srcName, - Mode: 0666, - Size: int64(len(content)), - } - require.NoError(t, tw.WriteHeader(hdr)) - _, err = tw.Write([]byte(content)) - require.NoError(t, err) - tw.Close() - - // Capture warnings to stderr for debugging. - output := (*testWriter)(t) - opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output}) - - require.NoError(t, opts.untarAll(buf, filepath.Join(dest), srcName)) - cmpFileData(t, dest, content) -} - -func createTmpFile(t *testing.T, filepath, data string) { - f, err := os.Create(filepath) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - defer f.Close() - if _, err := io.Copy(f, bytes.NewBuffer([]byte(data))); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } -} - -func cmpFileData(t *testing.T, filePath, data string) { - actual, err := ioutil.ReadFile(filePath) - require.NoError(t, err) - assert.EqualValues(t, data, actual) -} - -type testWriter testing.T - -func (t *testWriter) Write(p []byte) (n int, err error) { - t.Logf(string(p)) - return len(p), nil -}