mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 07:47:56 +00:00
add validation for customresourcedefintions
This commit is contained in:
parent
97889d4ff9
commit
bcf6c66c4c
@ -5,10 +5,29 @@ licenses(["notice"])
|
|||||||
load(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = ["validation.go"],
|
srcs = ["validation.go"],
|
||||||
tags = ["automanaged"],
|
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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
@ -15,3 +15,145 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
package validation
|
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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,5 +25,6 @@ go_library(
|
|||||||
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/storage/names: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:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
|
||||||
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiServerStrategy struct {
|
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 {
|
func (apiServerStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||||
return field.ErrorList{}
|
return validation.ValidateCustomResource(obj.(*apiextensions.CustomResource))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (apiServerStrategy) AllowCreateOnUpdate() bool {
|
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 {
|
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) {
|
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||||
|
@ -40,6 +40,7 @@ func NewNoxuCustomResourceDefinition() *apiextensionsv1alpha1.CustomResource {
|
|||||||
Kind: "WishIHadChosenNoxu",
|
Kind: "WishIHadChosenNoxu",
|
||||||
ListKind: "NoxuItemList",
|
ListKind: "NoxuItemList",
|
||||||
},
|
},
|
||||||
|
Scope: apiextensionsv1alpha1.NamespaceScoped,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user