k8s.io/kubernetes@v1.29.3/test/integration/apiserver/openapi/openapi_merge_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package openapi 18 19 import ( 20 "context" 21 "encoding/json" 22 "testing" 23 "time" 24 25 "github.com/stretchr/testify/assert" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 28 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/client-go/dynamic" 31 kubernetes "k8s.io/client-go/kubernetes" 32 "k8s.io/kube-openapi/pkg/spec3" 33 apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 34 "k8s.io/kubernetes/test/integration/framework" 35 ) 36 37 const scaleSchemaName = "io.k8s.api.autoscaling.v1.Scale" 38 const statusSchemaName = "io.k8s.apimachinery.pkg.apis.meta.v1.Status" 39 const objectMetaSchemaName = "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" 40 41 func TestOpenAPIV3MultipleCRDsSameGV(t *testing.T) { 42 server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) 43 if err != nil { 44 t.Fatal(err) 45 } 46 defer server.TearDownFn() 47 config := server.ClientConfig 48 49 apiExtensionClient, err := clientset.NewForConfig(config) 50 if err != nil { 51 t.Fatal(err) 52 } 53 dynamicClient, err := dynamic.NewForConfig(config) 54 if err != nil { 55 t.Fatal(err) 56 } 57 clientset, err := kubernetes.NewForConfig(config) 58 if err != nil { 59 t.Fatal(err) 60 } 61 62 fooWithSubresourceCRD := &apiextensionsv1.CustomResourceDefinition{ 63 ObjectMeta: metav1.ObjectMeta{ 64 Name: "foosubs.cr.bar.com", 65 }, 66 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 67 Group: "cr.bar.com", 68 Scope: apiextensionsv1.NamespaceScoped, 69 Names: apiextensionsv1.CustomResourceDefinitionNames{ 70 Plural: "foosubs", 71 Kind: "FooSub", 72 }, 73 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 74 { 75 Name: "v1", 76 Served: true, 77 Storage: true, 78 Schema: &apiextensionsv1.CustomResourceValidation{ 79 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 80 Type: "object", 81 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 82 "spec": { 83 Type: "object", 84 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 85 "replicas": { 86 Type: "integer", 87 }, 88 }, 89 }, 90 "status": { 91 Type: "object", 92 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 93 "replicas": { 94 Type: "integer", 95 }, 96 }, 97 }, 98 }, 99 }, 100 }, 101 Subresources: &apiextensionsv1.CustomResourceSubresources{ 102 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 103 Scale: &apiextensionsv1.CustomResourceSubresourceScale{ 104 SpecReplicasPath: ".spec.replicas", 105 StatusReplicasPath: ".status.replicas", 106 }, 107 }, 108 }, 109 }, 110 }, 111 } 112 113 bazWithSubresourceCRD := &apiextensionsv1.CustomResourceDefinition{ 114 ObjectMeta: metav1.ObjectMeta{ 115 Name: "bazsubs.cr.bar.com", 116 }, 117 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 118 Group: "cr.bar.com", 119 Scope: apiextensionsv1.NamespaceScoped, 120 Names: apiextensionsv1.CustomResourceDefinitionNames{ 121 Plural: "bazsubs", 122 Kind: "BazSub", 123 }, 124 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 125 { 126 Name: "v1", 127 Served: true, 128 Storage: true, 129 Schema: &apiextensionsv1.CustomResourceValidation{ 130 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 131 Type: "object", 132 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 133 "spec": { 134 Type: "object", 135 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 136 "replicas": { 137 Type: "integer", 138 }, 139 }, 140 }, 141 "status": { 142 Type: "object", 143 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 144 "replicas": { 145 Type: "integer", 146 }, 147 }, 148 }, 149 }, 150 }, 151 }, 152 Subresources: &apiextensionsv1.CustomResourceSubresources{ 153 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 154 Scale: &apiextensionsv1.CustomResourceSubresourceScale{ 155 SpecReplicasPath: ".spec.replicas", 156 StatusReplicasPath: ".status.replicas", 157 }, 158 }, 159 }, 160 }, 161 }, 162 } 163 164 _, err = fixtures.CreateNewV1CustomResourceDefinition(fooWithSubresourceCRD, apiExtensionClient, dynamicClient) 165 if err != nil { 166 t.Fatal(err) 167 } 168 169 _, err = fixtures.CreateNewV1CustomResourceDefinition(bazWithSubresourceCRD, apiExtensionClient, dynamicClient) 170 if err != nil { 171 t.Fatal(err) 172 } 173 174 // It takes a second for CRD specs to propagate to the aggregator 175 time.Sleep(4 * time.Second) 176 177 jsonData, err := clientset.RESTClient().Get().AbsPath("/openapi/v3/apis/cr.bar.com/v1").Do(context.TODO()).Raw() 178 if err != nil { 179 t.Fatal(err) 180 } 181 182 openAPISpec := spec3.OpenAPI{} 183 err = json.Unmarshal(jsonData, &openAPISpec) 184 if err != nil { 185 t.Fatal(err) 186 } 187 188 // Ensure both CRD schemas are present in the OpenAPI 189 // Scale, status, and metadata dependencies must also be present 190 assert.Contains(t, openAPISpec.Components.Schemas, scaleSchemaName) 191 assert.Contains(t, openAPISpec.Components.Schemas, statusSchemaName) 192 assert.Contains(t, openAPISpec.Components.Schemas, objectMetaSchemaName) 193 assert.Contains(t, openAPISpec.Components.Schemas, "com.bar.cr.v1.BazSub") 194 assert.Contains(t, openAPISpec.Components.Schemas, "com.bar.cr.v1.FooSub") 195 }