...

Source file src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go

Documentation: k8s.io/apimachinery/pkg/apis/meta/v1/validation

     1  /*
     2  Copyright 2016 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 validation
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"testing"
    23  
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  )
    28  
    29  func TestValidateLabels(t *testing.T) {
    30  	successCases := []map[string]string{
    31  		{"simple": "bar"},
    32  		{"now-with-dashes": "bar"},
    33  		{"1-starts-with-num": "bar"},
    34  		{"1234": "bar"},
    35  		{"simple/simple": "bar"},
    36  		{"now-with-dashes/simple": "bar"},
    37  		{"now-with-dashes/now-with-dashes": "bar"},
    38  		{"now.with.dots/simple": "bar"},
    39  		{"now-with.dashes-and.dots/simple": "bar"},
    40  		{"1-num.2-num/3-num": "bar"},
    41  		{"1234/5678": "bar"},
    42  		{"1.2.3.4/5678": "bar"},
    43  		{"UpperCaseAreOK123": "bar"},
    44  		{"goodvalue": "123_-.BaR"},
    45  	}
    46  	for i := range successCases {
    47  		errs := ValidateLabels(successCases[i], field.NewPath("field"))
    48  		if len(errs) != 0 {
    49  			t.Errorf("case[%d] expected success, got %#v", i, errs)
    50  		}
    51  	}
    52  
    53  	namePartErrMsg := "name part must consist of"
    54  	nameErrMsg := "a qualified name must consist of"
    55  	labelErrMsg := "a valid label must be an empty string or consist of"
    56  	maxLengthErrMsg := "must be no more than"
    57  
    58  	labelNameErrorCases := []struct {
    59  		labels map[string]string
    60  		expect string
    61  	}{
    62  		{map[string]string{"nospecialchars^=@": "bar"}, namePartErrMsg},
    63  		{map[string]string{"cantendwithadash-": "bar"}, namePartErrMsg},
    64  		{map[string]string{"only/one/slash": "bar"}, nameErrMsg},
    65  		{map[string]string{strings.Repeat("a", 254): "bar"}, maxLengthErrMsg},
    66  	}
    67  	for i := range labelNameErrorCases {
    68  		errs := ValidateLabels(labelNameErrorCases[i].labels, field.NewPath("field"))
    69  		if len(errs) != 1 {
    70  			t.Errorf("case[%d]: expected failure", i)
    71  		} else {
    72  			if !strings.Contains(errs[0].Detail, labelNameErrorCases[i].expect) {
    73  				t.Errorf("case[%d]: error details do not include %q: %q", i, labelNameErrorCases[i].expect, errs[0].Detail)
    74  			}
    75  		}
    76  	}
    77  
    78  	labelValueErrorCases := []struct {
    79  		labels map[string]string
    80  		expect string
    81  	}{
    82  		{map[string]string{"toolongvalue": strings.Repeat("a", 64)}, maxLengthErrMsg},
    83  		{map[string]string{"backslashesinvalue": "some\\bad\\value"}, labelErrMsg},
    84  		{map[string]string{"nocommasallowed": "bad,value"}, labelErrMsg},
    85  		{map[string]string{"strangecharsinvalue": "?#$notsogood"}, labelErrMsg},
    86  	}
    87  	for i := range labelValueErrorCases {
    88  		errs := ValidateLabels(labelValueErrorCases[i].labels, field.NewPath("field"))
    89  		if len(errs) != 1 {
    90  			t.Errorf("case[%d]: expected failure", i)
    91  		} else {
    92  			if !strings.Contains(errs[0].Detail, labelValueErrorCases[i].expect) {
    93  				t.Errorf("case[%d]: error details do not include %q: %q", i, labelValueErrorCases[i].expect, errs[0].Detail)
    94  			}
    95  		}
    96  	}
    97  }
    98  
    99  func TestValidDryRun(t *testing.T) {
   100  	tests := [][]string{
   101  		{},
   102  		{"All"},
   103  		{"All", "All"},
   104  	}
   105  
   106  	for _, test := range tests {
   107  		t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
   108  			if errs := ValidateDryRun(field.NewPath("dryRun"), test); len(errs) != 0 {
   109  				t.Errorf("%v should be a valid dry-run value: %v", test, errs)
   110  			}
   111  		})
   112  	}
   113  }
   114  
   115  func TestInvalidDryRun(t *testing.T) {
   116  	tests := [][]string{
   117  		{"False"},
   118  		{"All", "False"},
   119  	}
   120  
   121  	for _, test := range tests {
   122  		t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
   123  			if len(ValidateDryRun(field.NewPath("dryRun"), test)) == 0 {
   124  				t.Errorf("%v shouldn't be a valid dry-run value", test)
   125  			}
   126  		})
   127  	}
   128  
   129  }
   130  
   131  func boolPtr(b bool) *bool {
   132  	return &b
   133  }
   134  
   135  func TestValidPatchOptions(t *testing.T) {
   136  	tests := []struct {
   137  		opts      metav1.PatchOptions
   138  		patchType types.PatchType
   139  	}{{
   140  		opts: metav1.PatchOptions{
   141  			Force:        boolPtr(true),
   142  			FieldManager: "kubectl",
   143  		},
   144  		patchType: types.ApplyPatchType,
   145  	}, {
   146  		opts: metav1.PatchOptions{
   147  			FieldManager: "kubectl",
   148  		},
   149  		patchType: types.ApplyPatchType,
   150  	}, {
   151  		opts:      metav1.PatchOptions{},
   152  		patchType: types.MergePatchType,
   153  	}, {
   154  		opts: metav1.PatchOptions{
   155  			FieldManager: "patcher",
   156  		},
   157  		patchType: types.MergePatchType,
   158  	}}
   159  
   160  	for _, test := range tests {
   161  		t.Run(fmt.Sprintf("%v", test.opts), func(t *testing.T) {
   162  			errs := ValidatePatchOptions(&test.opts, test.patchType)
   163  			if len(errs) != 0 {
   164  				t.Fatalf("Expected no failures, got: %v", errs)
   165  			}
   166  		})
   167  	}
   168  }
   169  
   170  func TestInvalidPatchOptions(t *testing.T) {
   171  	tests := []struct {
   172  		opts      metav1.PatchOptions
   173  		patchType types.PatchType
   174  	}{
   175  		// missing manager
   176  		{
   177  			opts:      metav1.PatchOptions{},
   178  			patchType: types.ApplyPatchType,
   179  		},
   180  		// force on non-apply
   181  		{
   182  			opts: metav1.PatchOptions{
   183  				Force: boolPtr(true),
   184  			},
   185  			patchType: types.MergePatchType,
   186  		},
   187  		// manager and force on non-apply
   188  		{
   189  			opts: metav1.PatchOptions{
   190  				FieldManager: "kubectl",
   191  				Force:        boolPtr(false),
   192  			},
   193  			patchType: types.MergePatchType,
   194  		},
   195  	}
   196  
   197  	for _, test := range tests {
   198  		t.Run(fmt.Sprintf("%v", test.opts), func(t *testing.T) {
   199  			errs := ValidatePatchOptions(&test.opts, test.patchType)
   200  			if len(errs) == 0 {
   201  				t.Fatal("Expected failures, got none.")
   202  			}
   203  		})
   204  	}
   205  }
   206  
   207  func TestValidateFieldManagerValid(t *testing.T) {
   208  	tests := []string{
   209  		"filedManager",
   210  		"你好", // Hello
   211  		"🍔",
   212  	}
   213  
   214  	for _, test := range tests {
   215  		t.Run(test, func(t *testing.T) {
   216  			errs := ValidateFieldManager(test, field.NewPath("fieldManager"))
   217  			if len(errs) != 0 {
   218  				t.Errorf("Validation failed: %v", errs)
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestValidateFieldManagerInvalid(t *testing.T) {
   225  	tests := []string{
   226  		"field\nmanager", // Contains invalid character \n
   227  		"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // Has 129 chars
   228  	}
   229  
   230  	for _, test := range tests {
   231  		t.Run(test, func(t *testing.T) {
   232  			errs := ValidateFieldManager(test, field.NewPath("fieldManager"))
   233  			if len(errs) == 0 {
   234  				t.Errorf("Validation should have failed")
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestValidateManagedFieldsInvalid(t *testing.T) {
   241  	tests := []metav1.ManagedFieldsEntry{{
   242  		Operation:  metav1.ManagedFieldsOperationUpdate,
   243  		FieldsType: "RandomVersion",
   244  		APIVersion: "v1",
   245  	}, {
   246  		Operation:  "RandomOperation",
   247  		FieldsType: "FieldsV1",
   248  		APIVersion: "v1",
   249  	}, {
   250  		// Operation is missing
   251  		FieldsType: "FieldsV1",
   252  		APIVersion: "v1",
   253  	}, {
   254  		Operation:  metav1.ManagedFieldsOperationUpdate,
   255  		FieldsType: "FieldsV1",
   256  		// Invalid fieldManager
   257  		Manager:    "field\nmanager",
   258  		APIVersion: "v1",
   259  	}, {
   260  		Operation:   metav1.ManagedFieldsOperationApply,
   261  		FieldsType:  "FieldsV1",
   262  		APIVersion:  "v1",
   263  		Subresource: "TooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLong",
   264  	}}
   265  
   266  	for _, test := range tests {
   267  		t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
   268  			errs := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
   269  			if len(errs) == 0 {
   270  				t.Errorf("Validation should have failed")
   271  			}
   272  		})
   273  	}
   274  }
   275  
   276  func TestValidateMangedFieldsValid(t *testing.T) {
   277  	tests := []metav1.ManagedFieldsEntry{{
   278  		Operation:  metav1.ManagedFieldsOperationUpdate,
   279  		APIVersion: "v1",
   280  		// FieldsType is missing
   281  	}, {
   282  		Operation:  metav1.ManagedFieldsOperationUpdate,
   283  		FieldsType: "FieldsV1",
   284  		APIVersion: "v1",
   285  	}, {
   286  		Operation:   metav1.ManagedFieldsOperationApply,
   287  		FieldsType:  "FieldsV1",
   288  		APIVersion:  "v1",
   289  		Subresource: "scale",
   290  	}, {
   291  		Operation:  metav1.ManagedFieldsOperationApply,
   292  		FieldsType: "FieldsV1",
   293  		APIVersion: "v1",
   294  		Manager:    "🍔",
   295  	}}
   296  
   297  	for _, test := range tests {
   298  		t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
   299  			err := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
   300  			if err != nil {
   301  				t.Errorf("Validation failed: %v", err)
   302  			}
   303  		})
   304  	}
   305  }
   306  
   307  func TestValidateConditions(t *testing.T) {
   308  	tests := []struct {
   309  		name         string
   310  		conditions   []metav1.Condition
   311  		validateErrs func(t *testing.T, errs field.ErrorList)
   312  	}{{
   313  		name: "bunch-of-invalid-fields",
   314  		conditions: []metav1.Condition{{
   315  			Type:               ":invalid",
   316  			Status:             "unknown",
   317  			ObservedGeneration: -1,
   318  			LastTransitionTime: metav1.Time{},
   319  			Reason:             "invalid;val",
   320  			Message:            "",
   321  		}},
   322  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   323  			needle := `status.conditions[0].type: Invalid value: ":invalid": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`
   324  			if !hasError(errs, needle) {
   325  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   326  			}
   327  			needle = `status.conditions[0].status: Unsupported value: "unknown": supported values: "False", "True", "Unknown"`
   328  			if !hasError(errs, needle) {
   329  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   330  			}
   331  			needle = `status.conditions[0].observedGeneration: Invalid value: -1: must be greater than or equal to zero`
   332  			if !hasError(errs, needle) {
   333  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   334  			}
   335  			needle = `status.conditions[0].lastTransitionTime: Required value: must be set`
   336  			if !hasError(errs, needle) {
   337  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   338  			}
   339  			needle = `status.conditions[0].reason: Invalid value: "invalid;val": a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_' (e.g. 'my_name',  or 'MY_NAME',  or 'MyName',  or 'ReasonA,ReasonB',  or 'ReasonA:ReasonB', regex used for validation is '[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?')`
   340  			if !hasError(errs, needle) {
   341  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   342  			}
   343  		},
   344  	}, {
   345  		name: "duplicates",
   346  		conditions: []metav1.Condition{{
   347  			Type: "First",
   348  		}, {
   349  			Type: "Second",
   350  		}, {
   351  			Type: "First",
   352  		}},
   353  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   354  			needle := `status.conditions[2].type: Duplicate value: "First"`
   355  			if !hasError(errs, needle) {
   356  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   357  			}
   358  		},
   359  	}, {
   360  		name: "colon-allowed-in-reason",
   361  		conditions: []metav1.Condition{{
   362  			Type:   "First",
   363  			Reason: "valid:val",
   364  		}},
   365  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   366  			needle := `status.conditions[0].reason`
   367  			if hasPrefixError(errs, needle) {
   368  				t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
   369  			}
   370  		},
   371  	}, {
   372  		name: "comma-allowed-in-reason",
   373  		conditions: []metav1.Condition{{
   374  			Type:   "First",
   375  			Reason: "valid,val",
   376  		}},
   377  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   378  			needle := `status.conditions[0].reason`
   379  			if hasPrefixError(errs, needle) {
   380  				t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
   381  			}
   382  		},
   383  	}, {
   384  		name: "reason-does-not-end-in-delimiter",
   385  		conditions: []metav1.Condition{{
   386  			Type:   "First",
   387  			Reason: "valid,val:",
   388  		}},
   389  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   390  			needle := `status.conditions[0].reason: Invalid value: "valid,val:": a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_' (e.g. 'my_name',  or 'MY_NAME',  or 'MyName',  or 'ReasonA,ReasonB',  or 'ReasonA:ReasonB', regex used for validation is '[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?')`
   391  			if !hasError(errs, needle) {
   392  				t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
   393  			}
   394  		},
   395  	}}
   396  
   397  	for _, test := range tests {
   398  		t.Run(test.name, func(t *testing.T) {
   399  			errs := ValidateConditions(test.conditions, field.NewPath("status").Child("conditions"))
   400  			test.validateErrs(t, errs)
   401  		})
   402  	}
   403  }
   404  
   405  func TestLabelSelectorMatchExpression(t *testing.T) {
   406  	testCases := []struct {
   407  		name            string
   408  		labelSelector   *metav1.LabelSelector
   409  		wantErrorNumber int
   410  		validateErrs    func(t *testing.T, errs field.ErrorList)
   411  	}{{
   412  		name: "Valid LabelSelector",
   413  		labelSelector: &metav1.LabelSelector{
   414  			MatchExpressions: []metav1.LabelSelectorRequirement{{
   415  				Key:      "key",
   416  				Operator: metav1.LabelSelectorOpIn,
   417  				Values:   []string{"value"},
   418  			}},
   419  		},
   420  		wantErrorNumber: 0,
   421  		validateErrs:    nil,
   422  	}, {
   423  		name: "MatchExpression's key name isn't valid",
   424  		labelSelector: &metav1.LabelSelector{
   425  			MatchExpressions: []metav1.LabelSelectorRequirement{{
   426  				Key:      "-key",
   427  				Operator: metav1.LabelSelectorOpIn,
   428  				Values:   []string{"value"},
   429  			}},
   430  		},
   431  		wantErrorNumber: 1,
   432  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   433  			errMessage := "name part must consist of alphanumeric characters"
   434  			if !partStringInErrorMessage(errs, errMessage) {
   435  				t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
   436  			}
   437  		},
   438  	}, {
   439  		name: "MatchExpression's operator isn't valid",
   440  		labelSelector: &metav1.LabelSelector{
   441  			MatchExpressions: []metav1.LabelSelectorRequirement{{
   442  				Key:      "key",
   443  				Operator: "abc",
   444  				Values:   []string{"value"},
   445  			}},
   446  		},
   447  		wantErrorNumber: 1,
   448  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   449  			errMessage := "not a valid selector operator"
   450  			if !partStringInErrorMessage(errs, errMessage) {
   451  				t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
   452  			}
   453  		},
   454  	}, {
   455  		name: "MatchExpression's value name isn't valid",
   456  		labelSelector: &metav1.LabelSelector{
   457  			MatchExpressions: []metav1.LabelSelectorRequirement{{
   458  				Key:      "key",
   459  				Operator: metav1.LabelSelectorOpIn,
   460  				Values:   []string{"-value"},
   461  			}},
   462  		},
   463  		wantErrorNumber: 1,
   464  		validateErrs: func(t *testing.T, errs field.ErrorList) {
   465  			errMessage := "a valid label must be an empty string or consist of"
   466  			if !partStringInErrorMessage(errs, errMessage) {
   467  				t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
   468  			}
   469  		},
   470  	}}
   471  	for index, testCase := range testCases {
   472  		t.Run(testCase.name, func(t *testing.T) {
   473  			allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{false}, field.NewPath("labelSelector"))
   474  			if len(allErrs) != testCase.wantErrorNumber {
   475  				t.Errorf("case[%d]: expected failure", index)
   476  			}
   477  			if len(allErrs) >= 1 && testCase.validateErrs != nil {
   478  				testCase.validateErrs(t, allErrs)
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  func hasError(errs field.ErrorList, needle string) bool {
   485  	for _, curr := range errs {
   486  		if curr.Error() == needle {
   487  			return true
   488  		}
   489  	}
   490  	return false
   491  }
   492  
   493  func hasPrefixError(errs field.ErrorList, prefix string) bool {
   494  	for _, curr := range errs {
   495  		if strings.HasPrefix(curr.Error(), prefix) {
   496  			return true
   497  		}
   498  	}
   499  	return false
   500  }
   501  
   502  func partStringInErrorMessage(errs field.ErrorList, prefix string) bool {
   503  	for _, curr := range errs {
   504  		if strings.Contains(curr.Error(), prefix) {
   505  			return true
   506  		}
   507  	}
   508  	return false
   509  }
   510  
   511  func errorsAsString(errs field.ErrorList) string {
   512  	messages := []string{}
   513  	for _, curr := range errs {
   514  		messages = append(messages, curr.Error())
   515  	}
   516  	return strings.Join(messages, "\n")
   517  }
   518  

View as plain text