diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 189dce84756..67c37dd0f9f 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -16340,7 +16340,7 @@ "type": "array" }, "strategy": { - "description": "`strategy` specifies the conversion strategy. Allowed values are: - `None`: The converter only change the apiVersion and would not touch any other field in the CR. - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information is needed for this option.", + "description": "`strategy` specifies the conversion strategy. Allowed values are: - `None`: The converter only change the apiVersion and would not touch any other field in the CR. - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information\n is needed for this option. This requires spec.preserveUnknownFields to be false.", "type": "string" }, "webhookClientConfig": { diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 0ed154065cd..d340986f216 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -545,7 +545,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS // unintentionally on either side: apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: featuregate.Beta}, apiextensionsfeatures.CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta}, - apiextensionsfeatures.CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha}, + apiextensionsfeatures.CustomResourceWebhookConversion: {Default: true, PreRelease: featuregate.Beta}, apiextensionsfeatures.CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta}, apiextensionsfeatures.CustomResourceDefaulting: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go index 8daa1f960ff..9c2798f0307 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go @@ -85,7 +85,8 @@ type CustomResourceDefinitionSpec struct { type CustomResourceConversion struct { // `strategy` specifies the conversion strategy. Allowed values are: // - `None`: The converter only change the apiVersion and would not touch any other field in the CR. - // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information is needed for this option. + // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information + // is needed for this option. This requires spec.preserveUnknownFields to be false. Strategy ConversionStrategyType // `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto index 9308988f70e..47c06a296ad 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto @@ -107,7 +107,8 @@ message CustomResourceColumnDefinition { message CustomResourceConversion { // `strategy` specifies the conversion strategy. Allowed values are: // - `None`: The converter only change the apiVersion and would not touch any other field in the CR. - // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information is needed for this option. + // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information + // is needed for this option. This requires spec.preserveUnknownFields to be false. optional string strategy = 1; // `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`. This field is diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go index 63e25245d82..6d931ca95c1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go @@ -90,7 +90,8 @@ type CustomResourceDefinitionSpec struct { type CustomResourceConversion struct { // `strategy` specifies the conversion strategy. Allowed values are: // - `None`: The converter only change the apiVersion and would not touch any other field in the CR. - // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information is needed for this option. + // - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information + // is needed for this option. This requires spec.preserveUnknownFields to be false. Strategy ConversionStrategyType `json:"strategy" protobuf:"bytes,1,name=strategy"` // `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`. This field is diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index be134821a4c..2a0f663bdf7 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -121,10 +121,10 @@ func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResour // ValidateCustomResourceDefinitionSpec statically validates func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList { allowDefaults := utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting) - return validateCustomResourceDefinitionSpec(spec, true, allowDefaults, fldPath) + return validateCustomResourceDefinitionSpec(spec, true, allowDefaults, false, fldPath) } -func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion, allowDefaults bool, fldPath *field.Path) field.ErrorList { +func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion, allowDefaults, allowConversionWithPreserveUnknownFields bool, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(spec.Group) == 0 { @@ -237,11 +237,18 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi } } + if !allowConversionWithPreserveUnknownFields && conversionAndPreserveUnknownFields(spec) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("conversion").Child("strategy"), spec.Conversion.Strategy, "must be None if spec.preserveUnknownFields is true")) + } allErrs = append(allErrs, validateCustomResourceConversion(spec.Conversion, requireRecognizedVersion, fldPath.Child("conversion"))...) return allErrs } +func conversionAndPreserveUnknownFields(spec *apiextensions.CustomResourceDefinitionSpec) bool { + return (spec.Conversion != nil && spec.Conversion.Strategy != apiextensions.NoneConverter) && (spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields) +} + func validateEnumStrings(fldPath *field.Path, value string, accepted []string, required bool) field.ErrorList { if value == "" { if required { @@ -359,7 +366,7 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus // 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) + allErrs := validateCustomResourceDefinitionSpec(spec, requireRecognizedVersion, allowDefaults, conversionAndPreserveUnknownFields(oldSpec), fldPath) if established { // these effect the storage and cannot be changed therefore diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index 475f6110950..1d174e34ead 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -116,7 +116,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -161,7 +166,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -206,7 +216,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -247,7 +262,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { URL: strPtr(""), }, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -370,7 +390,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -412,7 +437,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, ConversionReviewVersions: []string{"0v"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -455,7 +485,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version", "v1beta1"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -495,7 +530,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, ConversionReviewVersions: []string{"v1beta1", "v1beta1"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -533,7 +573,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { Conversion: &apiextensions.CustomResourceConversion{ Strategy: apiextensions.ConversionStrategyType("Webhook"), }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -571,7 +616,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) { Conversion: &apiextensions.CustomResourceConversion{ Strategy: apiextensions.ConversionStrategyType("non_existing_conversion"), }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -581,6 +631,129 @@ func TestValidateCustomResourceDefinition(t *testing.T) { unsupported("spec", "conversion", "strategy"), }, }, + { + name: "none conversion without preserveUnknownFields=false", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version1", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Conversion: &apiextensions.CustomResourceConversion{ + Strategy: apiextensions.ConversionStrategyType("None"), + }, + PreserveUnknownFields: pointer.BoolPtr(true), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version1"}, + }, + }, + errors: []validationMatch{}, + }, + { + name: "webhook conversion without preserveUnknownFields=false", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version1", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Conversion: &apiextensions.CustomResourceConversion{ + Strategy: apiextensions.ConversionStrategyType("Webhook"), + WebhookClientConfig: &apiextensions.WebhookClientConfig{ + URL: strPtr("https://example.com/webhook"), + }, + ConversionReviewVersions: []string{"v1beta1"}, + }, + PreserveUnknownFields: pointer.BoolPtr(true), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version1"}, + }, + }, + errors: []validationMatch{ + invalid("spec", "conversion", "strategy"), + }, + }, + { + name: "webhook conversion with preserveUnknownFields=false", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version1", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Conversion: &apiextensions.CustomResourceConversion{ + Strategy: apiextensions.ConversionStrategyType("Webhook"), + WebhookClientConfig: &apiextensions.WebhookClientConfig{ + URL: strPtr("https://example.com/webhook"), + }, + ConversionReviewVersions: []string{"v1beta1"}, + }, + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version1"}, + }, + }, + errors: []validationMatch{}, + }, { name: "no_storage_version", resource: &apiextensions.CustomResourceDefinition{ @@ -1642,7 +1815,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1678,7 +1856,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version_0, invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1718,7 +1901,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1754,7 +1942,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { }, ConversionReviewVersions: []string{"v1beta1", "invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1796,7 +1989,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { }, ConversionReviewVersions: []string{"invalid-version"}, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1831,7 +2029,12 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { URL: strPtr("https://example.com/webhook"), }, }, - PreserveUnknownFields: pointer.BoolPtr(true), + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + }, + }, + PreserveUnknownFields: pointer.BoolPtr(false), }, Status: apiextensions.CustomResourceDefinitionStatus{ StoredVersions: []string{"version"}, @@ -1841,6 +2044,82 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { invalid("spec", "conversion", "conversionReviewVersions"), }, }, + { + name: "webhookconfig: should accept preserveUnknownFields=true if set before", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Conversion: &apiextensions.CustomResourceConversion{ + Strategy: apiextensions.ConversionStrategyType("Webhook"), + WebhookClientConfig: &apiextensions.WebhookClientConfig{ + URL: strPtr("https://example.com/webhook"), + }, + ConversionReviewVersions: []string{"v1beta1"}, + }, + PreserveUnknownFields: pointer.BoolPtr(true), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, + }, + old: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Conversion: &apiextensions.CustomResourceConversion{ + Strategy: apiextensions.ConversionStrategyType("Webhook"), + WebhookClientConfig: &apiextensions.WebhookClientConfig{ + URL: strPtr("https://example.com/webhook"), + }, + ConversionReviewVersions: []string{"v1beta1"}, + }, + PreserveUnknownFields: pointer.BoolPtr(true), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, + }, + errors: []validationMatch{}, + }, { name: "unchanged", old: &apiextensions.CustomResourceDefinition{ diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go index e5ed44f3cbf..e72a3c18e07 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/features/kube_features.go @@ -71,7 +71,7 @@ func init() { var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ CustomResourceValidation: {Default: true, PreRelease: featuregate.Beta}, CustomResourceSubresources: {Default: true, PreRelease: featuregate.Beta}, - CustomResourceWebhookConversion: {Default: false, PreRelease: featuregate.Alpha}, + CustomResourceWebhookConversion: {Default: true, PreRelease: featuregate.Beta}, CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.Beta}, CustomResourceDefaulting: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go index 2444702733d..9d42e7172d5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go @@ -157,7 +157,6 @@ func TestDropDisableFieldsCustomResourceDefinition(t *testing.T) { t.Run(fmt.Sprintf("subresources feature enabled=%v, old CRD %v, new CRD %v", validationEnabled, oldCRDInfo.name, newCRDInfo.name), func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceSubresources, validationEnabled)() - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() var oldCRDSpec *apiextensions.CustomResourceDefinitionSpec if oldCRD != nil { oldCRDSpec = &oldCRD.Spec diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/conversion/conversion_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/conversion/conversion_test.go index be28d93609b..520757a155e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/conversion/conversion_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/conversion/conversion_test.go @@ -28,10 +28,6 @@ import ( "github.com/google/go-cmp/cmp" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - "k8s.io/apiextensions-apiserver/pkg/cmd/server/options" - serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -48,6 +44,10 @@ import ( "k8s.io/utils/pointer" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/pkg/cmd/server/options" + serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/storage" ) @@ -59,18 +59,14 @@ func checks(checkers ...Checker) []Checker { } func TestWebhookConverter(t *testing.T) { - testWebhookConverter(t, false, false) -} - -func TestWebhookConverterWithPruning(t *testing.T) { - testWebhookConverter(t, true, false) + testWebhookConverter(t, false) } func TestWebhookConverterWithDefaulting(t *testing.T) { - testWebhookConverter(t, true, true) + testWebhookConverter(t, true) } -func testWebhookConverter(t *testing.T, pruning, defaulting bool) { +func testWebhookConverter(t *testing.T, defaulting bool) { tests := []struct { group string handler http.Handler @@ -115,7 +111,6 @@ func testWebhookConverter(t *testing.T, pruning, defaulting bool) { 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)() } @@ -139,7 +134,6 @@ func testWebhookConverter(t *testing.T, pruning, defaulting bool) { defer tearDown() crd := multiVersionFixture.DeepCopy() - crd.Spec.PreserveUnknownFields = pointer.BoolPtr(!pruning) if !defaulting { for i := range crd.Spec.Versions { @@ -994,7 +988,8 @@ var multiVersionFixture = &apiextensionsv1beta1.CustomResourceDefinition{ ListKind: "MultiVersionList", Categories: []string{"all"}, }, - Scope: apiextensionsv1beta1.NamespaceScoped, + Scope: apiextensionsv1beta1.NamespaceScoped, + PreserveUnknownFields: pointer.BoolPtr(false), Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ { // storage version, same schema as v1alpha1 diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go index 6f3cf72b49b..bafcaa28404 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go @@ -25,18 +25,16 @@ import ( "testing" autoscaling "k8s.io/api/autoscaling/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" - "k8s.io/apiextensions-apiserver/test/integration/fixtures" 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/types" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/dynamic" - featuregatetesting "k8s.io/component-base/featuregate/testing" + + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) var labelSelectorPath = ".status.labelSelector" @@ -148,7 +146,6 @@ func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.U } func TestStatusSubresource(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -261,7 +258,6 @@ func TestStatusSubresource(t *testing.T) { } func TestScaleSubresource(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() groupResource := schema.GroupResource{ Group: "mygroup.example.com", Resource: "noxus", @@ -407,7 +403,6 @@ func TestScaleSubresource(t *testing.T) { } func TestValidationSchemaWithStatus(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -455,7 +450,6 @@ func TestValidationSchemaWithStatus(t *testing.T) { } func TestValidateOnlyStatus(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -562,7 +556,6 @@ func TestValidateOnlyStatus(t *testing.T) { } func TestSubresourcesDiscovery(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -654,7 +647,6 @@ func TestSubresourcesDiscovery(t *testing.T) { } func TestGeneration(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -728,7 +720,6 @@ func TestGeneration(t *testing.T) { } func TestSubresourcePatch(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() groupResource := schema.GroupResource{ Group: "mygroup.example.com", Resource: "noxus", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go index c02922cb6f8..316fee107e2 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go @@ -21,7 +21,6 @@ import ( "testing" "time" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" @@ -29,13 +28,11 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" - featuregatetesting "k8s.io/component-base/featuregate/testing" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -104,7 +101,6 @@ func newTableInstance(name string) *unstructured.Unstructured { } func TestTableGet(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -262,7 +258,6 @@ func TestTableGet(t *testing.T) { // TestColumnsPatch tests the case that a CRD was created with no top-level or // per-version columns. One should be able to PATCH the CRD setting per-version columns. func TestColumnsPatch(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -307,7 +302,6 @@ func TestColumnsPatch(t *testing.T) { // One should be able to PATCH the CRD cleaning the top-level columns and setting per-version // columns. func TestPatchCleanTopLevelColumns(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go index 54a30fc50f6..f4f290e8a51 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -22,17 +22,14 @@ import ( "testing" "time" - clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" 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/util/wait" "k8s.io/apimachinery/pkg/util/yaml" - utilfeature "k8s.io/apiserver/pkg/util/feature" - featuregatetesting "k8s.io/component-base/featuregate/testing" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" + clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -224,7 +221,6 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure } func TestCustomResourceValidation(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -256,7 +252,6 @@ func TestCustomResourceValidation(t *testing.T) { } func TestCustomResourceUpdateValidation(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -310,7 +305,6 @@ func TestCustomResourceUpdateValidation(t *testing.T) { } func TestCustomResourceValidationErrors(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -411,7 +405,6 @@ func TestCustomResourceValidationErrors(t *testing.T) { } func TestCRValidationOnCRDUpdate(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -483,7 +476,6 @@ func TestCRValidationOnCRDUpdate(t *testing.T) { } func TestForbiddenFieldsInSchema(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -542,8 +534,6 @@ func TestForbiddenFieldsInSchema(t *testing.T) { } func TestNonStructuralSchemaConditionUpdate(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() - tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -684,8 +674,6 @@ spec: } func TestNonStructuralSchemaCondition(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() - tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go index acf7fd48bfb..861e300a995 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go @@ -27,13 +27,10 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/dynamic" - featuregatetesting "k8s.io/component-base/featuregate/testing" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -357,7 +354,6 @@ values: } func TestYAMLSubresource(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) diff --git a/test/e2e/apimachinery/crd_conversion_webhook.go b/test/e2e/apimachinery/crd_conversion_webhook.go index 07f2d3dbc35..3a378ba7103 100644 --- a/test/e2e/apimachinery/crd_conversion_webhook.go +++ b/test/e2e/apimachinery/crd_conversion_webhook.go @@ -19,11 +19,12 @@ package apimachinery import ( "time" + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + apps "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apiextensions-apiserver/test/integration" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -38,8 +39,8 @@ import ( imageutils "k8s.io/kubernetes/test/utils/image" "k8s.io/utils/pointer" - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/test/integration" // ensure libs have a chance to initialize _ "github.com/stretchr/testify/assert" @@ -60,11 +61,28 @@ var apiVersions = []v1beta1.CustomResourceDefinitionVersion{ Name: "v1", Served: true, Storage: true, + Schema: &v1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ + Type: "object", + Properties: map[string]v1beta1.JSONSchemaProps{ + "hostPort": {Type: "string"}, + }, + }, + }, }, { Name: "v2", Served: true, Storage: false, + Schema: &v1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ + Type: "object", + Properties: map[string]v1beta1.JSONSchemaProps{ + "host": {Type: "string"}, + "port": {Type: "string"}, + }, + }, + }, }, } @@ -73,15 +91,32 @@ var alternativeAPIVersions = []v1beta1.CustomResourceDefinitionVersion{ Name: "v1", Served: true, Storage: false, + Schema: &v1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ + Type: "object", + Properties: map[string]v1beta1.JSONSchemaProps{ + "hostPort": {Type: "string"}, + }, + }, + }, }, { Name: "v2", Served: true, Storage: true, + Schema: &v1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ + Type: "object", + Properties: map[string]v1beta1.JSONSchemaProps{ + "host": {Type: "string"}, + "port": {Type: "string"}, + }, + }, + }, }, } -var _ = SIGDescribe("CustomResourceConversionWebhook [Feature:CustomResourceWebhookConversion]", func() { +var _ = SIGDescribe("CustomResourceConversionWebhook", func() { var context *certContext f := framework.NewDefaultFramework("crd-webhook") @@ -121,6 +156,7 @@ var _ = SIGDescribe("CustomResourceConversionWebhook [Feature:CustomResourceWebh }, }, } + crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false) }) if err != nil { return @@ -144,6 +180,7 @@ var _ = SIGDescribe("CustomResourceConversionWebhook [Feature:CustomResourceWebh }, }, } + crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false) }) if err != nil { return