apiextensions: allow defaults in validation OpenAPI schema
This commit is contained in:
		| @@ -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" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -67,6 +79,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) { | |||||||
| 		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"} | ||||||
| @@ -1280,6 +1608,7 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { | |||||||
| 		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,9 +2509,182 @@ 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 { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			for _, gate := range tc.enabledFeatures { | ||||||
|  | 				defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)() | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old) | 			errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old) | ||||||
| 			seenErrs := make([]bool, len(errs)) | 			seenErrs := make([]bool, len(errs)) | ||||||
|  |  | ||||||
| @@ -2197,15 +2699,16 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if !found { | 				if !found { | ||||||
| 				t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs) | 					t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for i, seen := range seenErrs { | 			for i, seen := range seenErrs { | ||||||
| 				if !seen { | 				if !seen { | ||||||
| 				t.Errorf("%s: unexpected error: %v", tc.name, errs[i]) | 					t.Errorf("unexpected error: %v", 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) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Dr. Stefan Schimanski
					Dr. Stefan Schimanski