mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #77558 from sttts/sttts-structural-defaulting
apiextensions: implement defaulting
This commit is contained in:
commit
bdc665cf36
3
api/openapi-spec/swagger.json
generated
3
api/openapi-spec/swagger.json
generated
@ -16709,7 +16709,8 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON"
|
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON",
|
||||||
|
"description": "default is a default value for undefined object fields. Defaulting is an alpha feature under the CustomResourceDefaulting feature gate. Defaulting requires spec.preserveUnknownFields to be false."
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
|
@ -533,6 +533,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||||||
apiextensionsfeatures.CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
apiextensionsfeatures.CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
||||||
apiextensionsfeatures.CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
apiextensionsfeatures.CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
apiextensionsfeatures.CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta},
|
apiextensionsfeatures.CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
apiextensionsfeatures.CustomResourceDefaulting: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
|
||||||
// features that enable backwards compatibility but are scheduled to be removed
|
// features that enable backwards compatibility but are scheduled to be removed
|
||||||
// ...
|
// ...
|
||||||
|
@ -394,6 +394,9 @@ message JSONSchemaProps {
|
|||||||
|
|
||||||
optional string title = 7;
|
optional string title = 7;
|
||||||
|
|
||||||
|
// default is a default value for undefined object fields.
|
||||||
|
// Defaulting is an alpha feature under the CustomResourceDefaulting feature gate.
|
||||||
|
// Defaulting requires spec.preserveUnknownFields to be false.
|
||||||
optional JSON default = 8;
|
optional JSON default = 8;
|
||||||
|
|
||||||
optional double maximum = 9;
|
optional double maximum = 9;
|
||||||
|
@ -25,6 +25,9 @@ type JSONSchemaProps struct {
|
|||||||
Type string `json:"type,omitempty" protobuf:"bytes,5,opt,name=type"`
|
Type string `json:"type,omitempty" protobuf:"bytes,5,opt,name=type"`
|
||||||
Format string `json:"format,omitempty" protobuf:"bytes,6,opt,name=format"`
|
Format string `json:"format,omitempty" protobuf:"bytes,6,opt,name=format"`
|
||||||
Title string `json:"title,omitempty" protobuf:"bytes,7,opt,name=title"`
|
Title string `json:"title,omitempty" protobuf:"bytes,7,opt,name=title"`
|
||||||
|
// default is a default value for undefined object fields.
|
||||||
|
// Defaulting is an alpha feature under the CustomResourceDefaulting feature gate.
|
||||||
|
// Defaulting requires spec.preserveUnknownFields to be false.
|
||||||
Default *JSON `json:"default,omitempty" protobuf:"bytes,8,opt,name=default"`
|
Default *JSON `json:"default,omitempty" protobuf:"bytes,8,opt,name=default"`
|
||||||
Maximum *float64 `json:"maximum,omitempty" protobuf:"bytes,9,opt,name=maximum"`
|
Maximum *float64 `json:"maximum,omitempty" protobuf:"bytes,9,opt,name=maximum"`
|
||||||
ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" protobuf:"bytes,10,opt,name=exclusiveMaximum"`
|
ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" protobuf:"bytes,10,opt,name=exclusiveMaximum"`
|
||||||
|
@ -15,15 +15,19 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
||||||
|
"//vendor/github.com/go-openapi/strfmt:go_default_library",
|
||||||
|
"//vendor/github.com/go-openapi/validate:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,8 +37,18 @@ go_test(
|
|||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
|
"//staging/src/k8s.io/component-base/featuregate:go_default_library",
|
||||||
|
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
|
||||||
"//vendor/k8s.io/utils/pointer:go_default_library",
|
"//vendor/k8s.io/utils/pointer:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
||||||
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
|
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
|
||||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||||
@ -645,7 +646,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||||||
|
|
||||||
Creater: creator,
|
Creater: creator,
|
||||||
Convertor: safeConverter,
|
Convertor: safeConverter,
|
||||||
Defaulter: unstructuredDefaulter{parameterScheme},
|
Defaulter: unstructuredDefaulter{parameterScheme, structuralSchemas, kind.GroupKind()},
|
||||||
Typer: typer,
|
Typer: typer,
|
||||||
UnsafeConvertor: unsafeConverter,
|
UnsafeConvertor: unsafeConverter,
|
||||||
|
|
||||||
@ -771,7 +772,11 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco
|
|||||||
|
|
||||||
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||||
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields}}
|
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields}}
|
||||||
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
|
return versioning.NewCodec(nil, d, runtime.UnsafeObjectConvertor(Scheme), Scheme, Scheme, unstructuredDefaulter{
|
||||||
|
delegate: Scheme,
|
||||||
|
structuralSchemas: s.structuralSchemas,
|
||||||
|
structuralSchemaGK: s.structuralSchemaGK,
|
||||||
|
}, nil, gv, "unstructuredNegotiatedSerializer")
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnstructuredObjectTyper struct {
|
type UnstructuredObjectTyper struct {
|
||||||
@ -808,13 +813,19 @@ func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object,
|
|||||||
|
|
||||||
type unstructuredDefaulter struct {
|
type unstructuredDefaulter struct {
|
||||||
delegate runtime.ObjectDefaulter
|
delegate runtime.ObjectDefaulter
|
||||||
|
structuralSchemas map[string]*structuralschema.Structural // by version
|
||||||
|
structuralSchemaGK schema.GroupKind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d unstructuredDefaulter) Default(in runtime.Object) {
|
func (d unstructuredDefaulter) Default(in runtime.Object) {
|
||||||
// Delegate for things other than Unstructured.
|
// Delegate for things other than Unstructured, and other GKs
|
||||||
if _, ok := in.(runtime.Unstructured); !ok {
|
u, ok := in.(runtime.Unstructured)
|
||||||
|
if !ok || u.GetObjectKind().GroupVersionKind().GroupKind() != d.structuralSchemaGK {
|
||||||
d.delegate.Default(in)
|
d.delegate.Default(in)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
structuraldefaulting.Default(u.UnstructuredContent(), d.structuralSchemas[u.GetObjectKind().GroupVersionKind().Version])
|
||||||
}
|
}
|
||||||
|
|
||||||
type CRDRESTOptionsGetter struct {
|
type CRDRESTOptionsGetter struct {
|
||||||
@ -888,7 +899,11 @@ func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupReso
|
|||||||
c,
|
c,
|
||||||
&unstructuredCreator{},
|
&unstructuredCreator{},
|
||||||
crdserverscheme.NewUnstructuredObjectTyper(),
|
crdserverscheme.NewUnstructuredObjectTyper(),
|
||||||
&unstructuredDefaulter{delegate: Scheme},
|
&unstructuredDefaulter{
|
||||||
|
delegate: Scheme,
|
||||||
|
structuralSchemaGK: t.structuralSchemaGK,
|
||||||
|
structuralSchemas: t.structuralSchemas,
|
||||||
|
},
|
||||||
t.encoderVersion,
|
t.encoderVersion,
|
||||||
t.decoderVersion,
|
t.decoderVersion,
|
||||||
"crdRESTOptions",
|
"crdRESTOptions",
|
||||||
|
@ -34,6 +34,7 @@ filegroup(
|
|||||||
name = "all-srcs",
|
name = "all-srcs",
|
||||||
srcs = [
|
srcs = [
|
||||||
":package-srcs",
|
":package-srcs",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:all-srcs",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
|
||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["algorithm.go"],
|
||||||
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting",
|
||||||
|
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = ["algorithm_test.go"],
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
deps = [
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "package-srcs",
|
||||||
|
srcs = glob(["**"]),
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "all-srcs",
|
||||||
|
srcs = [":package-srcs"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 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 defaulting
|
||||||
|
|
||||||
|
import (
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default does defaulting of x depending on default values in s.
|
||||||
|
// Default values from s are deep-copied.
|
||||||
|
func Default(x interface{}, s *structuralschema.Structural) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch x := x.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, prop := range s.Properties {
|
||||||
|
if prop.Default.Object == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, found := x[k]; !found {
|
||||||
|
x[k] = runtime.DeepCopyJSONValue(prop.Default.Object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
if prop, found := s.Properties[k]; found {
|
||||||
|
Default(v, &prop)
|
||||||
|
} else if s.AdditionalProperties != nil {
|
||||||
|
Default(v, s.AdditionalProperties.Structural)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range x {
|
||||||
|
Default(v, s.Items)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// scalars, do nothing
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 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 defaulting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefault(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
schema *structuralschema.Structural
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"empty", "null", nil, "null"},
|
||||||
|
{"scalar", "4", &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"foo"},
|
||||||
|
},
|
||||||
|
}, "4"},
|
||||||
|
{"scalar array", "[1,2]", &structuralschema.Structural{
|
||||||
|
Items: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, "[1,2]"},
|
||||||
|
{"object array", `[{"a":1},{"b":1},{"c":1}]`, &structuralschema.Structural{
|
||||||
|
Items: &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"a": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"A"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"B"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"C"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, `[{"a":1,"b":"B","c":"C"},{"a":"A","b":1,"c":"C"},{"a":"A","b":"B","c":1}]`},
|
||||||
|
{"object array object", `{"array":[{"a":1},{"b":2}],"object":{"a":1},"additionalProperties":{"x":{"a":1},"y":{"b":2}}}`, &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"array": {
|
||||||
|
Items: &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"a": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"A"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"B"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"a": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"N"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"O"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
AdditionalProperties: &structuralschema.StructuralOrBool{
|
||||||
|
Structural: &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"a": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"alpha"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"beta"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, `{"array":[{"a":1,"b":"B"},{"a":"A","b":2}],"object":{"a":1,"b":"O"},"additionalProperties":{"x":{"a":1,"b":"beta"},"y":{"a":"alpha","b":2}},"foo":"bar"}`},
|
||||||
|
{"empty and null", `[{},{"a":1},{"a":0},{"a":0.0},{"a":""},{"a":null},{"a":[]},{"a":{}}]`, &structuralschema.Structural{
|
||||||
|
Items: &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"a": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Default: structuralschema.JSON{"A"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, `[{"a":"A"},{"a":1},{"a":0},{"a":0.0},{"a":""},{"a":null},{"a":[]},{"a":{}}]`},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var in interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Default(in, tt.schema)
|
||||||
|
if !reflect.DeepEqual(in, expected) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
err := enc.Encode(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected result mashalling error: %v", err)
|
||||||
|
}
|
||||||
|
t.Errorf("expected: %s\ngot: %s", tt.expected, buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,12 @@ const (
|
|||||||
//
|
//
|
||||||
// CustomResourceWebhookConversion defines the webhook conversion for Custom Resources.
|
// CustomResourceWebhookConversion defines the webhook conversion for Custom Resources.
|
||||||
CustomResourceWebhookConversion featuregate.Feature = "CustomResourceWebhookConversion"
|
CustomResourceWebhookConversion featuregate.Feature = "CustomResourceWebhookConversion"
|
||||||
|
|
||||||
|
// owner: @sttts
|
||||||
|
// alpha: v1.15
|
||||||
|
//
|
||||||
|
// CustomResourceDefaulting enables OpenAPI defaulting in CustomResources.
|
||||||
|
CustomResourceDefaulting featuregate.Feature = "CustomResourceDefaulting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -67,4 +73,5 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||||||
CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
||||||
CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta},
|
CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
CustomResourceDefaulting: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ go_test(
|
|||||||
"apply_test.go",
|
"apply_test.go",
|
||||||
"basic_test.go",
|
"basic_test.go",
|
||||||
"change_test.go",
|
"change_test.go",
|
||||||
|
"defaulting_test.go",
|
||||||
"finalization_test.go",
|
"finalization_test.go",
|
||||||
"objectmeta_test.go",
|
"objectmeta_test.go",
|
||||||
"pruning_test.go",
|
"pruning_test.go",
|
||||||
|
@ -59,14 +59,18 @@ func checks(checkers ...Checker) []Checker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookConverter(t *testing.T) {
|
func TestWebhookConverter(t *testing.T) {
|
||||||
testWebhookConverter(t, false)
|
testWebhookConverter(t, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookConverterWithPruning(t *testing.T) {
|
func TestWebhookConverterWithPruning(t *testing.T) {
|
||||||
testWebhookConverter(t, true)
|
testWebhookConverter(t, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWebhookConverter(t *testing.T, pruning bool) {
|
func TestWebhookConverterWithDefaulting(t *testing.T) {
|
||||||
|
testWebhookConverter(t, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWebhookConverter(t *testing.T, pruning, defaulting bool) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
group string
|
group string
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
@ -80,7 +84,7 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
|||||||
{
|
{
|
||||||
group: "nontrivial-converter",
|
group: "nontrivial-converter",
|
||||||
handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
|
handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
|
||||||
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning),
|
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "metadata-mutating-converter",
|
group: "metadata-mutating-converter",
|
||||||
@ -110,7 +114,12 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
|||||||
etcd3watcher.TestOnlySetFatalOnDecodeError(false)
|
etcd3watcher.TestOnlySetFatalOnDecodeError(false)
|
||||||
defer etcd3watcher.TestOnlySetFatalOnDecodeError(true)
|
defer etcd3watcher.TestOnlySetFatalOnDecodeError(true)
|
||||||
|
|
||||||
|
// enable necessary features
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
|
||||||
|
if defaulting {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceDefaulting, true)()
|
||||||
|
}
|
||||||
|
|
||||||
tearDown, config, options, err := fixtures.StartDefaultServer(t)
|
tearDown, config, options, err := fixtures.StartDefaultServer(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -132,6 +141,12 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
|||||||
crd := multiVersionFixture.DeepCopy()
|
crd := multiVersionFixture.DeepCopy()
|
||||||
crd.Spec.PreserveUnknownFields = pointer.BoolPtr(!pruning)
|
crd.Spec.PreserveUnknownFields = pointer.BoolPtr(!pruning)
|
||||||
|
|
||||||
|
if !defaulting {
|
||||||
|
for i := range crd.Spec.Versions {
|
||||||
|
delete(crd.Spec.Versions[i].Schema.OpenAPIV3Schema.Properties, "defaults")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
||||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -520,6 +535,91 @@ func validateUIDMutation(t *testing.T, ctc *conversionTestContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateDefaulting(t *testing.T, ctc *conversionTestContext) {
|
||||||
|
if _, defaulting := ctc.crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["defaults"]; !defaulting {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := ctc.namespace
|
||||||
|
storageVersion := "v1beta1"
|
||||||
|
|
||||||
|
for _, createVersion := range ctc.crd.Spec.Versions {
|
||||||
|
t.Run(fmt.Sprintf("getting objects created as %s", createVersion.Name), func(t *testing.T) {
|
||||||
|
name := "defaulting-" + createVersion.Name
|
||||||
|
client := ctc.versionedClient(ns, createVersion.Name)
|
||||||
|
|
||||||
|
fixture := newConversionMultiVersionFixture(ns, name, createVersion.Name)
|
||||||
|
if err := unstructured.SetNestedField(fixture.Object, map[string]interface{}{}, "defaults"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
created, err := client.Create(fixture, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that defaulting happens
|
||||||
|
// - in the request version when doing no-op conversion when deserializing
|
||||||
|
// - when reading back from storage in the storage version
|
||||||
|
// only the first is persisted.
|
||||||
|
defaults, found, err := unstructured.NestedMap(created.Object, "defaults")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !found {
|
||||||
|
t.Fatalf("expected .defaults to exist")
|
||||||
|
}
|
||||||
|
expectedLen := 1
|
||||||
|
if !createVersion.Storage {
|
||||||
|
expectedLen++
|
||||||
|
}
|
||||||
|
if len(defaults) != expectedLen {
|
||||||
|
t.Fatalf("after %s create expected .defaults to have %d values, but got: %v", createVersion.Name, expectedLen, defaults)
|
||||||
|
}
|
||||||
|
if _, found := defaults[createVersion.Name].(bool); !found {
|
||||||
|
t.Errorf("after %s create expected .defaults[%s] to be true, but .defaults is: %v", createVersion.Name, createVersion.Name, defaults)
|
||||||
|
}
|
||||||
|
if _, found := defaults[storageVersion].(bool); !found {
|
||||||
|
t.Errorf("after %s create expected .defaults[%s] to be true because it is the storage version, but .defaults is: %v", createVersion.Name, storageVersion, defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that only the request version default is persisted
|
||||||
|
persisted, err := ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, found, err := unstructured.NestedBool(persisted.Object, "defaults", storageVersion); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if createVersion.Name != storageVersion && found {
|
||||||
|
t.Errorf("after %s create .defaults[storage version %s] not to be persisted, but got in etcd: %v", createVersion.Name, storageVersion, defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that when reading any other version, we do not default that version, but only the (non-persisted) storage version default
|
||||||
|
for _, v := range ctc.crd.Spec.Versions {
|
||||||
|
if v.Name == createVersion.Name {
|
||||||
|
// create version is persisted anyway, nothing to verify
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ctc.versionedClient(ns, v.Name).Get(created.GetName(), metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found, err := unstructured.NestedBool(got.Object, "defaults", v.Name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if v.Name != storageVersion && found {
|
||||||
|
t.Errorf("after %s GET expected .defaults[%s] not to be true because only storage version %s is defaulted on read, but .defaults is: %v", v.Name, v.Name, storageVersion, defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found, err := unstructured.NestedBool(got.Object, "defaults", storageVersion); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !found {
|
||||||
|
t.Errorf("after non-create, non-storage %s GET expected .defaults[storage version %s] to be true, but .defaults is: %v", v.Name, storageVersion, defaults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func expectConversionFailureMessage(id, message string) func(t *testing.T, ctc *conversionTestContext) {
|
func expectConversionFailureMessage(id, message string) func(t *testing.T, ctc *conversionTestContext) {
|
||||||
return func(t *testing.T, ctc *conversionTestContext) {
|
return func(t *testing.T, ctc *conversionTestContext) {
|
||||||
ns := ctc.namespace
|
ns := ctc.namespace
|
||||||
@ -918,6 +1018,14 @@ var multiVersionFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
|||||||
"num2": {Type: "integer"},
|
"num2": {Type: "integer"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"defaults": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
"v1alpha1": {Type: "boolean"},
|
||||||
|
"v1beta1": {Type: "boolean", Default: jsonPtr(true)},
|
||||||
|
"v1beta2": {Type: "boolean"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -944,6 +1052,14 @@ var multiVersionFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
|||||||
"num2": {Type: "integer"},
|
"num2": {Type: "integer"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"defaults": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
"v1alpha1": {Type: "boolean", Default: jsonPtr(true)},
|
||||||
|
"v1beta1": {Type: "boolean"},
|
||||||
|
"v1beta2": {Type: "boolean"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -970,6 +1086,14 @@ var multiVersionFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
|||||||
"num2": {Type: "integer"},
|
"num2": {Type: "integer"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"defaults": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
"v1alpha1": {Type: "boolean"},
|
||||||
|
"v1beta1": {Type: "boolean"},
|
||||||
|
"v1beta2": {Type: "boolean", Default: jsonPtr(true)},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1089,3 +1213,12 @@ func closeOnCall(h http.Handler) (chan struct{}, http.Handler) {
|
|||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jsonPtr(x interface{}) *apiextensionsv1beta1.JSON {
|
||||||
|
bs, err := json.Marshal(x)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ret := apiextensionsv1beta1.JSON{Raw: bs}
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 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 integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
utilfeaturetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
|
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultingFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.tests.apiextensions.k8s.io"},
|
||||||
|
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "tests.apiextensions.k8s.io",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "foos",
|
||||||
|
Singular: "foo",
|
||||||
|
Kind: "Foo",
|
||||||
|
ListKind: "FooList",
|
||||||
|
},
|
||||||
|
Scope: apiextensionsv1beta1.ClusterScoped,
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
|
||||||
|
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultingFooSchema = `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
spec:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
a:
|
||||||
|
type: string
|
||||||
|
default: "A"
|
||||||
|
b:
|
||||||
|
type: string
|
||||||
|
default: "B"
|
||||||
|
status:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
a:
|
||||||
|
type: string
|
||||||
|
default: "A"
|
||||||
|
b:
|
||||||
|
type: string
|
||||||
|
default: "B"
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestCustomResourceDefaulting(t *testing.T) {
|
||||||
|
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceDefaulting, true)()
|
||||||
|
|
||||||
|
tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tearDownFn()
|
||||||
|
|
||||||
|
crd := defaultingFixture.DeepCopy()
|
||||||
|
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{}
|
||||||
|
if err := yaml.Unmarshal([]byte(defaultingFooSchema), &crd.Spec.Validation.OpenAPIV3Schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mustExist := func(obj map[string]interface{}, pths [][]string) {
|
||||||
|
for _, pth := range pths {
|
||||||
|
if _, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); !found {
|
||||||
|
t.Errorf("Expected '%s' field exist", strings.Join(pth, "."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustNotExist := func(obj map[string]interface{}, pths [][]string) {
|
||||||
|
for _, pth := range pths {
|
||||||
|
if fld, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); found {
|
||||||
|
t.Errorf("Expected '%s' field to not exist, but it does: %v", strings.Join(pth, "."), fld)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCRD := func(update func(*apiextensionsv1beta1.CustomResourceDefinition)) {
|
||||||
|
var err error
|
||||||
|
for retry := 0; retry < 10; retry++ {
|
||||||
|
var obj *apiextensionsv1beta1.CustomResourceDefinition
|
||||||
|
obj, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
update(obj)
|
||||||
|
obj, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(obj)
|
||||||
|
if err != nil && apierrors.IsConflict(err) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
crd = obj
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addDefault := func(key string, value interface{}) {
|
||||||
|
updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) {
|
||||||
|
for _, root := range []string{"spec", "status"} {
|
||||||
|
obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key] = apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
Default: jsonPtr(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
removeDefault := func(key string) {
|
||||||
|
updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) {
|
||||||
|
for _, root := range []string{"spec", "status"} {
|
||||||
|
props := obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key]
|
||||||
|
props.Default = nil
|
||||||
|
obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key] = props
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Creating CR and expecting defaulted fields in spec, but status does not exist at all")
|
||||||
|
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Plural})
|
||||||
|
foo := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(fooInstance), &foo.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(foo.Object, "a", "spec", "a")
|
||||||
|
unstructured.SetNestedField(foo.Object, "b", "status", "b")
|
||||||
|
foo, err = fooClient.Create(foo, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create CR: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("CR created: %#v", foo.UnstructuredContent())
|
||||||
|
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}})
|
||||||
|
mustNotExist(foo.Object, [][]string{{"status"}})
|
||||||
|
|
||||||
|
t.Logf("Updating status and expecting 'a' and 'b' to show up.")
|
||||||
|
unstructured.SetNestedField(foo.Object, map[string]interface{}{}, "status")
|
||||||
|
if foo, err = fooClient.UpdateStatus(foo, metav1.UpdateOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}})
|
||||||
|
|
||||||
|
t.Logf("Add 'c' default and wait until GET sees it in both status and spec")
|
||||||
|
addDefault("c", "C")
|
||||||
|
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
|
||||||
|
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_, found, _ := unstructured.NestedString(obj.Object, "spec", "c")
|
||||||
|
foo = obj
|
||||||
|
return found, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
|
||||||
|
|
||||||
|
t.Logf("Updating status, expecting 'c' to be set in spec and status")
|
||||||
|
if foo, err = fooClient.UpdateStatus(foo, metav1.UpdateOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
|
||||||
|
|
||||||
|
t.Logf("Removing 'a', 'b' and `c` properties. Expecting that 'c' goes away in spec, but not in status. 'a' and 'b' were peristed.")
|
||||||
|
removeDefault("a")
|
||||||
|
removeDefault("b")
|
||||||
|
removeDefault("c")
|
||||||
|
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
|
||||||
|
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_, found, _ := unstructured.NestedString(obj.Object, "spec", "c")
|
||||||
|
foo = obj
|
||||||
|
return !found, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
|
||||||
|
mustNotExist(foo.Object, [][]string{{"spec", "c"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonPtr(x interface{}) *apiextensionsv1beta1.JSON {
|
||||||
|
bs, err := json.Marshal(x)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ret := apiextensionsv1beta1.JSON{Raw: bs}
|
||||||
|
return &ret
|
||||||
|
}
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1073,6 +1073,7 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation
|
|||||||
k8s.io/apiextensions-apiserver/pkg/apiserver
|
k8s.io/apiextensions-apiserver/pkg/apiserver
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
|
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/schema
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema
|
||||||
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/validation
|
k8s.io/apiextensions-apiserver/pkg/apiserver/validation
|
||||||
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
||||||
|
Loading…
Reference in New Issue
Block a user