| /* |
| Copyright 2017 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package validation |
| |
| import ( |
| "math/rand" |
| "reflect" |
| "strings" |
| "testing" |
| "time" |
| |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/util/validation/field" |
| ) |
| |
| const ( |
| maxLengthErrMsg = "must be no more than" |
| namePartErrMsg = "name part must consist of" |
| nameErrMsg = "a qualified name must consist of" |
| ) |
| |
| // Ensure custom name functions are allowed |
| func TestValidateObjectMetaCustomName(t *testing.T) { |
| errs := ValidateObjectMeta( |
| &metav1.ObjectMeta{Name: "test", GenerateName: "foo"}, |
| false, |
| func(s string, prefix bool) []string { |
| if s == "test" { |
| return nil |
| } |
| return []string{"name-gen"} |
| }, |
| field.NewPath("field")) |
| if len(errs) != 1 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| if !strings.Contains(errs[0].Error(), "name-gen") { |
| t.Errorf("unexpected error message: %v", errs) |
| } |
| } |
| |
| // Ensure namespace names follow dns label format |
| func TestValidateObjectMetaNamespaces(t *testing.T) { |
| errs := ValidateObjectMeta( |
| &metav1.ObjectMeta{Name: "test", Namespace: "foo.bar"}, |
| true, |
| func(s string, prefix bool) []string { |
| return nil |
| }, |
| field.NewPath("field")) |
| if len(errs) != 1 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| if !strings.Contains(errs[0].Error(), `Invalid value: "foo.bar"`) { |
| t.Errorf("unexpected error message: %v", errs) |
| } |
| maxLength := 63 |
| letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") |
| b := make([]rune, maxLength+1) |
| for i := range b { |
| b[i] = letters[rand.Intn(len(letters))] |
| } |
| errs = ValidateObjectMeta( |
| &metav1.ObjectMeta{Name: "test", Namespace: string(b)}, |
| true, |
| func(s string, prefix bool) []string { |
| return nil |
| }, |
| field.NewPath("field")) |
| if len(errs) != 2 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| if !strings.Contains(errs[0].Error(), "Invalid value") || !strings.Contains(errs[1].Error(), "Invalid value") { |
| t.Errorf("unexpected error message: %v", errs) |
| } |
| } |
| |
| func TestValidateObjectMetaOwnerReferences(t *testing.T) { |
| trueVar := true |
| falseVar := false |
| testCases := []struct { |
| description string |
| ownerReferences []metav1.OwnerReference |
| expectError bool |
| expectedErrorMessage string |
| }{ |
| { |
| description: "simple success - third party extension.", |
| ownerReferences: []metav1.OwnerReference{ |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "1", |
| }, |
| }, |
| expectError: false, |
| expectedErrorMessage: "", |
| }, |
| { |
| description: "simple failures - event shouldn't be set as an owner", |
| ownerReferences: []metav1.OwnerReference{ |
| { |
| APIVersion: "v1", |
| Kind: "Event", |
| Name: "name", |
| UID: "1", |
| }, |
| }, |
| expectError: true, |
| expectedErrorMessage: "is disallowed from being an owner", |
| }, |
| { |
| description: "simple controller ref success - one reference with Controller set", |
| ownerReferences: []metav1.OwnerReference{ |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "1", |
| Controller: &falseVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "2", |
| Controller: &trueVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "3", |
| Controller: &falseVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "4", |
| }, |
| }, |
| expectError: false, |
| expectedErrorMessage: "", |
| }, |
| { |
| description: "simple controller ref failure - two references with Controller set", |
| ownerReferences: []metav1.OwnerReference{ |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "1", |
| Controller: &falseVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "2", |
| Controller: &trueVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "3", |
| Controller: &trueVar, |
| }, |
| { |
| APIVersion: "customresourceVersion", |
| Kind: "customresourceKind", |
| Name: "name", |
| UID: "4", |
| }, |
| }, |
| expectError: true, |
| expectedErrorMessage: "Only one reference can have Controller set to true", |
| }, |
| } |
| |
| for _, tc := range testCases { |
| errs := ValidateObjectMeta( |
| &metav1.ObjectMeta{Name: "test", Namespace: "test", OwnerReferences: tc.ownerReferences}, |
| true, |
| func(s string, prefix bool) []string { |
| return nil |
| }, |
| field.NewPath("field")) |
| if len(errs) != 0 && !tc.expectError { |
| t.Errorf("unexpected error: %v in test case %v", errs, tc.description) |
| } |
| if len(errs) == 0 && tc.expectError { |
| t.Errorf("expect error in test case %v", tc.description) |
| } |
| if len(errs) != 0 && !strings.Contains(errs[0].Error(), tc.expectedErrorMessage) { |
| t.Errorf("unexpected error message: %v in test case %v", errs, tc.description) |
| } |
| } |
| } |
| |
| func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) { |
| if errs := ValidateObjectMetaUpdate( |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: metav1.NewTime(time.Unix(10, 0))}, |
| field.NewPath("field"), |
| ); len(errs) != 1 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| if errs := ValidateObjectMetaUpdate( |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: metav1.NewTime(time.Unix(10, 0))}, |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| field.NewPath("field"), |
| ); len(errs) != 1 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| if errs := ValidateObjectMetaUpdate( |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: metav1.NewTime(time.Unix(10, 0))}, |
| &metav1.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: metav1.NewTime(time.Unix(11, 0))}, |
| field.NewPath("field"), |
| ); len(errs) != 1 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| } |
| |
| func TestValidateFinalizersUpdate(t *testing.T) { |
| testcases := map[string]struct { |
| Old metav1.ObjectMeta |
| New metav1.ObjectMeta |
| ExpectedErr string |
| }{ |
| "invalid adding finalizers": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/a"}}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/a", "y/b"}}, |
| ExpectedErr: "y/b", |
| }, |
| "invalid changing finalizers": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/a"}}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/b"}}, |
| ExpectedErr: "x/b", |
| }, |
| "valid removing finalizers": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/a", "y/b"}}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &metav1.Time{}, Finalizers: []string{"x/a"}}, |
| ExpectedErr: "", |
| }, |
| "valid adding finalizers for objects not being deleted": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Finalizers: []string{"x/a"}}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Finalizers: []string{"x/a", "y/b"}}, |
| ExpectedErr: "", |
| }, |
| } |
| for name, tc := range testcases { |
| errs := ValidateObjectMetaUpdate(&tc.New, &tc.Old, field.NewPath("field")) |
| if len(errs) == 0 { |
| if len(tc.ExpectedErr) != 0 { |
| t.Errorf("case: %q, expected error to contain %q", name, tc.ExpectedErr) |
| } |
| } else if e, a := tc.ExpectedErr, errs.ToAggregate().Error(); !strings.Contains(a, e) { |
| t.Errorf("case: %q, expected error to contain %q, got error %q", name, e, a) |
| } |
| } |
| } |
| |
| func TestValidateFinalizersPreventConflictingFinalizers(t *testing.T) { |
| testcases := map[string]struct { |
| ObjectMeta metav1.ObjectMeta |
| ExpectedErr string |
| }{ |
| "conflicting finalizers": { |
| ObjectMeta: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Finalizers: []string{metav1.FinalizerOrphanDependents, metav1.FinalizerDeleteDependents}}, |
| ExpectedErr: "cannot be both set", |
| }, |
| } |
| for name, tc := range testcases { |
| errs := ValidateObjectMeta(&tc.ObjectMeta, false, NameIsDNSSubdomain, field.NewPath("field")) |
| if len(errs) == 0 { |
| if len(tc.ExpectedErr) != 0 { |
| t.Errorf("case: %q, expected error to contain %q", name, tc.ExpectedErr) |
| } |
| } else if e, a := tc.ExpectedErr, errs.ToAggregate().Error(); !strings.Contains(a, e) { |
| t.Errorf("case: %q, expected error to contain %q, got error %q", name, e, a) |
| } |
| } |
| } |
| |
| func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) { |
| now := metav1.NewTime(time.Unix(1000, 0).UTC()) |
| later := metav1.NewTime(time.Unix(2000, 0).UTC()) |
| gracePeriodShort := int64(30) |
| gracePeriodLong := int64(40) |
| |
| testcases := map[string]struct { |
| Old metav1.ObjectMeta |
| New metav1.ObjectMeta |
| ExpectedNew metav1.ObjectMeta |
| ExpectedErrs []string |
| }{ |
| "valid without deletion fields": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedErrs: []string{}, |
| }, |
| "valid with deletion fields": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort}, |
| ExpectedErrs: []string{}, |
| }, |
| |
| "invalid set deletionTimestamp": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now}, |
| ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:16:40 +0000 UTC: field is immutable"}, |
| }, |
| "invalid clear deletionTimestamp": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"null\": field is immutable"}, |
| }, |
| "invalid change deletionTimestamp": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later}, |
| ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:33:20 +0000 UTC: field is immutable"}, |
| }, |
| |
| "invalid set deletionGracePeriodSeconds": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort}, |
| ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: 30: field is immutable"}, |
| }, |
| "invalid clear deletionGracePeriodSeconds": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"}, |
| ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: \"null\": field is immutable"}, |
| }, |
| "invalid change deletionGracePeriodSeconds": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodLong}, |
| ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodLong}, |
| ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: 40: field is immutable"}, |
| }, |
| } |
| |
| for k, tc := range testcases { |
| errs := ValidateObjectMetaUpdate(&tc.New, &tc.Old, field.NewPath("field")) |
| if len(errs) != len(tc.ExpectedErrs) { |
| t.Logf("%s: Expected: %#v", k, tc.ExpectedErrs) |
| t.Logf("%s: Got: %#v", k, errs) |
| t.Errorf("%s: expected %d errors, got %d", k, len(tc.ExpectedErrs), len(errs)) |
| continue |
| } |
| for i := range errs { |
| if errs[i].Error() != tc.ExpectedErrs[i] { |
| t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errs[i].Error()) |
| } |
| } |
| if !reflect.DeepEqual(tc.New, tc.ExpectedNew) { |
| t.Errorf("%s: Expected after validation:\n%#v\ngot\n%#v", k, tc.ExpectedNew, tc.New) |
| } |
| } |
| } |
| |
| func TestObjectMetaGenerationUpdate(t *testing.T) { |
| testcases := map[string]struct { |
| Old metav1.ObjectMeta |
| New metav1.ObjectMeta |
| ExpectedErrs []string |
| }{ |
| "invalid generation change - decremented": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 4}, |
| ExpectedErrs: []string{"field.generation: Invalid value: 4: must not be decremented"}, |
| }, |
| "valid generation change - incremented by one": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 1}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 2}, |
| ExpectedErrs: []string{}, |
| }, |
| "valid generation field - not updated": { |
| Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5}, |
| New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5}, |
| ExpectedErrs: []string{}, |
| }, |
| } |
| |
| for k, tc := range testcases { |
| errList := []string{} |
| errs := ValidateObjectMetaUpdate(&tc.New, &tc.Old, field.NewPath("field")) |
| if len(errs) != len(tc.ExpectedErrs) { |
| t.Logf("%s: Expected: %#v", k, tc.ExpectedErrs) |
| for _, err := range errs { |
| errList = append(errList, err.Error()) |
| } |
| t.Logf("%s: Got: %#v", k, errList) |
| t.Errorf("%s: expected %d errors, got %d", k, len(tc.ExpectedErrs), len(errs)) |
| continue |
| } |
| for i := range errList { |
| if errList[i] != tc.ExpectedErrs[i] { |
| t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errList[i]) |
| } |
| } |
| } |
| } |
| |
| // Ensure trailing slash is allowed in generate name |
| func TestValidateObjectMetaTrimsTrailingSlash(t *testing.T) { |
| errs := ValidateObjectMeta( |
| &metav1.ObjectMeta{Name: "test", GenerateName: "foo-"}, |
| false, |
| NameIsDNSSubdomain, |
| field.NewPath("field")) |
| if len(errs) != 0 { |
| t.Fatalf("unexpected errors: %v", errs) |
| } |
| } |
| |
| func TestValidateAnnotations(t *testing.T) { |
| successCases := []map[string]string{ |
| {"simple": "bar"}, |
| {"now-with-dashes": "bar"}, |
| {"1-starts-with-num": "bar"}, |
| {"1234": "bar"}, |
| {"simple/simple": "bar"}, |
| {"now-with-dashes/simple": "bar"}, |
| {"now-with-dashes/now-with-dashes": "bar"}, |
| {"now.with.dots/simple": "bar"}, |
| {"now-with.dashes-and.dots/simple": "bar"}, |
| {"1-num.2-num/3-num": "bar"}, |
| {"1234/5678": "bar"}, |
| {"1.2.3.4/5678": "bar"}, |
| {"UpperCase123": "bar"}, |
| {"a": strings.Repeat("b", totalAnnotationSizeLimitB-1)}, |
| { |
| "a": strings.Repeat("b", totalAnnotationSizeLimitB/2-1), |
| "c": strings.Repeat("d", totalAnnotationSizeLimitB/2-1), |
| }, |
| } |
| for i := range successCases { |
| errs := ValidateAnnotations(successCases[i], field.NewPath("field")) |
| if len(errs) != 0 { |
| t.Errorf("case[%d] expected success, got %#v", i, errs) |
| } |
| } |
| |
| nameErrorCases := []struct { |
| annotations map[string]string |
| expect string |
| }{ |
| {map[string]string{"nospecialchars^=@": "bar"}, namePartErrMsg}, |
| {map[string]string{"cantendwithadash-": "bar"}, namePartErrMsg}, |
| {map[string]string{"only/one/slash": "bar"}, nameErrMsg}, |
| {map[string]string{strings.Repeat("a", 254): "bar"}, maxLengthErrMsg}, |
| } |
| for i := range nameErrorCases { |
| errs := ValidateAnnotations(nameErrorCases[i].annotations, field.NewPath("field")) |
| if len(errs) != 1 { |
| t.Errorf("case[%d]: expected failure", i) |
| } else { |
| if !strings.Contains(errs[0].Detail, nameErrorCases[i].expect) { |
| t.Errorf("case[%d]: error details do not include %q: %q", i, nameErrorCases[i].expect, errs[0].Detail) |
| } |
| } |
| } |
| totalSizeErrorCases := []map[string]string{ |
| {"a": strings.Repeat("b", totalAnnotationSizeLimitB)}, |
| { |
| "a": strings.Repeat("b", totalAnnotationSizeLimitB/2), |
| "c": strings.Repeat("d", totalAnnotationSizeLimitB/2), |
| }, |
| } |
| for i := range totalSizeErrorCases { |
| errs := ValidateAnnotations(totalSizeErrorCases[i], field.NewPath("field")) |
| if len(errs) != 1 { |
| t.Errorf("case[%d] expected failure", i) |
| } |
| } |
| } |