diff --git a/.generated_docs b/.generated_docs
index 6d76553002a..2ae3d57dfcd 100644
--- a/.generated_docs
+++ b/.generated_docs
@@ -33,6 +33,7 @@ docs/man/man1/kubectl-config-view.1
docs/man/man1/kubectl-config.1
docs/man/man1/kubectl-convert.1
docs/man/man1/kubectl-cordon.1
+docs/man/man1/kubectl-cp.1
docs/man/man1/kubectl-create-configmap.1
docs/man/man1/kubectl-create-deployment.1
docs/man/man1/kubectl-create-namespace.1
@@ -107,6 +108,7 @@ docs/user-guide/kubectl/kubectl_config_use-context.md
docs/user-guide/kubectl/kubectl_config_view.md
docs/user-guide/kubectl/kubectl_convert.md
docs/user-guide/kubectl/kubectl_cordon.md
+docs/user-guide/kubectl/kubectl_cp.md
docs/user-guide/kubectl/kubectl_create.md
docs/user-guide/kubectl/kubectl_create_configmap.md
docs/user-guide/kubectl/kubectl_create_deployment.md
@@ -165,6 +167,7 @@ docs/yaml/kubectl/kubectl_completion.yaml
docs/yaml/kubectl/kubectl_config.yaml
docs/yaml/kubectl/kubectl_convert.yaml
docs/yaml/kubectl/kubectl_cordon.yaml
+docs/yaml/kubectl/kubectl_cp.yaml
docs/yaml/kubectl/kubectl_create.yaml
docs/yaml/kubectl/kubectl_delete.yaml
docs/yaml/kubectl/kubectl_describe.yaml
diff --git a/docs/man/man1/kubectl-cp.1 b/docs/man/man1/kubectl-cp.1
new file mode 100644
index 00000000000..b6fd7a0f989
--- /dev/null
+++ b/docs/man/man1/kubectl-cp.1
@@ -0,0 +1,3 @@
+This file is autogenerated, but we've stopped checking such files into the
+repository to reduce the need for rebases. Please run hack/generate-docs.sh to
+populate this file.
diff --git a/docs/user-guide/kubectl/kubectl_cp.md b/docs/user-guide/kubectl/kubectl_cp.md
new file mode 100644
index 00000000000..b6c946c509b
--- /dev/null
+++ b/docs/user-guide/kubectl/kubectl_cp.md
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
PLEASE NOTE: This document applies to the HEAD of the source tree
+
+If you are using a released version of Kubernetes, you should
+refer to the docs that go with that version.
+
+Documentation for other releases can be found at
+[releases.k8s.io](http://releases.k8s.io).
+
+--
+
+
+
+
+
+This file is autogenerated, but we've stopped checking such files into the
+repository to reduce the need for rebases. Please run hack/generate-docs.sh to
+populate this file.
+
+
+[]()
+
diff --git a/docs/yaml/kubectl/kubectl_cp.yaml b/docs/yaml/kubectl/kubectl_cp.yaml
new file mode 100644
index 00000000000..b6fd7a0f989
--- /dev/null
+++ b/docs/yaml/kubectl/kubectl_cp.yaml
@@ -0,0 +1,3 @@
+This file is autogenerated, but we've stopped checking such files into the
+repository to reduce the need for rebases. Please run hack/generate-docs.sh to
+populate this file.
diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD
index f1da523b696..5a05ef69756 100644
--- a/pkg/kubectl/cmd/BUILD
+++ b/pkg/kubectl/cmd/BUILD
@@ -23,6 +23,7 @@ go_library(
"cmd.go",
"completion.go",
"convert.go",
+ "cp.go",
"create.go",
"create_configmap.go",
"create_deployment.go",
@@ -110,6 +111,7 @@ go_library(
"//vendor:github.com/evanphx/json-patch",
"//vendor:github.com/golang/glog",
"//vendor:github.com/jonboulle/clockwork",
+ "//vendor:github.com/renstrom/dedent",
"//vendor:github.com/spf13/cobra",
],
)
@@ -122,6 +124,7 @@ go_test(
"attach_test.go",
"clusterinfo_dump_test.go",
"cmd_test.go",
+ "cp_test.go",
"create_configmap_test.go",
"create_deployment_test.go",
"create_namespace_test.go",
diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go
index 3ef365d14cd..abdb1244768 100644
--- a/pkg/kubectl/cmd/cmd.go
+++ b/pkg/kubectl/cmd/cmd.go
@@ -265,6 +265,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
NewCmdExec(f, in, out, err),
NewCmdPortForward(f, out, err),
NewCmdProxy(f, out),
+ NewCmdCp(f, in, out, err),
},
},
{
diff --git a/pkg/kubectl/cmd/cp.go b/pkg/kubectl/cmd/cp.go
new file mode 100644
index 00000000000..c4895b95d84
--- /dev/null
+++ b/pkg/kubectl/cmd/cp.go
@@ -0,0 +1,303 @@
+/*
+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 cmd
+
+import (
+ "archive/tar"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+
+ "k8s.io/kubernetes/pkg/kubectl/cmd/templates"
+ cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
+
+ "github.com/renstrom/dedent"
+ "github.com/spf13/cobra"
+)
+
+var (
+ cp_example = templates.Examples(`
+ # !!!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`)
+)
+
+// NewCmdCp creates a new Copy command.
+func NewCmdCp(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "cp ",
+ Short: "Copy files and directories to and from containers.",
+ Long: "Copy files and directories to and from containers.",
+ Example: cp_example,
+ Run: func(cmd *cobra.Command, args []string) {
+ cmdutil.CheckErr(runCopy(f, cmd, cmdOut, cmdErr, args))
+ },
+ }
+ cmd.Flags().StringP("container", "c", "", "Container name. If omitted, the first container in the pod will be chosen")
+
+ return cmd
+}
+
+type fileSpec struct {
+ PodNamespace string
+ PodName string
+ File string
+}
+
+func extractFileSpec(arg string) (fileSpec, error) {
+ pieces := strings.Split(arg, ":")
+ if len(pieces) == 1 {
+ return fileSpec{File: arg}, nil
+ }
+ if len(pieces) != 2 {
+ return fileSpec{}, fmt.Errorf("Unexpected fileSpec: %s, expected [[namespace/]pod:]file/path", arg)
+ }
+ file := pieces[1]
+
+ pieces = strings.Split(pieces[0], "/")
+ 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{}, fmt.Errorf("Unexpected file spec: %s, expected [[namespace/]pod:]file/path", arg)
+}
+
+func runCopy(f cmdutil.Factory, cmd *cobra.Command, out, cmderr io.Writer, args []string) error {
+ if len(args) != 2 {
+ return cmdutil.UsageError(cmd, cpUsageStr)
+ }
+ 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 {
+ return copyFromPod(f, cmd, out, cmderr, srcSpec, destSpec)
+ }
+ if len(destSpec.PodName) != 0 {
+ return copyToPod(f, cmd, out, cmderr, srcSpec, destSpec)
+ }
+ return cmdutil.UsageError(cmd, "One of src or dest must be a remote file specification")
+}
+
+func copyToPod(f cmdutil.Factory, cmd *cobra.Command, stdout, stderr io.Writer, src, dest fileSpec) error {
+ reader, writer := io.Pipe()
+ go func() {
+ defer writer.Close()
+ err := makeTar(src.File, writer)
+ cmdutil.CheckErr(err)
+ }()
+
+ // TODO: Improve error messages by first testing if 'tar' is present in the container?
+ cmdArr := []string{"tar", "xf", "-"}
+ destDir := path.Dir(dest.File)
+ if len(destDir) > 0 {
+ cmdArr = append(cmdArr, "-C", destDir)
+ }
+
+ options := &ExecOptions{
+ StreamOptions: StreamOptions{
+ In: reader,
+ Out: stdout,
+ Err: stderr,
+ Stdin: true,
+
+ Namespace: dest.PodNamespace,
+ PodName: dest.PodName,
+ },
+
+ Command: cmdArr,
+ Executor: &DefaultRemoteExecutor{},
+ }
+ return execute(f, cmd, options)
+}
+
+func copyFromPod(f cmdutil.Factory, cmd *cobra.Command, out, cmderr io.Writer, src, dest fileSpec) error {
+ reader, outStream := io.Pipe()
+ options := &ExecOptions{
+ StreamOptions: StreamOptions{
+ In: nil,
+ Out: outStream,
+ Err: cmderr,
+
+ 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: &DefaultRemoteExecutor{},
+ }
+
+ go func() {
+ defer outStream.Close()
+ execute(f, cmd, options)
+ }()
+ prefix := getPrefix(src.File)
+
+ return untarAll(reader, dest.File, prefix)
+}
+
+func makeTar(filepath string, writer io.Writer) error {
+ // TODO: use compression here?
+ tarWriter := tar.NewWriter(writer)
+ defer tarWriter.Close()
+ return recursiveTar(path.Dir(filepath), path.Base(filepath), tarWriter)
+}
+
+func recursiveTar(base, file string, tw *tar.Writer) error {
+ filepath := path.Join(base, file)
+ stat, err := os.Stat(filepath)
+ if err != nil {
+ return err
+ }
+ if stat.IsDir() {
+ files, err := ioutil.ReadDir(filepath)
+ if err != nil {
+ return err
+ }
+ for _, f := range files {
+ if err := recursiveTar(base, path.Join(file, f.Name()), tw); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ hdr, err := tar.FileInfoHeader(stat, filepath)
+ if err != nil {
+ return err
+ }
+ hdr.Name = file
+ if err := tw.WriteHeader(hdr); err != nil {
+ return err
+ }
+ f, err := os.Open(filepath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = io.Copy(tw, f)
+ return err
+}
+
+func untarAll(reader io.Reader, destFile, 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
+ }
+ outFileName := path.Join(destFile, header.Name[len(prefix):])
+ baseName := path.Dir(outFileName)
+ if err := os.MkdirAll(baseName, 0755); err != nil {
+ return err
+ }
+ if header.FileInfo().IsDir() {
+ os.MkdirAll(outFileName, 0755)
+ continue
+ }
+ outFile, err := os.Create(outFileName)
+ if err != nil {
+ return err
+ }
+ defer outFile.Close()
+ io.Copy(outFile, tarReader)
+ }
+ return nil
+}
+
+func getPrefix(file string) string {
+ if file[0] == '/' {
+ // tar strips the leading '/' if it's there, so we will too
+ return file[1:]
+ }
+ return file
+}
+
+func execute(f cmdutil.Factory, cmd *cobra.Command, options *ExecOptions) error {
+ if len(options.Namespace) == 0 {
+ namespace, _, err := f.DefaultNamespace()
+ if err != nil {
+ return err
+ }
+ options.Namespace = namespace
+ }
+
+ container := cmdutil.GetFlagString(cmd, "container")
+ if len(container) > 0 {
+ options.ContainerName = container
+ }
+
+ config, err := f.ClientConfig()
+ if err != nil {
+ return err
+ }
+ options.Config = config
+
+ clientset, err := f.ClientSet()
+ if err != nil {
+ return err
+ }
+ options.PodClient = clientset.Core()
+
+ if err := options.Validate(); err != nil {
+ return err
+ }
+
+ if err := options.Run(); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/pkg/kubectl/cmd/cp_test.go b/pkg/kubectl/cmd/cp_test.go
new file mode 100644
index 00000000000..0494fc78e2f
--- /dev/null
+++ b/pkg/kubectl/cmd/cp_test.go
@@ -0,0 +1,180 @@
+/*
+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 cmd
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "testing"
+)
+
+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: "some:bad:spec",
+ expectErr: true,
+ },
+ }
+ 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 TestTarUntar(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",
+ },
+ }
+
+ 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()
+ }
+ f, err := os.Create(filepath)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ t.FailNow()
+ }
+ defer f.Close()
+ if _, err := io.Copy(f, bytes.NewBuffer([]byte(file.data))); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ t.FailNow()
+ }
+ }
+
+ writer := &bytes.Buffer{}
+ if err := makeTar(dir, writer); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ reader := bytes.NewBuffer(writer.Bytes())
+ if err := untarAll(reader, dir2, ""); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ t.FailNow()
+ }
+
+ for _, file := range files {
+ filepath := path.Join(dir, file.name)
+ f, err := os.Open(filepath)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ defer f.Close()
+ buff := &bytes.Buffer{}
+ io.Copy(buff, f)
+ if file.data != string(buff.Bytes()) {
+ t.Errorf("expected: %s, saw: %s", file.data, string(buff.Bytes()))
+ }
+ }
+}