diff --git a/pkg/cmd/cp/cp.go b/pkg/cmd/cp/cp.go new file mode 100644 index 000000000..47737f473 --- /dev/null +++ b/pkg/cmd/cp/cp.go @@ -0,0 +1,536 @@ +/* +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" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "github.com/lithammer/dedent" + "github.com/spf13/cobra" + + "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" +) + +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. + # + # For advanced use cases, such as symlinks, wildcard expansion or + # file mode preservation consider using 'kubectl exec'. + + # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace + tar cf - /tmp/foo | kubectl exec -i -n -- tar xf - -C /tmp/bar + + # Copy /tmp/foo from a remote pod to /tmp/bar locally + kubectl exec -n -- tar cf - /tmp/foo | tar xf - -C /tmp/bar + + # 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 + ExecParentCmdName string + + 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) { + i := strings.Index(arg, ":") + + if i == -1 { + return fileSpec{File: arg}, nil + } + // filespec starting with a semicolon is invalid + if i == 0 { + return fileSpec{}, errFileSpecDoesntMatchFormat + } + + pod, file := arg[:i], arg[i+1:] + pieces := strings.Split(pod, "/") + switch len(pieces) { + case 1: + return fileSpec{ + PodName: pieces[0], + File: file, + }, nil + case 2: + return fileSpec{ + PodNamespace: pieces[0], + PodName: pieces[1], + File: file, + }, nil + default: + return fileSpec{}, errFileSpecDoesntMatchFormat + } +} + +// Complete completes all the required options +func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + if cmd.Parent() != nil { + o.ExecParentCmdName = cmd.Parent().CommandPath() + } + + 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(src, 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(src fileSpec, reader io.Reader, destDir, prefix string) error { + symlinkWarningPrinted := false + // 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 + } + + if mode&os.ModeSymlink != 0 { + if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 { + fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q (consider using \"%s exec -n %q %q -- tar cf - %q | tar xf -\")\n", destFileName, header.Linkname, o.ExecParentCmdName, src.PodNamespace, src.PodName, src.File) + symlinkWarningPrinted = true + continue + } + fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname) + continue + } + 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 new file mode 100644 index 000000000..e7ada179b --- /dev/null +++ b/pkg/cmd/cp/cp_test.go @@ -0,0 +1,933 @@ +/* +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", + omitted: true, + fileType: SymLink, + }, + { + name: "relative_to_dest", + data: path.Join(dir2, "foo"), + omitted: true, + 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(fileSpec{}, 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(fileSpec{}, 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(fileSpec{}, &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) + } + } +} + +func TestCopyToPod(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("test") + ns := scheme.Codecs.WithoutConversion() + 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.WithoutConversion() + 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: "", + }} + + links := []file{} + for _, f := range files { + links = append(links, file{ + path: f.path + "-innerlink", + linkTarget: "link-target", + expected: "", + }, file{ + path: f.path + "-innerlink-abs", + linkTarget: filepath.Join(basedir, "link-target"), + expected: "", + }, file{ + path: f.path + "-backlink", + linkTarget: filepath.Join("..", "link-target"), + expected: "", + }, 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: "", + }, + 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: "", + }, + 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(fileSpec{}, 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(fileSpec{}, 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 +}