1
16
17 package builder
18
19 import (
20 "reflect"
21 "strings"
22 "testing"
23
24 "github.com/google/go-cmp/cmp"
25 "github.com/stretchr/testify/assert"
26 "github.com/stretchr/testify/require"
27
28 apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
29 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
30 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
31 "k8s.io/apimachinery/pkg/util/json"
32 "k8s.io/apimachinery/pkg/util/sets"
33 "k8s.io/apiserver/pkg/endpoints"
34 "k8s.io/kube-openapi/pkg/validation/spec"
35 utilpointer "k8s.io/utils/pointer"
36 "k8s.io/utils/ptr"
37 )
38
39 func TestNewBuilder(t *testing.T) {
40 tests := []struct {
41 name string
42
43 schema string
44
45 wantedSchema string
46 wantedItemsSchema string
47
48 v2 bool
49 }{
50 {
51 "nil",
52 "",
53 `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
54 true,
55 },
56 {"with properties",
57 `{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
58 `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
59 `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
60 true,
61 },
62 {"type only",
63 `{"type":"object"}`,
64 `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
65 `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
66 true,
67 },
68 {"preserve unknown at root v2",
69 `{"type":"object","x-kubernetes-preserve-unknown-fields":true}`,
70 `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
71 `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
72 true,
73 },
74 {"with extensions",
75 `
76 {
77 "type":"object",
78 "properties": {
79 "int-or-string-1": {
80 "x-kubernetes-int-or-string": true,
81 "anyOf": [
82 {"type":"integer"},
83 {"type":"string"}
84 ]
85 },
86 "int-or-string-2": {
87 "x-kubernetes-int-or-string": true,
88 "allOf": [{
89 "anyOf": [
90 {"type":"integer"},
91 {"type":"string"}
92 ]
93 }, {
94 "anyOf": [
95 {"minimum": 42.0}
96 ]
97 }]
98 },
99 "int-or-string-3": {
100 "x-kubernetes-int-or-string": true,
101 "anyOf": [
102 {"type":"integer"},
103 {"type":"string"}
104 ],
105 "allOf": [{
106 "anyOf": [
107 {"minimum": 42.0}
108 ]
109 }]
110 },
111 "int-or-string-4": {
112 "x-kubernetes-int-or-string": true,
113 "anyOf": [
114 {"minimum": 42.0}
115 ]
116 },
117 "int-or-string-5": {
118 "x-kubernetes-int-or-string": true,
119 "anyOf": [
120 {"minimum": 42.0}
121 ],
122 "allOf": [
123 {"minimum": 42.0}
124 ]
125 },
126 "int-or-string-6": {
127 "x-kubernetes-int-or-string": true
128 },
129 "preserve-unknown-fields": {
130 "x-kubernetes-preserve-unknown-fields": true
131 },
132 "embedded-object": {
133 "x-kubernetes-embedded-resource": true,
134 "x-kubernetes-preserve-unknown-fields": true,
135 "type": "object"
136 }
137 }
138 }`,
139 `
140 {
141 "type":"object",
142 "properties": {
143 "apiVersion": {"type":"string"},
144 "kind": {"type":"string"},
145 "metadata": {"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},
146 "int-or-string-1": {
147 "x-kubernetes-int-or-string": true
148 },
149 "int-or-string-2": {
150 "x-kubernetes-int-or-string": true
151 },
152 "int-or-string-3": {
153 "x-kubernetes-int-or-string": true
154 },
155 "int-or-string-4": {
156 "x-kubernetes-int-or-string": true
157 },
158 "int-or-string-5": {
159 "x-kubernetes-int-or-string": true
160 },
161 "int-or-string-6": {
162 "x-kubernetes-int-or-string": true
163 },
164 "preserve-unknown-fields": {
165 "x-kubernetes-preserve-unknown-fields": true
166 },
167 "embedded-object": {
168 "x-kubernetes-embedded-resource": true,
169 "x-kubernetes-preserve-unknown-fields": true
170 }
171 },
172 "x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
173 }`,
174 `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
175 true,
176 },
177 }
178 for _, tt := range tests {
179 t.Run(tt.name, func(t *testing.T) {
180 var schema *structuralschema.Structural
181 if len(tt.schema) > 0 {
182 v1beta1Schema := &apiextensionsv1.JSONSchemaProps{}
183 if err := json.Unmarshal([]byte(tt.schema), &v1beta1Schema); err != nil {
184 t.Fatal(err)
185 }
186 internalSchema := &apiextensionsinternal.JSONSchemaProps{}
187 apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v1beta1Schema, internalSchema, nil)
188 var err error
189 schema, err = structuralschema.NewStructural(internalSchema)
190 if err != nil {
191 t.Fatalf("structural schema error: %v", err)
192 }
193 if errs := structuralschema.ValidateStructural(nil, schema); len(errs) > 0 {
194 t.Fatalf("structural schema validation error: %v", errs.ToAggregate())
195 }
196 schema = schema.Unfold()
197 }
198
199 got := newBuilder(&apiextensionsv1.CustomResourceDefinition{
200 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
201 Group: "bar.k8s.io",
202 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
203 {
204 Name: "v1",
205 },
206 },
207 Names: apiextensionsv1.CustomResourceDefinitionNames{
208 Plural: "foos",
209 Singular: "foo",
210 Kind: "Foo",
211 ListKind: "FooList",
212 },
213 Scope: apiextensionsv1.NamespaceScoped,
214 },
215 }, "v1", schema, Options{V2: tt.v2})
216
217 var wantedSchema, wantedItemsSchema spec.Schema
218 if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
219 t.Fatal(err)
220 }
221 if err := json.Unmarshal([]byte(tt.wantedItemsSchema), &wantedItemsSchema); err != nil {
222 t.Fatal(err)
223 }
224
225 gotProperties := properties(got.schema.Properties)
226 wantedProperties := properties(wantedSchema.Properties)
227 if !gotProperties.Equal(wantedProperties) {
228 t.Fatalf("unexpected properties, got: %s, expected: %s", gotProperties.List(), wantedProperties.List())
229 }
230
231
232 for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
233 if _, found := got.schema.Properties["kind"]; found {
234 prop := got.schema.Properties[metaField]
235 prop.Description = ""
236 got.schema.Properties[metaField] = prop
237 }
238 }
239
240 if !reflect.DeepEqual(&wantedSchema, got.schema) {
241 t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, got.schema), &wantedSchema, got.schema)
242 }
243
244 gotListProperties := properties(got.listSchema.Properties)
245 if want := sets.NewString("apiVersion", "kind", "metadata", "items"); !gotListProperties.Equal(want) {
246 t.Fatalf("unexpected list properties, got: %s, expected: %s", gotListProperties.List(), want.List())
247 }
248
249 if e, a := (spec.StringOrArray{"string"}), got.listSchema.Properties["apiVersion"].Type; !reflect.DeepEqual(e, a) {
250 t.Errorf("expected %#v, got %#v", e, a)
251 }
252 if e, a := (spec.StringOrArray{"string"}), got.listSchema.Properties["kind"].Type; !reflect.DeepEqual(e, a) {
253 t.Errorf("expected %#v, got %#v", e, a)
254 }
255 listRef := got.listSchema.Properties["metadata"].Ref
256 if e, a := "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta", (&listRef).String(); e != a {
257 t.Errorf("expected %q, got %q", e, a)
258 }
259
260 gotListSchema := got.listSchema.Properties["items"].Items.Schema
261 if !reflect.DeepEqual(&wantedItemsSchema, gotListSchema) {
262 t.Errorf("unexpected list schema:\n%s", schemaDiff(&wantedItemsSchema, gotListSchema))
263 }
264 })
265 }
266 }
267
268 func TestCRDRouteParameterBuilder(t *testing.T) {
269 testCRDKind := "Foo"
270 testCRDGroup := "foo-group"
271 testCRDVersion := "foo-version"
272 testCRDResourceName := "foos"
273
274 testCases := []struct {
275 scope apiextensionsv1.ResourceScope
276 paths map[string]struct {
277 expectNamespaceParam bool
278 expectNameParam bool
279 expectedActions sets.String
280 }
281 }{
282 {
283 scope: apiextensionsv1.NamespaceScoped,
284 paths: map[string]struct {
285 expectNamespaceParam bool
286 expectNameParam bool
287 expectedActions sets.String
288 }{
289 "/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("list")},
290 "/apis/foo-group/foo-version/namespaces/{namespace}/foos": {expectNamespaceParam: true, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
291 "/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
292 "/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/scale": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
293 "/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/status": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
294 },
295 },
296 {
297 scope: apiextensionsv1.ClusterScoped,
298 paths: map[string]struct {
299 expectNamespaceParam bool
300 expectNameParam bool
301 expectedActions sets.String
302 }{
303 "/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
304 "/apis/foo-group/foo-version/foos/{name}": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
305 "/apis/foo-group/foo-version/foos/{name}/scale": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
306 "/apis/foo-group/foo-version/foos/{name}/status": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
307 },
308 },
309 }
310
311 for _, testCase := range testCases {
312 testNamespacedCRD := &apiextensionsv1.CustomResourceDefinition{
313 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
314 Scope: testCase.scope,
315 Group: testCRDGroup,
316 Names: apiextensionsv1.CustomResourceDefinitionNames{
317 Kind: testCRDKind,
318 Plural: testCRDResourceName,
319 },
320 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
321 {
322 Name: testCRDVersion,
323 Subresources: &apiextensionsv1.CustomResourceSubresources{
324 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
325 Scale: &apiextensionsv1.CustomResourceSubresourceScale{},
326 },
327 },
328 },
329 },
330 }
331 swagger, err := BuildOpenAPIV2(testNamespacedCRD, testCRDVersion, Options{V2: true})
332 require.NoError(t, err)
333 require.Equal(t, len(testCase.paths), len(swagger.Paths.Paths), testCase.scope)
334 for path, expected := range testCase.paths {
335 t.Run(path, func(t *testing.T) {
336 path, ok := swagger.Paths.Paths[path]
337 if !ok {
338 t.Errorf("unexpected path %v", path)
339 }
340
341 hasNamespaceParam := false
342 hasNameParam := false
343 for _, param := range path.Parameters {
344 if strings.HasPrefix(param.Ref.String(), "#/parameters/namespace-") {
345 hasNamespaceParam = true
346 }
347 if param.In == "path" && param.Name == "name" {
348 hasNameParam = true
349 }
350 }
351 assert.Equal(t, expected.expectNamespaceParam, hasNamespaceParam)
352 assert.Equal(t, expected.expectNameParam, hasNameParam)
353
354 actions := sets.NewString()
355 for _, operation := range []*spec.Operation{path.Get, path.Post, path.Put, path.Patch, path.Delete} {
356 if operation != nil {
357 action, ok := operation.VendorExtensible.Extensions.GetString(endpoints.RouteMetaAction)
358 if ok {
359 actions.Insert(action)
360 }
361 if action == "patch" {
362 expected := []string{"application/json-patch+json", "application/merge-patch+json", "application/apply-patch+yaml"}
363 assert.Equal(t, expected, operation.Consumes)
364 } else {
365 assert.Equal(t, []string{"application/json", "application/yaml"}, operation.Consumes)
366 }
367 }
368 }
369 assert.Equal(t, expected.expectedActions, actions)
370 })
371 }
372 }
373 }
374
375 func properties(p map[string]spec.Schema) sets.String {
376 ret := sets.NewString()
377 for k := range p {
378 ret.Insert(k)
379 }
380 return ret
381 }
382
383 func schemaDiff(a, b *spec.Schema) string {
384
385 return cmp.Diff(a, b, cmp.Exporter(func(reflect.Type) bool { return true }))
386 }
387
388 func TestBuildOpenAPIV2(t *testing.T) {
389 tests := []struct {
390 name string
391 schema string
392 preserveUnknownFields *bool
393 wantedSchema string
394 opts Options
395 selectableFields []apiextensionsv1.SelectableField
396 }{
397 {
398 name: "nil",
399 wantedSchema: `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
400 opts: Options{V2: true},
401 },
402 {
403 name: "with properties",
404 schema: `{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
405 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
406 opts: Options{V2: true},
407 },
408 {
409 name: "with invalid-typed properties",
410 schema: `{"type":"object","properties":{"spec":{"type":"bug"},"status":{"type":"object"}}}`,
411 wantedSchema: `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
412 opts: Options{V2: true},
413 },
414 {
415 name: "with non-structural schema",
416 schema: `{"type":"object","properties":{"foo":{"type":"array"}}}`,
417 wantedSchema: `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
418 opts: Options{V2: true},
419 },
420 {
421 name: "with spec.preseveUnknownFields=true",
422 schema: `{"type":"object","properties":{"foo":{"type":"string"}}}`,
423 preserveUnknownFields: ptr.To(true),
424 wantedSchema: `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
425 opts: Options{V2: true},
426 },
427 {
428 name: "v2",
429 schema: `{"type":"object","properties":{"foo":{"type":"string","oneOf":[{"pattern":"a"},{"pattern":"b"}]}}}`,
430 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"foo":{"type":"string"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
431 opts: Options{V2: true},
432 },
433 {
434 name: "with selectable fields enabled",
435 schema: `{"type":"object","properties":{"foo":{"type":"string"}}}`,
436 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"foo":{"type":"string"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}], "x-kubernetes-selectable-fields": [{"fieldPath":"foo"}]}`,
437 opts: Options{V2: true, IncludeSelectableFields: true},
438 selectableFields: []apiextensionsv1.SelectableField{{JSONPath: "foo"}},
439 },
440 {
441 name: "with selectable fields disabled",
442 schema: `{"type":"object","properties":{"foo":{"type":"string"}}}`,
443 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"foo":{"type":"string"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
444 opts: Options{V2: true},
445 selectableFields: []apiextensionsv1.SelectableField{{JSONPath: "foo"}},
446 },
447 }
448 for _, tt := range tests {
449 t.Run(tt.name, func(t *testing.T) {
450 var validation *apiextensionsv1.CustomResourceValidation
451 if len(tt.schema) > 0 {
452 v1Schema := &apiextensionsv1.JSONSchemaProps{}
453 if err := json.Unmarshal([]byte(tt.schema), &v1Schema); err != nil {
454 t.Fatal(err)
455 }
456 validation = &apiextensionsv1.CustomResourceValidation{
457 OpenAPIV3Schema: v1Schema,
458 }
459 }
460 if tt.preserveUnknownFields != nil && *tt.preserveUnknownFields {
461 validation.OpenAPIV3Schema.XPreserveUnknownFields = utilpointer.BoolPtr(true)
462 }
463
464
465 got, err := BuildOpenAPIV2(&apiextensionsv1.CustomResourceDefinition{
466 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
467 Group: "bar.k8s.io",
468 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
469 {
470 Name: "v1",
471 Schema: validation,
472 SelectableFields: tt.selectableFields,
473 },
474 },
475 Names: apiextensionsv1.CustomResourceDefinitionNames{
476 Plural: "foos",
477 Singular: "foo",
478 Kind: "Foo",
479 ListKind: "FooList",
480 },
481 Scope: apiextensionsv1.NamespaceScoped,
482 },
483 }, "v1", tt.opts)
484 if err != nil {
485 t.Fatal(err)
486 }
487
488 var wantedSchema spec.Schema
489 if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
490 t.Fatal(err)
491 }
492
493 gotSchema := got.Definitions["io.k8s.bar.v1.Foo"]
494 gotProperties := properties(gotSchema.Properties)
495 wantedProperties := properties(wantedSchema.Properties)
496 if !gotProperties.Equal(wantedProperties) {
497 t.Fatalf("unexpected properties, got: %s, expected: %s", gotProperties.List(), wantedProperties.List())
498 }
499
500
501 for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
502 if _, found := gotSchema.Properties["kind"]; found {
503 prop := gotSchema.Properties[metaField]
504 prop.Description = ""
505 gotSchema.Properties[metaField] = prop
506 }
507 }
508
509 if !reflect.DeepEqual(&wantedSchema, &gotSchema) {
510 t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, &gotSchema), &wantedSchema, &gotSchema)
511 }
512 })
513 }
514 }
515
516 func TestBuildOpenAPIV3(t *testing.T) {
517 tests := []struct {
518 name string
519 schema string
520 preserveUnknownFields *bool
521 wantedSchema string
522 opts Options
523 selectableFields []apiextensionsv1.SelectableField
524 }{
525 {
526 name: "nil",
527 wantedSchema: `{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
528 },
529 {
530 name: "with properties",
531 schema: `{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
532 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}]},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
533 },
534 {
535 name: "with v3 nullable field",
536 schema: `{"type":"object","properties":{"spec":{"type":"object", "nullable": true},"status":{"type":"object"}}}`,
537 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}]},"spec":{"type":"object", "nullable": true},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
538 },
539 {
540 name: "with default not pruned for v3",
541 schema: `{"type":"object","properties":{"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}}}`,
542 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}]},"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
543 },
544 {
545 name: "with selectable fields enabled",
546 schema: `{"type":"object","properties":{"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}}}`,
547 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}]},"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}], "x-kubernetes-selectable-fields": [{"fieldPath":"spec.field"}]}`,
548 opts: Options{IncludeSelectableFields: true},
549 selectableFields: []apiextensionsv1.SelectableField{{JSONPath: "spec.field"}},
550 },
551 {
552 name: "with selectable fields disabled",
553 schema: `{"type":"object","properties":{"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}}}`,
554 wantedSchema: `{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}]},"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
555 selectableFields: []apiextensionsv1.SelectableField{{JSONPath: "spec.field"}},
556 },
557 }
558 for _, tt := range tests {
559 t.Run(tt.name, func(t *testing.T) {
560 var validation *apiextensionsv1.CustomResourceValidation
561 if len(tt.schema) > 0 {
562 v1Schema := &apiextensionsv1.JSONSchemaProps{}
563 if err := json.Unmarshal([]byte(tt.schema), &v1Schema); err != nil {
564 t.Fatal(err)
565 }
566 validation = &apiextensionsv1.CustomResourceValidation{
567 OpenAPIV3Schema: v1Schema,
568 }
569 }
570 if tt.preserveUnknownFields != nil && *tt.preserveUnknownFields {
571 validation.OpenAPIV3Schema.XPreserveUnknownFields = utilpointer.BoolPtr(true)
572 }
573
574 got, err := BuildOpenAPIV3(&apiextensionsv1.CustomResourceDefinition{
575 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
576 Group: "bar.k8s.io",
577 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
578 {
579 Name: "v1",
580 Schema: validation,
581 SelectableFields: tt.selectableFields,
582 },
583 },
584 Names: apiextensionsv1.CustomResourceDefinitionNames{
585 Plural: "foos",
586 Singular: "foo",
587 Kind: "Foo",
588 ListKind: "FooList",
589 },
590 Scope: apiextensionsv1.NamespaceScoped,
591 },
592 }, "v1", tt.opts)
593 if err != nil {
594 t.Fatal(err)
595 }
596
597 var wantedSchema spec.Schema
598 if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
599 t.Fatal(err)
600 }
601
602 gotSchema := *got.Components.Schemas["io.k8s.bar.v1.Foo"]
603 listSchemaRef := got.Components.Schemas["io.k8s.bar.v1.FooList"].Properties["items"].Items.Schema.Ref.String()
604 if strings.Contains(listSchemaRef, "#/definitions/") || !strings.Contains(listSchemaRef, "#/components/schemas/") {
605 t.Errorf("Expected list schema ref to contain #/components/schemas/ prefix. Got %s", listSchemaRef)
606 }
607 gotProperties := properties(gotSchema.Properties)
608 wantedProperties := properties(wantedSchema.Properties)
609 if !gotProperties.Equal(wantedProperties) {
610 t.Fatalf("unexpected properties, got: %s, expected: %s", gotProperties.List(), wantedProperties.List())
611 }
612
613
614 for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
615 if _, found := gotSchema.Properties["kind"]; found {
616 prop := gotSchema.Properties[metaField]
617 prop.Description = ""
618 gotSchema.Properties[metaField] = prop
619 }
620 }
621
622 if !reflect.DeepEqual(&wantedSchema, &gotSchema) {
623 t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, &gotSchema), &wantedSchema, &gotSchema)
624 }
625 })
626 }
627 }
628
629
630
631
632
633 func TestGetDefinitionRefPrefix(t *testing.T) {
634
635
636
637
638
639 managedFieldsTypePath := "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry"
640
641 v2Ref := getDefinition(managedFieldsTypePath, true).SchemaProps.Properties["time"].SchemaProps.Ref
642 v3Ref := getDefinition(managedFieldsTypePath, false).SchemaProps.Properties["time"].SchemaProps.Ref
643
644 v2String := v2Ref.String()
645 v3String := v3Ref.String()
646
647 if !strings.HasPrefix(v3String, v3DefinitionPrefix) {
648 t.Errorf("v3 ref (%v) does not have the correct prefix (%v)", v3String, v3DefinitionPrefix)
649 }
650
651 if !strings.HasPrefix(v2String, definitionPrefix) {
652 t.Errorf("v2 ref (%v) does not have the correct prefix (%v)", v2String, definitionPrefix)
653 }
654 }
655
View as plain text