From bcf6c66c4cdd7c79097231b6349f92ff8d194762 Mon Sep 17 00:00:00 2001 From: deads2k Date: Tue, 9 May 2017 14:47:27 -0400 Subject: [PATCH] add validation for customresourcedefintions --- .../pkg/apis/apiextensions/validation/BUILD | 19 ++ .../apiextensions/validation/validation.go | 142 +++++++++++++++ .../validation/validation_test.go | 165 ++++++++++++++++++ .../pkg/registry/customresource/BUILD | 1 + .../pkg/registry/customresource/strategy.go | 5 +- .../test/integration/testserver/resources.go | 1 + 6 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation_test.go diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/BUILD index 851573d1c5f..286f8c18f78 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/BUILD @@ -5,10 +5,29 @@ licenses(["notice"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( name = "go_default_library", srcs = ["validation.go"], tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + ], ) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation.go index 59f1df4414e..fb99f3e5d3c 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation.go @@ -15,3 +15,145 @@ limitations under the License. */ package validation + +import ( + "fmt" + "strings" + + genericvalidation "k8s.io/apimachinery/pkg/api/validation" + validationutil "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" +) + +// ValidateCustomResource statically validates +func ValidateCustomResource(obj *apiextensions.CustomResource) field.ErrorList { + nameValidationFn := func(name string, prefix bool) []string { + ret := genericvalidation.NameIsDNSSubdomain(name, prefix) + requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group + if name != requiredName { + ret = append(ret, fmt.Sprintf(`must be spec.names.plural+"."+spec.group`)) + } + return ret + } + + allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateCustomResourceSpec(&obj.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateCustomResourceStatus(&obj.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateCustomResourceUpdate statically validates +func ValidateCustomResourceUpdate(obj, oldObj *apiextensions.CustomResource) field.ErrorList { + allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateCustomResourceSpecUpdate(&obj.Spec, &oldObj.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateCustomResourceStatus(&obj.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateUpdateCustomResourceStatus statically validates +func ValidateUpdateCustomResourceStatus(obj, oldObj *apiextensions.CustomResource) field.ErrorList { + allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateCustomResourceStatus(&obj.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateCustomResourceSpec statically validates +func ValidateCustomResourceSpec(spec *apiextensions.CustomResourceSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(spec.Group) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("group"), "")) + } + if errs := validationutil.IsDNS1123Subdomain(spec.Group); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ","))) + } + if len(strings.Split(spec.Group, ".")) < 2 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot")) + } + + if len(spec.Version) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("version"), "")) + } + if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ","))) + } + + switch spec.Scope { + case apiextensions.ClusterScoped, apiextensions.NamespaceScoped: + default: + allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)})) + } + + // in addition to the basic name restrictions, some names are required for spec, but not for status + if len(spec.Names.Plural) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), "")) + } + if len(spec.Names.Singular) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("names", "singular"), "")) + } + if len(spec.Names.Kind) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("names", "kind"), "")) + } + if len(spec.Names.ListKind) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("names", "listKind"), "")) + } + + allErrs = append(allErrs, ValidateCustomResourceNames(&spec.Names, fldPath.Child("names"))...) + + return allErrs +} + +// ValidateCustomResourceSpecUpdate statically validates +func ValidateCustomResourceSpecUpdate(spec, oldSpec *apiextensions.CustomResourceSpec, fldPath *field.Path) field.ErrorList { + allErrs := ValidateCustomResourceSpec(spec, fldPath) + + // these all affect the storage, so you can't change them + genericvalidation.ValidateImmutableField(spec.Group, oldSpec.Group, fldPath.Child("group")) + genericvalidation.ValidateImmutableField(spec.Version, oldSpec.Version, fldPath.Child("version")) + genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope")) + genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind")) + + // this affects the expected resource name, so you can't change it either + genericvalidation.ValidateImmutableField(spec.Names.Plural, oldSpec.Names.Plural, fldPath.Child("names", "plural")) + + return allErrs +} + +// ValidateCustomResourceStatus statically validates +func ValidateCustomResourceStatus(status *apiextensions.CustomResourceStatus, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidateCustomResourceNames(&status.AcceptedNames, fldPath.Child("acceptedNames"))...) + return allErrs +} + +// ValidateCustomResourceNames statically validates +func ValidateCustomResourceNames(names *apiextensions.CustomResourceNames, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if errs := validationutil.IsDNS1035Label(names.Plural); len(names.Plural) > 0 && len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("plural"), names.Plural, strings.Join(errs, ","))) + } + if errs := validationutil.IsDNS1035Label(names.Singular); len(names.Singular) > 0 && len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("singular"), names.Singular, strings.Join(errs, ","))) + } + if errs := validationutil.IsDNS1035Label(strings.ToLower(names.Kind)); len(names.Kind) > 0 && len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), names.Kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ","))) + } + if errs := validationutil.IsDNS1035Label(strings.ToLower(names.ListKind)); len(names.ListKind) > 0 && len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ","))) + } + + for i, shortName := range names.ShortNames { + if errs := validationutil.IsDNS1035Label(shortName); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("shortNames").Index(i), shortName, strings.Join(errs, ","))) + } + + } + + // kind and listKind may not be the same or parsing become ambiguous + if len(names.Kind) > 0 && names.Kind == names.ListKind { + allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "kind and listKind may not be the same")) + } + + return allErrs +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation_test.go new file mode 100644 index 00000000000..47fdf1b44ff --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation/validation_test.go @@ -0,0 +1,165 @@ +/* +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 ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" +) + +type validationMatch struct { + path *field.Path + errorType field.ErrorType +} + +func required(path *field.Path) validationMatch { + return validationMatch{path: path, errorType: field.ErrorTypeRequired} +} +func invalid(path *field.Path) validationMatch { + return validationMatch{path: path, errorType: field.ErrorTypeInvalid} +} + +func (v validationMatch) matches(err *field.Error) bool { + return err.Type == v.errorType && err.Field == v.path.String() +} + +func TestValidateCustomResource(t *testing.T) { + tests := []struct { + name string + resource *apiextensions.CustomResource + errors []validationMatch + }{ + { + name: "mismatched name", + resource: &apiextensions.CustomResource{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.not.group.com"}, + Spec: apiextensions.CustomResourceSpec{ + Group: "group.com", + Names: apiextensions.CustomResourceNames{ + Plural: "plural", + }, + }, + }, + errors: []validationMatch{ + invalid(field.NewPath("metadata", "name")), + }, + }, + { + name: "missing values", + resource: &apiextensions.CustomResource{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + }, + errors: []validationMatch{ + required(field.NewPath("spec", "group")), + required(field.NewPath("spec", "version")), + {path: field.NewPath("spec", "scope"), errorType: field.ErrorTypeNotSupported}, + required(field.NewPath("spec", "names", "plural")), + required(field.NewPath("spec", "names", "singular")), + required(field.NewPath("spec", "names", "kind")), + required(field.NewPath("spec", "names", "listKind")), + }, + }, + { + name: "bad names 01", + resource: &apiextensions.CustomResource{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group"}, + Spec: apiextensions.CustomResourceSpec{ + Group: "group", + Version: "ve()*rsion", + Scope: apiextensions.ResourceScope("foo"), + Names: apiextensions.CustomResourceNames{ + Plural: "pl()*ural", + Singular: "value()*a", + Kind: "value()*a", + ListKind: "value()*a", + }, + }, + Status: apiextensions.CustomResourceStatus{ + AcceptedNames: apiextensions.CustomResourceNames{ + Plural: "pl()*ural", + Singular: "value()*a", + Kind: "value()*a", + ListKind: "value()*a", + }, + }, + }, + errors: []validationMatch{ + invalid(field.NewPath("spec", "group")), + invalid(field.NewPath("spec", "version")), + {path: field.NewPath("spec", "scope"), errorType: field.ErrorTypeNotSupported}, + invalid(field.NewPath("spec", "names", "plural")), + invalid(field.NewPath("spec", "names", "singular")), + invalid(field.NewPath("spec", "names", "kind")), + invalid(field.NewPath("spec", "names", "listKind")), + invalid(field.NewPath("status", "acceptedNames", "plural")), + invalid(field.NewPath("status", "acceptedNames", "singular")), + invalid(field.NewPath("status", "acceptedNames", "kind")), + invalid(field.NewPath("status", "acceptedNames", "listKind")), + }, + }, + { + name: "bad names 02", + resource: &apiextensions.CustomResource{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group"}, + Spec: apiextensions.CustomResourceSpec{ + Group: "group.c(*&om", + Version: "version", + Names: apiextensions.CustomResourceNames{ + Plural: "plural", + Singular: "singular", + Kind: "matching", + ListKind: "matching", + }, + }, + Status: apiextensions.CustomResourceStatus{ + AcceptedNames: apiextensions.CustomResourceNames{ + Plural: "plural", + Singular: "singular", + Kind: "matching", + ListKind: "matching", + }, + }, + }, + errors: []validationMatch{ + invalid(field.NewPath("spec", "group")), + invalid(field.NewPath("spec", "names", "listKind")), + invalid(field.NewPath("status", "acceptedNames", "listKind")), + }, + }, + } + + for _, tc := range tests { + errs := ValidateCustomResource(tc.resource) + + for _, expectedError := range tc.errors { + found := false + for _, err := range errs { + if expectedError.matches(err) { + found = true + break + } + } + + if !found { + t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs) + } + } + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/BUILD index 17ed1add66d..9ab5ce3118e 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/BUILD @@ -25,5 +25,6 @@ go_library( "//vendor/k8s.io/apiserver/pkg/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation:go_default_library", ], ) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/strategy.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/strategy.go index 59b275e70d1..4e66f2080d9 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/strategy.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresource/strategy.go @@ -29,6 +29,7 @@ import ( "k8s.io/apiserver/pkg/storage/names" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation" ) type apiServerStrategy struct { @@ -51,7 +52,7 @@ func (apiServerStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, ol } func (apiServerStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + return validation.ValidateCustomResource(obj.(*apiextensions.CustomResource)) } func (apiServerStrategy) AllowCreateOnUpdate() bool { @@ -66,7 +67,7 @@ func (apiServerStrategy) Canonicalize(obj runtime.Object) { } func (apiServerStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { - return field.ErrorList{} + return validation.ValidateCustomResourceUpdate(obj.(*apiextensions.CustomResource), old.(*apiextensions.CustomResource)) } func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go index 70a33526d38..28082b77fbf 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go @@ -40,6 +40,7 @@ func NewNoxuCustomResourceDefinition() *apiextensionsv1alpha1.CustomResource { Kind: "WishIHadChosenNoxu", ListKind: "NoxuItemList", }, + Scope: apiextensionsv1alpha1.NamespaceScoped, }, } }