1
16
17 package integration
18
19 import (
20 "context"
21 "fmt"
22 "testing"
23 "time"
24
25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27 metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/runtime/schema"
30 "k8s.io/apimachinery/pkg/runtime/serializer"
31 "k8s.io/apimachinery/pkg/types"
32 "k8s.io/client-go/dynamic"
33 "k8s.io/client-go/rest"
34
35 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
36 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
37 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
38 )
39
40 func newTableCRD() *apiextensionsv1.CustomResourceDefinition {
41 return &apiextensionsv1.CustomResourceDefinition{
42 ObjectMeta: metav1.ObjectMeta{Name: "tables.mygroup.example.com"},
43 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
44 Group: "mygroup.example.com",
45 Names: apiextensionsv1.CustomResourceDefinitionNames{
46 Plural: "tables",
47 Singular: "table",
48 Kind: "Table",
49 ListKind: "TablemList",
50 },
51 Scope: apiextensionsv1.ClusterScoped,
52 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
53 {
54 Name: "v1beta1",
55 Served: true,
56 Storage: false,
57 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
58 {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
59 {Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"},
60 {Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"},
61 {Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"},
62 {Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"},
63 },
64 Schema: fixtures.AllowAllSchema(),
65 },
66 {
67 Name: "v1",
68 Served: true,
69 Storage: true,
70 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
71 {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
72 {Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"},
73 {Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"},
74 {Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"},
75 {Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"},
76 {Name: "Zeta", Type: "integer", Description: "the zeta field", Format: "int64", Priority: 42, JSONPath: ".spec.zeta"},
77 },
78 Schema: fixtures.AllowAllSchema(),
79 },
80 },
81 },
82 }
83 }
84
85 func newTableInstance(name string) *unstructured.Unstructured {
86 return &unstructured.Unstructured{
87 Object: map[string]interface{}{
88 "apiVersion": "mygroup.example.com/v1",
89 "kind": "Table",
90 "metadata": map[string]interface{}{
91 "name": name,
92 },
93 "spec": map[string]interface{}{
94 "alpha": "foo_123",
95 "beta": 10,
96 "gamma": "bar",
97 "delta": "hello",
98 "epsilon": []int64{1, 2, 3},
99 "zeta": 5,
100 },
101 },
102 }
103 }
104
105 func TestTableGet(t *testing.T) {
106 tearDown, config, _, err := fixtures.StartDefaultServer(t)
107 if err != nil {
108 t.Fatal(err)
109 }
110 defer tearDown()
111
112 apiExtensionClient, err := clientset.NewForConfig(config)
113 if err != nil {
114 t.Fatal(err)
115 }
116
117 dynamicClient, err := dynamic.NewForConfig(config)
118 if err != nil {
119 t.Fatal(err)
120 }
121
122 crd := newTableCRD()
123 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
124 if err != nil {
125 t.Fatal(err)
126 }
127
128 crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{})
129 if err != nil {
130 t.Fatal(err)
131 }
132 t.Logf("table crd created: %#v", crd)
133
134 crClient := newNamespacedCustomResourceVersionedClient("", dynamicClient, crd, "v1")
135 foo, err := crClient.Create(context.TODO(), newTableInstance("foo"), metav1.CreateOptions{})
136 if err != nil {
137 t.Fatalf("unable to create noxu instance: %v", err)
138 }
139 t.Logf("foo created: %#v", foo.UnstructuredContent())
140
141 for i, v := range crd.Spec.Versions {
142 gv := schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
143 gvk := gv.WithKind(crd.Spec.Names.Kind)
144
145 scheme := runtime.NewScheme()
146 codecs := serializer.NewCodecFactory(scheme)
147 parameterCodec := runtime.NewParameterCodec(scheme)
148 metav1.AddToGroupVersion(scheme, gv)
149 scheme.AddKnownTypes(gv, &metav1beta1.TableOptions{})
150 scheme.AddKnownTypes(gv, &metav1.TableOptions{})
151 scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
152 scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{})
153
154 crConfig := *config
155 crConfig.GroupVersion = &gv
156 crConfig.APIPath = "/apis"
157 crConfig.NegotiatedSerializer = codecs.WithoutConversion()
158 crRestClient, err := rest.RESTClientFor(&crConfig)
159 if err != nil {
160 t.Fatal(err)
161 }
162
163
164 {
165 ret, err := crRestClient.Get().
166 Resource(crd.Spec.Names.Plural).
167 SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
168 VersionedParams(&metav1beta1.TableOptions{}, parameterCodec).
169 Do(context.TODO()).
170 Get()
171 if err != nil {
172 t.Fatalf("failed to list %v resources: %v", gvk, err)
173 }
174
175 tbl, ok := ret.(*metav1beta1.Table)
176 if !ok {
177 t.Fatalf("expected metav1beta1.Table, got %T", ret)
178 }
179 t.Logf("%v table list: %#v", gvk, tbl)
180
181 columns, err := getColumnsForVersion(crd, v.Name)
182 if err != nil {
183 t.Fatal(err)
184 }
185 expectColumnNum := len(columns) + 1
186 if got, expected := len(tbl.ColumnDefinitions), expectColumnNum; got != expected {
187 t.Errorf("expected %d headers, got %d", expected, got)
188 } else {
189 age := metav1beta1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0}
190 if got, expected := tbl.ColumnDefinitions[1], age; got != expected {
191 t.Errorf("expected column definition %#v, got %#v", expected, got)
192 }
193
194 alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0}
195 if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected {
196 t.Errorf("expected column definition %#v, got %#v", expected, got)
197 }
198
199 beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42}
200 if got, expected := tbl.ColumnDefinitions[3], beta; got != expected {
201 t.Errorf("expected column definition %#v, got %#v", expected, got)
202 }
203
204 gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"}
205 if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected {
206 t.Errorf("expected column definition %#v, got %#v", expected, got)
207 }
208
209 epsilon := metav1beta1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"}
210 if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected {
211 t.Errorf("expected column definition %#v, got %#v", expected, got)
212 }
213
214
215 if i == 1 {
216 zeta := metav1beta1.TableColumnDefinition{Name: "Zeta", Type: "integer", Format: "int64", Description: "the zeta field", Priority: 42}
217 if got, expected := tbl.ColumnDefinitions[6], zeta; got != expected {
218 t.Errorf("expected column definition %#v, got %#v", expected, got)
219 }
220 }
221 }
222 if got, expected := len(tbl.Rows), 1; got != expected {
223 t.Errorf("expected %d rows, got %d", expected, got)
224 } else if got, expected := len(tbl.Rows[0].Cells), expectColumnNum; got != expected {
225 t.Errorf("expected %d cells, got %d", expected, got)
226 } else {
227 if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected {
228 t.Errorf("expected cell[0] to equal %q, got %q", expected, got)
229 }
230 if s, ok := tbl.Rows[0].Cells[1].(string); !ok {
231 t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1])
232 } else {
233 dur, err := time.ParseDuration(s)
234 if err != nil {
235 t.Errorf("expected cell[1] to be a duration: %v", err)
236 } else if abs(dur.Seconds()) > 30.0 {
237 t.Errorf("expected cell[1] to be a small age, but got: %v", dur)
238 }
239 }
240 if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected {
241 t.Errorf("expected cell[2] to equal %q, got %q", expected, got)
242 }
243 if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected {
244 t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got)
245 }
246 if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
247 t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
248 }
249 if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected {
250 t.Errorf("expected cell[5] to equal %q, got %q", expected, got)
251 }
252
253 if i == 1 {
254 if got, expected := tbl.Rows[0].Cells[6], int64(5); got != expected {
255 t.Errorf("expected cell[6] to equal %q, got %q", expected, got)
256 }
257 }
258 }
259 }
260
261
262 {
263 ret, err := crRestClient.Get().
264 Resource(crd.Spec.Names.Plural).
265 SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1.SchemeGroupVersion.Version, metav1.GroupName)).
266 VersionedParams(&metav1.TableOptions{}, parameterCodec).
267 Do(context.TODO()).
268 Get()
269 if err != nil {
270 t.Fatalf("failed to list %v resources: %v", gvk, err)
271 }
272
273 tbl, ok := ret.(*metav1.Table)
274 if !ok {
275 t.Fatalf("expected metav1.Table, got %T", ret)
276 }
277 t.Logf("%v table list: %#v", gvk, tbl)
278
279 columns, err := getColumnsForVersion(crd, v.Name)
280 if err != nil {
281 t.Fatal(err)
282 }
283 expectColumnNum := len(columns) + 1
284 if got, expected := len(tbl.ColumnDefinitions), expectColumnNum; got != expected {
285 t.Errorf("expected %d headers, got %d", expected, got)
286 } else {
287 age := metav1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0}
288 if got, expected := tbl.ColumnDefinitions[1], age; got != expected {
289 t.Errorf("expected column definition %#v, got %#v", expected, got)
290 }
291
292 alpha := metav1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0}
293 if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected {
294 t.Errorf("expected column definition %#v, got %#v", expected, got)
295 }
296
297 beta := metav1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42}
298 if got, expected := tbl.ColumnDefinitions[3], beta; got != expected {
299 t.Errorf("expected column definition %#v, got %#v", expected, got)
300 }
301
302 gamma := metav1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"}
303 if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected {
304 t.Errorf("expected column definition %#v, got %#v", expected, got)
305 }
306
307 epsilon := metav1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"}
308 if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected {
309 t.Errorf("expected column definition %#v, got %#v", expected, got)
310 }
311
312
313 if i == 1 {
314 zeta := metav1.TableColumnDefinition{Name: "Zeta", Type: "integer", Format: "int64", Description: "the zeta field", Priority: 42}
315 if got, expected := tbl.ColumnDefinitions[6], zeta; got != expected {
316 t.Errorf("expected column definition %#v, got %#v", expected, got)
317 }
318 }
319 }
320 if got, expected := len(tbl.Rows), 1; got != expected {
321 t.Errorf("expected %d rows, got %d", expected, got)
322 } else if got, expected := len(tbl.Rows[0].Cells), expectColumnNum; got != expected {
323 t.Errorf("expected %d cells, got %d", expected, got)
324 } else {
325 if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected {
326 t.Errorf("expected cell[0] to equal %q, got %q", expected, got)
327 }
328 if s, ok := tbl.Rows[0].Cells[1].(string); !ok {
329 t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1])
330 } else {
331 dur, err := time.ParseDuration(s)
332 if err != nil {
333 t.Errorf("expected cell[1] to be a duration: %v", err)
334 } else if abs(dur.Seconds()) > 30.0 {
335 t.Errorf("expected cell[1] to be a small age, but got: %v", dur)
336 }
337 }
338 if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected {
339 t.Errorf("expected cell[2] to equal %q, got %q", expected, got)
340 }
341 if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected {
342 t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got)
343 }
344 if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
345 t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
346 }
347 if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected {
348 t.Errorf("expected cell[5] to equal %q, got %q", expected, got)
349 }
350
351 if i == 1 {
352 if got, expected := tbl.Rows[0].Cells[6], int64(5); got != expected {
353 t.Errorf("expected cell[6] to equal %q, got %q", expected, got)
354 }
355 }
356 }
357 }
358 }
359 }
360
361
362
363 func TestColumnsPatch(t *testing.T) {
364 tearDown, config, _, err := fixtures.StartDefaultServer(t)
365 if err != nil {
366 t.Fatal(err)
367 }
368 defer tearDown()
369
370 apiExtensionClient, err := clientset.NewForConfig(config)
371 if err != nil {
372 t.Fatal(err)
373 }
374
375 dynamicClient, err := dynamic.NewForConfig(config)
376 if err != nil {
377 t.Fatal(err)
378 }
379
380
381 crd := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)[0]
382 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
383 if err != nil {
384 t.Fatal(err)
385 }
386
387
388
389
390 patch := []byte(`{
391 "spec": {
392 "versions": [
393 {
394 "name": "v1beta1",
395 "served": true,
396 "storage": true,
397 "additionalPrinterColumns": [
398 {
399 "name": "Age",
400 "type": "date",
401 "jsonPath": ".metadata.creationTimestamp"
402 }
403 ],
404 "schema": {
405 "openAPIV3Schema": {"x-kubernetes-preserve-unknown-fields": true, "type": "object"}
406 }
407 },
408 {
409 "name": "v1",
410 "served": true,
411 "storage": false,
412 "additionalPrinterColumns": [
413 {
414 "name": "Age2",
415 "type": "date",
416 "jsonPath": ".metadata.creationTimestamp"
417 }
418 ],
419 "schema": {
420 "openAPIV3Schema": {"x-kubernetes-preserve-unknown-fields": true, "type": "object"}
421 }
422 }
423 ]
424 }
425 }
426 `)
427
428 _, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(context.TODO(), crd.Name, types.MergePatchType, patch, metav1.PatchOptions{})
429 if err != nil {
430 t.Fatal(err)
431 }
432
433 crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{})
434 if err != nil {
435 t.Fatal(err)
436 }
437 t.Logf("columns crd patched: %#v", crd)
438 }
439
440
441
442
443 func TestPatchCleanTopLevelColumns(t *testing.T) {
444 tearDown, config, _, err := fixtures.StartDefaultServer(t)
445 if err != nil {
446 t.Fatal(err)
447 }
448 defer tearDown()
449
450 apiExtensionClient, err := clientset.NewForConfig(config)
451 if err != nil {
452 t.Fatal(err)
453 }
454
455 dynamicClient, err := dynamic.NewForConfig(config)
456 if err != nil {
457 t.Fatal(err)
458 }
459
460 crd := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)[0]
461 crd.Spec.Versions[0].AdditionalPrinterColumns = []apiextensionsv1.CustomResourceColumnDefinition{
462 {Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
463 }
464 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
465 if err != nil {
466 t.Fatal(err)
467 }
468 t.Logf("columns crd created: %#v", crd)
469
470
471
472 patch := []byte(`{
473 "spec": {
474 "additionalPrinterColumns": null,
475 "versions": [
476 {
477 "name": "v1beta1",
478 "served": true,
479 "storage": true,
480 "additionalPrinterColumns": [
481 {
482 "name": "Age",
483 "type": "date",
484 "jsonPath": ".metadata.creationTimestamp"
485 }
486 ],
487 "schema": {
488 "openAPIV3Schema": {"x-kubernetes-preserve-unknown-fields": true, "type": "object"}
489 }
490 },
491 {
492 "name": "v1",
493 "served": true,
494 "storage": false,
495 "additionalPrinterColumns": [
496 {
497 "name": "Age2",
498 "type": "date",
499 "jsonPath": ".metadata.creationTimestamp"
500 }
501 ],
502 "schema": {
503 "openAPIV3Schema": {"x-kubernetes-preserve-unknown-fields": true, "type": "object"}
504 }
505 }
506 ]
507 }
508 }
509 `)
510
511 _, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(context.TODO(), crd.Name, types.MergePatchType, patch, metav1.PatchOptions{})
512 if err != nil {
513 t.Fatal(err)
514 }
515
516 crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{})
517 if err != nil {
518 t.Fatal(err)
519 }
520 t.Logf("columns crd patched: %#v", crd)
521 }
522
523 func abs(x float64) float64 {
524 if x < 0 {
525 return -x
526 }
527 return x
528 }
529
View as plain text