...

Source file src/k8s.io/apiextensions-apiserver/test/integration/table_test.go

Documentation: k8s.io/apiextensions-apiserver/test/integration

     1  /*
     2  Copyright 2017 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 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  		// metav1beta1 table
   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  				// Validate extra column for v1
   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  				// Validate extra column for v1
   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  		// metav1 table
   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  				// Validate extra column for v1
   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  				// Validate extra column for v1
   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  // TestColumnsPatch tests the case that a CRD was created with no top-level or
   362  // per-version columns. One should be able to PATCH the CRD setting per-version columns.
   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  	// CRD with no top-level and per-version columns should be created successfully
   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  	// One should be able to patch the CRD to use per-version columns. The top-level columns
   388  	// should not be defaulted during creation, and apiserver should not return validation
   389  	// error about top-level and per-version columns being mutual exclusive.
   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  // TestPatchCleanTopLevelColumns tests the case that a CRD was created with top-level columns.
   441  // One should be able to PATCH the CRD cleaning the top-level columns and setting per-version
   442  // columns.
   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  	// One should be able to patch the CRD to use per-version columns by cleaning
   471  	// the top-level columns.
   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