1
16
17 package openapi
18
19 import (
20 "context"
21 "io"
22 "net/http"
23 "net/http/httptest"
24 "testing"
25 "time"
26
27 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
29 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
30 "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
31 "k8s.io/kube-openapi/pkg/handler"
32
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/util/wait"
35
36 "k8s.io/kube-openapi/pkg/validation/spec"
37 )
38
39 func TestBasicAddRemove(t *testing.T) {
40 env, ctx := setup(t)
41 env.runFunc()
42 defer env.cleanFunc()
43
44 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
45 env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
46 s := env.fetchOpenAPIOrDie()
47 env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
48 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
49
50 t.Logf("Removing CRD %s", coolFooCRD.Name)
51 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Delete(ctx, coolFooCRD.Name, metav1.DeleteOptions{})
52 env.pollForPathNotExists("/apis/stable.example.com/v1/coolfoos")
53 s = env.fetchOpenAPIOrDie()
54 env.expectNoPath(s, "/apis/stable.example.com/v1/coolfoos")
55 }
56
57 func TestTwoCRDsSameGroup(t *testing.T) {
58 env, ctx := setup(t)
59 env.runFunc()
60 defer env.cleanFunc()
61
62 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
63 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolBarCRD, metav1.CreateOptions{})
64 env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
65 env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
66 s := env.fetchOpenAPIOrDie()
67 env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
68 env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
69 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
70 }
71
72 func TestCRDMultiVersion(t *testing.T) {
73 env, ctx := setup(t)
74 env.runFunc()
75 defer env.cleanFunc()
76
77 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolMultiVersion, metav1.CreateOptions{})
78 env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
79 env.pollForPathExists("/apis/stable.example.com/v1beta1/coolbars")
80 s := env.fetchOpenAPIOrDie()
81 env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
82 env.expectPath(s, "/apis/stable.example.com/v1beta1/coolbars")
83 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
84 }
85
86 func TestCRDMultiVersionUpdate(t *testing.T) {
87 env, ctx := setup(t)
88 env.runFunc()
89 defer env.cleanFunc()
90
91 crd, _ := env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolMultiVersion, metav1.CreateOptions{})
92 env.pollForPathExists("/apis/stable.example.com/v1/coolbars")
93 env.pollForPathExists("/apis/stable.example.com/v1beta1/coolbars")
94 s := env.fetchOpenAPIOrDie()
95 env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
96 env.expectPath(s, "/apis/stable.example.com/v1beta1/coolbars")
97 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
98
99 t.Log("Removing version v1beta1")
100 crd.Spec.Versions = crd.Spec.Versions[1:]
101 crd.Generation += 1
102
103 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd, metav1.UpdateOptions{})
104 env.pollForPathNotExists("/apis/stable.example.com/v1beta1/coolbars")
105 s = env.fetchOpenAPIOrDie()
106 env.expectPath(s, "/apis/stable.example.com/v1/coolbars")
107 env.expectNoPath(s, "/apis/stable.example.com/v1beta1/coolbars")
108 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
109 }
110
111 func TestExistingCRDBeforeAPIServerStart(t *testing.T) {
112 env, ctx := setup(t)
113 defer env.cleanFunc()
114
115 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
116 env.runFunc()
117 env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
118 s := env.fetchOpenAPIOrDie()
119
120 env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
121 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
122 }
123
124 func TestUpdate(t *testing.T) {
125 env, ctx := setup(t)
126 env.runFunc()
127 defer env.cleanFunc()
128
129 crd, _ := env.Interface.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, coolFooCRD, metav1.CreateOptions{})
130 env.pollForPathExists("/apis/stable.example.com/v1/coolfoos")
131 s := env.fetchOpenAPIOrDie()
132 env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
133 env.expectPath(s, "/apis/apiextensions.k8s.io/v1")
134
135 t.Log("Updating CRD CoolFoo")
136 crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"] = v1.JSONSchemaProps{Type: "integer", Description: "updated description"}
137 crd.Generation += 1
138
139
140 env.Interface.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd, metav1.UpdateOptions{})
141 env.pollForCondition(func(s *spec.Swagger) bool {
142 return s.Definitions["com.example.stable.v1.CoolFoo"].Properties["num"].Description == crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"].Description
143 })
144 s = env.fetchOpenAPIOrDie()
145
146
147 if s.Definitions["com.example.stable.v1.CoolFoo"].Properties["num"].Description != crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["num"].Description {
148 t.Error("Error: Description not updated")
149 }
150 env.expectPath(s, "/apis/stable.example.com/v1/coolfoos")
151 }
152
153 var coolFooCRD = &v1.CustomResourceDefinition{
154 TypeMeta: metav1.TypeMeta{
155 APIVersion: "apiextensions.k8s.io/v1",
156 Kind: "CustomResourceDefinition",
157 },
158 ObjectMeta: metav1.ObjectMeta{
159 Name: "coolfoo.stable.example.com",
160 },
161 Spec: v1.CustomResourceDefinitionSpec{
162 Group: "stable.example.com",
163 Names: v1.CustomResourceDefinitionNames{
164 Plural: "coolfoos",
165 Singular: "coolfoo",
166 ShortNames: []string{"foo"},
167 Kind: "CoolFoo",
168 ListKind: "CoolFooList",
169 },
170 Scope: v1.ClusterScoped,
171 Versions: []v1.CustomResourceDefinitionVersion{
172 {
173 Name: "v1",
174 Served: true,
175 Storage: true,
176 Deprecated: false,
177 Subresources: &v1.CustomResourceSubresources{
178
179 Status: &v1.CustomResourceSubresourceStatus{},
180 },
181 Schema: &v1.CustomResourceValidation{
182 OpenAPIV3Schema: &v1.JSONSchemaProps{
183 Type: "object",
184 Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
185 },
186 },
187 },
188 },
189 Conversion: &v1.CustomResourceConversion{},
190 },
191 Status: v1.CustomResourceDefinitionStatus{
192 Conditions: []v1.CustomResourceDefinitionCondition{
193 {
194 Type: v1.Established,
195 Status: v1.ConditionTrue,
196 },
197 },
198 },
199 }
200
201 var coolBarCRD = &v1.CustomResourceDefinition{
202 TypeMeta: metav1.TypeMeta{
203 APIVersion: "apiextensions.k8s.io/v1",
204 Kind: "CustomResourceDefinition",
205 },
206 ObjectMeta: metav1.ObjectMeta{
207 Name: "coolbar.stable.example.com",
208 },
209 Spec: v1.CustomResourceDefinitionSpec{
210 Group: "stable.example.com",
211 Names: v1.CustomResourceDefinitionNames{
212 Plural: "coolbars",
213 Singular: "coolbar",
214 ShortNames: []string{"bar"},
215 Kind: "CoolBar",
216 ListKind: "CoolBarList",
217 },
218 Scope: v1.ClusterScoped,
219 Versions: []v1.CustomResourceDefinitionVersion{
220 {
221 Name: "v1",
222 Served: true,
223 Storage: true,
224 Deprecated: false,
225 Subresources: &v1.CustomResourceSubresources{
226
227 Status: &v1.CustomResourceSubresourceStatus{},
228 },
229 Schema: &v1.CustomResourceValidation{
230 OpenAPIV3Schema: &v1.JSONSchemaProps{
231 Type: "object",
232 Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
233 },
234 },
235 },
236 },
237 Conversion: &v1.CustomResourceConversion{},
238 },
239 Status: v1.CustomResourceDefinitionStatus{
240 Conditions: []v1.CustomResourceDefinitionCondition{
241 {
242 Type: v1.Established,
243 Status: v1.ConditionTrue,
244 },
245 },
246 },
247 }
248
249 var coolMultiVersion = &v1.CustomResourceDefinition{
250 TypeMeta: metav1.TypeMeta{
251 APIVersion: "apiextensions.k8s.io/v1",
252 Kind: "CustomResourceDefinition",
253 },
254 ObjectMeta: metav1.ObjectMeta{
255 Name: "coolbar.stable.example.com",
256 },
257 Spec: v1.CustomResourceDefinitionSpec{
258 Group: "stable.example.com",
259 Names: v1.CustomResourceDefinitionNames{
260 Plural: "coolbars",
261 Singular: "coolbar",
262 ShortNames: []string{"bar"},
263 Kind: "CoolBar",
264 ListKind: "CoolBarList",
265 },
266 Scope: v1.ClusterScoped,
267 Versions: []v1.CustomResourceDefinitionVersion{
268 {
269 Name: "v1beta1",
270 Served: true,
271 Storage: true,
272 Deprecated: false,
273 Subresources: &v1.CustomResourceSubresources{
274
275 Status: &v1.CustomResourceSubresourceStatus{},
276 },
277 Schema: &v1.CustomResourceValidation{
278 OpenAPIV3Schema: &v1.JSONSchemaProps{
279 Type: "object",
280 Properties: map[string]v1.JSONSchemaProps{"num": {Type: "integer", Description: "description"}},
281 },
282 },
283 },
284
285 {
286 Name: "v1",
287 Served: true,
288 Storage: true,
289 Deprecated: false,
290 Subresources: &v1.CustomResourceSubresources{
291
292 Status: &v1.CustomResourceSubresourceStatus{},
293 },
294 Schema: &v1.CustomResourceValidation{
295 OpenAPIV3Schema: &v1.JSONSchemaProps{
296 Type: "object",
297 Properties: map[string]v1.JSONSchemaProps{"test": {Type: "integer", Description: "foo"}},
298 },
299 },
300 },
301 },
302 Conversion: &v1.CustomResourceConversion{},
303 },
304 Status: v1.CustomResourceDefinitionStatus{
305 Conditions: []v1.CustomResourceDefinitionCondition{
306 {
307 Type: v1.Established,
308 Status: v1.ConditionTrue,
309 },
310 },
311 },
312 }
313
314 type testEnv struct {
315 t *testing.T
316 clientset.Interface
317 mux *http.ServeMux
318 cleanFunc func()
319 runFunc func()
320 }
321
322 func setup(t *testing.T) (*testEnv, context.Context) {
323 env := &testEnv{
324 Interface: fake.NewSimpleClientset(),
325 t: t,
326 }
327
328 factory := externalversions.NewSharedInformerFactoryWithOptions(
329 env.Interface, 30*time.Second)
330
331 c := NewController(factory.Apiextensions().V1().CustomResourceDefinitions())
332 ctx, cancel := context.WithCancel(context.Background())
333
334 factory.Start(ctx.Done())
335 factory.WaitForCacheSync(ctx.Done())
336
337 env.mux = http.NewServeMux()
338 h := handler.NewOpenAPIService(&spec.Swagger{})
339 h.RegisterOpenAPIVersionedService("/openapi/v2", env.mux)
340
341 stopCh := make(chan struct{})
342
343 env.runFunc = func() {
344 go c.Run(&spec.Swagger{
345 SwaggerProps: spec.SwaggerProps{
346 Paths: &spec.Paths{
347 Paths: map[string]spec.PathItem{
348 "/apis/apiextensions.k8s.io/v1": {},
349 },
350 },
351 },
352 }, h, stopCh)
353 }
354
355 env.cleanFunc = func() {
356 cancel()
357 close(stopCh)
358 }
359 return env, ctx
360 }
361
362 func (t *testEnv) pollForCondition(conditionFunc func(*spec.Swagger) bool) {
363 wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
364 openapi := t.fetchOpenAPIOrDie()
365 if conditionFunc(openapi) {
366 return true, nil
367 }
368 return false, nil
369 })
370 }
371
372 func (t *testEnv) pollForPathExists(path string) {
373 wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
374 openapi := t.fetchOpenAPIOrDie()
375 if _, ok := openapi.Paths.Paths[path]; !ok {
376 return false, nil
377 }
378 return true, nil
379 })
380 }
381
382 func (t *testEnv) pollForPathNotExists(path string) {
383 wait.Poll(time.Second*1, wait.ForeverTestTimeout, func() (bool, error) {
384 openapi := t.fetchOpenAPIOrDie()
385 if _, ok := openapi.Paths.Paths[path]; ok {
386 return false, nil
387 }
388 return true, nil
389 })
390 }
391
392 func (t *testEnv) fetchOpenAPIOrDie() *spec.Swagger {
393 server := httptest.NewServer(t.mux)
394 defer server.Close()
395 client := server.Client()
396
397 req, err := http.NewRequest("GET", server.URL+"/openapi/v2", nil)
398 if err != nil {
399 t.t.Error(err)
400 }
401 resp, err := client.Do(req)
402 if err != nil {
403 t.t.Error(err)
404 }
405 body, err := io.ReadAll(resp.Body)
406 if err != nil {
407 t.t.Error(err)
408 }
409 swagger := &spec.Swagger{}
410 if err := swagger.UnmarshalJSON(body); err != nil {
411 t.t.Error(err)
412 }
413 return swagger
414 }
415
416 func (t *testEnv) expectPath(swagger *spec.Swagger, path string) {
417 if _, ok := swagger.Paths.Paths[path]; !ok {
418 t.t.Errorf("Expected path %s to exist in OpenAPI", path)
419 }
420 }
421
422 func (t *testEnv) expectNoPath(swagger *spec.Swagger, path string) {
423 if _, ok := swagger.Paths.Paths[path]; ok {
424 t.t.Errorf("Expected path %s to not exist in OpenAPI", path)
425 }
426 }
427
View as plain text