diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go index 615e8b2eb70..125c4bfe952 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go @@ -46,6 +46,10 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 { obj.Names.ListKind = obj.Names.Kind + "List" } + if len(obj.Versions) == 0 && len(obj.Version) == 0 { + // internal object must have a version to roundtrip all fields + obj.Version = "v1" + } if len(obj.Versions) == 0 && len(obj.Version) != 0 { obj.Versions = []apiextensions.CustomResourceDefinitionVersion{ { @@ -73,6 +77,23 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { if obj.PreserveUnknownFields == nil { obj.PreserveUnknownFields = pointer.BoolPtr(true) } + + // Move per-version schema, subresources, additionalPrinterColumns to the top-level. + // This is required by validation in v1beta1, and by round-tripping in v1. + if len(obj.Versions) == 1 { + if obj.Versions[0].Schema != nil { + obj.Validation = obj.Versions[0].Schema + obj.Versions[0].Schema = nil + } + if obj.Versions[0].AdditionalPrinterColumns != nil { + obj.AdditionalPrinterColumns = obj.Versions[0].AdditionalPrinterColumns + obj.Versions[0].AdditionalPrinterColumns = nil + } + if obj.Versions[0].Subresources != nil { + obj.Subresources = obj.Versions[0].Subresources + obj.Versions[0].Subresources = nil + } + } }, func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) { c.FuzzNoCustom(obj) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion.go index 4173241bd5d..ed755e03e23 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion.go @@ -17,11 +17,11 @@ limitations under the License. package v1 import ( + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/json" - - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" ) func addConversionFuncs(scheme *runtime.Scheme) error { @@ -71,3 +71,104 @@ func Convert_v1_JSON_To_apiextensions_JSON(in *JSON, out *apiextensions.JSON, s } return nil } + +func Convert_apiextensions_CustomResourceDefinitionSpec_To_v1_CustomResourceDefinitionSpec(in *apiextensions.CustomResourceDefinitionSpec, out *CustomResourceDefinitionSpec, s conversion.Scope) error { + if err := autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1_CustomResourceDefinitionSpec(in, out, s); err != nil { + return err + } + + if len(out.Versions) == 0 && len(in.Version) > 0 { + // no versions were specified, and a version name was specified + out.Versions = []CustomResourceDefinitionVersion{{Name: in.Version, Served: true, Storage: true}} + } + + // If spec.{subresources,validation,additionalPrinterColumns} exists, move to versions + if in.Subresources != nil { + subresources := &CustomResourceSubresources{} + if err := Convert_apiextensions_CustomResourceSubresources_To_v1_CustomResourceSubresources(in.Subresources, subresources, s); err != nil { + return err + } + for i := range out.Versions { + out.Versions[i].Subresources = subresources + } + } + if in.Validation != nil { + schema := &CustomResourceValidation{} + if err := Convert_apiextensions_CustomResourceValidation_To_v1_CustomResourceValidation(in.Validation, schema, s); err != nil { + return err + } + for i := range out.Versions { + out.Versions[i].Schema = schema + } + } + if in.AdditionalPrinterColumns != nil { + additionalPrinterColumns := make([]CustomResourceColumnDefinition, len(in.AdditionalPrinterColumns)) + for i := range in.AdditionalPrinterColumns { + if err := Convert_apiextensions_CustomResourceColumnDefinition_To_v1_CustomResourceColumnDefinition(&in.AdditionalPrinterColumns[i], &additionalPrinterColumns[i], s); err != nil { + return err + } + } + for i := range out.Versions { + out.Versions[i].AdditionalPrinterColumns = additionalPrinterColumns + } + } + return nil +} + +func Convert_v1_CustomResourceDefinitionSpec_To_apiextensions_CustomResourceDefinitionSpec(in *CustomResourceDefinitionSpec, out *apiextensions.CustomResourceDefinitionSpec, s conversion.Scope) error { + if err := autoConvert_v1_CustomResourceDefinitionSpec_To_apiextensions_CustomResourceDefinitionSpec(in, out, s); err != nil { + return nil + } + + if len(out.Versions) == 0 { + return nil + } + + // Copy versions[0] to version + out.Version = out.Versions[0].Name + + // If versions[*].{subresources,schema,additionalPrinterColumns} are identical, move to spec + subresources := out.Versions[0].Subresources + subresourcesIdentical := true + validation := out.Versions[0].Schema + validationIdentical := true + additionalPrinterColumns := out.Versions[0].AdditionalPrinterColumns + additionalPrinterColumnsIdentical := true + + // Detect if per-version fields are identical + for _, v := range out.Versions { + if subresourcesIdentical && !apiequality.Semantic.DeepEqual(v.Subresources, subresources) { + subresourcesIdentical = false + } + if validationIdentical && !apiequality.Semantic.DeepEqual(v.Schema, validation) { + validationIdentical = false + } + if additionalPrinterColumnsIdentical && !apiequality.Semantic.DeepEqual(v.AdditionalPrinterColumns, additionalPrinterColumns) { + additionalPrinterColumnsIdentical = false + } + } + + // If they are, set the top-level fields and clear the per-version fields + if subresourcesIdentical { + out.Subresources = subresources + } + if validationIdentical { + out.Validation = validation + } + if additionalPrinterColumnsIdentical { + out.AdditionalPrinterColumns = additionalPrinterColumns + } + for i := range out.Versions { + if subresourcesIdentical { + out.Versions[i].Subresources = nil + } + if validationIdentical { + out.Versions[i].Schema = nil + } + if additionalPrinterColumnsIdentical { + out.Versions[i].AdditionalPrinterColumns = nil + } + } + + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion_test.go index e2e2e69374c..bcfedbc117c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/conversion_test.go @@ -18,13 +18,395 @@ package v1 import ( "reflect" + "strings" "testing" - "k8s.io/apimachinery/pkg/runtime" + "github.com/google/go-cmp/cmp" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" ) +func TestConversion(t *testing.T) { + testcases := []struct { + Name string + In runtime.Object + Out runtime.Object + ExpectOut runtime.Object + ExpectErr string + }{ + // Versions + { + Name: "internal to v1, no versions", + In: &apiextensions.CustomResourceDefinition{}, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{}, + }, + { + Name: "internal to v1, top-level version", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{{Name: "v1", Served: true, Storage: true}}, + }, + }, + }, + { + Name: "internal to v1, multiple versions", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: false, Storage: false}, + }, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: false, Storage: false}, + }, + }, + }, + }, + { + Name: "v1 to internal, no versions", + In: &CustomResourceDefinition{}, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + { + Name: "v1 to internal, single version", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{{Name: "v1", Served: true, Storage: true}}, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{{Name: "v1", Served: true, Storage: true}}, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + { + Name: "v1 to internal, multiple versions", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: false, Storage: false}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: false, Storage: false}, + }, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + // Validation + { + Name: "internal to v1, top-level validation moves to per-version", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Validation: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Type: "object"}}, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Type: "object"}}}, + }, + }, + }, + }, + { + Name: "internal to v1, per-version validation is preserved", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Description: "v1", Type: "object"}}}, + {Name: "v2", Served: false, Storage: false, Schema: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Description: "v2", Type: "object"}}}, + }, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Description: "v1", Type: "object"}}}, + {Name: "v2", Served: false, Storage: false, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Description: "v2", Type: "object"}}}, + }, + }, + }, + }, + { + Name: "v1 to internal, identical validation moves to top-level", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Type: "object"}}}, + {Name: "v2", Served: true, Storage: false, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Type: "object"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: true, Storage: false}, + }, + Validation: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Type: "object"}}, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + { + Name: "v1 to internal, distinct validation remains per-version", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Description: "v1", Type: "object"}}}, + {Name: "v2", Served: true, Storage: false, Schema: &CustomResourceValidation{OpenAPIV3Schema: &JSONSchemaProps{Description: "v2", Type: "object"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Schema: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Description: "v1", Type: "object"}}}, + {Name: "v2", Served: true, Storage: false, Schema: &apiextensions.CustomResourceValidation{OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Description: "v2", Type: "object"}}}, + }, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + // Subresources + { + Name: "internal to v1, top-level subresources moves to per-version", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas"}}, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas"}}}, + }, + }, + }, + }, + { + Name: "internal to v1, per-version subresources is preserved", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas1"}}}, + {Name: "v2", Served: false, Storage: false, Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas2"}}}, + }, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas1"}}}, + {Name: "v2", Served: false, Storage: false, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas2"}}}, + }, + }, + }, + }, + { + Name: "v1 to internal, identical subresources moves to top-level", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas"}}}, + {Name: "v2", Served: true, Storage: false, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: true, Storage: false}, + }, + Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas"}}, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + { + Name: "v1 to internal, distinct subresources remains per-version", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas1"}}}, + {Name: "v2", Served: true, Storage: false, Subresources: &CustomResourceSubresources{Scale: &CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas2"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas1"}}}, + {Name: "v2", Served: true, Storage: false, Subresources: &apiextensions.CustomResourceSubresources{Scale: &apiextensions.CustomResourceSubresourceScale{SpecReplicasPath: "spec.replicas2"}}}, + }, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + // Additional Printer Columns + { + Name: "internal to v1, top-level printer columns moves to per-version", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column1"}}, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column1"}}}, + }, + }, + }, + }, + { + Name: "internal to v1, per-version printer columns is preserved", + In: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column1"}}}, + {Name: "v2", Served: false, Storage: false, AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column2"}}}, + }, + }, + }, + Out: &CustomResourceDefinition{}, + ExpectOut: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column1"}}}, + {Name: "v2", Served: false, Storage: false, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column2"}}}, + }, + }, + }, + }, + { + Name: "v1 to internal, identical printer columns moves to top-level", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column1"}}}, + {Name: "v2", Served: true, Storage: false, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column1"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true}, + {Name: "v2", Served: true, Storage: false}, + }, + AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column1"}}, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + { + Name: "v1 to internal, distinct printer columns remains per-version", + In: &CustomResourceDefinition{ + Spec: CustomResourceDefinitionSpec{ + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column1"}}}, + {Name: "v2", Served: true, Storage: false, AdditionalPrinterColumns: []CustomResourceColumnDefinition{{Name: "column2"}}}, + }, + }, + }, + Out: &apiextensions.CustomResourceDefinition{}, + ExpectOut: &apiextensions.CustomResourceDefinition{ + Spec: apiextensions.CustomResourceDefinitionSpec{ + Version: "v1", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + {Name: "v1", Served: true, Storage: true, AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column1"}}}, + {Name: "v2", Served: true, Storage: false, AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "column2"}}}, + }, + PreserveUnknownFields: pointer.BoolPtr(false), + }, + }, + }, + } + + scheme := runtime.NewScheme() + // add internal and external types + if err := apiextensions.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := AddToScheme(scheme); err != nil { + t.Fatal(err) + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + err := scheme.Convert(tc.In, tc.Out, nil) + if err != nil { + if len(tc.ExpectErr) == 0 { + t.Fatalf("unexpected error %v", err) + } + if !strings.Contains(err.Error(), tc.ExpectErr) { + t.Fatalf("expected error %s, got %v", tc.ExpectErr, err) + } + return + } + if len(tc.ExpectErr) > 0 { + t.Fatalf("expected error %s, got none", tc.ExpectErr) + } + if !reflect.DeepEqual(tc.Out, tc.ExpectOut) { + t.Fatalf("unexpected result:\n %s", cmp.Diff(tc.ExpectOut, tc.Out)) + } + }) + } +} + func TestJSONConversion(t *testing.T) { nilJSON := apiextensions.JSON(nil) nullJSON := apiextensions.JSON("null") diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults.go index a368619a35a..9bdc6f63145 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults.go @@ -49,18 +49,6 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 { obj.Names.ListKind = obj.Names.Kind + "List" } - // If there is no list of versions, create on using deprecated Version field. - if len(obj.Versions) == 0 && len(obj.Version) != 0 { - obj.Versions = []CustomResourceDefinitionVersion{{ - Name: obj.Version, - Storage: true, - Served: true, - }} - } - // For backward compatibility set the version field to the first item in versions list. - if len(obj.Version) == 0 && len(obj.Versions) != 0 { - obj.Version = obj.Versions[0].Name - } if obj.Conversion == nil { obj.Conversion = &CustomResourceConversion{ Strategy: NoneConverter, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults_test.go index 1ada1644c77..20f9dece1bb 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/defaults_test.go @@ -92,7 +92,6 @@ func TestDefaults(t *testing.T) { Scope: NamespaceScoped, Conversion: &CustomResourceConversion{Strategy: NoneConverter}, PreserveUnknownFields: utilpointer.BoolPtr(true), - Version: "v1", Versions: []CustomResourceDefinitionVersion{ {Name: "v1", Storage: false, Served: true}, {Name: "v2", Storage: true, Served: true}, @@ -111,7 +110,9 @@ func TestDefaults(t *testing.T) { Scope: NamespaceScoped, Conversion: &CustomResourceConversion{Strategy: NoneConverter}, PreserveUnknownFields: utilpointer.BoolPtr(true), - Version: "v1", + Versions: []CustomResourceDefinitionVersion{ + {Name: "v1", Storage: true}, + }, }, }, expected: &CustomResourceDefinition{ @@ -119,9 +120,8 @@ func TestDefaults(t *testing.T) { Scope: NamespaceScoped, Conversion: &CustomResourceConversion{Strategy: NoneConverter}, PreserveUnknownFields: utilpointer.BoolPtr(true), - Version: "v1", Versions: []CustomResourceDefinitionVersion{ - {Name: "v1", Storage: true, Served: true}, + {Name: "v1", Storage: true}, }, }, Status: CustomResourceDefinitionStatus{ diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/types.go index f95c5bb9768..3d38de05a54 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/types.go @@ -36,30 +36,11 @@ const ( type CustomResourceDefinitionSpec struct { // Group is the group this resource belongs in Group string `json:"group" protobuf:"bytes,1,opt,name=group"` - // Version is the version this resource belongs in - // Should be always first item in Versions field if provided. - // Optional, but at least one of Version or Versions must be set. - // Deprecated: Please use `Versions`. - // +optional - Version string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"` // Names are the names used to describe this custom resource Names CustomResourceDefinitionNames `json:"names" protobuf:"bytes,3,opt,name=names"` // Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced Scope ResourceScope `json:"scope" protobuf:"bytes,4,opt,name=scope,casttype=ResourceScope"` - // Validation describes the validation methods for CustomResources - // Optional, the global validation schema for all versions. - // Top-level and per-version schemas are mutually exclusive. - // +optional - Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"` - // Subresources describes the subresources for CustomResource - // Optional, the global subresources for all versions. - // Top-level and per-version subresources are mutually exclusive. - // +optional - Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"` // Versions is the list of all supported versions for this resource. - // If Version field is provided, this field is optional. - // Validation: All versions must use the same validation schema for now. i.e., top - // level Validation field is applied to all of these versions. // Order: The version name will be used to compute the order. // If the version string is "kube-like", it will sort above non "kube-like" version strings, which are ordered // lexicographically. "Kube-like" versions start with a "v", then are followed by a number (the major version), @@ -67,13 +48,7 @@ type CustomResourceDefinitionSpec struct { // by GA > beta > alpha (where GA is a version with no suffix such as beta or alpha), and then by comparing // major version, then minor version. An example sorted list of versions: // v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10. - // +optional - Versions []CustomResourceDefinitionVersion `json:"versions,omitempty" protobuf:"bytes,7,rep,name=versions"` - // AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. - // Optional, the global columns for all versions. - // Top-level and per-version columns are mutually exclusive. - // +optional - AdditionalPrinterColumns []CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty" protobuf:"bytes,8,rep,name=additionalPrinterColumns"` + Versions []CustomResourceDefinitionVersion `json:"versions" protobuf:"bytes,7,rep,name=versions"` // `conversion` defines conversion settings for the CRD. // +optional @@ -187,24 +162,12 @@ type CustomResourceDefinitionVersion struct { // flagged as storage version. Storage bool `json:"storage" protobuf:"varint,3,opt,name=storage"` // Schema describes the schema for CustomResource used in validation, pruning, and defaulting. - // Top-level and per-version schemas are mutually exclusive. - // Per-version schemas must not all be set to identical values (top-level validation schema should be used instead) - // This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature. // +optional Schema *CustomResourceValidation `json:"schema,omitempty" protobuf:"bytes,4,opt,name=schema"` // Subresources describes the subresources for CustomResource - // Top-level and per-version subresources are mutually exclusive. - // Per-version subresources must not all be set to identical values (top-level subresources should be used instead) - // This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature. // +optional Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,5,opt,name=subresources"` // AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. - // Top-level and per-version columns are mutually exclusive. - // Per-version columns must not all be set to identical values (top-level columns should be used instead) - // This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature. - // NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an - // update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must - // be explicitly set to null // +optional AdditionalPrinterColumns []CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty" protobuf:"bytes,6,rep,name=additionalPrinterColumns"` }