mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 20:53:33 +00:00
apiextensions: allow defaults in validation OpenAPI schema
This commit is contained in:
parent
7fb176d91b
commit
e41e7249a4
@ -21,9 +21,12 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
"github.com/go-openapi/strfmt"
|
||||||
|
govalidate "github.com/go-openapi/validate"
|
||||||
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
@ -32,6 +35,8 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
|
||||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
)
|
)
|
||||||
@ -103,9 +108,9 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCustomResourceDefinitionVersion statically validates.
|
// ValidateCustomResourceDefinitionVersion statically validates.
|
||||||
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, mustBeStructural, statusEnabled bool) field.ErrorList {
|
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, mustBeStructural, statusEnabled, allowDefaults bool) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, mustBeStructural, statusEnabled, fldPath.Child("schema"))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, mustBeStructural, statusEnabled, allowDefaults, fldPath.Child("schema"))...)
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
|
||||||
for i := range version.AdditionalPrinterColumns {
|
for i := range version.AdditionalPrinterColumns {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
|
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
|
||||||
@ -115,10 +120,11 @@ func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResour
|
|||||||
|
|
||||||
// ValidateCustomResourceDefinitionSpec statically validates
|
// ValidateCustomResourceDefinitionSpec statically validates
|
||||||
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
|
||||||
return validateCustomResourceDefinitionSpec(spec, true, fldPath)
|
allowDefaults := utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting)
|
||||||
|
return validateCustomResourceDefinitionSpec(spec, true, allowDefaults, fldPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion, allowDefaults bool, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
if len(spec.Group) == 0 {
|
if len(spec.Group) == 0 {
|
||||||
@ -144,6 +150,12 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if allowDefaults && specHasDefaults(spec) {
|
||||||
|
mustBeStructural = true
|
||||||
|
if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == true {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
storageFlagCount := 0
|
storageFlagCount := 0
|
||||||
versionsMap := map[string]bool{}
|
versionsMap := map[string]bool{}
|
||||||
@ -161,7 +173,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
|
||||||
}
|
}
|
||||||
subresources := getSubresourcesForVersion(spec, version.Name)
|
subresources := getSubresourcesForVersion(spec, version.Name)
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), mustBeStructural, hasStatusEnabled(subresources))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), mustBeStructural, hasStatusEnabled(subresources), allowDefaults)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The top-level and per-version fields are mutual exclusive
|
// The top-level and per-version fields are mutual exclusive
|
||||||
@ -216,7 +228,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, mustBeStructural, hasAnyStatusEnabled(spec), fldPath.Child("validation"))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, mustBeStructural, hasAnyStatusEnabled(spec), allowDefaults, fldPath.Child("validation"))...)
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
|
||||||
|
|
||||||
for i := range spec.AdditionalPrinterColumns {
|
for i := range spec.AdditionalPrinterColumns {
|
||||||
@ -343,7 +355,11 @@ func validateCustomResourceConversion(conversion *apiextensions.CustomResourceCo
|
|||||||
// ValidateCustomResourceDefinitionSpecUpdate statically validates
|
// ValidateCustomResourceDefinitionSpecUpdate statically validates
|
||||||
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
|
||||||
requireRecognizedVersion := oldSpec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldSpec.Conversion.ConversionReviewVersions)
|
requireRecognizedVersion := oldSpec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldSpec.Conversion.ConversionReviewVersions)
|
||||||
allErrs := validateCustomResourceDefinitionSpec(spec, requireRecognizedVersion, fldPath)
|
|
||||||
|
// find out whether any schema had default before. Then we keep allowing it.
|
||||||
|
allowDefaults := utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting) || specHasDefaults(oldSpec)
|
||||||
|
|
||||||
|
allErrs := validateCustomResourceDefinitionSpec(spec, requireRecognizedVersion, allowDefaults, fldPath)
|
||||||
|
|
||||||
if established {
|
if established {
|
||||||
// these effect the storage and cannot be changed therefore
|
// these effect the storage and cannot be changed therefore
|
||||||
@ -546,7 +562,7 @@ type specStandardValidator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCustomResourceDefinitionValidation statically validates
|
// ValidateCustomResourceDefinitionValidation statically validates
|
||||||
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, mustBeStructural, statusSubresourceEnabled bool, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, mustBeStructural, statusSubresourceEnabled, allowDefaults bool, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
if customResourceValidation == nil {
|
if customResourceValidation == nil {
|
||||||
@ -586,7 +602,9 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
|||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("openAPIV3Schema.nullable"), fmt.Sprintf(`nullable cannot be true at the root`)))
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("openAPIV3Schema.nullable"), fmt.Sprintf(`nullable cannot be true at the root`)))
|
||||||
}
|
}
|
||||||
|
|
||||||
openAPIV3Schema := &specStandardValidatorV3{}
|
openAPIV3Schema := &specStandardValidatorV3{
|
||||||
|
allowDefaults: allowDefaults,
|
||||||
|
}
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
|
||||||
|
|
||||||
if mustBeStructural {
|
if mustBeStructural {
|
||||||
@ -706,7 +724,9 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
type specStandardValidatorV3 struct{}
|
type specStandardValidatorV3 struct {
|
||||||
|
allowDefaults bool
|
||||||
|
}
|
||||||
|
|
||||||
// validate validates against OpenAPI Schema v3.
|
// validate validates against OpenAPI Schema v3.
|
||||||
func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
|
func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
|
||||||
@ -721,7 +741,24 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||||||
//
|
//
|
||||||
|
|
||||||
if schema.Default != nil {
|
if schema.Default != nil {
|
||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported"))
|
if v.allowDefaults {
|
||||||
|
if s, err := structuralschema.NewStructural(schema); err == nil {
|
||||||
|
// ignore errors here locally. They will show up for the root of the schema.
|
||||||
|
pruned := runtime.DeepCopyJSONValue(*schema.Default)
|
||||||
|
pruning.Prune(pruned, s)
|
||||||
|
if !reflect.DeepEqual(pruned, *schema.Default) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unspecified fields"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the default value. Only validating and pruned defaults are allowed.
|
||||||
|
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
|
||||||
|
if err := apiservervalidation.ValidateCustomResource(pruned, validator); err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, fmt.Sprintf("must validate: %v", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must not be set"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if schema.ID != "" {
|
if schema.ID != "" {
|
||||||
@ -830,3 +867,86 @@ func allowedAtRootSchema(field string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
|
if spec.Validation != nil && schemaHasDefaults(spec.Validation.OpenAPIV3Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, v := range spec.Versions {
|
||||||
|
if v.Schema != nil && schemaHasDefaults(v.Schema.OpenAPIV3Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Default != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Items != nil {
|
||||||
|
if s.Items != nil && schemaHasDefaults(s.Items.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, s := range s.Items.JSONSchemas {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.AllOf {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.AnyOf {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.OneOf {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if schemaHasDefaults(s.Not) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, s := range s.Properties {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.AdditionalProperties != nil {
|
||||||
|
if schemaHasDefaults(s.AdditionalProperties.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.PatternProperties {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.AdditionalItems != nil {
|
||||||
|
if schemaHasDefaults(s.AdditionalItems.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.Definitions {
|
||||||
|
if schemaHasDefaults(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range s.Dependencies {
|
||||||
|
if schemaHasDefaults(d.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -17,11 +17,23 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||||
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
|
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/component-base/featuregate"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,9 +76,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
resource *apiextensions.CustomResourceDefinition
|
resource *apiextensions.CustomResourceDefinition
|
||||||
errors []validationMatch
|
errors []validationMatch
|
||||||
|
enabledFeatures []featuregate.Feature
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "webhookconfig: invalid port 0",
|
name: "webhookconfig: invalid port 0",
|
||||||
@ -1239,10 +1252,325 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"),
|
invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "defaults with disabled feature gate",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {Default: jsonPtr(42.0)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults with enabled feature gate, unstructural schema",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {Default: jsonPtr(42.0)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "properties[a]", "type"),
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "type"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults with enabled feature gate, structural schema",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults in value validation with enabled feature gate, structural schema",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Not: &apiextensions.JSONSchemaProps{
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
AnyOf: []apiextensions.JSONSchemaProps{
|
||||||
|
{
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AllOf: []apiextensions.JSONSchemaProps{
|
||||||
|
{
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OneOf: []apiextensions.JSONSchemaProps{
|
||||||
|
{
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "not", "default"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "allOf[0]", "default"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "anyOf[0]", "default"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "oneOf[0]", "default"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid defaults with enabled feature gate, structural schema",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": int64(42.0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": int64(42),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"d": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"good": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "a",
|
||||||
|
},
|
||||||
|
"bad": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "+",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"good": "a",
|
||||||
|
"bad": "a",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"e": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"preserveUnknownFields": {
|
||||||
|
Type: "object",
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
// this is under x-kubernetes-preserve-unknown-fields
|
||||||
|
"bar": int64(42.0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"nestedProperties": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": int64(42.0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[a]", "default"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[c]", "default"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[d]", "default"),
|
||||||
|
// we also expected unpruned and valid defaults under x-kubernetes-preserve-unknown-fields. We could be more
|
||||||
|
// strict here, but want to encourage proper specifications by forbidding other defaults.
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[preserveUnknownFields]", "default"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[nestedProperties]", "default"),
|
||||||
|
},
|
||||||
|
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults with enabled feature gate, structural schema, without pruning",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalid("spec", "preserveUnknownFields"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
for _, gate := range tc.enabledFeatures {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)()
|
||||||
|
}
|
||||||
// duplicate defaulting behaviour
|
// duplicate defaulting behaviour
|
||||||
if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 {
|
if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 {
|
||||||
tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"}
|
tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"}
|
||||||
@ -1276,10 +1604,11 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
old *apiextensions.CustomResourceDefinition
|
old *apiextensions.CustomResourceDefinition
|
||||||
resource *apiextensions.CustomResourceDefinition
|
resource *apiextensions.CustomResourceDefinition
|
||||||
errors []validationMatch
|
errors []validationMatch
|
||||||
|
enabledFeatures []featuregate.Feature
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "webhookconfig: should pass on invalid ConversionReviewVersion with old invalid versions",
|
name: "webhookconfig: should pass on invalid ConversionReviewVersion with old invalid versions",
|
||||||
@ -2180,32 +2509,206 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
errors: []validationMatch{},
|
errors: []validationMatch{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "setting defaults with enabled feature gate",
|
||||||
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plural.group.com",
|
||||||
|
ResourceVersion: "42",
|
||||||
|
},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plural.group.com",
|
||||||
|
ResourceVersion: "42",
|
||||||
|
},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ratcheting validation of defaults with disabled feature gate",
|
||||||
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plural.group.com",
|
||||||
|
ResourceVersion: "42",
|
||||||
|
},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plural.group.com",
|
||||||
|
ResourceVersion: "42",
|
||||||
|
},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(43.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
seenErrs := make([]bool, len(errs))
|
for _, gate := range tc.enabledFeatures {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)()
|
||||||
|
}
|
||||||
|
|
||||||
for _, expectedError := range tc.errors {
|
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old)
|
||||||
found := false
|
seenErrs := make([]bool, len(errs))
|
||||||
for i, err := range errs {
|
|
||||||
if expectedError.matches(err) && !seenErrs[i] {
|
for _, expectedError := range tc.errors {
|
||||||
found = true
|
found := false
|
||||||
seenErrs[i] = true
|
for i, err := range errs {
|
||||||
break
|
if expectedError.matches(err) && !seenErrs[i] {
|
||||||
|
found = true
|
||||||
|
seenErrs[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
for i, seen := range seenErrs {
|
||||||
t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs)
|
if !seen {
|
||||||
|
t.Errorf("unexpected error: %v", errs[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
for i, seen := range seenErrs {
|
|
||||||
if !seen {
|
|
||||||
t.Errorf("%s: unexpected error: %v", tc.name, errs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2356,7 +2859,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := ValidateCustomResourceDefinitionValidation(&tt.input, tt.mustBeStructural, tt.statusEnabled, field.NewPath("spec", "validation"))
|
got := ValidateCustomResourceDefinitionValidation(&tt.input, tt.mustBeStructural, tt.statusEnabled, false, field.NewPath("spec", "validation"))
|
||||||
if !tt.wantError && len(got) > 0 {
|
if !tt.wantError && len(got) > 0 {
|
||||||
t.Errorf("Expected no error, but got: %v", got)
|
t.Errorf("Expected no error, but got: %v", got)
|
||||||
} else if tt.wantError && len(got) == 0 {
|
} else if tt.wantError && len(got) == 0 {
|
||||||
@ -2366,6 +2869,40 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSchemaHasDefaults(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
if err := apiextensions.AddToScheme(scheme); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seed := rand.Int63()
|
||||||
|
t.Logf("seed: %d", seed)
|
||||||
|
fuzzerFuncs := fuzzer.MergeFuzzerFuncs(apiextensionsfuzzer.Funcs)
|
||||||
|
f := fuzzer.FuzzerFor(fuzzerFuncs, rand.NewSource(seed), codecs)
|
||||||
|
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
// fuzz internal types
|
||||||
|
schema := &apiextensions.JSONSchemaProps{}
|
||||||
|
f.Fuzz(schema)
|
||||||
|
|
||||||
|
v1beta1Schema := &apiextensionsv1beta1.JSONSchemaProps{}
|
||||||
|
if err := apiextensionsv1beta1.Convert_apiextensions_JSONSchemaProps_To_v1beta1_JSONSchemaProps(schema, v1beta1Schema, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := json.Marshal(v1beta1Schema)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.Contains(strings.Replace(string(bs), `"default":null`, `"deleted":null`, -1), `"default":`)
|
||||||
|
if got := schemaHasDefaults(schema); got != expected {
|
||||||
|
t.Errorf("expected %v, got %v for: %s", expected, got, string(bs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var example = apiextensions.JSON(`"This is an example"`)
|
var example = apiextensions.JSON(`"This is an example"`)
|
||||||
|
|
||||||
var validValidationSchema = &apiextensions.JSONSchemaProps{
|
var validValidationSchema = &apiextensions.JSONSchemaProps{
|
||||||
@ -2442,3 +2979,8 @@ func float64Ptr(f float64) *float64 {
|
|||||||
func int64Ptr(f int64) *int64 {
|
func int64Ptr(f int64) *int64 {
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jsonPtr(x interface{}) *apiextensions.JSON {
|
||||||
|
ret := apiextensions.JSON(x)
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Prune removes object fields in obj which are not specified in s.
|
// Prune removes object fields in obj which are not specified in s.
|
||||||
func Prune(obj map[string]interface{}, s *structuralschema.Structural) {
|
func Prune(obj interface{}, s *structuralschema.Structural) {
|
||||||
prune(obj, s)
|
prune(obj, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user