...

Source file src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go

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

     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 util
    18  
    19  import (
    20  	goerrors "errors"
    21  	"fmt"
    22  	"net/http"
    23  	"os"
    24  	"strings"
    25  	"syscall"
    26  	"testing"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	"github.com/spf13/cobra"
    31  
    32  	corev1 "k8s.io/api/core/v1"
    33  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    34  	"k8s.io/apimachinery/pkg/api/errors"
    35  	"k8s.io/apimachinery/pkg/api/meta"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  	"k8s.io/apimachinery/pkg/util/validation/field"
    40  	"k8s.io/kubectl/pkg/scheme"
    41  	"k8s.io/utils/exec"
    42  )
    43  
    44  func TestMerge(t *testing.T) {
    45  	tests := []struct {
    46  		obj       runtime.Object
    47  		fragment  string
    48  		expected  runtime.Object
    49  		expectErr bool
    50  	}{
    51  		{
    52  			obj: &corev1.Pod{
    53  				ObjectMeta: metav1.ObjectMeta{
    54  					Name: "foo",
    55  				},
    56  			},
    57  			fragment: fmt.Sprintf(`{ "apiVersion": "%s" }`, "v1"),
    58  			expected: &corev1.Pod{
    59  				TypeMeta: metav1.TypeMeta{
    60  					Kind:       "Pod",
    61  					APIVersion: "v1",
    62  				},
    63  				ObjectMeta: metav1.ObjectMeta{
    64  					Name: "foo",
    65  				},
    66  				Spec: corev1.PodSpec{},
    67  			},
    68  		},
    69  		{
    70  			obj: &corev1.Pod{
    71  				ObjectMeta: metav1.ObjectMeta{
    72  					Name: "foo",
    73  				},
    74  			},
    75  			fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "volumes": [ {"name": "v1"}, {"name": "v2"} ] } }`, "v1"),
    76  			expected: &corev1.Pod{
    77  				TypeMeta: metav1.TypeMeta{
    78  					Kind:       "Pod",
    79  					APIVersion: "v1",
    80  				},
    81  				ObjectMeta: metav1.ObjectMeta{
    82  					Name: "foo",
    83  				},
    84  				Spec: corev1.PodSpec{
    85  					Volumes: []corev1.Volume{
    86  						{
    87  							Name: "v1",
    88  						},
    89  						{
    90  							Name: "v2",
    91  						},
    92  					},
    93  				},
    94  			},
    95  		},
    96  		{
    97  			obj:       &corev1.Pod{},
    98  			fragment:  "invalid json",
    99  			expected:  &corev1.Pod{},
   100  			expectErr: true,
   101  		},
   102  		{
   103  			obj:       &corev1.Service{},
   104  			fragment:  `{ "apiVersion": "badVersion" }`,
   105  			expectErr: true,
   106  		},
   107  		{
   108  			obj: &corev1.Service{
   109  				Spec: corev1.ServiceSpec{},
   110  			},
   111  			fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "ports": [ { "port": 0 } ] } }`, "v1"),
   112  			expected: &corev1.Service{
   113  				TypeMeta: metav1.TypeMeta{
   114  					Kind:       "Service",
   115  					APIVersion: "v1",
   116  				},
   117  				Spec: corev1.ServiceSpec{
   118  					Ports: []corev1.ServicePort{
   119  						{
   120  							Port: 0,
   121  						},
   122  					},
   123  				},
   124  			},
   125  		},
   126  		{
   127  			obj: &corev1.Service{
   128  				Spec: corev1.ServiceSpec{
   129  					Selector: map[string]string{
   130  						"version": "v1",
   131  					},
   132  				},
   133  			},
   134  			fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "selector": { "version": "v2" } } }`, "v1"),
   135  			expected: &corev1.Service{
   136  				TypeMeta: metav1.TypeMeta{
   137  					Kind:       "Service",
   138  					APIVersion: "v1",
   139  				},
   140  				Spec: corev1.ServiceSpec{
   141  					Selector: map[string]string{
   142  						"version": "v2",
   143  					},
   144  				},
   145  			},
   146  		},
   147  	}
   148  
   149  	codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
   150  		scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
   151  	for i, test := range tests {
   152  		out, err := Merge(codec, test.obj, test.fragment)
   153  		if !test.expectErr {
   154  			if err != nil {
   155  				t.Errorf("testcase[%d], unexpected error: %v", i, err)
   156  			} else if !apiequality.Semantic.DeepEqual(test.expected, out) {
   157  				t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
   158  			}
   159  		}
   160  		if test.expectErr && err == nil {
   161  			t.Errorf("testcase[%d], unexpected non-error", i)
   162  		}
   163  	}
   164  }
   165  
   166  func TestStrategicMerge(t *testing.T) {
   167  	tests := []struct {
   168  		obj        runtime.Object
   169  		dataStruct runtime.Object
   170  		fragment   string
   171  		expected   runtime.Object
   172  		expectErr  bool
   173  	}{
   174  		{
   175  			obj: &corev1.Pod{
   176  				ObjectMeta: metav1.ObjectMeta{
   177  					Name: "foo",
   178  				},
   179  				Spec: corev1.PodSpec{
   180  					Containers: []corev1.Container{
   181  						{
   182  							Name:  "c1",
   183  							Image: "red-image",
   184  						},
   185  						{
   186  							Name:  "c2",
   187  							Image: "blue-image",
   188  						},
   189  					},
   190  				},
   191  			},
   192  			dataStruct: &corev1.Pod{},
   193  			fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`,
   194  				schema.GroupVersion{Group: "", Version: "v1"}.String()),
   195  			expected: &corev1.Pod{
   196  				TypeMeta: metav1.TypeMeta{
   197  					Kind:       "Pod",
   198  					APIVersion: "v1",
   199  				},
   200  				ObjectMeta: metav1.ObjectMeta{
   201  					Name: "foo",
   202  				},
   203  				Spec: corev1.PodSpec{
   204  					Containers: []corev1.Container{
   205  						{
   206  							Name:  "c1",
   207  							Image: "green-image",
   208  						},
   209  						{
   210  							Name:  "c2",
   211  							Image: "blue-image",
   212  						},
   213  					},
   214  				},
   215  			},
   216  		},
   217  		{
   218  			obj:        &corev1.Pod{},
   219  			dataStruct: &corev1.Pod{},
   220  			fragment:   "invalid json",
   221  			expected:   &corev1.Pod{},
   222  			expectErr:  true,
   223  		},
   224  		{
   225  			obj:        &corev1.Service{},
   226  			dataStruct: &corev1.Pod{},
   227  			fragment:   `{ "apiVersion": "badVersion" }`,
   228  			expectErr:  true,
   229  		},
   230  	}
   231  
   232  	codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
   233  		scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
   234  	for i, test := range tests {
   235  		out, err := StrategicMerge(codec, test.obj, test.fragment, test.dataStruct)
   236  		if !test.expectErr {
   237  			if err != nil {
   238  				t.Errorf("testcase[%d], unexpected error: %v", i, err)
   239  			} else if !apiequality.Semantic.DeepEqual(test.expected, out) {
   240  				t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
   241  			}
   242  		}
   243  		if test.expectErr && err == nil {
   244  			t.Errorf("testcase[%d], unexpected non-error", i)
   245  		}
   246  	}
   247  }
   248  
   249  func TestJSONPatch(t *testing.T) {
   250  	tests := []struct {
   251  		obj       runtime.Object
   252  		fragment  string
   253  		expected  runtime.Object
   254  		expectErr bool
   255  	}{
   256  		{
   257  			obj: &corev1.Pod{
   258  				ObjectMeta: metav1.ObjectMeta{
   259  					Name: "foo",
   260  					Labels: map[string]string{
   261  						"run": "test",
   262  					},
   263  				},
   264  			},
   265  			fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`,
   266  			expected: &corev1.Pod{
   267  				TypeMeta: metav1.TypeMeta{
   268  					Kind:       "Pod",
   269  					APIVersion: "v1",
   270  				},
   271  				ObjectMeta: metav1.ObjectMeta{
   272  					Name: "foo",
   273  					Labels: map[string]string{
   274  						"run": "test",
   275  						"foo": "bar",
   276  					},
   277  				},
   278  				Spec: corev1.PodSpec{},
   279  			},
   280  		},
   281  		{
   282  			obj:       &corev1.Pod{},
   283  			fragment:  "invalid json",
   284  			expected:  &corev1.Pod{},
   285  			expectErr: true,
   286  		},
   287  		{
   288  			obj:       &corev1.Pod{},
   289  			fragment:  `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`,
   290  			expectErr: true,
   291  		},
   292  		{
   293  			obj: &corev1.Pod{
   294  				ObjectMeta: metav1.ObjectMeta{
   295  					Name:       "foo",
   296  					Finalizers: []string{"foo", "bar", "test"},
   297  				},
   298  			},
   299  			fragment: `[ {"op": "replace", "path": "/metadata/finalizers/-1", "value": "baz"} ]`,
   300  			expected: &corev1.Pod{
   301  				TypeMeta: metav1.TypeMeta{
   302  					Kind:       "Pod",
   303  					APIVersion: "v1",
   304  				},
   305  				ObjectMeta: metav1.ObjectMeta{
   306  					Name:       "foo",
   307  					Finalizers: []string{"foo", "bar", "baz"},
   308  				},
   309  				Spec: corev1.PodSpec{},
   310  			},
   311  		},
   312  	}
   313  
   314  	codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
   315  		scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
   316  	for i, test := range tests {
   317  		out, err := JSONPatch(codec, test.obj, test.fragment)
   318  		if !test.expectErr {
   319  			if err != nil {
   320  				t.Errorf("testcase[%d], unexpected error: %v", i, err)
   321  			} else if !apiequality.Semantic.DeepEqual(test.expected, out) {
   322  				t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
   323  			}
   324  		}
   325  		if test.expectErr && err == nil {
   326  			t.Errorf("testcase[%d], unexpected non-error", i)
   327  		}
   328  	}
   329  }
   330  
   331  type checkErrTestCase struct {
   332  	err          error
   333  	expectedErr  string
   334  	expectedCode int
   335  }
   336  
   337  func TestCheckInvalidErr(t *testing.T) {
   338  	testCheckError(t, []checkErrTestCase{
   339  		{
   340  			errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid1").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field"), "single", "details")}),
   341  			"The Invalid1 \"invalidation\" is invalid: field: Invalid value: \"single\": details\n",
   342  			DefaultErrorExitCode,
   343  		},
   344  		{
   345  			errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid2").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field1"), "multi1", "details"), field.Invalid(field.NewPath("field2"), "multi2", "details")}),
   346  			"The Invalid2 \"invalidation\" is invalid: \n* field1: Invalid value: \"multi1\": details\n* field2: Invalid value: \"multi2\": details\n",
   347  			DefaultErrorExitCode,
   348  		},
   349  		{
   350  			errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid3").GroupKind(), "invalidation", field.ErrorList{}),
   351  			"The Invalid3 \"invalidation\" is invalid",
   352  			DefaultErrorExitCode,
   353  		},
   354  		{
   355  			errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid4").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field4"), "multi4", "details"), field.Invalid(field.NewPath("field4"), "multi4", "details")}),
   356  			"The Invalid4 \"invalidation\" is invalid: field4: Invalid value: \"multi4\": details\n",
   357  			DefaultErrorExitCode,
   358  		},
   359  		{
   360  			&errors.StatusError{ErrStatus: metav1.Status{
   361  				Status: metav1.StatusFailure,
   362  				Code:   http.StatusUnprocessableEntity,
   363  				Reason: metav1.StatusReasonInvalid,
   364  				// Details is nil.
   365  			}},
   366  			"The request is invalid",
   367  			DefaultErrorExitCode,
   368  		},
   369  		// invalid error that that includes a message but no details
   370  		{
   371  			&errors.StatusError{ErrStatus: metav1.Status{
   372  				Status: metav1.StatusFailure,
   373  				Code:   http.StatusUnprocessableEntity,
   374  				Reason: metav1.StatusReasonInvalid,
   375  				// Details is nil.
   376  				Message: "Some message",
   377  			}},
   378  			"The request is invalid: Some message",
   379  			DefaultErrorExitCode,
   380  		},
   381  		// webhook response that sets code=422 with no reason
   382  		{
   383  			&errors.StatusError{ErrStatus: metav1.Status{
   384  				Status:  "Failure",
   385  				Message: `admission webhook "my.webhook" denied the request without explanation`,
   386  				Code:    422,
   387  			}},
   388  			`Error from server: admission webhook "my.webhook" denied the request without explanation`,
   389  			DefaultErrorExitCode,
   390  		},
   391  		// webhook response that sets code=422 with no reason and non-nil details
   392  		{
   393  			&errors.StatusError{ErrStatus: metav1.Status{
   394  				Status:  "Failure",
   395  				Message: `admission webhook "my.webhook" denied the request without explanation`,
   396  				Code:    422,
   397  				Details: &metav1.StatusDetails{},
   398  			}},
   399  			`Error from server: admission webhook "my.webhook" denied the request without explanation`,
   400  			DefaultErrorExitCode,
   401  		},
   402  		// source-wrapped webhook response that sets code=422 with no reason
   403  		{
   404  			AddSourceToErr("creating", "configmap.yaml", &errors.StatusError{ErrStatus: metav1.Status{
   405  				Status:  "Failure",
   406  				Message: `admission webhook "my.webhook" denied the request without explanation`,
   407  				Code:    422,
   408  			}}),
   409  			`Error from server: error when creating "configmap.yaml": admission webhook "my.webhook" denied the request without explanation`,
   410  			DefaultErrorExitCode,
   411  		},
   412  		// webhook response that sets reason=Invalid and code=422 and a message
   413  		{
   414  			&errors.StatusError{ErrStatus: metav1.Status{
   415  				Status:  "Failure",
   416  				Reason:  "Invalid",
   417  				Message: `admission webhook "my.webhook" denied the request without explanation`,
   418  				Code:    422,
   419  			}},
   420  			`The request is invalid: admission webhook "my.webhook" denied the request without explanation`,
   421  			DefaultErrorExitCode,
   422  		},
   423  	})
   424  }
   425  
   426  func TestCheckNoResourceMatchError(t *testing.T) {
   427  	testCheckError(t, []checkErrTestCase{
   428  		{
   429  			&meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Resource: "foo"}},
   430  			`the server doesn't have a resource type "foo"`,
   431  			DefaultErrorExitCode,
   432  		},
   433  		{
   434  			&meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Version: "theversion", Resource: "foo"}},
   435  			`the server doesn't have a resource type "foo" in version "theversion"`,
   436  			DefaultErrorExitCode,
   437  		},
   438  		{
   439  			&meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Version: "theversion", Resource: "foo"}},
   440  			`the server doesn't have a resource type "foo" in group "thegroup" and version "theversion"`,
   441  			DefaultErrorExitCode,
   442  		},
   443  		{
   444  			&meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Resource: "foo"}},
   445  			`the server doesn't have a resource type "foo" in group "thegroup"`,
   446  			DefaultErrorExitCode,
   447  		},
   448  	})
   449  }
   450  
   451  func TestCheckExitError(t *testing.T) {
   452  	testCheckError(t, []checkErrTestCase{
   453  		{
   454  			exec.CodeExitError{Err: fmt.Errorf("pod foo/bar terminated"), Code: 42},
   455  			"pod foo/bar terminated",
   456  			42,
   457  		},
   458  	})
   459  }
   460  
   461  func testCheckError(t *testing.T, tests []checkErrTestCase) {
   462  	var errReturned string
   463  	var codeReturned int
   464  	errHandle := func(err string, code int) {
   465  		errReturned = err
   466  		codeReturned = code
   467  	}
   468  
   469  	for _, test := range tests {
   470  		checkErr(test.err, errHandle)
   471  
   472  		if errReturned != test.expectedErr {
   473  			t.Fatalf("Got: %s, expected: %s", errReturned, test.expectedErr)
   474  		}
   475  		if codeReturned != test.expectedCode {
   476  			t.Fatalf("Got: %d, expected: %d", codeReturned, test.expectedCode)
   477  		}
   478  	}
   479  }
   480  
   481  func TestDumpReaderToFile(t *testing.T) {
   482  	testString := "TEST STRING"
   483  	tempFile, err := os.CreateTemp(os.TempDir(), "hlpers_test_dump_")
   484  	if err != nil {
   485  		t.Errorf("unexpected error setting up a temporary file %v", err)
   486  	}
   487  	defer syscall.Unlink(tempFile.Name())
   488  	defer tempFile.Close()
   489  	defer func() {
   490  		if !t.Failed() {
   491  			os.Remove(tempFile.Name())
   492  		}
   493  	}()
   494  	err = DumpReaderToFile(strings.NewReader(testString), tempFile.Name())
   495  	if err != nil {
   496  		t.Errorf("error in DumpReaderToFile: %v", err)
   497  	}
   498  	data, err := os.ReadFile(tempFile.Name())
   499  	if err != nil {
   500  		t.Errorf("error when reading %s: %v", tempFile.Name(), err)
   501  	}
   502  	stringData := string(data)
   503  	if stringData != testString {
   504  		t.Fatalf("Wrong file content %s != %s", testString, stringData)
   505  	}
   506  }
   507  
   508  func TestDifferenceFunc(t *testing.T) {
   509  	tests := []struct {
   510  		name      string
   511  		fullArray []string
   512  		subArray  []string
   513  		expected  []string
   514  	}{
   515  		{
   516  			name:      "remove some",
   517  			fullArray: []string{"a", "b", "c", "d"},
   518  			subArray:  []string{"c", "b"},
   519  			expected:  []string{"a", "d"},
   520  		},
   521  		{
   522  			name:      "remove all",
   523  			fullArray: []string{"a", "b", "c", "d"},
   524  			subArray:  []string{"b", "d", "a", "c"},
   525  			expected:  nil,
   526  		},
   527  		{
   528  			name:      "remove none",
   529  			fullArray: []string{"a", "b", "c", "d"},
   530  			subArray:  nil,
   531  			expected:  []string{"a", "b", "c", "d"},
   532  		},
   533  	}
   534  
   535  	for _, tc := range tests {
   536  		result := Difference(tc.fullArray, tc.subArray)
   537  		if !cmp.Equal(tc.expected, result, cmpopts.SortSlices(func(x, y string) bool {
   538  			return x < y
   539  		})) {
   540  			t.Errorf("%s -> Expected: %v, but got: %v", tc.name, tc.expected, result)
   541  		}
   542  	}
   543  }
   544  
   545  func TestGetValidationDirective(t *testing.T) {
   546  	tests := []struct {
   547  		validateFlag      string
   548  		expectedDirective string
   549  		expectedErr       error
   550  	}{
   551  		{
   552  			expectedDirective: metav1.FieldValidationStrict,
   553  		},
   554  		{
   555  			validateFlag:      "true",
   556  			expectedDirective: metav1.FieldValidationStrict,
   557  		},
   558  		{
   559  			validateFlag:      "True",
   560  			expectedDirective: metav1.FieldValidationStrict,
   561  		},
   562  		{
   563  			validateFlag:      "strict",
   564  			expectedDirective: metav1.FieldValidationStrict,
   565  		},
   566  		{
   567  			validateFlag:      "warn",
   568  			expectedDirective: metav1.FieldValidationWarn,
   569  		},
   570  		{
   571  			validateFlag:      "ignore",
   572  			expectedDirective: metav1.FieldValidationIgnore,
   573  		},
   574  		{
   575  			validateFlag:      "false",
   576  			expectedDirective: metav1.FieldValidationIgnore,
   577  		},
   578  		{
   579  			validateFlag:      "False",
   580  			expectedDirective: metav1.FieldValidationIgnore,
   581  		},
   582  		{
   583  			validateFlag:      "foo",
   584  			expectedDirective: metav1.FieldValidationStrict,
   585  			expectedErr:       goerrors.New(`invalid - validate option "foo"; must be one of: strict (or true), warn, ignore (or false)`),
   586  		},
   587  	}
   588  
   589  	for _, tc := range tests {
   590  		cmd := &cobra.Command{}
   591  		AddValidateFlags(cmd)
   592  		if tc.validateFlag != "" {
   593  			cmd.Flags().Set("validate", tc.validateFlag)
   594  		}
   595  		directive, err := GetValidationDirective(cmd)
   596  		if directive != tc.expectedDirective {
   597  			t.Errorf("validation directive, expected: %v, but got: %v", tc.expectedDirective, directive)
   598  		}
   599  		if tc.expectedErr != nil {
   600  			if err.Error() != tc.expectedErr.Error() {
   601  				t.Errorf("GetValidationDirective error, expected: %v, but got: %v", tc.expectedErr, err)
   602  			}
   603  		} else {
   604  			if err != nil {
   605  				t.Errorf("expecte no error, but got: %v", err)
   606  			}
   607  		}
   608  
   609  	}
   610  }
   611  

View as plain text