...

Source file src/k8s.io/kubectl/pkg/cmd/cp/cp_test.go

Documentation: k8s.io/kubectl/pkg/cmd/cp

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cp
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"os"
    26  	"path/filepath"
    27  	"reflect"
    28  	"strings"
    29  	"testing"
    30  
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  
    34  	v1 "k8s.io/api/core/v1"
    35  	"k8s.io/apimachinery/pkg/api/errors"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/cli-runtime/pkg/genericiooptions"
    39  	"k8s.io/client-go/rest/fake"
    40  	kexec "k8s.io/kubectl/pkg/cmd/exec"
    41  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    42  	"k8s.io/kubectl/pkg/scheme"
    43  )
    44  
    45  type FileType int
    46  
    47  const (
    48  	RegularFile FileType = 0
    49  	SymLink     FileType = 1
    50  	RegexFile   FileType = 2
    51  )
    52  
    53  func TestExtractFileSpec(t *testing.T) {
    54  	tests := []struct {
    55  		spec              string
    56  		expectedPod       string
    57  		expectedNamespace string
    58  		expectedFile      string
    59  		expectErr         bool
    60  	}{
    61  		{
    62  			spec:              "namespace/pod:/some/file",
    63  			expectedPod:       "pod",
    64  			expectedNamespace: "namespace",
    65  			expectedFile:      "/some/file",
    66  		},
    67  		{
    68  			spec:         "pod:/some/file",
    69  			expectedPod:  "pod",
    70  			expectedFile: "/some/file",
    71  		},
    72  		{
    73  			spec:         "/some/file",
    74  			expectedFile: "/some/file",
    75  		},
    76  		{
    77  			spec:      ":file:not:exist:in:local:filesystem",
    78  			expectErr: true,
    79  		},
    80  		{
    81  			spec:      "namespace/pod/invalid:/some/file",
    82  			expectErr: true,
    83  		},
    84  		{
    85  			spec:         "pod:/some/filenamewith:in",
    86  			expectedPod:  "pod",
    87  			expectedFile: "/some/filenamewith:in",
    88  		},
    89  	}
    90  	for _, test := range tests {
    91  		spec, err := extractFileSpec(test.spec)
    92  		if test.expectErr && err == nil {
    93  			t.Errorf("unexpected non-error")
    94  			continue
    95  		}
    96  		if err != nil && !test.expectErr {
    97  			t.Errorf("unexpected error: %v", err)
    98  			continue
    99  		}
   100  		if spec.PodName != test.expectedPod {
   101  			t.Errorf("expected: %s, saw: %s", test.expectedPod, spec.PodName)
   102  		}
   103  		if spec.PodNamespace != test.expectedNamespace {
   104  			t.Errorf("expected: %s, saw: %s", test.expectedNamespace, spec.PodNamespace)
   105  		}
   106  		specFile := ""
   107  		if spec.File != nil {
   108  			specFile = spec.File.String()
   109  		}
   110  		if specFile != test.expectedFile {
   111  			t.Errorf("expected: %s, saw: %s", test.expectedFile, specFile)
   112  		}
   113  	}
   114  }
   115  
   116  func TestGetPrefix(t *testing.T) {
   117  	remoteSeparator := '/'
   118  	osSeparator := os.PathSeparator
   119  	tests := []struct {
   120  		input    string
   121  		expected string
   122  	}{
   123  		{
   124  			input:    "%[1]cfoo%[1]cbar",
   125  			expected: "foo%[1]cbar",
   126  		},
   127  		{
   128  			input:    "foo%[1]cbar",
   129  			expected: "foo%[1]cbar",
   130  		},
   131  	}
   132  	for _, test := range tests {
   133  		outRemote := newRemotePath(fmt.Sprintf(test.input, remoteSeparator)).StripSlashes()
   134  		expectedRemote := fmt.Sprintf(test.expected, remoteSeparator)
   135  		if outRemote.String() != expectedRemote {
   136  			t.Errorf("remote expected: %s, saw: %s", expectedRemote, outRemote.String())
   137  		}
   138  		outLocal := newLocalPath(fmt.Sprintf(test.input, osSeparator)).StripSlashes()
   139  		expectedLocal := fmt.Sprintf(test.expected, osSeparator)
   140  		if outLocal.String() != expectedLocal {
   141  			t.Errorf("local expected: %s, saw: %s", expectedLocal, outLocal.String())
   142  		}
   143  	}
   144  }
   145  
   146  func TestStripPathShortcuts(t *testing.T) {
   147  	tests := []struct {
   148  		name     string
   149  		input    string
   150  		expected string
   151  	}{
   152  		{
   153  			name:     "test single path shortcut prefix",
   154  			input:    "../foo/bar",
   155  			expected: "foo/bar",
   156  		},
   157  		{
   158  			name:     "test single path shortcut prefix",
   159  			input:    `..\foo\bar`,
   160  			expected: "foo/bar",
   161  		},
   162  		{
   163  			name:     "test multiple path shortcuts",
   164  			input:    "../../foo/bar",
   165  			expected: "foo/bar",
   166  		},
   167  		{
   168  			name:     "test multiple path shortcuts",
   169  			input:    `..\..\foo\bar`,
   170  			expected: "foo/bar",
   171  		},
   172  		{
   173  			name:     "test multiple path shortcuts with absolute path",
   174  			input:    "/tmp/one/two/../../foo/bar",
   175  			expected: "tmp/foo/bar",
   176  		},
   177  		{
   178  			name:     "test multiple path shortcuts with absolute path",
   179  			input:    `\tmp\one\two\..\..\foo\bar`,
   180  			expected: "tmp/foo/bar",
   181  		},
   182  		{
   183  			name:     "test multiple path shortcuts with no named directory",
   184  			input:    "../../",
   185  			expected: "",
   186  		},
   187  		{
   188  			name:     "test multiple path shortcuts with no named directory",
   189  			input:    `..\..\`,
   190  			expected: "",
   191  		},
   192  		{
   193  			name:     "test multiple path shortcuts with no named directory and no trailing slash",
   194  			input:    "../..",
   195  			expected: "",
   196  		},
   197  		{
   198  			name:     "test multiple path shortcuts with no named directory and no trailing slash",
   199  			input:    `..\..`,
   200  			expected: "",
   201  		},
   202  		{
   203  			name:     "test multiple path shortcuts with absolute path and filename containing leading dots",
   204  			input:    "/tmp/one/two/../../foo/..bar",
   205  			expected: "tmp/foo/..bar",
   206  		},
   207  		{
   208  			name:     "test multiple path shortcuts with absolute path and filename containing leading dots",
   209  			input:    `\tmp\one\two\..\..\foo\..bar`,
   210  			expected: "tmp/foo/..bar",
   211  		},
   212  		{
   213  			name:     "test multiple path shortcuts with no named directory and filename containing leading dots",
   214  			input:    "../...foo",
   215  			expected: "...foo",
   216  		},
   217  		{
   218  			name:     "test multiple path shortcuts with no named directory and filename containing leading dots",
   219  			input:    `..\...foo`,
   220  			expected: "...foo",
   221  		},
   222  		{
   223  			name:     "test filename containing leading dots",
   224  			input:    "...foo",
   225  			expected: "...foo",
   226  		},
   227  		{
   228  			name:     "test root directory",
   229  			input:    "/",
   230  			expected: "",
   231  		},
   232  		{
   233  			name:     "test root directory",
   234  			input:    `\`,
   235  			expected: "",
   236  		},
   237  	}
   238  
   239  	for i, test := range tests {
   240  		out := newRemotePath(test.input).StripShortcuts()
   241  		if out.String() != test.expected {
   242  			t.Errorf("expected[%d]: %s, saw: %s", i, test.expected, out)
   243  		}
   244  	}
   245  }
   246  func TestIsDestRelative(t *testing.T) {
   247  	tests := []struct {
   248  		base     string
   249  		dest     string
   250  		relative bool
   251  	}{
   252  		{
   253  			base:     "/dir",
   254  			dest:     "/dir/../link",
   255  			relative: false,
   256  		},
   257  		{
   258  			base:     "/dir",
   259  			dest:     "/dir/../../link",
   260  			relative: false,
   261  		},
   262  		{
   263  			base:     "/dir",
   264  			dest:     "/link",
   265  			relative: false,
   266  		},
   267  		{
   268  			base:     "/dir",
   269  			dest:     "/dir/link",
   270  			relative: true,
   271  		},
   272  		{
   273  			base:     "/dir",
   274  			dest:     "/dir/int/../link",
   275  			relative: true,
   276  		},
   277  		{
   278  			base:     "dir",
   279  			dest:     "dir/link",
   280  			relative: true,
   281  		},
   282  		{
   283  			base:     "dir",
   284  			dest:     "dir/int/../link",
   285  			relative: true,
   286  		},
   287  		{
   288  			base:     "dir",
   289  			dest:     "dir/../../link",
   290  			relative: false,
   291  		},
   292  	}
   293  
   294  	for i, test := range tests {
   295  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   296  			if test.relative != isRelative(newLocalPath(test.base), newLocalPath(test.dest)) {
   297  				t.Errorf("unexpected result for: base %q, dest %q", test.base, test.dest)
   298  			}
   299  		})
   300  	}
   301  }
   302  
   303  func checkErr(t *testing.T, err error) {
   304  	if err != nil {
   305  		t.Errorf("unexpected error: %v", err)
   306  		t.FailNow()
   307  	}
   308  }
   309  
   310  func TestTarUntar(t *testing.T) {
   311  	dir, err := os.MkdirTemp("", "input")
   312  	checkErr(t, err)
   313  	dir = dir + "/"
   314  
   315  	dir2, err := os.MkdirTemp("", "output")
   316  	checkErr(t, err)
   317  	dir2 = dir2 + "/"
   318  
   319  	dir3, err := os.MkdirTemp("", "dir")
   320  	checkErr(t, err)
   321  
   322  	defer func() {
   323  		os.RemoveAll(dir)
   324  		os.RemoveAll(dir2)
   325  		os.RemoveAll(dir3)
   326  	}()
   327  
   328  	files := []struct {
   329  		name     string
   330  		nameList []string
   331  		data     string
   332  		omitted  bool
   333  		fileType FileType
   334  	}{
   335  		{
   336  			name:     "foo",
   337  			data:     "foobarbaz",
   338  			fileType: RegularFile,
   339  		},
   340  		{
   341  			name:     "dir/blah",
   342  			data:     "bazblahfoo",
   343  			fileType: RegularFile,
   344  		},
   345  		{
   346  			name:     "some/other/directory",
   347  			data:     "with more data here",
   348  			fileType: RegularFile,
   349  		},
   350  		{
   351  			name:     "blah",
   352  			data:     "same file name different data",
   353  			fileType: RegularFile,
   354  		},
   355  		{
   356  			name:     "gakki",
   357  			data:     "tmp/gakki",
   358  			omitted:  true,
   359  			fileType: SymLink,
   360  		},
   361  		{
   362  			name:     "relative_to_dest",
   363  			data:     dir2 + "/foo",
   364  			omitted:  true,
   365  			fileType: SymLink,
   366  		},
   367  		{
   368  			name:     "tricky_relative",
   369  			data:     dir3 + "/xyz",
   370  			omitted:  true,
   371  			fileType: SymLink,
   372  		},
   373  		{
   374  			name:     "absolute_path",
   375  			data:     "/tmp/gakki",
   376  			omitted:  true,
   377  			fileType: SymLink,
   378  		},
   379  		{
   380  			name:     "blah*",
   381  			nameList: []string{"blah1", "blah2"},
   382  			data:     "regexp file name",
   383  			fileType: RegexFile,
   384  		},
   385  	}
   386  
   387  	for _, file := range files {
   388  		completePath := dir + file.name
   389  		if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil {
   390  			t.Fatalf("unexpected error: %v", err)
   391  		}
   392  		if file.fileType == RegularFile {
   393  			createTmpFile(t, completePath, file.data)
   394  		} else if file.fileType == SymLink {
   395  			err := os.Symlink(file.data, completePath)
   396  			if err != nil {
   397  				t.Fatalf("unexpected error: %v", err)
   398  			}
   399  		} else if file.fileType == RegexFile {
   400  			for _, fileName := range file.nameList {
   401  				createTmpFile(t, dir+fileName, file.data)
   402  			}
   403  		} else {
   404  			t.Fatalf("unexpected file type: %v", file)
   405  		}
   406  	}
   407  
   408  	opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard())
   409  
   410  	writer := &bytes.Buffer{}
   411  	if err := makeTar(newLocalPath(dir), newRemotePath(dir), writer); err != nil {
   412  		t.Fatalf("unexpected error: %v", err)
   413  	}
   414  
   415  	reader := bytes.NewBuffer(writer.Bytes())
   416  	if err := opts.untarAll("", "", "", remotePath{}, newLocalPath(dir2), reader); err != nil {
   417  		t.Fatalf("unexpected error: %v", err)
   418  	}
   419  
   420  	for _, file := range files {
   421  		absPath := dir2 + strings.TrimPrefix(dir, os.TempDir())
   422  		filePath := absPath + file.name
   423  
   424  		if file.fileType == RegularFile {
   425  			cmpFileData(t, filePath, file.data)
   426  		} else if file.fileType == SymLink {
   427  			dest, err := os.Readlink(filePath)
   428  			if file.omitted {
   429  				if err != nil && strings.Contains(err.Error(), "no such file or directory") {
   430  					continue
   431  				}
   432  				t.Fatalf("expected to omit symlink for %s", filePath)
   433  			}
   434  			if err != nil {
   435  				t.Fatalf("unexpected error: %v", err)
   436  			}
   437  
   438  			if file.data != dest {
   439  				t.Fatalf("expected: %s, saw: %s", file.data, dest)
   440  			}
   441  		} else if file.fileType == RegexFile {
   442  			for _, fileName := range file.nameList {
   443  				cmpFileData(t, dir+fileName, file.data)
   444  			}
   445  		} else {
   446  			t.Fatalf("unexpected file type: %v", file)
   447  		}
   448  	}
   449  }
   450  
   451  func TestTarUntarWrongPrefix(t *testing.T) {
   452  	dir, err := os.MkdirTemp("", "input")
   453  	checkErr(t, err)
   454  	dir = dir + "/"
   455  
   456  	dir2, err := os.MkdirTemp("", "output")
   457  	checkErr(t, err)
   458  
   459  	defer func() {
   460  		os.RemoveAll(dir)
   461  		os.RemoveAll(dir2)
   462  	}()
   463  
   464  	completePath := dir + "foo"
   465  	if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil {
   466  		t.Fatalf("unexpected error: %v", err)
   467  	}
   468  	createTmpFile(t, completePath, "sample data")
   469  
   470  	opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard())
   471  
   472  	writer := &bytes.Buffer{}
   473  	if err := makeTar(newLocalPath(dir), newRemotePath(dir), writer); err != nil {
   474  		t.Fatalf("unexpected error: %v", err)
   475  	}
   476  
   477  	reader := bytes.NewBuffer(writer.Bytes())
   478  	err = opts.untarAll("", "", "verylongprefix-showing-the-tar-was-tempered-with", remotePath{}, newLocalPath(dir2), reader)
   479  	if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") {
   480  		t.Fatalf("unexpected error: %v", err)
   481  	}
   482  }
   483  
   484  func TestTarDestinationName(t *testing.T) {
   485  	dir, err := os.MkdirTemp(os.TempDir(), "input")
   486  	dir2, err2 := os.MkdirTemp(os.TempDir(), "output")
   487  	if err != nil || err2 != nil {
   488  		t.Errorf("unexpected error: %v | %v", err, err2)
   489  		t.FailNow()
   490  	}
   491  	defer func() {
   492  		if err := os.RemoveAll(dir); err != nil {
   493  			t.Errorf("Unexpected error cleaning up: %v", err)
   494  		}
   495  		if err := os.RemoveAll(dir2); err != nil {
   496  			t.Errorf("Unexpected error cleaning up: %v", err)
   497  		}
   498  	}()
   499  
   500  	files := []struct {
   501  		name string
   502  		data string
   503  	}{
   504  		{
   505  			name: "foo",
   506  			data: "foobarbaz",
   507  		},
   508  		{
   509  			name: "dir/blah",
   510  			data: "bazblahfoo",
   511  		},
   512  		{
   513  			name: "some/other/directory",
   514  			data: "with more data here",
   515  		},
   516  		{
   517  			name: "blah",
   518  			data: "same file name different data",
   519  		},
   520  	}
   521  
   522  	// ensure files exist on disk
   523  	for _, file := range files {
   524  		completePath := dir + "/" + file.name
   525  		if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil {
   526  			t.Errorf("unexpected error: %v", err)
   527  			t.FailNow()
   528  		}
   529  		createTmpFile(t, completePath, file.data)
   530  	}
   531  
   532  	reader, writer := io.Pipe()
   533  	go func() {
   534  		if err := makeTar(newLocalPath(dir), newRemotePath(dir2), writer); err != nil {
   535  			t.Errorf("unexpected error: %v", err)
   536  		}
   537  	}()
   538  
   539  	tarReader := tar.NewReader(reader)
   540  	for {
   541  		hdr, err := tarReader.Next()
   542  		if err == io.EOF {
   543  			break
   544  		} else if err != nil {
   545  			t.Errorf("unexpected error: %v", err)
   546  			t.FailNow()
   547  		}
   548  
   549  		if !strings.HasPrefix(hdr.Name, filepath.Base(dir2)) {
   550  			t.Errorf("expected %q as destination filename prefix, saw: %q", filepath.Base(dir2), hdr.Name)
   551  		}
   552  	}
   553  }
   554  
   555  func TestBadTar(t *testing.T) {
   556  	dir, err := os.MkdirTemp(os.TempDir(), "dest")
   557  	if err != nil {
   558  		t.Errorf("unexpected error: %v ", err)
   559  		t.FailNow()
   560  	}
   561  	defer os.RemoveAll(dir)
   562  
   563  	// More or less cribbed from https://golang.org/pkg/archive/tar/#example__minimal
   564  	var buf bytes.Buffer
   565  	tw := tar.NewWriter(&buf)
   566  	var files = []struct {
   567  		name string
   568  		body string
   569  	}{
   570  		{"/prefix/foo/bar/../../home/bburns/names.txt", "Down and back"},
   571  	}
   572  	for _, file := range files {
   573  		hdr := &tar.Header{
   574  			Name: file.name,
   575  			Mode: 0600,
   576  			Size: int64(len(file.body)),
   577  		}
   578  		if err := tw.WriteHeader(hdr); err != nil {
   579  			t.Errorf("unexpected error: %v ", err)
   580  			t.FailNow()
   581  		}
   582  		if _, err := tw.Write([]byte(file.body)); err != nil {
   583  			t.Errorf("unexpected error: %v ", err)
   584  			t.FailNow()
   585  		}
   586  	}
   587  	if err := tw.Close(); err != nil {
   588  		t.Errorf("unexpected error: %v ", err)
   589  		t.FailNow()
   590  	}
   591  
   592  	opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard())
   593  	if err := opts.untarAll("", "", "/prefix", remotePath{}, newLocalPath(dir), &buf); err != nil {
   594  		t.Errorf("unexpected error: %v ", err)
   595  		t.FailNow()
   596  	}
   597  
   598  	for _, file := range files {
   599  		_, err := os.Stat(dir + filepath.Clean(file.name[len("/prefix"):]))
   600  		if err != nil {
   601  			t.Errorf("Error finding file: %v", err)
   602  		}
   603  	}
   604  }
   605  
   606  func TestCopyToPod(t *testing.T) {
   607  	tf := cmdtesting.NewTestFactory().WithNamespace("test")
   608  	ns := scheme.Codecs.WithoutConversion()
   609  	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   610  
   611  	tf.Client = &fake.RESTClient{
   612  		GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   613  		NegotiatedSerializer: ns,
   614  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   615  			responsePod := &v1.Pod{}
   616  			return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil
   617  		}),
   618  	}
   619  
   620  	tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   621  	ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   622  
   623  	cmd := NewCmdCp(tf, ioStreams)
   624  
   625  	srcFile, err := os.MkdirTemp("", "test")
   626  	if err != nil {
   627  		t.Errorf("unexpected error: %v", err)
   628  		t.FailNow()
   629  	}
   630  	defer os.RemoveAll(srcFile)
   631  
   632  	tests := map[string]struct {
   633  		src         string
   634  		dest        string
   635  		expectedErr bool
   636  	}{
   637  		"copy to directory": {
   638  			src:         srcFile,
   639  			dest:        "/tmp/",
   640  			expectedErr: false,
   641  		},
   642  		"copy to root": {
   643  			src:         srcFile,
   644  			dest:        "/",
   645  			expectedErr: false,
   646  		},
   647  		"copy to empty file name": {
   648  			src:         srcFile,
   649  			dest:        "",
   650  			expectedErr: true,
   651  		},
   652  		"copy unexisting file": {
   653  			src:         filepath.Join(srcFile, "nope"),
   654  			dest:        "/tmp",
   655  			expectedErr: true,
   656  		},
   657  	}
   658  
   659  	for name, test := range tests {
   660  		opts := NewCopyOptions(ioStreams)
   661  		opts.Complete(tf, cmd, []string{test.src, fmt.Sprintf("pod-ns/pod-name:%s", test.dest)})
   662  		t.Run(name, func(t *testing.T) {
   663  			err = opts.Run()
   664  			//If error is NotFound error , it indicates that the
   665  			//request has been sent correctly.
   666  			//Treat this as no error.
   667  			if test.expectedErr && errors.IsNotFound(err) {
   668  				t.Errorf("expected error but didn't get one")
   669  			}
   670  			if !test.expectedErr && !errors.IsNotFound(err) {
   671  				t.Errorf("unexpected error: %v", err)
   672  			}
   673  		})
   674  	}
   675  }
   676  
   677  func TestCopyToPodNoPreserve(t *testing.T) {
   678  	tf := cmdtesting.NewTestFactory().WithNamespace("test")
   679  	ns := scheme.Codecs.WithoutConversion()
   680  	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   681  
   682  	tf.Client = &fake.RESTClient{
   683  		GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   684  		NegotiatedSerializer: ns,
   685  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   686  			responsePod := &v1.Pod{}
   687  			return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil
   688  		}),
   689  	}
   690  
   691  	tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   692  	ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   693  
   694  	cmd := NewCmdCp(tf, ioStreams)
   695  
   696  	srcFile, err := os.MkdirTemp("", "test")
   697  	if err != nil {
   698  		t.Errorf("unexpected error: %v", err)
   699  		t.FailNow()
   700  	}
   701  	defer os.RemoveAll(srcFile)
   702  
   703  	tests := map[string]struct {
   704  		expectedCmd []string
   705  		nopreserve  bool
   706  	}{
   707  		"copy to pod no preserve user and permissions": {
   708  			expectedCmd: []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-", "-C", "."},
   709  			nopreserve:  true,
   710  		},
   711  		"copy to pod preserve user and permissions": {
   712  			expectedCmd: []string{"tar", "-xmf", "-", "-C", "."},
   713  			nopreserve:  false,
   714  		},
   715  	}
   716  	opts := NewCopyOptions(ioStreams)
   717  	src := fileSpec{
   718  		File: newLocalPath(srcFile),
   719  	}
   720  	dest := fileSpec{
   721  		PodNamespace: "pod-ns",
   722  		PodName:      "pod-name",
   723  		File:         newRemotePath("foo"),
   724  	}
   725  	opts.Complete(tf, cmd, nil)
   726  
   727  	for name, test := range tests {
   728  		t.Run(name, func(t *testing.T) {
   729  			options := &kexec.ExecOptions{}
   730  			opts.NoPreserve = test.nopreserve
   731  			err = opts.copyToPod(src, dest, options)
   732  			if !(reflect.DeepEqual(test.expectedCmd, options.Command)) {
   733  				t.Errorf("expected cmd: %v, got: %v", test.expectedCmd, options.Command)
   734  			}
   735  		})
   736  	}
   737  }
   738  
   739  func TestValidate(t *testing.T) {
   740  	tests := []struct {
   741  		name        string
   742  		args        []string
   743  		expectedErr bool
   744  	}{
   745  		{
   746  			name:        "Validate Succeed",
   747  			args:        []string{"1", "2"},
   748  			expectedErr: false,
   749  		},
   750  		{
   751  			name:        "Validate Fail",
   752  			args:        []string{"1", "2", "3"},
   753  			expectedErr: true,
   754  		},
   755  	}
   756  	ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   757  	opts := NewCopyOptions(ioStreams)
   758  
   759  	for _, test := range tests {
   760  		t.Run(test.name, func(t *testing.T) {
   761  			opts.args = test.args
   762  			err := opts.Validate()
   763  			if (err != nil) != test.expectedErr {
   764  				t.Errorf("expected error: %v, saw: %v, error: %v", test.expectedErr, err != nil, err)
   765  			}
   766  		})
   767  	}
   768  }
   769  
   770  func TestUntar(t *testing.T) {
   771  	testdir, err := os.MkdirTemp("", "test-untar")
   772  	require.NoError(t, err)
   773  	defer os.RemoveAll(testdir)
   774  	t.Logf("Test base: %s", testdir)
   775  
   776  	basedir := testdir + "/" + "base"
   777  
   778  	type file struct {
   779  		path       string
   780  		linkTarget string // For link types
   781  		expected   string // Expect to find the file here (or not, if empty)
   782  	}
   783  	files := []file{{
   784  		// Absolute file within dest
   785  		path:     basedir + "/" + "abs",
   786  		expected: basedir + basedir + "/" + "abs",
   787  	}, { // Absolute file outside dest
   788  		path:     testdir + "/" + "abs-out",
   789  		expected: basedir + testdir + "/" + "abs-out",
   790  	}, { // Absolute nested file within dest
   791  		path:     basedir + "/" + "nested/nest-abs",
   792  		expected: basedir + basedir + "/" + "nested/nest-abs",
   793  	}, { // Absolute nested file outside dest
   794  		path:     basedir + "/" + "nested/../../nest-abs-out",
   795  		expected: basedir + testdir + "/" + "nest-abs-out",
   796  	}, { // Relative file inside dest
   797  		path:     "relative",
   798  		expected: basedir + "/" + "relative",
   799  	}, { // Relative file outside dest
   800  		path:     "../unrelative",
   801  		expected: "",
   802  	}, { // Relative file outside dest (windows)
   803  		path:     `..\unrelative-windows`,
   804  		expected: "",
   805  	}, { // Nested relative file inside dest
   806  		path:     "nested/nest-rel",
   807  		expected: basedir + "/" + "nested/nest-rel",
   808  	}, { // Nested relative file outside dest
   809  		path:     "nested/../../nest-unrelative",
   810  		expected: "",
   811  	}, { // Nested relative file outside dest (windows)
   812  		path:     `nested\..\..\nest-unrelative`,
   813  		expected: "",
   814  	}}
   815  
   816  	links := []file{}
   817  	for _, f := range files {
   818  		links = append(links, file{
   819  			path:       f.path + "-innerlink",
   820  			linkTarget: "link-target",
   821  			expected:   "",
   822  		}, file{
   823  			path:       f.path + "-innerlink-abs",
   824  			linkTarget: basedir + "/" + "link-target",
   825  			expected:   "",
   826  		}, file{
   827  			path:       f.path + "-backlink",
   828  			linkTarget: ".." + "/" + "link-target",
   829  			expected:   "",
   830  		}, file{
   831  			path:       f.path + "-outerlink-abs",
   832  			linkTarget: testdir + "/" + "link-target",
   833  			expected:   "",
   834  		})
   835  
   836  		if f.expected != "" {
   837  			// outerlink is the number of backticks to escape to testdir
   838  			outerlink, _ := filepath.Rel(f.expected, testdir)
   839  			links = append(links, file{
   840  				path:       f.path + "outerlink",
   841  				linkTarget: outerlink + "/" + "link-target",
   842  				expected:   "",
   843  			})
   844  		}
   845  	}
   846  	files = append(files, links...)
   847  
   848  	// Test back-tick escaping through a symlink.
   849  	files = append(files,
   850  		file{
   851  			path:       "nested/again/back-link",
   852  			linkTarget: "../../nested",
   853  			expected:   "",
   854  		},
   855  		file{
   856  			path:     "nested/again/back-link/../../../back-link-file",
   857  			expected: basedir + "/" + "back-link-file",
   858  		})
   859  
   860  	// Test chaining back-tick symlinks.
   861  	files = append(files,
   862  		file{
   863  			path:       "nested/back-link-first",
   864  			linkTarget: "../",
   865  			expected:   "",
   866  		},
   867  		file{
   868  			path:       "nested/back-link-first/back-link-second",
   869  			linkTarget: "../",
   870  			expected:   "",
   871  		})
   872  
   873  	files = append(files,
   874  		file{ // Relative directory path with terminating /
   875  			path:     "direct/dir/",
   876  			expected: "",
   877  		})
   878  
   879  	buf := &bytes.Buffer{}
   880  	tw := tar.NewWriter(buf)
   881  	expectations := map[string]bool{}
   882  	for _, f := range files {
   883  		if f.expected != "" {
   884  			expectations[f.expected] = false
   885  		}
   886  		if f.linkTarget == "" {
   887  			hdr := &tar.Header{
   888  				Name: f.path,
   889  				Mode: 0666,
   890  				Size: int64(len(f.path)),
   891  			}
   892  			require.NoError(t, tw.WriteHeader(hdr), f.path)
   893  			if !strings.HasSuffix(f.path, "/") {
   894  				_, err := tw.Write([]byte(f.path))
   895  				require.NoError(t, err, f.path)
   896  			}
   897  		} else {
   898  			hdr := &tar.Header{
   899  				Name:     f.path,
   900  				Mode:     int64(0777 | os.ModeSymlink),
   901  				Typeflag: tar.TypeSymlink,
   902  				Linkname: f.linkTarget,
   903  			}
   904  			require.NoError(t, tw.WriteHeader(hdr), f.path)
   905  		}
   906  	}
   907  	tw.Close()
   908  
   909  	// Capture warnings to stderr for debugging.
   910  	output := (*testWriter)(t)
   911  	opts := NewCopyOptions(genericiooptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
   912  
   913  	require.NoError(t, opts.untarAll("", "", "", remotePath{}, newLocalPath(basedir), buf))
   914  
   915  	filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error {
   916  		if err != nil {
   917  			return err
   918  		}
   919  		if info.IsDir() {
   920  			return nil // Ignore directories.
   921  		}
   922  		if _, ok := expectations[path]; !ok {
   923  			t.Errorf("Unexpected file at %s", path)
   924  		} else {
   925  			expectations[path] = true
   926  		}
   927  		return nil
   928  	})
   929  	for path, found := range expectations {
   930  		if !found {
   931  			t.Errorf("Missing expected file %s", path)
   932  		}
   933  	}
   934  }
   935  
   936  func TestUntar_SingleFile(t *testing.T) {
   937  	testdir, err := os.MkdirTemp("", "test-untar")
   938  	require.NoError(t, err)
   939  	defer os.RemoveAll(testdir)
   940  
   941  	dest := testdir + "/" + "target"
   942  
   943  	buf := &bytes.Buffer{}
   944  	tw := tar.NewWriter(buf)
   945  
   946  	const (
   947  		srcName = "source"
   948  		content = "file contents"
   949  	)
   950  	hdr := &tar.Header{
   951  		Name: srcName,
   952  		Mode: 0666,
   953  		Size: int64(len(content)),
   954  	}
   955  	require.NoError(t, tw.WriteHeader(hdr))
   956  	_, err = tw.Write([]byte(content))
   957  	require.NoError(t, err)
   958  	tw.Close()
   959  
   960  	// Capture warnings to stderr for debugging.
   961  	output := (*testWriter)(t)
   962  	opts := NewCopyOptions(genericiooptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
   963  
   964  	require.NoError(t, opts.untarAll("", "", srcName, remotePath{}, newLocalPath(dest), buf))
   965  	cmpFileData(t, dest, content)
   966  }
   967  
   968  func createTmpFile(t *testing.T, filepath, data string) {
   969  	f, err := os.Create(filepath)
   970  	if err != nil {
   971  		t.Fatalf("unexpected error: %v", err)
   972  	}
   973  	defer f.Close()
   974  	if _, err := io.Copy(f, bytes.NewBuffer([]byte(data))); err != nil {
   975  		t.Fatalf("unexpected error: %v", err)
   976  	}
   977  	if err := f.Close(); err != nil {
   978  		t.Fatal(err)
   979  	}
   980  }
   981  
   982  func cmpFileData(t *testing.T, filePath, data string) {
   983  	actual, err := os.ReadFile(filePath)
   984  	require.NoError(t, err)
   985  	assert.EqualValues(t, data, actual)
   986  }
   987  
   988  type testWriter testing.T
   989  
   990  func (t *testWriter) Write(p []byte) (n int, err error) {
   991  	t.Logf(string(p))
   992  	return len(p), nil
   993  }
   994  

View as plain text