...

Source file src/k8s.io/kubectl/pkg/cmd/diff/diff_test.go

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

     1  /*
     2  Copyright 2017 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 diff
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/cli-runtime/pkg/genericiooptions"
    33  	"k8s.io/utils/exec"
    34  )
    35  
    36  type FakeObject struct {
    37  	name   string
    38  	merged map[string]interface{}
    39  	live   map[string]interface{}
    40  }
    41  
    42  var _ Object = &FakeObject{}
    43  
    44  func (f *FakeObject) Name() string {
    45  	return f.name
    46  }
    47  
    48  func (f *FakeObject) Merged() (runtime.Object, error) {
    49  	// Return nil if merged object does not exist
    50  	if f.merged == nil {
    51  		return nil, nil
    52  	}
    53  	return &unstructured.Unstructured{Object: f.merged}, nil
    54  }
    55  
    56  func (f *FakeObject) Live() runtime.Object {
    57  	// Return nil if live object does not exist
    58  	if f.live == nil {
    59  		return nil
    60  	}
    61  	return &unstructured.Unstructured{Object: f.live}
    62  }
    63  
    64  func TestDiffProgram(t *testing.T) {
    65  	externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"}
    66  
    67  	t.Setenv("LANG", "C")
    68  
    69  	for i, c := range externalDiffCommands {
    70  		t.Setenv("KUBECTL_EXTERNAL_DIFF", c)
    71  		streams, _, stdout, _ := genericiooptions.NewTestIOStreams()
    72  		diff := DiffProgram{
    73  			IOStreams: streams,
    74  			Exec:      exec.New(),
    75  		}
    76  		err := diff.Run("/dev/zero", "/dev/zero")
    77  		if err != nil {
    78  			t.Fatal(err)
    79  		}
    80  
    81  		// Testing diff --report-identical-files
    82  		if i == 2 {
    83  			output_msg := "Files /dev/zero and /dev/zero are identical\n"
    84  			if output := stdout.String(); output != output_msg {
    85  				t.Fatalf(`stdout = %q, expected = %s"`, output, output_msg)
    86  			}
    87  		}
    88  	}
    89  }
    90  
    91  func TestPrinter(t *testing.T) {
    92  	printer := Printer{}
    93  
    94  	obj := &unstructured.Unstructured{Object: map[string]interface{}{
    95  		"string": "string",
    96  		"list":   []int{1, 2, 3},
    97  		"int":    12,
    98  	}}
    99  	buf := bytes.Buffer{}
   100  	printer.Print(obj, &buf)
   101  	want := `int: 12
   102  list:
   103  - 1
   104  - 2
   105  - 3
   106  string: string
   107  `
   108  	if buf.String() != want {
   109  		t.Errorf("Print() = %q, want %q", buf.String(), want)
   110  	}
   111  }
   112  
   113  func TestDiffVersion(t *testing.T) {
   114  	diff, err := NewDiffVersion("MERGED")
   115  	if err != nil {
   116  		t.Fatal(err)
   117  	}
   118  	defer diff.Dir.Delete()
   119  
   120  	obj := FakeObject{
   121  		name:   "bla",
   122  		live:   map[string]interface{}{"live": true},
   123  		merged: map[string]interface{}{"merged": true},
   124  	}
   125  	rObj, err := obj.Merged()
   126  	if err != nil {
   127  		t.Fatal(err)
   128  	}
   129  	err = diff.Print(obj.Name(), rObj, Printer{})
   130  	if err != nil {
   131  		t.Fatal(err)
   132  	}
   133  	fcontent, err := os.ReadFile(path.Join(diff.Dir.Name, obj.Name()))
   134  	if err != nil {
   135  		t.Fatal(err)
   136  	}
   137  	econtent := "merged: true\n"
   138  	if string(fcontent) != econtent {
   139  		t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
   140  	}
   141  }
   142  
   143  func TestDirectory(t *testing.T) {
   144  	dir, err := CreateDirectory("prefix")
   145  	defer dir.Delete()
   146  	if err != nil {
   147  		t.Fatal(err)
   148  	}
   149  	_, err = os.Stat(dir.Name)
   150  	if err != nil {
   151  		t.Fatal(err)
   152  	}
   153  	if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") {
   154  		t.Fatalf(`Directory doesn't start with "prefix": %q`, dir.Name)
   155  	}
   156  	entries, err := os.ReadDir(dir.Name)
   157  	if err != nil {
   158  		t.Fatal(err)
   159  	}
   160  	if len(entries) != 0 {
   161  		t.Fatalf("Directory should be empty, has %d elements", len(entries))
   162  	}
   163  	_, err = dir.NewFile("ONE")
   164  	if err != nil {
   165  		t.Fatal(err)
   166  	}
   167  	_, err = dir.NewFile("TWO")
   168  	if err != nil {
   169  		t.Fatal(err)
   170  	}
   171  	entries, err = os.ReadDir(dir.Name)
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	if len(entries) != 2 {
   176  		t.Fatalf("ReadDir should have two elements, has %d elements", len(entries))
   177  	}
   178  	err = dir.Delete()
   179  	if err != nil {
   180  		t.Fatal(err)
   181  	}
   182  	_, err = os.Stat(dir.Name)
   183  	if err == nil {
   184  		t.Fatal("Directory should be gone, still present.")
   185  	}
   186  }
   187  
   188  func TestDiffer(t *testing.T) {
   189  	diff, err := NewDiffer("LIVE", "MERGED")
   190  	if err != nil {
   191  		t.Fatal(err)
   192  	}
   193  	defer diff.TearDown()
   194  
   195  	obj := FakeObject{
   196  		name:   "bla",
   197  		live:   map[string]interface{}{"live": true},
   198  		merged: map[string]interface{}{"merged": true},
   199  	}
   200  	err = diff.Diff(&obj, Printer{}, true)
   201  	if err != nil {
   202  		t.Fatal(err)
   203  	}
   204  	fcontent, err := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
   205  	if err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	econtent := "live: true\n"
   209  	if string(fcontent) != econtent {
   210  		t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
   211  	}
   212  
   213  	fcontent, err = os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  	econtent = "merged: true\n"
   218  	if string(fcontent) != econtent {
   219  		t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
   220  	}
   221  }
   222  
   223  func TestShowManagedFields(t *testing.T) {
   224  	diff, err := NewDiffer("LIVE", "MERGED")
   225  	if err != nil {
   226  		t.Fatal(err)
   227  	}
   228  	defer diff.TearDown()
   229  
   230  	testCases := []struct {
   231  		name                string
   232  		showManagedFields   bool
   233  		expectedFromContent string
   234  		expectedToContent   string
   235  	}{
   236  		{
   237  			name:              "without managed fields",
   238  			showManagedFields: false,
   239  			expectedFromContent: `live: true
   240  metadata:
   241    name: foo
   242  `,
   243  			expectedToContent: `merged: true
   244  metadata:
   245    name: foo
   246  `,
   247  		},
   248  		{
   249  			name:              "with managed fields",
   250  			showManagedFields: true,
   251  			expectedFromContent: `live: true
   252  metadata:
   253    managedFields: mf-data
   254    name: foo
   255  `,
   256  			expectedToContent: `merged: true
   257  metadata:
   258    managedFields: mf-data
   259    name: foo
   260  `,
   261  		},
   262  	}
   263  
   264  	for i, tc := range testCases {
   265  		t.Run(tc.name, func(t *testing.T) {
   266  			obj := FakeObject{
   267  				name: fmt.Sprintf("TestCase%d", i),
   268  				live: map[string]interface{}{
   269  					"live": true,
   270  					"metadata": map[string]interface{}{
   271  						"managedFields": "mf-data",
   272  						"name":          "foo",
   273  					},
   274  				},
   275  				merged: map[string]interface{}{
   276  					"merged": true,
   277  					"metadata": map[string]interface{}{
   278  						"managedFields": "mf-data",
   279  						"name":          "foo",
   280  					},
   281  				},
   282  			}
   283  
   284  			err = diff.Diff(&obj, Printer{}, tc.showManagedFields)
   285  			if err != nil {
   286  				t.Fatal(err)
   287  			}
   288  
   289  			actualFromContent, _ := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
   290  			if string(actualFromContent) != tc.expectedFromContent {
   291  				t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent)
   292  			}
   293  
   294  			actualToContent, _ := os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
   295  			if string(actualToContent) != tc.expectedToContent {
   296  				t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent)
   297  			}
   298  		})
   299  	}
   300  }
   301  
   302  func TestMasker(t *testing.T) {
   303  	type diff struct {
   304  		from runtime.Object
   305  		to   runtime.Object
   306  	}
   307  	cases := []struct {
   308  		name  string
   309  		input diff
   310  		want  diff
   311  	}{
   312  		{
   313  			name: "no_changes",
   314  			input: diff{
   315  				from: &unstructured.Unstructured{
   316  					Object: map[string]interface{}{
   317  						"data": map[string]interface{}{
   318  							"username": "abc",
   319  							"password": "123",
   320  						},
   321  					},
   322  				},
   323  				to: &unstructured.Unstructured{
   324  					Object: map[string]interface{}{
   325  						"data": map[string]interface{}{
   326  							"username": "abc",
   327  							"password": "123",
   328  						},
   329  					},
   330  				},
   331  			},
   332  			want: diff{
   333  				from: &unstructured.Unstructured{
   334  					Object: map[string]interface{}{
   335  						"data": map[string]interface{}{
   336  							"username": "***", // still masked
   337  							"password": "***", // still masked
   338  						},
   339  					},
   340  				},
   341  				to: &unstructured.Unstructured{
   342  					Object: map[string]interface{}{
   343  						"data": map[string]interface{}{
   344  							"username": "***", // still masked
   345  							"password": "***", // still masked
   346  						},
   347  					},
   348  				},
   349  			},
   350  		},
   351  		{
   352  			name: "object_created",
   353  			input: diff{
   354  				from: nil, // does not exist yet
   355  				to: &unstructured.Unstructured{
   356  					Object: map[string]interface{}{
   357  						"data": map[string]interface{}{
   358  							"username": "abc",
   359  							"password": "123",
   360  						},
   361  					},
   362  				},
   363  			},
   364  			want: diff{
   365  				from: nil, // does not exist yet
   366  				to: &unstructured.Unstructured{
   367  					Object: map[string]interface{}{
   368  						"data": map[string]interface{}{
   369  							"username": "***", // no suffix needed
   370  							"password": "***", // no suffix needed
   371  						},
   372  					},
   373  				},
   374  			},
   375  		},
   376  		{
   377  			name: "object_removed",
   378  			input: diff{
   379  				from: &unstructured.Unstructured{
   380  					Object: map[string]interface{}{
   381  						"data": map[string]interface{}{
   382  							"username": "abc",
   383  							"password": "123",
   384  						},
   385  					},
   386  				},
   387  				to: nil, // removed
   388  			},
   389  			want: diff{
   390  				from: &unstructured.Unstructured{
   391  					Object: map[string]interface{}{
   392  						"data": map[string]interface{}{
   393  							"username": "***", // no suffix needed
   394  							"password": "***", // no suffix needed
   395  						},
   396  					},
   397  				},
   398  				to: nil, // removed
   399  			},
   400  		},
   401  		{
   402  			name: "data_key_added",
   403  			input: diff{
   404  				from: &unstructured.Unstructured{
   405  					Object: map[string]interface{}{
   406  						"data": map[string]interface{}{
   407  							"username": "abc",
   408  						},
   409  					},
   410  				},
   411  				to: &unstructured.Unstructured{
   412  					Object: map[string]interface{}{
   413  						"data": map[string]interface{}{
   414  							"username": "abc",
   415  							"password": "123", // added
   416  						},
   417  					},
   418  				},
   419  			},
   420  			want: diff{
   421  				from: &unstructured.Unstructured{
   422  					Object: map[string]interface{}{
   423  						"data": map[string]interface{}{
   424  							"username": "***",
   425  						},
   426  					},
   427  				},
   428  				to: &unstructured.Unstructured{
   429  					Object: map[string]interface{}{
   430  						"data": map[string]interface{}{
   431  							"username": "***",
   432  							"password": "***", // no suffix needed
   433  						},
   434  					},
   435  				},
   436  			},
   437  		},
   438  		{
   439  			name: "data_key_changed",
   440  			input: diff{
   441  				from: &unstructured.Unstructured{
   442  					Object: map[string]interface{}{
   443  						"data": map[string]interface{}{
   444  							"username": "abc",
   445  							"password": "123",
   446  						},
   447  					},
   448  				},
   449  				to: &unstructured.Unstructured{
   450  					Object: map[string]interface{}{
   451  						"data": map[string]interface{}{
   452  							"username": "abc",
   453  							"password": "456", // changed
   454  						},
   455  					},
   456  				},
   457  			},
   458  			want: diff{
   459  				from: &unstructured.Unstructured{
   460  					Object: map[string]interface{}{
   461  						"data": map[string]interface{}{
   462  							"username": "***",
   463  							"password": "*** (before)", // added suffix for diff
   464  						},
   465  					},
   466  				},
   467  				to: &unstructured.Unstructured{
   468  					Object: map[string]interface{}{
   469  						"data": map[string]interface{}{
   470  							"username": "***",
   471  							"password": "*** (after)", // added suffix for diff
   472  						},
   473  					},
   474  				},
   475  			},
   476  		},
   477  		{
   478  			name: "data_key_removed",
   479  			input: diff{
   480  				from: &unstructured.Unstructured{
   481  					Object: map[string]interface{}{
   482  						"data": map[string]interface{}{
   483  							"username": "abc",
   484  							"password": "123",
   485  						},
   486  					},
   487  				},
   488  				to: &unstructured.Unstructured{
   489  					Object: map[string]interface{}{
   490  						"data": map[string]interface{}{
   491  							"username": "abc",
   492  							// "password": "123", // removed
   493  						},
   494  					},
   495  				},
   496  			},
   497  			want: diff{
   498  				from: &unstructured.Unstructured{
   499  					Object: map[string]interface{}{
   500  						"data": map[string]interface{}{
   501  							"username": "***",
   502  							"password": "***", // no suffix needed
   503  						},
   504  					},
   505  				},
   506  				to: &unstructured.Unstructured{
   507  					Object: map[string]interface{}{
   508  						"data": map[string]interface{}{
   509  							"username": "***",
   510  							// "password": "***",
   511  						},
   512  					},
   513  				},
   514  			},
   515  		},
   516  		{
   517  			name: "empty_secret_from",
   518  			input: diff{
   519  				from: &unstructured.Unstructured{
   520  					Object: map[string]interface{}{}, // no data key
   521  				},
   522  				to: &unstructured.Unstructured{
   523  					Object: map[string]interface{}{
   524  						"data": map[string]interface{}{
   525  							"username": "abc",
   526  							"password": "123",
   527  						},
   528  					},
   529  				},
   530  			},
   531  			want: diff{
   532  				from: &unstructured.Unstructured{
   533  					Object: map[string]interface{}{}, // no data key
   534  				},
   535  				to: &unstructured.Unstructured{
   536  					Object: map[string]interface{}{
   537  						"data": map[string]interface{}{
   538  							"username": "***",
   539  							"password": "***",
   540  						},
   541  					},
   542  				},
   543  			},
   544  		},
   545  		{
   546  			name: "empty_secret_to",
   547  			input: diff{
   548  				from: &unstructured.Unstructured{
   549  					Object: map[string]interface{}{
   550  						"data": map[string]interface{}{
   551  							"username": "abc",
   552  							"password": "123",
   553  						},
   554  					},
   555  				},
   556  				to: &unstructured.Unstructured{
   557  					Object: map[string]interface{}{}, // no data key
   558  				},
   559  			},
   560  			want: diff{
   561  				from: &unstructured.Unstructured{
   562  					Object: map[string]interface{}{
   563  						"data": map[string]interface{}{
   564  							"username": "***",
   565  							"password": "***",
   566  						},
   567  					},
   568  				},
   569  				to: &unstructured.Unstructured{
   570  					Object: map[string]interface{}{}, // no data key
   571  				},
   572  			},
   573  		},
   574  		{
   575  			name: "invalid_data_key",
   576  			input: diff{
   577  				from: &unstructured.Unstructured{
   578  					Object: map[string]interface{}{
   579  						"some_other_key": map[string]interface{}{ // invalid key
   580  							"username": "abc",
   581  							"password": "123",
   582  						},
   583  					},
   584  				},
   585  				to: &unstructured.Unstructured{
   586  					Object: map[string]interface{}{
   587  						"some_other_key": map[string]interface{}{ // invalid key
   588  							"username": "abc",
   589  							"password": "123",
   590  						},
   591  					},
   592  				},
   593  			},
   594  			want: diff{
   595  				from: &unstructured.Unstructured{
   596  					Object: map[string]interface{}{
   597  						"some_other_key": map[string]interface{}{
   598  							"username": "abc", // skipped
   599  							"password": "123", // skipped
   600  						},
   601  					},
   602  				},
   603  				to: &unstructured.Unstructured{
   604  					Object: map[string]interface{}{
   605  						"some_other_key": map[string]interface{}{
   606  							"username": "abc", // skipped
   607  							"password": "123", // skipped
   608  						},
   609  					},
   610  				},
   611  			},
   612  		},
   613  	}
   614  	for _, tc := range cases {
   615  		tc := tc // capture range variable
   616  		t.Run(tc.name, func(t *testing.T) {
   617  			t.Parallel()
   618  			m, err := NewMasker(tc.input.from, tc.input.to)
   619  			if err != nil {
   620  				t.Fatal(err)
   621  			}
   622  			from, to := m.From(), m.To()
   623  			if from != nil && tc.want.from != nil {
   624  				if diff := cmp.Diff(from, tc.want.from); diff != "" {
   625  					t.Errorf("from: (-want +got):\n%s", diff)
   626  				}
   627  			}
   628  			if to != nil && tc.want.to != nil {
   629  				if diff := cmp.Diff(to, tc.want.to); diff != "" {
   630  					t.Errorf("to: (-want +got):\n%s", diff)
   631  				}
   632  			}
   633  		})
   634  	}
   635  }
   636  

View as plain text