...

Source file src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go

Documentation: k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tableconvertor
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/client-go/util/jsonpath"
    32  )
    33  
    34  func Test_cellForJSONValue(t *testing.T) {
    35  	tests := []struct {
    36  		headerType string
    37  		value      interface{}
    38  		want       interface{}
    39  	}{
    40  		{"integer", int64(42), int64(42)},
    41  		{"integer", float64(3.14), int64(3)},
    42  		{"integer", true, nil},
    43  		{"integer", "foo", nil},
    44  
    45  		{"number", int64(42), float64(42)},
    46  		{"number", float64(3.14), float64(3.14)},
    47  		{"number", true, nil},
    48  		{"number", "foo", nil},
    49  
    50  		{"boolean", int64(42), nil},
    51  		{"boolean", float64(3.14), nil},
    52  		{"boolean", true, true},
    53  		{"boolean", "foo", nil},
    54  
    55  		{"string", int64(42), nil},
    56  		{"string", float64(3.14), nil},
    57  		{"string", true, nil},
    58  		{"string", "foo", "foo"},
    59  
    60  		{"date", int64(42), nil},
    61  		{"date", float64(3.14), nil},
    62  		{"date", true, nil},
    63  		{"date", time.Now().Add(-time.Hour*12 - 30*time.Minute).UTC().Format(time.RFC3339), "12h"},
    64  		{"date", time.Now().Add(+time.Hour*12 + 30*time.Minute).UTC().Format(time.RFC3339), "<invalid>"},
    65  		{"date", "", "<unknown>"},
    66  
    67  		{"unknown", "foo", nil},
    68  	}
    69  	for _, tt := range tests {
    70  		t.Run(fmt.Sprintf("%#v of type %s", tt.value, tt.headerType), func(t *testing.T) {
    71  			if got := cellForJSONValue(tt.headerType, tt.value); !reflect.DeepEqual(got, tt.want) {
    72  				t.Errorf("cellForJSONValue() = %#v, want %#v", got, tt.want)
    73  			}
    74  		})
    75  	}
    76  }
    77  
    78  func Test_convertor_ConvertToTable(t *testing.T) {
    79  	type fields struct {
    80  		headers           []metav1.TableColumnDefinition
    81  		additionalColumns []columnPrinter
    82  	}
    83  	type args struct {
    84  		ctx          context.Context
    85  		obj          runtime.Object
    86  		tableOptions runtime.Object
    87  	}
    88  	tests := []struct {
    89  		name    string
    90  		fields  fields
    91  		args    args
    92  		want    *metav1.Table
    93  		wantErr bool
    94  	}{
    95  		{
    96  			name: "Return table for object",
    97  			fields: fields{
    98  				headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
    99  			},
   100  			args: args{
   101  				obj: &metav1beta1.PartialObjectMetadata{
   102  					ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   103  				},
   104  				tableOptions: nil,
   105  			},
   106  			want: &metav1.Table{
   107  				ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   108  				Rows: []metav1.TableRow{
   109  					{
   110  						Cells: []interface{}{"blah"},
   111  						Object: runtime.RawExtension{
   112  							Object: &metav1beta1.PartialObjectMetadata{
   113  								ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   114  							},
   115  						},
   116  					},
   117  				},
   118  			},
   119  		},
   120  		{
   121  			name: "Return table for list",
   122  			fields: fields{
   123  				headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   124  			},
   125  			args: args{
   126  				obj: &metav1beta1.PartialObjectMetadataList{
   127  					Items: []metav1beta1.PartialObjectMetadata{
   128  						{ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}},
   129  						{ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))}},
   130  					},
   131  				},
   132  				tableOptions: nil,
   133  			},
   134  			want: &metav1.Table{
   135  				ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   136  				Rows: []metav1.TableRow{
   137  					{
   138  						Cells: []interface{}{"blah"},
   139  						Object: runtime.RawExtension{
   140  							Object: &metav1beta1.PartialObjectMetadata{
   141  								ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   142  							},
   143  						},
   144  					},
   145  					{
   146  						Cells: []interface{}{"blah-2"},
   147  						Object: runtime.RawExtension{
   148  							Object: &metav1beta1.PartialObjectMetadata{
   149  								ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))},
   150  							},
   151  						},
   152  					},
   153  				},
   154  			},
   155  		},
   156  		{
   157  			name: "Accept TableOptions",
   158  			fields: fields{
   159  				headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   160  			},
   161  			args: args{
   162  				obj: &metav1beta1.PartialObjectMetadata{
   163  					ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   164  				},
   165  				tableOptions: &metav1.TableOptions{},
   166  			},
   167  			want: &metav1.Table{
   168  				ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   169  				Rows: []metav1.TableRow{
   170  					{
   171  						Cells: []interface{}{"blah"},
   172  						Object: runtime.RawExtension{
   173  							Object: &metav1beta1.PartialObjectMetadata{
   174  								ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   175  							},
   176  						},
   177  					},
   178  				},
   179  			},
   180  		},
   181  		{
   182  			name: "Omit headers from TableOptions",
   183  			fields: fields{
   184  				headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
   185  			},
   186  			args: args{
   187  				obj: &metav1beta1.PartialObjectMetadata{
   188  					ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   189  				},
   190  				tableOptions: &metav1.TableOptions{NoHeaders: true},
   191  			},
   192  			want: &metav1.Table{
   193  				Rows: []metav1.TableRow{
   194  					{
   195  						Cells: []interface{}{"blah"},
   196  						Object: runtime.RawExtension{
   197  							Object: &metav1beta1.PartialObjectMetadata{
   198  								ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
   199  							},
   200  						},
   201  					},
   202  				},
   203  			},
   204  		},
   205  		{
   206  			name: "Return table with additional column containing multiple string values",
   207  			fields: fields{
   208  				headers: []metav1.TableColumnDefinition{
   209  					{Name: "name", Type: "string"},
   210  					{Name: "valueOnly", Type: "string"},
   211  					{Name: "single1", Type: "string"},
   212  					{Name: "single2", Type: "string"},
   213  					{Name: "multi", Type: "string"},
   214  				},
   215  				additionalColumns: []columnPrinter{
   216  					newJSONPath("valueOnly", "{.spec.servers[0].hosts[0]}"),
   217  					newJSONPath("single1", "{.spec.servers[0].hosts}"),
   218  					newJSONPath("single2", "{.spec.servers[1].hosts}"),
   219  					newJSONPath("multi", "{.spec.servers[*].hosts}"),
   220  				},
   221  			},
   222  			args: args{
   223  				obj: &unstructured.Unstructured{
   224  					Object: map[string]interface{}{
   225  						"apiVersion": "example.istio.io/v1alpha1",
   226  						"kind":       "Blah",
   227  						"metadata": map[string]interface{}{
   228  							"name": "blah",
   229  						},
   230  						"spec": map[string]interface{}{
   231  							"servers": []map[string]interface{}{
   232  								{"hosts": []string{"foo"}},
   233  								{"hosts": []string{"bar", "baz"}},
   234  							},
   235  						},
   236  					},
   237  				},
   238  				tableOptions: nil,
   239  			},
   240  			want: &metav1.Table{
   241  				ColumnDefinitions: []metav1.TableColumnDefinition{
   242  					{Name: "name", Type: "string"},
   243  					{Name: "valueOnly", Type: "string"},
   244  					{Name: "single1", Type: "string"},
   245  					{Name: "single2", Type: "string"},
   246  					{Name: "multi", Type: "string"},
   247  				},
   248  				Rows: []metav1.TableRow{
   249  					{
   250  						Cells: []interface{}{
   251  							"blah",
   252  							"foo",
   253  							`["foo"]`,
   254  							`["bar","baz"]`,
   255  							`["foo"]`, // TODO: TableConverter should be changed so that the response is this: `["foo"] ["bar","baz"]`,
   256  						},
   257  						Object: runtime.RawExtension{
   258  							Object: &unstructured.Unstructured{
   259  								Object: map[string]interface{}{
   260  									"apiVersion": "example.istio.io/v1alpha1",
   261  									"kind":       "Blah",
   262  									"metadata": map[string]interface{}{
   263  										"name": "blah",
   264  									},
   265  									"spec": map[string]interface{}{
   266  										"servers": []map[string]interface{}{
   267  											{"hosts": []string{"foo"}},
   268  											{"hosts": []string{"bar", "baz"}},
   269  										},
   270  									},
   271  								},
   272  							},
   273  						},
   274  					},
   275  				},
   276  			},
   277  		},
   278  		{
   279  			name: "Return table with additional column containing multiple integer values as string",
   280  			fields: fields{
   281  				headers: []metav1.TableColumnDefinition{
   282  					{Name: "name", Type: "string"},
   283  					{Name: "valueOnly", Type: "string"},
   284  					{Name: "single1", Type: "string"},
   285  					{Name: "single2", Type: "string"},
   286  					{Name: "multi", Type: "string"},
   287  				},
   288  				additionalColumns: []columnPrinter{
   289  					newJSONPath("valueOnly", "{.spec.foo[0].bar[0]}"),
   290  					newJSONPath("single1", "{.spec.foo[0].bar}"),
   291  					newJSONPath("single2", "{.spec.foo[1].bar}"),
   292  					newJSONPath("multi", "{.spec.foo[*].bar}"),
   293  				},
   294  			},
   295  			args: args{
   296  				obj: &unstructured.Unstructured{
   297  					Object: map[string]interface{}{
   298  						"apiVersion": "example.istio.io/v1alpha1",
   299  						"kind":       "Blah",
   300  						"metadata": map[string]interface{}{
   301  							"name": "blah",
   302  						},
   303  						"spec": map[string]interface{}{
   304  							"foo": []map[string]interface{}{
   305  								{"bar": []int64{1}},
   306  								{"bar": []int64{2, 3}},
   307  							},
   308  						},
   309  					},
   310  				},
   311  				tableOptions: nil,
   312  			},
   313  			want: &metav1.Table{
   314  				ColumnDefinitions: []metav1.TableColumnDefinition{
   315  					{Name: "name", Type: "string"},
   316  					{Name: "valueOnly", Type: "string"},
   317  					{Name: "single1", Type: "string"},
   318  					{Name: "single2", Type: "string"},
   319  					{Name: "multi", Type: "string"},
   320  				},
   321  				Rows: []metav1.TableRow{
   322  					{
   323  						Cells: []interface{}{
   324  							"blah",
   325  							"1",
   326  							"[1]",
   327  							"[2,3]",
   328  							"[1]", // TODO: TableConverter should be changed so that the response is this: `[1] [2,3]`,
   329  						},
   330  						Object: runtime.RawExtension{
   331  							Object: &unstructured.Unstructured{
   332  								Object: map[string]interface{}{
   333  									"apiVersion": "example.istio.io/v1alpha1",
   334  									"kind":       "Blah",
   335  									"metadata": map[string]interface{}{
   336  										"name": "blah",
   337  									},
   338  									"spec": map[string]interface{}{
   339  										"foo": []map[string]interface{}{
   340  											{"bar": []int64{1}},
   341  											{"bar": []int64{2, 3}},
   342  										},
   343  									},
   344  								},
   345  							},
   346  						},
   347  					},
   348  				},
   349  			},
   350  		},
   351  		{
   352  			name: "Return table with additional column containing multiple integer values",
   353  			fields: fields{
   354  				headers: []metav1.TableColumnDefinition{
   355  					{Name: "name", Type: "string"},
   356  					{Name: "valueOnly", Type: "integer"},
   357  					{Name: "single1", Type: "integer"},
   358  					{Name: "single2", Type: "integer"},
   359  					{Name: "multi", Type: "integer"},
   360  				},
   361  				additionalColumns: []columnPrinter{
   362  					newJSONPath("valueOnly", "{.spec.foo[0].bar[0]}"),
   363  					newJSONPath("single1", "{.spec.foo[0].bar}"),
   364  					newJSONPath("single2", "{.spec.foo[1].bar}"),
   365  					newJSONPath("multi", "{.spec.foo[*].bar}"),
   366  				},
   367  			},
   368  			args: args{
   369  				obj: &unstructured.Unstructured{
   370  					Object: map[string]interface{}{
   371  						"apiVersion": "example.istio.io/v1alpha1",
   372  						"kind":       "Blah",
   373  						"metadata": map[string]interface{}{
   374  							"name": "blah",
   375  						},
   376  						"spec": map[string]interface{}{
   377  							"foo": []map[string]interface{}{
   378  								{"bar": []int64{1}},
   379  								{"bar": []int64{2, 3}},
   380  							},
   381  						},
   382  					},
   383  				},
   384  				tableOptions: nil,
   385  			},
   386  			want: &metav1.Table{
   387  				ColumnDefinitions: []metav1.TableColumnDefinition{
   388  					{Name: "name", Type: "string"},
   389  					{Name: "valueOnly", Type: "integer"},
   390  					{Name: "single1", Type: "integer"},
   391  					{Name: "single2", Type: "integer"},
   392  					{Name: "multi", Type: "integer"},
   393  				},
   394  				Rows: []metav1.TableRow{
   395  					{
   396  						Cells: []interface{}{
   397  							"blah",
   398  							int64(1),
   399  							nil, // TODO: Seems like this should either return some data or return an error, not just be nil
   400  							nil, // TODO: Seems like this should either return some data or return an error, not just be nil
   401  							nil, // TODO: Seems like this should either return some data or return an error, not just be nil
   402  						},
   403  						Object: runtime.RawExtension{
   404  							Object: &unstructured.Unstructured{
   405  								Object: map[string]interface{}{
   406  									"apiVersion": "example.istio.io/v1alpha1",
   407  									"kind":       "Blah",
   408  									"metadata": map[string]interface{}{
   409  										"name": "blah",
   410  									},
   411  									"spec": map[string]interface{}{
   412  										"foo": []map[string]interface{}{
   413  											{"bar": []int64{1}},
   414  											{"bar": []int64{2, 3}},
   415  										},
   416  									},
   417  								},
   418  							},
   419  						},
   420  					},
   421  				},
   422  			},
   423  		},
   424  	}
   425  	for _, tt := range tests {
   426  		t.Run(tt.name, func(t *testing.T) {
   427  			c := &convertor{
   428  				headers:           tt.fields.headers,
   429  				additionalColumns: tt.fields.additionalColumns,
   430  			}
   431  			got, err := c.ConvertToTable(tt.args.ctx, tt.args.obj, tt.args.tableOptions)
   432  			if (err != nil) != tt.wantErr {
   433  				t.Errorf("convertor.ConvertToTable() error = %v, wantErr %v", err, tt.wantErr)
   434  				return
   435  			}
   436  			if !reflect.DeepEqual(got, tt.want) {
   437  				t.Errorf("convertor.ConvertToTable() = %s", cmp.Diff(tt.want, got))
   438  			}
   439  		})
   440  	}
   441  }
   442  
   443  func newJSONPath(name string, jsonPathExpression string) columnPrinter {
   444  	jp := jsonpath.New(name)
   445  	_ = jp.Parse(jsonPathExpression)
   446  	return jp
   447  }
   448  

View as plain text