Change:
Noticed while reproducing with client-side validation: Steps to reproduce: published openapi only contains expected metadata definitions: a CRD, e.g. simple- at published openapi: where the _v2 definition is referenced: For some reason, the same server publishing openapi for built-in types and custom resource definitions thinks the openapi definition differs. Reproduced against at least the following branches: master release-1.23 release-1.22 release-1.21 In addition to fixing this to make coherently refer to ObjectMeta definitions from built-in and custom types, we need a test for this scenario to ensure it doesn't creep back in.
/sig api-machinery /priority important-longterm
cc - this is the sort of thing I was remembering which prompted the question about merging openapi internally () This needs to be resolved and test coverage improved here ahead of making v2-facing openapi changes
is there a rule that we can validate and enforce here? i.e. are there cases where it's normal to have two metadata? Probably in the case where an extension apiserver has a different version of kubernetes that has a slightly different metadata. I wish we could catch somehow that this doesn't happen.
In a server with no aggregated API servers, we should never have multiple definitions for ObjectMeta (CRDs and built-in types should reference the same ObjectMeta)
/triage accepted
/cc
This is fixed by on master. For 1.24, 1.23, 1.22 and 1.21, cherry picks have been created: 1.24: 1.23: 1.22: 1.21:
also need it on 1.24?
Updated the list above.

Discussion:
if !v.Served { continue } <del> // Defaults are not pruned here, but before being served. </del> spec, err := builder.BuildOpenAPIV2(crd, v.Name, builder.Options{V2: true}) <ins> // Defaults must be pruned here for CRDs to cleanly merge with the static // spec that already has defaults pruned spec.Definitions = handler.PruneDefaults(spec.Definitions) </ins> if err != nil { return nil, false, err }

<ins> /* Copyright 2022 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 openapi import ( "context" "encoding/json" "strings" "testing" "time" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/test/integration/fixtures" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/dynamic" kubernetes "k8s.io/client-go/kubernetes" "k8s.io/kube-openapi/pkg/validation/spec" apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" "k8s.io/kubernetes/test/integration/framework" ) func TestOpenAPIV2CRDMergeNoDuplicateTypes(t *testing.T) { server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) if err != nil { t.Fatal(err) } defer server.TearDownFn() config := server.ClientConfig apiExtensionClient, err := clientset.NewForConfig(config) if err != nil { t.Fatal(err) } dynamicClient, err := dynamic.NewForConfig(config) if err != nil { t.Fatal(err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { t.Fatal(err) } foo := &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "foosubs.cr.bar.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "cr.bar.com", Scope: apiextensionsv1.NamespaceScoped, Names: apiextensionsv1.CustomResourceDefinitionNames{ Plural: "foosubs", Kind:   "FooSub", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ { Name:    "v1", Served:  true, Storage: true, Schema: &apiextensionsv1.CustomResourceValidation{ OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "spec": { Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "replicas": { Type: "integer", }, }, }, "status": { Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "replicas": { Type: "integer", }, }, }, }, }, }, Subresources: &apiextensionsv1.CustomResourceSubresources{ Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, Scale: &apiextensionsv1.CustomResourceSubresourceScale{ SpecReplicasPath:   ".spec.replicas", StatusReplicasPath: ".status.replicas", }, }, }, }, }, } baz := &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "bazsubs.cr.bar.com", }, Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "cr.bar.com", Scope: apiextensionsv1.NamespaceScoped, Names: apiextensionsv1.CustomResourceDefinitionNames{ Plural: "bazsubs", Kind:   "BazSub", }, Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ { Name:    "v1", Served:  true, Storage: true, Schema: &apiextensionsv1.CustomResourceValidation{ OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "spec": { Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "replicas": { Type: "integer", }, }, }, "status": { Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "replicas": { Type: "integer", }, }, }, }, }, }, Subresources: &apiextensionsv1.CustomResourceSubresources{ Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, Scale: &apiextensionsv1.CustomResourceSubresourceScale{ SpecReplicasPath:   ".spec.replicas", StatusReplicasPath: ".status.replicas", }, }, }, }, }, } _, err = fixtures.CreateNewV1CustomResourceDefinition(foo, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } _, err = fixtures.CreateNewV1CustomResourceDefinition(baz, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } var openAPISpec spec.Swagger // Poll for the OpenAPI to be updated with the new CRD wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) { jsonData, err := clientset.RESTClient().Get().AbsPath("/openapi/v2").Do(context.TODO()).Raw() if err != nil { t.Fatal(err) } openAPISpec = spec.Swagger{} err = json.Unmarshal(jsonData, &openAPISpec) if err != nil { t.Fatal(err) } for schemaName := range openAPISpec.Definitions { if strings.HasPrefix(schemaName, "com.bar.cr.v1.BazSub") { return true, nil } } return false, nil }) for schemaName := range openAPISpec.Definitions { if strings.HasSuffix(schemaName, "_v2") { t.Errorf("Error: Expected no _v2 types, got %s", schemaName) } } } </ins>