Merge pull request #45556 from deads2k/tpr-10-validation

Automatic merge from submit-queue

add validation for customresourcedefintions

Add basic validation for customresource definitions.

@adohe if you had review bandwidth, this is a relatively small one.
pull/6/head
Kubernetes Submit Queue 2017-05-10 22:21:21 -07:00 committed by GitHub
commit 3126e73400
6 changed files with 331 additions and 2 deletions

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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",
],
)

View File

@ -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) {

View File

@ -40,6 +40,7 @@ func NewNoxuCustomResourceDefinition() *apiextensionsv1alpha1.CustomResource {
Kind: "WishIHadChosenNoxu",
ListKind: "NoxuItemList",
},
Scope: apiextensionsv1alpha1.NamespaceScoped,
},
}
}