1
16
17 package apimachinery
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "io"
24 "net/http"
25 "regexp"
26 "strings"
27 "time"
28
29 "github.com/onsi/ginkgo/v2"
30 "sigs.k8s.io/yaml"
31
32 openapiutil "k8s.io/kube-openapi/pkg/util"
33 "k8s.io/utils/pointer"
34
35 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
36 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
37 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
38 apiequality "k8s.io/apimachinery/pkg/api/equality"
39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40 "k8s.io/apimachinery/pkg/types"
41 "k8s.io/apimachinery/pkg/util/wait"
42 k8sclientset "k8s.io/client-go/kubernetes"
43 "k8s.io/client-go/rest"
44 "k8s.io/kube-openapi/pkg/validation/spec"
45 "k8s.io/kubernetes/test/e2e/framework"
46 e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
47 "k8s.io/kubernetes/test/utils/crd"
48 admissionapi "k8s.io/pod-security-admission/api"
49 )
50
51 var (
52 metaPattern = `"kind":"%s","apiVersion":"%s/%s","metadata":{"name":"%s"}`
53 )
54
55 var _ = SIGDescribe("CustomResourcePublishOpenAPI [Privileged:ClusterAdmin]", func() {
56 f := framework.NewDefaultFramework("crd-publish-openapi")
57 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
58
59
70 framework.ConformanceIt("works for CRD with validation schema", func(ctx context.Context) {
71 crd, err := setupCRD(f, schemaFoo, "foo", "v1")
72 if err != nil {
73 framework.Failf("%v", err)
74 }
75
76 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-foo")
77 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name)
78
79 ginkgo.By("kubectl validation (kubectl create and apply) allows request with known and required properties")
80 validCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar"}]}}`, meta)
81 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, validCR, ns, "create", "-f", "-"); err != nil {
82 framework.Failf("failed to create valid CR %s: %v", validCR, err)
83 }
84 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-foo"); err != nil {
85 framework.Failf("failed to delete valid CR: %v", err)
86 }
87 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, validCR, ns, "apply", "-f", "-"); err != nil {
88 framework.Failf("failed to apply valid CR %s: %v", validCR, err)
89 }
90 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-foo"); err != nil {
91 framework.Failf("failed to delete valid CR: %v", err)
92 }
93
94 ginkgo.By("kubectl validation (kubectl create and apply) rejects request with value outside defined enum values")
95 badEnumValueCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar", "feeling":"NonExistentValue"}]}}`, meta)
96 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, badEnumValueCR, ns, "create", "-f", "-"); err == nil || !strings.Contains(err.Error(), `Unsupported value: "NonExistentValue"`) {
97 framework.Failf("unexpected no error when creating CR with unknown enum value: %v", err)
98 }
99
100
101
102 ginkgo.By("kubectl validation (kubectl create and apply) rejects request with unknown properties when disallowed by the schema")
103 unknownCR := fmt.Sprintf(`{%s,"spec":{"foo":true}}`, meta)
104 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, unknownCR, ns, "create", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `unknown field "foo"`) && !strings.Contains(err.Error(), `unknown field "spec.foo"`)) {
105 framework.Failf("unexpected no error when creating CR with unknown field: %v", err)
106 }
107 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, unknownCR, ns, "apply", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `unknown field "foo"`) && !strings.Contains(err.Error(), `unknown field "spec.foo"`)) {
108 framework.Failf("unexpected no error when applying CR with unknown field: %v", err)
109 }
110
111
112 ginkgo.By("kubectl validation (kubectl create and apply) rejects request without required properties")
113 noRequireCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"age":"10"}]}}`, meta)
114 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, noRequireCR, ns, "create", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `missing required field "name"`) && !strings.Contains(err.Error(), `spec.bars[0].name: Required value`)) {
115 framework.Failf("unexpected no error when creating CR without required field: %v", err)
116 }
117 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, noRequireCR, ns, "apply", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `missing required field "name"`) && !strings.Contains(err.Error(), `spec.bars[0].name: Required value`)) {
118 framework.Failf("unexpected no error when applying CR without required field: %v", err)
119 }
120
121 ginkgo.By("kubectl explain works to explain CR properties")
122 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*Foo CRD for Testing.*FIELDS:.*apiVersion.*<string>.*APIVersion defines.*spec.*<Object>.*Specification of Foo`); err != nil {
123 framework.Failf("%v", err)
124 }
125
126 ginkgo.By("kubectl explain works to explain CR properties recursively")
127 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".metadata", `(?s)DESCRIPTION:.*Standard object's metadata.*FIELDS:.*creationTimestamp.*<string>.*CreationTimestamp is a timestamp`); err != nil {
128 framework.Failf("%v", err)
129 }
130 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".spec", `(?s)DESCRIPTION:.*Specification of Foo.*FIELDS:.*bars.*<\[\]Object>.*List of Bars and their specs`); err != nil {
131 framework.Failf("%v", err)
132 }
133 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".spec.bars", `(?s)(FIELD|RESOURCE):.*bars.*<\[\]Object>.*DESCRIPTION:.*List of Bars and their specs.*FIELDS:.*bazs.*<\[\]string>.*List of Bazs.*name.*<string>.*Name of Bar`); err != nil {
134 framework.Failf("%v", err)
135 }
136
137 ginkgo.By("kubectl explain works to return error when explain is called on property that doesn't exist")
138 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, "explain", crd.Crd.Spec.Names.Plural+".spec.bars2"); err == nil || !strings.Contains(err.Error(), `field "bars2" does not exist`) {
139 framework.Failf("unexpected no error when explaining property that doesn't exist: %v", err)
140 }
141
142 if err := cleanupCRD(ctx, f, crd); err != nil {
143 framework.Failf("%v", err)
144 }
145 })
146
147
154 framework.ConformanceIt("works for CRD without validation schema", func(ctx context.Context) {
155 crd, err := setupCRD(f, nil, "empty", "v1")
156 if err != nil {
157 framework.Failf("%v", err)
158 }
159
160 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr")
161 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name)
162
163 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties")
164 randomCR := fmt.Sprintf(`{%s,"a":{"b":[{"c":"d"}]}}`, meta)
165 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil {
166 framework.Failf("failed to create random CR %s for CRD without schema: %v", randomCR, err)
167 }
168 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
169 framework.Failf("failed to delete random CR: %v", err)
170 }
171 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil {
172 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err)
173 }
174 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
175 framework.Failf("failed to delete random CR: %v", err)
176 }
177
178 ginkgo.By("kubectl explain works to explain CR without validation schema")
179 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*<empty>`); err != nil {
180 framework.Failf("%v", err)
181 }
182
183 if err := cleanupCRD(ctx, f, crd); err != nil {
184 framework.Failf("%v", err)
185 }
186 })
187
188
195 framework.ConformanceIt("works for CRD preserving unknown fields at the schema root", func(ctx context.Context) {
196 crd, err := setupCRDAndVerifySchema(f, schemaPreserveRoot, nil, "unknown-at-root", "v1")
197 if err != nil {
198 framework.Failf("%v", err)
199 }
200
201 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr")
202 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name)
203
204 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties")
205 randomCR := fmt.Sprintf(`{%s,"a":{"b":[{"c":"d"}]}}`, meta)
206 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil {
207 framework.Failf("failed to create random CR %s for CRD that allows unknown properties at the root: %v", randomCR, err)
208 }
209 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
210 framework.Failf("failed to delete random CR: %v", err)
211 }
212 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil {
213 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err)
214 }
215 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
216 framework.Failf("failed to delete random CR: %v", err)
217 }
218
219 ginkgo.By("kubectl explain works to explain CR")
220 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, fmt.Sprintf(`(?s)KIND:.*%s`, crd.Crd.Spec.Names.Kind)); err != nil {
221 framework.Failf("%v", err)
222 }
223
224 if err := cleanupCRD(ctx, f, crd); err != nil {
225 framework.Failf("%v", err)
226 }
227 })
228
229
237 framework.ConformanceIt("works for CRD preserving unknown fields in an embedded object", func(ctx context.Context) {
238 crd, err := setupCRDAndVerifySchema(f, schemaPreserveNested, nil, "unknown-in-nested", "v1")
239 if err != nil {
240 framework.Failf("%v", err)
241 }
242
243 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr")
244 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name)
245
246 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties")
247 randomCR := fmt.Sprintf(`{%s,"spec":{"a":null,"b":[{"c":"d"}]}}`, meta)
248 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil {
249 framework.Failf("failed to create random CR %s for CRD that allows unknown properties in a nested object: %v", randomCR, err)
250 }
251 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
252 framework.Failf("failed to delete random CR: %v", err)
253 }
254 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil {
255 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err)
256 }
257 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil {
258 framework.Failf("failed to delete random CR: %v", err)
259 }
260
261 ginkgo.By("kubectl explain works to explain CR")
262 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*preserve-unknown-properties in nested field for Testing`); err != nil {
263 framework.Failf("%v", err)
264 }
265
266 if err := cleanupCRD(ctx, f, crd); err != nil {
267 framework.Failf("%v", err)
268 }
269 })
270
271
277 framework.ConformanceIt("works for multiple CRDs of different groups", func(ctx context.Context) {
278 ginkgo.By("CRs in different groups (two CRDs) show up in OpenAPI documentation")
279 crdFoo, err := setupCRD(f, schemaFoo, "foo", "v1")
280 if err != nil {
281 framework.Failf("%v", err)
282 }
283 crdWaldo, err := setupCRD(f, schemaWaldo, "waldo", "v1beta1")
284 if err != nil {
285 framework.Failf("%v", err)
286 }
287 if crdFoo.Crd.Spec.Group == crdWaldo.Crd.Spec.Group {
288 framework.Failf("unexpected: CRDs should be of different group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group)
289 }
290 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v1beta1"), schemaWaldo); err != nil {
291 framework.Failf("%v", err)
292 }
293 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v1"), schemaFoo); err != nil {
294 framework.Failf("%v", err)
295 }
296 if err := cleanupCRD(ctx, f, crdFoo); err != nil {
297 framework.Failf("%v", err)
298 }
299 if err := cleanupCRD(ctx, f, crdWaldo); err != nil {
300 framework.Failf("%v", err)
301 }
302 })
303
304
310 framework.ConformanceIt("works for multiple CRDs of same group but different versions", func(ctx context.Context) {
311 ginkgo.By("CRs in the same group but different versions (one multiversion CRD) show up in OpenAPI documentation")
312 crdMultiVer, err := setupCRD(f, schemaFoo, "multi-ver", "v2", "v3")
313 if err != nil {
314 framework.Failf("%v", err)
315 }
316 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v3"), schemaFoo); err != nil {
317 framework.Failf("%v", err)
318 }
319 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil {
320 framework.Failf("%v", err)
321 }
322 if err := cleanupCRD(ctx, f, crdMultiVer); err != nil {
323 framework.Failf("%v", err)
324 }
325
326 ginkgo.By("CRs in the same group but different versions (two CRDs) show up in OpenAPI documentation")
327 crdFoo, err := setupCRD(f, schemaFoo, "common-group", "v4")
328 if err != nil {
329 framework.Failf("%v", err)
330 }
331 crdWaldo, err := setupCRD(f, schemaWaldo, "common-group", "v5")
332 if err != nil {
333 framework.Failf("%v", err)
334 }
335 if crdFoo.Crd.Spec.Group != crdWaldo.Crd.Spec.Group {
336 framework.Failf("unexpected: CRDs should be of the same group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group)
337 }
338 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v5"), schemaWaldo); err != nil {
339 framework.Failf("%v", err)
340 }
341 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v4"), schemaFoo); err != nil {
342 framework.Failf("%v", err)
343 }
344 if err := cleanupCRD(ctx, f, crdFoo); err != nil {
345 framework.Failf("%v", err)
346 }
347 if err := cleanupCRD(ctx, f, crdWaldo); err != nil {
348 framework.Failf("%v", err)
349 }
350 })
351
352
358 framework.ConformanceIt("works for multiple CRDs of same group and version but different kinds", func(ctx context.Context) {
359 ginkgo.By("CRs in the same group and version but different kinds (two CRDs) show up in OpenAPI documentation")
360 crdFoo, err := setupCRD(f, schemaFoo, "common-group", "v6")
361 if err != nil {
362 framework.Failf("%v", err)
363 }
364 crdWaldo, err := setupCRD(f, schemaWaldo, "common-group", "v6")
365 if err != nil {
366 framework.Failf("%v", err)
367 }
368 if crdFoo.Crd.Spec.Group != crdWaldo.Crd.Spec.Group {
369 framework.Failf("unexpected: CRDs should be of the same group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group)
370 }
371 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v6"), schemaWaldo); err != nil {
372 framework.Failf("%v", err)
373 }
374 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v6"), schemaFoo); err != nil {
375 framework.Failf("%v", err)
376 }
377 if err := cleanupCRD(ctx, f, crdFoo); err != nil {
378 framework.Failf("%v", err)
379 }
380 if err := cleanupCRD(ctx, f, crdWaldo); err != nil {
381 framework.Failf("%v", err)
382 }
383 })
384
385
392 framework.ConformanceIt("updates the published spec when one version gets renamed", func(ctx context.Context) {
393 ginkgo.By("set up a multi version CRD")
394 crdMultiVer, err := setupCRD(f, schemaFoo, "multi-ver", "v2", "v3")
395 if err != nil {
396 framework.Failf("%v", err)
397 }
398 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v3"), schemaFoo); err != nil {
399 framework.Failf("%v", err)
400 }
401 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil {
402 framework.Failf("%v", err)
403 }
404
405 ginkgo.By("rename a version")
406 patch := []byte(`[
407 {"op":"test","path":"/spec/versions/1/name","value":"v3"},
408 {"op": "replace", "path": "/spec/versions/1/name", "value": "v4"}
409 ]`)
410 crdMultiVer.Crd, err = crdMultiVer.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(ctx, crdMultiVer.Crd.Name, types.JSONPatchType, patch, metav1.PatchOptions{})
411 if err != nil {
412 framework.Failf("%v", err)
413 }
414
415 ginkgo.By("check the new version name is served")
416 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v4"), schemaFoo); err != nil {
417 framework.Failf("%v", err)
418 }
419 ginkgo.By("check the old version name is removed")
420 if err := waitForDefinitionCleanup(f.ClientSet, definitionName(crdMultiVer, "v3")); err != nil {
421 framework.Failf("%v", err)
422 }
423 ginkgo.By("check the other version is not changed")
424 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil {
425 framework.Failf("%v", err)
426 }
427
428
429
430 crdMultiVer.Crd.Spec.Versions[1].Name = "v4"
431 if err := cleanupCRD(ctx, f, crdMultiVer); err != nil {
432 framework.Failf("%v", err)
433 }
434 })
435
436
443 framework.ConformanceIt("removes definition from spec when one version gets changed to not be served", func(ctx context.Context) {
444 ginkgo.By("set up a multi version CRD")
445 crd, err := setupCRD(f, schemaFoo, "multi-to-single-ver", "v5", "v6alpha1")
446 if err != nil {
447 framework.Failf("%v", err)
448 }
449
450 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v6alpha1"), schemaFoo); err != nil {
451 framework.Failf("%v", err)
452 }
453 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v5"), schemaFoo); err != nil {
454 framework.Failf("%v", err)
455 }
456
457 ginkgo.By("mark a version not serverd")
458 crd.Crd, err = crd.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Crd.Name, metav1.GetOptions{})
459 if err != nil {
460 framework.Failf("%v", err)
461 }
462 crd.Crd.Spec.Versions[1].Served = false
463 crd.Crd, err = crd.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd.Crd, metav1.UpdateOptions{})
464 if err != nil {
465 framework.Failf("%v", err)
466 }
467
468 ginkgo.By("check the unserved version gets removed")
469 if err := waitForDefinitionCleanup(f.ClientSet, definitionName(crd, "v6alpha1")); err != nil {
470 framework.Failf("%v", err)
471 }
472 ginkgo.By("check the other version is not changed")
473 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v5"), schemaFoo); err != nil {
474 framework.Failf("%v", err)
475 }
476
477 if err := cleanupCRD(ctx, f, crd); err != nil {
478 framework.Failf("%v", err)
479 }
480 })
481
482
483 f.It(f.WithFlaky(), "kubectl explain works for CR with the same resource name as built-in object.", func(ctx context.Context) {
484 customServiceShortName := fmt.Sprintf("ksvc-%d", time.Now().Unix())
485 opt := func(crd *apiextensionsv1.CustomResourceDefinition) {
486 crd.ObjectMeta = metav1.ObjectMeta{Name: "services." + crd.Spec.Group}
487 crd.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{
488 Plural: "services",
489 Singular: "service",
490 ListKind: "ServiceList",
491 Kind: "Service",
492 ShortNames: []string{customServiceShortName},
493 }
494 }
495 crdSvc, err := setupCRDAndVerifySchemaWithOptions(f, schemaCustomService, schemaCustomService, "service", []string{"v1"}, opt)
496 if err != nil {
497 framework.Failf("%v", err)
498 }
499
500 if err := verifyKubectlExplain(f.Namespace.Name, customServiceShortName+".spec", `(?s)DESCRIPTION:.*Specification of CustomService.*FIELDS:.*dummy.*<string>.*Dummy property`); err != nil {
501 _ = cleanupCRD(ctx, f, crdSvc)
502 framework.Failf("%v", err)
503 }
504
505 if err := cleanupCRD(ctx, f, crdSvc); err != nil {
506 framework.Failf("%v", err)
507 }
508 })
509 })
510
511 func setupCRD(f *framework.Framework, schema []byte, groupSuffix string, versions ...string) (*crd.TestCrd, error) {
512 expect := schema
513 if schema == nil {
514
515
516 expect = []byte(`type: object`)
517 }
518 return setupCRDAndVerifySchema(f, schema, expect, groupSuffix, versions...)
519 }
520
521 func setupCRDAndVerifySchema(f *framework.Framework, schema, expect []byte, groupSuffix string, versions ...string) (*crd.TestCrd, error) {
522 return setupCRDAndVerifySchemaWithOptions(f, schema, expect, groupSuffix, versions)
523 }
524
525 func setupCRDAndVerifySchemaWithOptions(f *framework.Framework, schema, expect []byte, groupSuffix string, versions []string, options ...crd.Option) (*crd.TestCrd, error) {
526 group := fmt.Sprintf("%s-test-%s.example.com", f.BaseName, groupSuffix)
527 if len(versions) == 0 {
528 return nil, fmt.Errorf("require at least one version for CRD")
529 }
530
531 props := &apiextensionsv1.JSONSchemaProps{}
532 if schema != nil {
533 if err := yaml.Unmarshal(schema, props); err != nil {
534 return nil, err
535 }
536 }
537
538 options = append(options, func(crd *apiextensionsv1.CustomResourceDefinition) {
539 var apiVersions []apiextensionsv1.CustomResourceDefinitionVersion
540 for i, version := range versions {
541 version := apiextensionsv1.CustomResourceDefinitionVersion{
542 Name: version,
543 Served: true,
544 Storage: i == 0,
545 }
546
547 if schema != nil {
548 version.Schema = &apiextensionsv1.CustomResourceValidation{
549 OpenAPIV3Schema: props,
550 }
551 } else {
552 version.Schema = &apiextensionsv1.CustomResourceValidation{
553 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
554 XPreserveUnknownFields: pointer.BoolPtr(true),
555 Type: "object",
556 },
557 }
558 }
559 apiVersions = append(apiVersions, version)
560 }
561 crd.Spec.Versions = apiVersions
562 })
563 crd, err := crd.CreateMultiVersionTestCRD(f, group, options...)
564 if err != nil {
565 return nil, fmt.Errorf("failed to create CRD: %w", err)
566 }
567
568 for _, v := range crd.Crd.Spec.Versions {
569 if err := waitForDefinition(f.ClientSet, definitionName(crd, v.Name), expect); err != nil {
570 return nil, fmt.Errorf("%v", err)
571 }
572 }
573 return crd, nil
574 }
575
576 func cleanupCRD(ctx context.Context, f *framework.Framework, crd *crd.TestCrd) error {
577 _ = crd.CleanUp(ctx)
578 for _, v := range crd.Crd.Spec.Versions {
579 name := definitionName(crd, v.Name)
580 if err := waitForDefinitionCleanup(f.ClientSet, name); err != nil {
581 return fmt.Errorf("%v", err)
582 }
583 }
584 return nil
585 }
586
587 const waitSuccessThreshold = 10
588
589
590
591
592 func mustSucceedMultipleTimes(n int, f func() (bool, error)) func() (bool, error) {
593 return func() (bool, error) {
594 for i := 0; i < n; i++ {
595 ok, err := f()
596 if err != nil || !ok {
597 return ok, err
598 }
599 }
600 return true, nil
601 }
602 }
603
604
605
606 func waitForDefinition(c k8sclientset.Interface, name string, schema []byte) error {
607 expect := spec.Schema{}
608 if err := convertJSONSchemaProps(schema, &expect); err != nil {
609 return err
610 }
611
612 err := waitForOpenAPISchema(c, func(spec *spec.Swagger) (bool, string) {
613 d, ok := spec.SwaggerProps.Definitions[name]
614 if !ok {
615 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] not found", name)
616 }
617 if schema != nil {
618
619 dropDefaults(&d)
620 if !apiequality.Semantic.DeepEqual(expect, d) {
621 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] not match; expect: %v, actual: %v", name, expect, d)
622 }
623 }
624 return true, ""
625 })
626 if err != nil {
627 return fmt.Errorf("failed to wait for definition %q to be served with the right OpenAPI schema: %w", name, err)
628 }
629 return nil
630 }
631
632
633 func waitForDefinitionCleanup(c k8sclientset.Interface, name string) error {
634 err := waitForOpenAPISchema(c, func(spec *spec.Swagger) (bool, string) {
635 if _, ok := spec.SwaggerProps.Definitions[name]; ok {
636 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] still exists", name)
637 }
638 return true, ""
639 })
640 if err != nil {
641 return fmt.Errorf("failed to wait for definition %q not to be served anymore: %w", name, err)
642 }
643 return nil
644 }
645
646 func waitForOpenAPISchema(c k8sclientset.Interface, pred func(*spec.Swagger) (bool, string)) error {
647 client := c.Discovery().RESTClient().(*rest.RESTClient).Client
648 url := c.Discovery().RESTClient().Get().AbsPath("openapi", "v2").URL()
649 lastMsg := ""
650 etag := ""
651 var etagSpec *spec.Swagger
652 if err := wait.Poll(500*time.Millisecond, 60*time.Second, mustSucceedMultipleTimes(waitSuccessThreshold, func() (bool, error) {
653
654 spec := &spec.Swagger{}
655 req, err := http.NewRequest("GET", url.String(), nil)
656 if err != nil {
657 return false, err
658 }
659 req.Close = true
660 if len(etag) > 0 {
661 req.Header.Set("If-None-Match", fmt.Sprintf(`"%s"`, etag))
662 }
663 resp, err := client.Do(req)
664 if err != nil {
665 return false, err
666 }
667 defer resp.Body.Close()
668 if resp.StatusCode == http.StatusNotModified {
669 spec = etagSpec
670 } else if resp.StatusCode != http.StatusOK {
671 return false, fmt.Errorf("unexpected response: %d", resp.StatusCode)
672 } else if bs, err := io.ReadAll(resp.Body); err != nil {
673 return false, err
674 } else if err := json.Unmarshal(bs, spec); err != nil {
675 return false, err
676 } else {
677 etag = strings.Trim(resp.Header.Get("ETag"), `"`)
678 etagSpec = spec
679 }
680
681 var ok bool
682 ok, lastMsg = pred(spec)
683 return ok, nil
684 })); err != nil {
685 return fmt.Errorf("failed to wait for OpenAPI spec validating condition: %v; lastMsg: %s", err, lastMsg)
686 }
687 return nil
688 }
689
690
691 func convertJSONSchemaProps(in []byte, out *spec.Schema) error {
692 external := apiextensionsv1.JSONSchemaProps{}
693 if err := yaml.UnmarshalStrict(in, &external); err != nil {
694 return err
695 }
696 internal := apiextensions.JSONSchemaProps{}
697 if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(&external, &internal, nil); err != nil {
698 return err
699 }
700 kubeOut := spec.Schema{}
701 if err := validation.ConvertJSONSchemaPropsWithPostProcess(&internal, &kubeOut, validation.StripUnsupportedFormatsPostProcess); err != nil {
702 return err
703 }
704 bs, err := json.Marshal(kubeOut)
705 if err != nil {
706 return err
707 }
708 return json.Unmarshal(bs, out)
709 }
710
711
712 func dropDefaults(s *spec.Schema) {
713 delete(s.Properties, "metadata")
714 delete(s.Properties, "apiVersion")
715 delete(s.Properties, "kind")
716 delete(s.Extensions, "x-kubernetes-group-version-kind")
717 delete(s.Extensions, "x-kubernetes-selectable-fields")
718 }
719
720 func verifyKubectlExplain(ns, name, pattern string) error {
721 result, err := e2ekubectl.RunKubectl(ns, "explain", name)
722 if err != nil {
723 return fmt.Errorf("failed to explain %s: %w", name, err)
724 }
725 r := regexp.MustCompile(pattern)
726 if !r.Match([]byte(result)) {
727 return fmt.Errorf("kubectl explain %s result {%s} doesn't match pattern {%s}", name, result, pattern)
728 }
729 return nil
730 }
731
732
733 func definitionName(crd *crd.TestCrd, version string) string {
734 return openapiutil.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", crd.Crd.Spec.Group, version, crd.Crd.Spec.Names.Kind))
735 }
736
737 var schemaFoo = []byte(`description: Foo CRD for Testing
738 type: object
739 properties:
740 spec:
741 type: object
742 description: Specification of Foo
743 properties:
744 bars:
745 description: List of Bars and their specs.
746 type: array
747 items:
748 type: object
749 required:
750 - name
751 properties:
752 name:
753 description: Name of Bar.
754 type: string
755 age:
756 description: Age of Bar.
757 type: string
758 feeling:
759 description: Whether Bar is feeling great.
760 type: string
761 enum:
762 - Great
763 - Down
764 bazs:
765 description: List of Bazs.
766 items:
767 type: string
768 type: array
769 status:
770 description: Status of Foo
771 type: object
772 properties:
773 bars:
774 description: List of Bars and their statuses.
775 type: array
776 items:
777 type: object
778 properties:
779 name:
780 description: Name of Bar.
781 type: string
782 available:
783 description: Whether the Bar is installed.
784 type: boolean
785 quxType:
786 description: Indicates to external qux type.
787 pattern: in-tree|out-of-tree
788 type: string`)
789
790 var schemaCustomService = []byte(`description: CustomService CRD for Testing
791 type: object
792 properties:
793 spec:
794 description: Specification of CustomService
795 type: object
796 properties:
797 dummy:
798 description: Dummy property.
799 type: string
800 `)
801
802 var schemaWaldo = []byte(`description: Waldo CRD for Testing
803 type: object
804 properties:
805 spec:
806 description: Specification of Waldo
807 type: object
808 properties:
809 dummy:
810 description: Dummy property.
811 type: object
812 status:
813 description: Status of Waldo
814 type: object
815 properties:
816 bars:
817 description: List of Bars and their statuses.
818 type: array
819 items:
820 type: object`)
821
822 var schemaPreserveRoot = []byte(`description: preserve-unknown-properties at root for Testing
823 x-kubernetes-preserve-unknown-fields: true
824 type: object
825 properties:
826 spec:
827 description: Specification of Waldo
828 type: object
829 properties:
830 dummy:
831 description: Dummy property.
832 type: object
833 status:
834 description: Status of Waldo
835 type: object
836 properties:
837 bars:
838 description: List of Bars and their statuses.
839 type: array
840 items:
841 type: object`)
842
843 var schemaPreserveNested = []byte(`description: preserve-unknown-properties in nested field for Testing
844 type: object
845 properties:
846 spec:
847 description: Specification of Waldo
848 type: object
849 x-kubernetes-preserve-unknown-fields: true
850 properties:
851 dummy:
852 description: Dummy property.
853 type: object
854 status:
855 description: Status of Waldo
856 type: object
857 properties:
858 bars:
859 description: List of Bars and their statuses.
860 type: array
861 items:
862 type: object`)
863
View as plain text