1
16
17 package schemaconv_test
18
19 import (
20 "encoding/json"
21 "os"
22 "reflect"
23 "sort"
24 "strings"
25 "testing"
26
27 openapi_v2 "github.com/google/gnostic-models/openapiv2"
28 "github.com/stretchr/testify/require"
29
30 "k8s.io/kube-openapi/pkg/schemaconv"
31 "k8s.io/kube-openapi/pkg/spec3"
32 "k8s.io/kube-openapi/pkg/util/proto"
33 "k8s.io/kube-openapi/pkg/validation/spec"
34 "sigs.k8s.io/structured-merge-diff/v4/schema"
35 )
36
37 var swaggerJSONPath = "testdata/swagger.json"
38 var testCRDPath = "testdata/crds"
39
40 var deducedName string = "__untyped_deduced_"
41 var untypedName string = "__untyped_atomic_"
42
43 const (
44 quantityResource = "io.k8s.apimachinery.pkg.api.resource.Quantity"
45 rawExtensionResource = "io.k8s.apimachinery.pkg.runtime.RawExtension"
46 )
47
48 func toPtrMap[T comparable, V any](m map[T]V) map[T]*V {
49 if m == nil {
50 return nil
51 }
52
53 res := map[T]*V{}
54 for k, v := range m {
55 vCopy := v
56 res[k] = &vCopy
57 }
58 return res
59 }
60
61 func normalizeTypeRef(tr *schema.TypeRef) {
62 var untypedScalar schema.Scalar = "untyped"
63
64
65 if tr.Inlined.Equals(&schema.Atom{
66 Scalar: &untypedScalar,
67 List: &schema.List{
68 ElementType: schema.TypeRef{
69 NamedType: &untypedName,
70 },
71 ElementRelationship: schema.Atomic,
72 },
73 Map: &schema.Map{
74 ElementType: schema.TypeRef{
75 NamedType: &deducedName,
76 },
77 ElementRelationship: schema.Separable,
78 },
79 }) {
80 *tr = schema.TypeRef{
81 NamedType: &deducedName,
82 }
83 } else if tr.NamedType != nil && *tr.NamedType == rawExtensionResource {
84
85
86
87
88
89
90 *tr = schema.TypeRef{
91 NamedType: &untypedName,
92 }
93 } else {
94 normalizeType(&tr.Inlined)
95 }
96 }
97
98
99
100
101
102
103
104
105 func normalizeType(typ *schema.Atom) {
106 if typ.List != nil {
107 if typ.List.ElementType.Inlined != (schema.Atom{}) {
108 typ.List = &*typ.List
109 normalizeTypeRef(&typ.List.ElementType)
110 }
111 }
112
113 if typ.Map != nil {
114 typ.Map = &*typ.Map
115
116 fields := make([]schema.StructField, 0)
117 copy(typ.Fields, fields)
118 typ.Fields = fields
119
120 for i, f := range typ.Fields {
121
122
123 if reflect.DeepEqual(f.Default, map[any]any{}) {
124 f.Default = map[string]any{}
125 }
126
127 normalizeTypeRef(&f.Type)
128 typ.Fields[i] = f
129 }
130
131 sort.SliceStable(typ.Fields, func(i, j int) bool {
132 return strings.Compare(typ.Fields[i].Name, typ.Fields[j].Name) < 0
133 })
134
135
136
137 typ.Unions = nil
138
139 if typ.Map.ElementType.NamedType != nil {
140 if len(typ.Map.ElementRelationship) == 0 && typ.Scalar != nil && typ.List != nil && *typ.Map.ElementType.NamedType == deducedName {
141
142
143
144
145 typ.Map.ElementRelationship = schema.Separable
146 }
147 }
148
149 normalizeTypeRef(&typ.Map.ElementType)
150 }
151 }
152
153
154
155
156
157 func normalizeTypes(types []schema.TypeDef) map[string]schema.TypeDef {
158 res := map[string]schema.TypeDef{}
159 for _, typ := range types {
160 if _, exists := res[typ.Name]; !exists {
161 normalizeType(&typ.Atom)
162 res[typ.Name] = typ
163 }
164 }
165
166
167
168
169
170
171
172
173 res["io.k8s.apimachinery.pkg.runtime.RawExtension"] = schema.TypeDef{
174 Name: "io.k8s.apimachinery.pkg.runtime.RawExtension",
175 Atom: schema.Atom{
176 Map: &schema.Map{
177 ElementType: schema.TypeRef{
178 NamedType: &deducedName,
179 },
180 },
181 },
182 }
183
184
185
186 ignoreList := []string{
187 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceColumnDefinition",
188 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceConversion",
189 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition",
190 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionCondition",
191 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionNames",
192 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionSpec",
193 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionStatus",
194 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionVersion",
195 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceScale",
196 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceStatus",
197 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresources",
198 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation",
199 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ExternalDocumentation",
200 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSON",
201 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps",
202 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrArray",
203 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool",
204 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrStringArray",
205 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ServiceReference",
206 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ValidationRule",
207 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookClientConfig",
208 "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookConversion",
209 "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions",
210 "io.k8s.apimachinery.pkg.apis.meta.v1.Patch",
211 "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions",
212 "io.k8s.apimachinery.pkg.apis.meta.v1.Status",
213 "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause",
214 "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails",
215 }
216
217 for _, k := range ignoreList {
218 delete(res, k)
219 }
220
221 return res
222 }
223
224 func TestCRDOpenAPIConversion(t *testing.T) {
225 files, err := os.ReadDir("testdata/crds/openapiv2")
226 require.NoError(t, err)
227 for _, entry := range files {
228 t.Run(entry.Name(), func(t *testing.T) {
229 t.Parallel()
230 openAPIV2Contents, err := os.ReadFile("testdata/crds/openapiv2/" + entry.Name())
231 require.NoError(t, err)
232
233 openAPIV3Contents, err := os.ReadFile("testdata/crds/openapiv3/" + entry.Name())
234 require.NoError(t, err)
235
236 var v3 spec3.OpenAPI
237
238 err = json.Unmarshal(openAPIV3Contents, &v3)
239 require.NoError(t, err)
240
241 v2Types, err := specToSchemaViaProtoModels(openAPIV2Contents)
242 require.NoError(t, err)
243 v3Types, err := schemaconv.ToSchemaFromOpenAPI(v3.Components.Schemas, false)
244 require.NoError(t, err)
245
246 require.Equal(t, normalizeTypes(v2Types.Types), normalizeTypes(v3Types.Types))
247 })
248 }
249 }
250
251
252
253
254
255
256
257 func TestOpenAPIImplementation(t *testing.T) {
258 swaggerJSON, err := os.ReadFile(swaggerJSONPath)
259 require.NoError(t, err)
260
261 protoModels, err := specToSchemaViaProtoModels(swaggerJSON)
262 require.NoError(t, err)
263
264 var swag spec.Swagger
265 err = json.Unmarshal(swaggerJSON, &swag)
266 require.NoError(t, err)
267
268 newConversionTypes, err := schemaconv.ToSchemaFromOpenAPI(toPtrMap(swag.Definitions), false)
269 require.NoError(t, err)
270
271 require.Equal(t, normalizeTypes(protoModels.Types), normalizeTypes(newConversionTypes.Types))
272 }
273
274 func specToSchemaViaProtoModels(input []byte) (*schema.Schema, error) {
275 document, err := openapi_v2.ParseDocument(input)
276 if err != nil {
277 return nil, err
278 }
279
280 models, err := proto.NewOpenAPIData(document)
281 if err != nil {
282 return nil, err
283 }
284
285 newSchema, err := schemaconv.ToSchema(models)
286 if err != nil {
287 return nil, err
288 }
289
290 return newSchema, nil
291 }
292
293 func BenchmarkOpenAPIConversion(b *testing.B) {
294 swaggerJSON, err := os.ReadFile(swaggerJSONPath)
295 require.NoError(b, err)
296
297 doc := spec.Swagger{}
298 require.NoError(b, doc.UnmarshalJSON(swaggerJSON))
299
300
301
302 b.Run("spec.Schema->schema.Schema", func(b *testing.B) {
303 b.ReportAllocs()
304 for i := 0; i < b.N; i++ {
305 _, err := schemaconv.ToSchemaFromOpenAPI(toPtrMap(doc.Definitions), false)
306 require.NoError(b, err)
307 }
308 })
309
310 b.Run("spec.Schema->json->gnostic_v2->proto.Models->schema.Schema", func(b *testing.B) {
311 b.ReportAllocs()
312 for i := 0; i < b.N; i++ {
313 jsonText, err := doc.MarshalJSON()
314 require.NoError(b, err)
315
316 _, err = specToSchemaViaProtoModels(jsonText)
317 require.NoError(b, err)
318 }
319 })
320 }
321
322 func BenchmarkOpenAPICRDConversion(b *testing.B) {
323 files, err := os.ReadDir("testdata/crds/openapiv2")
324 require.NoError(b, err)
325 for _, entry := range files {
326 b.Run(entry.Name(), func(b *testing.B) {
327 openAPIV2Contents, err := os.ReadFile("testdata/crds/openapiv2/" + entry.Name())
328 require.NoError(b, err)
329
330 openAPIV3Contents, err := os.ReadFile("testdata/crds/openapiv3/" + entry.Name())
331 require.NoError(b, err)
332
333 var v2 spec.Swagger
334 var v3 spec3.OpenAPI
335
336 err = json.Unmarshal(openAPIV2Contents, &v2)
337 require.NoError(b, err)
338
339 err = json.Unmarshal(openAPIV3Contents, &v3)
340 require.NoError(b, err)
341
342
343
344 b.Run("spec.Schema->schema.Schema", func(b *testing.B) {
345 b.ReportAllocs()
346 for i := 0; i < b.N; i++ {
347 _, err := schemaconv.ToSchemaFromOpenAPI(v3.Components.Schemas, false)
348 require.NoError(b, err)
349 }
350 })
351
352 b.Run("spec.Schema->json->gnostic_v2->proto.Models->schema.Schema", func(b *testing.B) {
353 b.ReportAllocs()
354 for i := 0; i < b.N; i++ {
355 jsonText, err := v2.MarshalJSON()
356 require.NoError(b, err)
357
358 _, err = specToSchemaViaProtoModels(jsonText)
359 require.NoError(b, err)
360 }
361 })
362 })
363 }
364 }
365
View as plain text