Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add validation for customresourcedefintions #45556

Merged
merged 1 commit into from
May 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func NewNoxuCustomResourceDefinition() *apiextensionsv1alpha1.CustomResource {
Kind: "WishIHadChosenNoxu",
ListKind: "NoxuItemList",
},
Scope: apiextensionsv1alpha1.NamespaceScoped,
},
}
}
Expand Down