mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 14:07:14 +00:00
Merge pull request #51215 from juanvallejo/jvallejo/preserve-specified-destination-path
Automatic merge from submit-queue (batch tested with PRs 53668, 53624, 52639, 53581, 51215). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. preserve specified destination path **Release note**: ```release-note "kubectl cp" updated to honor destination names ``` **Before** ``` $ kubectl cp foo_dir pod_name:/tmp/bar_dir $ kubectl exec pod_name -it -- /bin/sh sh-4.2$ sh-4.2$ ls /tmp sh-4.2$ foo_dir ``` **After** ``` $ kubectl cp foo_dir pod_name:/tmp/bar_dir $ kubectl exec pod_name -it -- /bin/sh sh-4.2$ sh-4.2$ ls /tmp sh-4.2$ bar_dir ``` **Notable changes to `kubectl cp` After This Patch** - Copying a directory `bar_dir` to an existing directory in the pod will copy the directory itself, rather than just the file contents: ```bash *Before* > remote-pod-shell$ ls /tmp existing_remote_dir $ kubectl cp ./my/local/awesome_dir mypod:/tmp/existing_remote_dir > remote-pod-shell$ ls /tmp existing_remote_dir awesome_dir ``` ```bash *After* > remote-pod-shell$ ls /tmp existing_remote_dir $ kubectl cp ./my/local/awesome_dir mypod:/tmp/existing_remote_dir > remote-pod-shell$ ls /tmp existing_remote_dir > remote-pod-shell$ ls /tmp/existing_remote_dir awesome_dir ``` ``` *Before*: Directory contents were merged if a local and remote directory shared the same name *After*: A new name will be honored for the copied local directory on the remote pod. If a new name was not specified for the local directory being copied, and it shares the same name as an already-existing directory on the pod, current behavior will follow and its contents will be added to those of the already-existing directory. ``` ``` *Before*: If a trailing slash (e.g. kubectl cp ./local/dir pod:/tmp) was not added to a directory name in the destination path (...:/tmp vs /tmp/...), when copying to a pod, `kubectl` would attempt to copy the local directory under the parent of the remote directory rather than inside of it. *After*: Slashes do not alter the behavior of the command, or destination of the intended source file or directory. With a command such as (kubectl cp ./local_dir pod:/tmp), `local_dir` would be copied inside of <pod:/tmp> (an error is returned if pod:/tmp is a file). ``` Related downstream bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=1469411 @fabianofranz @kubernetes/sig-cli-misc
This commit is contained in:
commit
7e384479d7
@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -134,11 +135,44 @@ func runCopy(f cmdutil.Factory, cmd *cobra.Command, out, cmderr io.Writer, args
|
|||||||
return cmdutil.UsageErrorf(cmd, "One of src or dest must be a remote file specification")
|
return cmdutil.UsageErrorf(cmd, "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 checkDestinationIsDir(dest fileSpec, f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
|
options := &ExecOptions{
|
||||||
|
StreamOptions: StreamOptions{
|
||||||
|
Out: bytes.NewBuffer([]byte{}),
|
||||||
|
Err: bytes.NewBuffer([]byte{}),
|
||||||
|
|
||||||
|
Namespace: dest.PodNamespace,
|
||||||
|
PodName: dest.PodName,
|
||||||
|
},
|
||||||
|
|
||||||
|
Command: []string{"test", "-d", dest.File},
|
||||||
|
Executor: &DefaultRemoteExecutor{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return execute(f, cmd, options)
|
||||||
|
}
|
||||||
|
|
||||||
func copyToPod(f cmdutil.Factory, cmd *cobra.Command, stdout, stderr io.Writer, src, dest fileSpec) error {
|
func copyToPod(f cmdutil.Factory, cmd *cobra.Command, stdout, stderr io.Writer, src, dest fileSpec) error {
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe()
|
||||||
|
|
||||||
|
// strip trailing slash (if any)
|
||||||
|
if strings.HasSuffix(string(dest.File[len(dest.File)-1]), "/") {
|
||||||
|
dest.File = dest.File[:len(dest.File)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkDestinationIsDir(dest, f, cmd); 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() {
|
go func() {
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
err := makeTar(src.File, writer)
|
err := makeTar(src.File, dest.File, writer)
|
||||||
cmdutil.CheckErr(err)
|
cmdutil.CheckErr(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -192,17 +226,18 @@ func copyFromPod(f cmdutil.Factory, cmd *cobra.Command, cmderr io.Writer, src, d
|
|||||||
return untarAll(reader, dest.File, prefix)
|
return untarAll(reader, dest.File, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTar(filepath string, writer io.Writer) error {
|
func makeTar(srcPath, destPath string, writer io.Writer) error {
|
||||||
// TODO: use compression here?
|
// TODO: use compression here?
|
||||||
tarWriter := tar.NewWriter(writer)
|
tarWriter := tar.NewWriter(writer)
|
||||||
defer tarWriter.Close()
|
defer tarWriter.Close()
|
||||||
|
|
||||||
filepath = path.Clean(filepath)
|
srcPath = path.Clean(srcPath)
|
||||||
return recursiveTar(path.Dir(filepath), path.Base(filepath), tarWriter)
|
destPath = path.Clean(destPath)
|
||||||
|
return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recursiveTar(base, file string, tw *tar.Writer) error {
|
func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) error {
|
||||||
filepath := path.Join(base, file)
|
filepath := path.Join(srcBase, srcFile)
|
||||||
stat, err := os.Stat(filepath)
|
stat, err := os.Stat(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -213,7 +248,7 @@ func recursiveTar(base, file string, tw *tar.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if err := recursiveTar(base, path.Join(file, f.Name()), tw); err != nil {
|
if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +258,7 @@ func recursiveTar(base, file string, tw *tar.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hdr.Name = file
|
hdr.Name = destFile
|
||||||
if err := tw.WriteHeader(hdr); err != nil {
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -129,7 +131,7 @@ func TestTarUntar(t *testing.T) {
|
|||||||
data: "bazblahfoo",
|
data: "bazblahfoo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "some/other/directory",
|
name: "some/other/directory/",
|
||||||
data: "with more data here",
|
data: "with more data here",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -157,7 +159,7 @@ func TestTarUntar(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writer := &bytes.Buffer{}
|
writer := &bytes.Buffer{}
|
||||||
if err := makeTar(dir, writer); err != nil {
|
if err := makeTar(dir, dir2, writer); err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,3 +294,83 @@ func TestCopyToLocalFileOrDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user