mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +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"
|
||||
},
|
||||
"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": {
|
||||
"additionalProperties": {
|
||||
|
@ -533,6 +533,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
apiextensionsfeatures.CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
||||
apiextensionsfeatures.CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
||||
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
|
||||
// ...
|
||||
|
@ -394,6 +394,9 @@ message JSONSchemaProps {
|
||||
|
||||
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 double maximum = 9;
|
||||
|
@ -18,13 +18,16 @@ package v1beta1
|
||||
|
||||
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
|
||||
type JSONSchemaProps struct {
|
||||
ID string `json:"id,omitempty" protobuf:"bytes,1,opt,name=id"`
|
||||
Schema JSONSchemaURL `json:"$schema,omitempty" protobuf:"bytes,2,opt,name=schema"`
|
||||
Ref *string `json:"$ref,omitempty" protobuf:"bytes,3,opt,name=ref"`
|
||||
Description string `json:"description,omitempty" protobuf:"bytes,4,opt,name=description"`
|
||||
Type string `json:"type,omitempty" protobuf:"bytes,5,opt,name=type"`
|
||||
Format string `json:"format,omitempty" protobuf:"bytes,6,opt,name=format"`
|
||||
Title string `json:"title,omitempty" protobuf:"bytes,7,opt,name=title"`
|
||||
ID string `json:"id,omitempty" protobuf:"bytes,1,opt,name=id"`
|
||||
Schema JSONSchemaURL `json:"$schema,omitempty" protobuf:"bytes,2,opt,name=schema"`
|
||||
Ref *string `json:"$ref,omitempty" protobuf:"bytes,3,opt,name=ref"`
|
||||
Description string `json:"description,omitempty" protobuf:"bytes,4,opt,name=description"`
|
||||
Type string `json:"type,omitempty" protobuf:"bytes,5,opt,name=type"`
|
||||
Format string `json:"format,omitempty" protobuf:"bytes,6,opt,name=format"`
|
||||
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"`
|
||||
Maximum *float64 `json:"maximum,omitempty" protobuf:"bytes,9,opt,name=maximum"`
|
||||
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/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/pruning: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/apimachinery/pkg/api/equality: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/validation: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/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"],
|
||||
deps = [
|
||||
"//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/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/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",
|
||||
],
|
||||
)
|
||||
|
@ -21,9 +21,12 @@ import (
|
||||
"reflect"
|
||||
"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"
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"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/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"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
)
|
||||
@ -103,9 +108,9 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus
|
||||
}
|
||||
|
||||
// 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 = 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"))...)
|
||||
for i := range version.AdditionalPrinterColumns {
|
||||
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
|
||||
@ -115,10 +120,11 @@ func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResour
|
||||
|
||||
// ValidateCustomResourceDefinitionSpec statically validates
|
||||
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{}
|
||||
|
||||
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
|
||||
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, ",")))
|
||||
}
|
||||
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
|
||||
@ -216,7 +228,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
||||
}
|
||||
|
||||
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"))...)
|
||||
|
||||
for i := range spec.AdditionalPrinterColumns {
|
||||
@ -343,7 +355,11 @@ func validateCustomResourceConversion(conversion *apiextensions.CustomResourceCo
|
||||
// ValidateCustomResourceDefinitionSpecUpdate statically validates
|
||||
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
|
||||
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 {
|
||||
// these effect the storage and cannot be changed therefore
|
||||
@ -546,7 +562,7 @@ type specStandardValidator interface {
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
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`)))
|
||||
}
|
||||
|
||||
openAPIV3Schema := &specStandardValidatorV3{}
|
||||
openAPIV3Schema := &specStandardValidatorV3{
|
||||
allowDefaults: allowDefaults,
|
||||
}
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
|
||||
|
||||
if mustBeStructural {
|
||||
@ -706,7 +724,9 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
return allErrs
|
||||
}
|
||||
|
||||
type specStandardValidatorV3 struct{}
|
||||
type specStandardValidatorV3 struct {
|
||||
allowDefaults bool
|
||||
}
|
||||
|
||||
// validate validates against OpenAPI Schema v3.
|
||||
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 {
|
||||
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 != "" {
|
||||
@ -830,3 +867,86 @@ func allowedAtRootSchema(field string) bool {
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -64,9 +76,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
resource *apiextensions.CustomResourceDefinition
|
||||
errors []validationMatch
|
||||
name string
|
||||
resource *apiextensions.CustomResourceDefinition
|
||||
errors []validationMatch
|
||||
enabledFeatures []featuregate.Feature
|
||||
}{
|
||||
{
|
||||
name: "webhookconfig: invalid port 0",
|
||||
@ -1239,10 +1252,325 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
||||
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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, gate := range tc.enabledFeatures {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)()
|
||||
}
|
||||
// duplicate defaulting behaviour
|
||||
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"}
|
||||
@ -1276,10 +1604,11 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
||||
|
||||
func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
old *apiextensions.CustomResourceDefinition
|
||||
resource *apiextensions.CustomResourceDefinition
|
||||
errors []validationMatch
|
||||
name string
|
||||
old *apiextensions.CustomResourceDefinition
|
||||
resource *apiextensions.CustomResourceDefinition
|
||||
errors []validationMatch
|
||||
enabledFeatures []featuregate.Feature
|
||||
}{
|
||||
{
|
||||
name: "webhookconfig: should pass on invalid ConversionReviewVersion with old invalid versions",
|
||||
@ -2180,32 +2509,206 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old)
|
||||
seenErrs := make([]bool, len(errs))
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, gate := range tc.enabledFeatures {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)()
|
||||
}
|
||||
|
||||
for _, expectedError := range tc.errors {
|
||||
found := false
|
||||
for i, err := range errs {
|
||||
if expectedError.matches(err) && !seenErrs[i] {
|
||||
found = true
|
||||
seenErrs[i] = true
|
||||
break
|
||||
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old)
|
||||
seenErrs := make([]bool, len(errs))
|
||||
|
||||
for _, expectedError := range tc.errors {
|
||||
found := false
|
||||
for i, err := range errs {
|
||||
if expectedError.matches(err) && !seenErrs[i] {
|
||||
found = true
|
||||
seenErrs[i] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs)
|
||||
for i, seen := range seenErrs {
|
||||
if !seen {
|
||||
t.Errorf("unexpected error: %v", errs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, seen := range seenErrs {
|
||||
if !seen {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, errs[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2356,7 +2859,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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 {
|
||||
t.Errorf("Expected no error, but got: %v", got)
|
||||
} 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 validValidationSchema = &apiextensions.JSONSchemaProps{
|
||||
@ -2442,3 +2979,8 @@ func float64Ptr(f float64) *float64 {
|
||||
func int64Ptr(f int64) *int64 {
|
||||
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/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/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/validation: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/apiserver/conversion"
|
||||
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"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
@ -645,7 +646,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
||||
|
||||
Creater: creator,
|
||||
Convertor: safeConverter,
|
||||
Defaulter: unstructuredDefaulter{parameterScheme},
|
||||
Defaulter: unstructuredDefaulter{parameterScheme, structuralSchemas, kind.GroupKind()},
|
||||
Typer: typer,
|
||||
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 {
|
||||
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 {
|
||||
@ -807,14 +812,20 @@ func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object,
|
||||
}
|
||||
|
||||
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) {
|
||||
// Delegate for things other than Unstructured.
|
||||
if _, ok := in.(runtime.Unstructured); !ok {
|
||||
// Delegate for things other than Unstructured, and other GKs
|
||||
u, ok := in.(runtime.Unstructured)
|
||||
if !ok || u.GetObjectKind().GroupVersionKind().GroupKind() != d.structuralSchemaGK {
|
||||
d.delegate.Default(in)
|
||||
return
|
||||
}
|
||||
|
||||
structuraldefaulting.Default(u.UnstructuredContent(), d.structuralSchemas[u.GetObjectKind().GroupVersionKind().Version])
|
||||
}
|
||||
|
||||
type CRDRESTOptionsGetter struct {
|
||||
@ -888,7 +899,11 @@ func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupReso
|
||||
c,
|
||||
&unstructuredCreator{},
|
||||
crdserverscheme.NewUnstructuredObjectTyper(),
|
||||
&unstructuredDefaulter{delegate: Scheme},
|
||||
&unstructuredDefaulter{
|
||||
delegate: Scheme,
|
||||
structuralSchemaGK: t.structuralSchemaGK,
|
||||
structuralSchemas: t.structuralSchemas,
|
||||
},
|
||||
t.encoderVersion,
|
||||
t.decoderVersion,
|
||||
"crdRESTOptions",
|
||||
|
@ -34,6 +34,7 @@ filegroup(
|
||||
name = "all-srcs",
|
||||
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",
|
||||
],
|
||||
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.
|
||||
func Prune(obj map[string]interface{}, s *structuralschema.Structural) {
|
||||
func Prune(obj interface{}, s *structuralschema.Structural) {
|
||||
prune(obj, s)
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,12 @@ const (
|
||||
//
|
||||
// CustomResourceWebhookConversion defines the webhook conversion for Custom Resources.
|
||||
CustomResourceWebhookConversion featuregate.Feature = "CustomResourceWebhookConversion"
|
||||
|
||||
// owner: @sttts
|
||||
// alpha: v1.15
|
||||
//
|
||||
// CustomResourceDefaulting enables OpenAPI defaulting in CustomResources.
|
||||
CustomResourceDefaulting featuregate.Feature = "CustomResourceDefaulting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -67,4 +73,5 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta},
|
||||
CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha},
|
||||
CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta},
|
||||
CustomResourceDefaulting: {Default: false, PreRelease: featuregate.Alpha},
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ go_test(
|
||||
"apply_test.go",
|
||||
"basic_test.go",
|
||||
"change_test.go",
|
||||
"defaulting_test.go",
|
||||
"finalization_test.go",
|
||||
"objectmeta_test.go",
|
||||
"pruning_test.go",
|
||||
|
@ -59,14 +59,18 @@ func checks(checkers ...Checker) []Checker {
|
||||
}
|
||||
|
||||
func TestWebhookConverter(t *testing.T) {
|
||||
testWebhookConverter(t, false)
|
||||
testWebhookConverter(t, false, false)
|
||||
}
|
||||
|
||||
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 {
|
||||
group string
|
||||
handler http.Handler
|
||||
@ -80,7 +84,7 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
||||
{
|
||||
group: "nontrivial-converter",
|
||||
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",
|
||||
@ -110,7 +114,12 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
||||
etcd3watcher.TestOnlySetFatalOnDecodeError(false)
|
||||
defer etcd3watcher.TestOnlySetFatalOnDecodeError(true)
|
||||
|
||||
// enable necessary features
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -132,6 +141,12 @@ func testWebhookConverter(t *testing.T, pruning bool) {
|
||||
crd := multiVersionFixture.DeepCopy()
|
||||
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)
|
||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
||||
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) {
|
||||
return func(t *testing.T, ctc *conversionTestContext) {
|
||||
ns := ctc.namespace
|
||||
@ -918,6 +1018,14 @@ var multiVersionFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
"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)
|
||||
})
|
||||
}
|
||||
|
||||
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/conversion
|
||||
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/validation
|
||||
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
||||
|
Loading…
Reference in New Issue
Block a user