...

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

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

     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 customresource_test
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  
    29  	autoscalingv1 "k8s.io/api/autoscaling/v1"
    30  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    31  	"k8s.io/apimachinery/pkg/api/errors"
    32  	metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/runtime/schema"
    37  	"k8s.io/apimachinery/pkg/util/managedfields"
    38  	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    39  	"k8s.io/apiserver/pkg/registry/generic"
    40  	registrytest "k8s.io/apiserver/pkg/registry/generic/testing"
    41  	"k8s.io/apiserver/pkg/registry/rest"
    42  	etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
    43  
    44  	apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    45  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    46  	"k8s.io/apiextensions-apiserver/pkg/apiserver"
    47  	"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
    48  	"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
    49  	"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
    50  )
    51  
    52  func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcd3testing.EtcdTestServer) {
    53  	server, etcdStorage := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
    54  	etcdStorage.Codec = unstructured.UnstructuredJSONScheme
    55  	groupResource := schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"}
    56  	restOptions := generic.RESTOptions{StorageConfig: etcdStorage.ForResource(groupResource), Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1, ResourcePrefix: "noxus"}
    57  
    58  	parameterScheme := runtime.NewScheme()
    59  	parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: "mygroup.example.com", Version: "v1beta1"},
    60  		&metav1.ListOptions{},
    61  		&metav1.GetOptions{},
    62  		&metav1.DeleteOptions{},
    63  	)
    64  
    65  	typer := apiserver.UnstructuredObjectTyper{
    66  		Delegate:          parameterScheme,
    67  		UnstructuredTyper: crdserverscheme.NewUnstructuredObjectTyper(),
    68  	}
    69  
    70  	kind := schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "Noxu"}
    71  
    72  	labelSelectorPath := ".status.labelSelector"
    73  	scale := &apiextensionsinternal.CustomResourceSubresourceScale{
    74  		SpecReplicasPath:   ".spec.replicas",
    75  		StatusReplicasPath: ".status.replicas",
    76  		LabelSelectorPath:  &labelSelectorPath,
    77  	}
    78  
    79  	status := &apiextensionsinternal.CustomResourceSubresourceStatus{}
    80  
    81  	headers := []apiextensionsv1.CustomResourceColumnDefinition{
    82  		{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
    83  		{Name: "Replicas", Type: "integer", JSONPath: ".spec.replicas"},
    84  		{Name: "Missing", Type: "string", JSONPath: ".spec.missing"},
    85  		{Name: "Invalid", Type: "integer", JSONPath: ".spec.string"},
    86  		{Name: "String", Type: "string", JSONPath: ".spec.string"},
    87  		{Name: "StringFloat64", Type: "string", JSONPath: ".spec.float64"},
    88  		{Name: "StringInt64", Type: "string", JSONPath: ".spec.replicas"},
    89  		{Name: "StringBool", Type: "string", JSONPath: ".spec.bool"},
    90  		{Name: "Float64", Type: "number", JSONPath: ".spec.float64"},
    91  		{Name: "Bool", Type: "boolean", JSONPath: ".spec.bool"},
    92  	}
    93  	table, _ := tableconvertor.New(headers)
    94  
    95  	storage := customresource.NewStorage(
    96  		groupResource,
    97  		groupResource,
    98  		kind,
    99  		schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"},
   100  		customresource.NewStrategy(
   101  			typer,
   102  			true,
   103  			kind,
   104  			nil,
   105  			nil,
   106  			nil,
   107  			status,
   108  			scale,
   109  			nil,
   110  		),
   111  		restOptions,
   112  		[]string{"all"},
   113  		table,
   114  		managedfields.ResourcePathMappings{},
   115  	)
   116  
   117  	return storage, server
   118  }
   119  
   120  // createCustomResource is a helper function that returns a CustomResource with the updated resource version.
   121  func createCustomResource(storage *customresource.REST, cr unstructured.Unstructured, t *testing.T) (unstructured.Unstructured, error) {
   122  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), cr.GetNamespace())
   123  	obj, err := storage.Create(ctx, &cr, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
   124  	if err != nil {
   125  		t.Errorf("Failed to create CustomResource, %v", err)
   126  	}
   127  	newCR := obj.(*unstructured.Unstructured)
   128  	return *newCR, nil
   129  }
   130  
   131  func validNewCustomResource() *unstructured.Unstructured {
   132  	return &unstructured.Unstructured{
   133  		Object: map[string]interface{}{
   134  			"apiVersion": "mygroup.example.com/v1beta1",
   135  			"kind":       "Noxu",
   136  			"metadata": map[string]interface{}{
   137  				"namespace":         "default",
   138  				"name":              "foo",
   139  				"creationTimestamp": time.Now().Add(-time.Hour*12 - 30*time.Minute).UTC().Format(time.RFC3339),
   140  			},
   141  			"spec": map[string]interface{}{
   142  				"replicas":         int64(7),
   143  				"string":           "string",
   144  				"float64":          float64(3.1415926),
   145  				"bool":             true,
   146  				"stringList":       []interface{}{"foo", "bar"},
   147  				"mixedList":        []interface{}{"foo", int64(42)},
   148  				"nonPrimitiveList": []interface{}{"foo", []interface{}{int64(1), int64(2)}},
   149  			},
   150  		},
   151  	}
   152  }
   153  
   154  var validCustomResource = *validNewCustomResource()
   155  
   156  func TestCreate(t *testing.T) {
   157  	storage, server := newStorage(t)
   158  	defer server.Terminate(t)
   159  	defer storage.CustomResource.Store.DestroyFunc()
   160  	test := registrytest.New(t, storage.CustomResource.Store)
   161  	cr := validNewCustomResource()
   162  	cr.SetNamespace("")
   163  	test.TestCreate(
   164  		cr,
   165  	)
   166  }
   167  
   168  func TestGet(t *testing.T) {
   169  	storage, server := newStorage(t)
   170  	defer server.Terminate(t)
   171  	defer storage.CustomResource.Store.DestroyFunc()
   172  	test := registrytest.New(t, storage.CustomResource.Store)
   173  	test.TestGet(validNewCustomResource())
   174  }
   175  
   176  func TestList(t *testing.T) {
   177  	storage, server := newStorage(t)
   178  	defer server.Terminate(t)
   179  	defer storage.CustomResource.Store.DestroyFunc()
   180  	test := registrytest.New(t, storage.CustomResource.Store)
   181  	test.TestList(validNewCustomResource())
   182  }
   183  
   184  func TestDelete(t *testing.T) {
   185  	storage, server := newStorage(t)
   186  	defer server.Terminate(t)
   187  	defer storage.CustomResource.Store.DestroyFunc()
   188  	test := registrytest.New(t, storage.CustomResource.Store)
   189  	test.TestDelete(validNewCustomResource())
   190  }
   191  
   192  func TestGenerationNumber(t *testing.T) {
   193  	storage, server := newStorage(t)
   194  	defer server.Terminate(t)
   195  	defer storage.CustomResource.Store.DestroyFunc()
   196  	modifiedRno := *validNewCustomResource()
   197  	modifiedRno.SetGeneration(10)
   198  	ctx := genericapirequest.NewDefaultContext()
   199  	cr, err := createCustomResource(storage.CustomResource, modifiedRno, t)
   200  	if err != nil {
   201  		t.Errorf("unexpected error: %v", err)
   202  	}
   203  	etcdCR, err := storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
   204  	if err != nil {
   205  		t.Errorf("unexpected error: %v", err)
   206  	}
   207  	storedCR, _ := etcdCR.(*unstructured.Unstructured)
   208  
   209  	// Generation initialization
   210  	if storedCR.GetGeneration() != 1 {
   211  		t.Fatalf("Unexpected generation number %v", storedCR.GetGeneration())
   212  	}
   213  
   214  	// Updates to spec should increment the generation number
   215  	setSpecReplicas(storedCR, getSpecReplicas(storedCR)+1)
   216  	if _, _, err := storage.CustomResource.Update(ctx, storedCR.GetName(), rest.DefaultUpdatedObjectInfo(storedCR), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   217  		t.Errorf("unexpected error: %v", err)
   218  	}
   219  	etcdCR, err = storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
   220  	if err != nil {
   221  		t.Errorf("unexpected error: %v", err)
   222  	}
   223  	storedCR, _ = etcdCR.(*unstructured.Unstructured)
   224  	if storedCR.GetGeneration() != 2 {
   225  		t.Fatalf("Unexpected generation, spec: %v", storedCR.GetGeneration())
   226  	}
   227  
   228  	// Updates to status should not increment the generation number
   229  	setStatusReplicas(storedCR, getStatusReplicas(storedCR)+1)
   230  	if _, _, err := storage.CustomResource.Update(ctx, storedCR.GetName(), rest.DefaultUpdatedObjectInfo(storedCR), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   231  		t.Errorf("unexpected error: %v", err)
   232  	}
   233  	etcdCR, err = storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
   234  	if err != nil {
   235  		t.Errorf("unexpected error: %v", err)
   236  	}
   237  	storedCR, _ = etcdCR.(*unstructured.Unstructured)
   238  	if storedCR.GetGeneration() != 2 {
   239  		t.Fatalf("Unexpected generation, spec: %v", storedCR.GetGeneration())
   240  	}
   241  
   242  }
   243  
   244  func TestCategories(t *testing.T) {
   245  	storage, server := newStorage(t)
   246  	defer server.Terminate(t)
   247  	defer storage.CustomResource.Store.DestroyFunc()
   248  
   249  	expected := []string{"all"}
   250  	actual := storage.CustomResource.Categories()
   251  	ok := reflect.DeepEqual(actual, expected)
   252  	if !ok {
   253  		t.Errorf("categories are not equal. expected = %v actual = %v", expected, actual)
   254  	}
   255  }
   256  
   257  func TestColumns(t *testing.T) {
   258  	storage, server := newStorage(t)
   259  	defer server.Terminate(t)
   260  	defer storage.CustomResource.Store.DestroyFunc()
   261  
   262  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   263  	key := "/noxus/" + metav1.NamespaceDefault + "/foo"
   264  	validCustomResource := validNewCustomResource()
   265  	if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0, false); err != nil {
   266  		t.Fatalf("unexpected error: %v", err)
   267  	}
   268  
   269  	gottenList, err := storage.CustomResource.List(ctx, &metainternal.ListOptions{})
   270  	if err != nil {
   271  		t.Fatalf("unexpected error: %v", err)
   272  	}
   273  
   274  	tbl, err := storage.CustomResource.ConvertToTable(ctx, gottenList, &metav1.TableOptions{})
   275  	if err != nil {
   276  		t.Fatalf("unexpected error: %v", err)
   277  	}
   278  
   279  	expectedColumns := []struct {
   280  		Name, Type string
   281  	}{
   282  		{"Name", "string"},
   283  		{"Age", "date"},
   284  		{"Replicas", "integer"},
   285  		{"Missing", "string"},
   286  		{"Invalid", "integer"},
   287  		{"String", "string"},
   288  		{"StringFloat64", "string"},
   289  		{"StringInt64", "string"},
   290  		{"StringBool", "string"},
   291  		{"Float64", "number"},
   292  		{"Bool", "boolean"},
   293  	}
   294  	if len(tbl.ColumnDefinitions) != len(expectedColumns) {
   295  		t.Fatalf("got %d columns, expected %d. Got: %+v", len(tbl.ColumnDefinitions), len(expectedColumns), tbl.ColumnDefinitions)
   296  	}
   297  	for i, d := range tbl.ColumnDefinitions {
   298  		if d.Name != expectedColumns[i].Name {
   299  			t.Errorf("got column %d name %q, expected %q", i, d.Name, expectedColumns[i].Name)
   300  		}
   301  		if d.Type != expectedColumns[i].Type {
   302  			t.Errorf("got column %d type %q, expected %q", i, d.Type, expectedColumns[i].Type)
   303  		}
   304  	}
   305  
   306  	expectedRows := [][]interface{}{
   307  		{
   308  			"foo",
   309  			"12h",
   310  			int64(7),
   311  			nil,
   312  			nil,
   313  			"string",
   314  			"3.1415926",
   315  			"7",
   316  			"true",
   317  			float64(3.1415926),
   318  			true,
   319  		},
   320  	}
   321  	for i, r := range tbl.Rows {
   322  		if !reflect.DeepEqual(r.Cells, expectedRows[i]) {
   323  			t.Errorf("got row %d with cells %#v, expected %#v", i, r.Cells, expectedRows[i])
   324  		}
   325  	}
   326  }
   327  
   328  func TestStatusUpdate(t *testing.T) {
   329  	storage, server := newStorage(t)
   330  	defer server.Terminate(t)
   331  	defer storage.CustomResource.Store.DestroyFunc()
   332  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   333  	key := "/noxus/" + metav1.NamespaceDefault + "/foo"
   334  	validCustomResource := validNewCustomResource()
   335  	if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0, false); err != nil {
   336  		t.Fatalf("unexpected error: %v", err)
   337  	}
   338  
   339  	gottenObj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
   340  	if err != nil {
   341  		t.Fatalf("unexpected error: %v", err)
   342  	}
   343  
   344  	update := gottenObj.(*unstructured.Unstructured)
   345  	updateContent := update.Object
   346  	updateContent["status"] = map[string]interface{}{
   347  		"replicas": int64(7),
   348  	}
   349  
   350  	if _, _, err := storage.Status.Update(ctx, update.GetName(), rest.DefaultUpdatedObjectInfo(update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   351  		t.Fatalf("unexpected error: %v", err)
   352  	}
   353  
   354  	obj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
   355  	if err != nil {
   356  		t.Fatalf("unexpected error: %v", err)
   357  	}
   358  
   359  	cr, ok := obj.(*unstructured.Unstructured)
   360  	if !ok {
   361  		t.Fatal("unexpected error: custom resource should be of type Unstructured")
   362  	}
   363  	content := cr.UnstructuredContent()
   364  
   365  	spec := content["spec"].(map[string]interface{})
   366  	status := content["status"].(map[string]interface{})
   367  
   368  	if spec["replicas"].(int64) != 7 {
   369  		t.Errorf("we expected .spec.replicas to not be updated but it was updated to %v", spec["replicas"].(int64))
   370  	}
   371  	if status["replicas"].(int64) != 7 {
   372  		t.Errorf("we expected .status.replicas to be updated to %d but it was %v", 7, status["replicas"].(int64))
   373  	}
   374  }
   375  
   376  func TestScaleGet(t *testing.T) {
   377  	storage, server := newStorage(t)
   378  	defer server.Terminate(t)
   379  	defer storage.CustomResource.Store.DestroyFunc()
   380  
   381  	name := "foo"
   382  
   383  	var cr unstructured.Unstructured
   384  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   385  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   386  	if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
   387  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
   388  	}
   389  
   390  	want := &autoscalingv1.Scale{
   391  		TypeMeta: metav1.TypeMeta{
   392  			Kind:       "Scale",
   393  			APIVersion: "autoscaling/v1",
   394  		},
   395  		ObjectMeta: metav1.ObjectMeta{
   396  			Name:              cr.GetName(),
   397  			Namespace:         metav1.NamespaceDefault,
   398  			UID:               cr.GetUID(),
   399  			ResourceVersion:   cr.GetResourceVersion(),
   400  			CreationTimestamp: cr.GetCreationTimestamp(),
   401  		},
   402  		Spec: autoscalingv1.ScaleSpec{
   403  			Replicas: int32(7),
   404  		},
   405  	}
   406  
   407  	obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   408  	if err != nil {
   409  		t.Fatalf("error fetching scale for %s: %v", name, err)
   410  	}
   411  
   412  	got := obj.(*autoscalingv1.Scale)
   413  	if !apiequality.Semantic.DeepEqual(got, want) {
   414  		t.Errorf("unexpected scale: %s", cmp.Diff(got, want))
   415  	}
   416  }
   417  
   418  func TestScaleGetWithoutSpecReplicas(t *testing.T) {
   419  	storage, server := newStorage(t)
   420  	defer server.Terminate(t)
   421  	defer storage.CustomResource.Store.DestroyFunc()
   422  
   423  	name := "foo"
   424  
   425  	var cr unstructured.Unstructured
   426  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   427  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   428  	withoutSpecReplicas := validCustomResource.DeepCopy()
   429  	unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
   430  	if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0, false); err != nil {
   431  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
   432  	}
   433  
   434  	_, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   435  	if err == nil {
   436  		t.Fatalf("error expected for %s", name)
   437  	}
   438  	if expected := `the spec replicas field ".spec.replicas" does not exist`; !strings.Contains(err.Error(), expected) {
   439  		t.Fatalf("expected error string %q, got: %v", expected, err)
   440  	}
   441  }
   442  
   443  func TestScaleUpdate(t *testing.T) {
   444  	storage, server := newStorage(t)
   445  	defer server.Terminate(t)
   446  	defer storage.CustomResource.Store.DestroyFunc()
   447  
   448  	name := "foo"
   449  
   450  	var cr unstructured.Unstructured
   451  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   452  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   453  	if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
   454  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
   455  	}
   456  
   457  	obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   458  	if err != nil {
   459  		t.Fatalf("error fetching scale for %s: %v", name, err)
   460  	}
   461  	scale, ok := obj.(*autoscalingv1.Scale)
   462  	if !ok {
   463  		t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
   464  	}
   465  
   466  	replicas := 12
   467  	update := autoscalingv1.Scale{
   468  		ObjectMeta: scale.ObjectMeta,
   469  		Spec: autoscalingv1.ScaleSpec{
   470  			Replicas: int32(replicas),
   471  		},
   472  	}
   473  
   474  	if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   475  		t.Fatalf("error updating scale %v: %v", update, err)
   476  	}
   477  
   478  	obj, err = storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   479  	if err != nil {
   480  		t.Fatalf("error fetching scale for %s: %v", name, err)
   481  	}
   482  	scale = obj.(*autoscalingv1.Scale)
   483  	if scale.Spec.Replicas != int32(replicas) {
   484  		t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
   485  	}
   486  
   487  	update.ResourceVersion = scale.ResourceVersion
   488  	update.Spec.Replicas = 15
   489  
   490  	if _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil && !errors.IsConflict(err) {
   491  		t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
   492  	}
   493  }
   494  
   495  func TestScaleUpdateWithoutSpecReplicas(t *testing.T) {
   496  	storage, server := newStorage(t)
   497  	defer server.Terminate(t)
   498  	defer storage.CustomResource.Store.DestroyFunc()
   499  
   500  	name := "foo"
   501  
   502  	var cr unstructured.Unstructured
   503  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   504  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   505  	withoutSpecReplicas := validCustomResource.DeepCopy()
   506  	unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
   507  	if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0, false); err != nil {
   508  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
   509  	}
   510  
   511  	replicas := 12
   512  	update := autoscalingv1.Scale{
   513  		ObjectMeta: metav1.ObjectMeta{
   514  			Name:            name,
   515  			ResourceVersion: cr.GetResourceVersion(),
   516  		},
   517  		Spec: autoscalingv1.ScaleSpec{
   518  			Replicas: int32(replicas),
   519  		},
   520  	}
   521  
   522  	if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   523  		t.Fatalf("error updating scale %v: %v", update, err)
   524  	}
   525  
   526  	obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   527  	if err != nil {
   528  		t.Fatalf("error fetching scale for %s: %v", name, err)
   529  	}
   530  	scale := obj.(*autoscalingv1.Scale)
   531  	if scale.Spec.Replicas != int32(replicas) {
   532  		t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
   533  	}
   534  }
   535  
   536  func TestScaleUpdateWithoutResourceVersion(t *testing.T) {
   537  	storage, server := newStorage(t)
   538  	defer server.Terminate(t)
   539  	defer storage.CustomResource.Store.DestroyFunc()
   540  
   541  	name := "foo"
   542  
   543  	var cr unstructured.Unstructured
   544  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   545  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   546  	if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
   547  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
   548  	}
   549  
   550  	replicas := int32(8)
   551  	update := autoscalingv1.Scale{
   552  		ObjectMeta: metav1.ObjectMeta{
   553  			Name: name,
   554  		},
   555  		Spec: autoscalingv1.ScaleSpec{
   556  			Replicas: replicas,
   557  		},
   558  	}
   559  
   560  	if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   561  		t.Fatalf("error updating scale %v: %v", update, err)
   562  	}
   563  
   564  	obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   565  	if err != nil {
   566  		t.Fatalf("error fetching scale for %s: %v", name, err)
   567  	}
   568  	scale := obj.(*autoscalingv1.Scale)
   569  	if scale.Spec.Replicas != replicas {
   570  		t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
   571  	}
   572  }
   573  
   574  func TestScaleUpdateWithoutResourceVersionWithConflicts(t *testing.T) {
   575  	storage, server := newStorage(t)
   576  	defer server.Terminate(t)
   577  	defer storage.CustomResource.Store.DestroyFunc()
   578  
   579  	name := "foo"
   580  
   581  	var cr unstructured.Unstructured
   582  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   583  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   584  	if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
   585  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
   586  	}
   587  
   588  	fetchObject := func(name string) (*unstructured.Unstructured, error) {
   589  		gotObj, err := storage.CustomResource.Get(ctx, name, &metav1.GetOptions{})
   590  		if err != nil {
   591  			return nil, fmt.Errorf("error fetching custom resource %s: %v", name, err)
   592  		}
   593  		return gotObj.(*unstructured.Unstructured), nil
   594  	}
   595  
   596  	applyPatch := func(labelName, labelValue string) rest.TransformFunc {
   597  		return func(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) {
   598  			o := currentObject.(metav1.Object)
   599  			o.SetLabels(map[string]string{
   600  				labelName: labelValue,
   601  			})
   602  			return currentObject, nil
   603  		}
   604  	}
   605  
   606  	errs := make(chan error, 1)
   607  	rounds := 100
   608  	go func() {
   609  		// continuously submits a patch that updates a label and verifies the label update was effective
   610  		labelName := "timestamp"
   611  		for i := 0; i < rounds; i++ {
   612  			expectedLabelValue := fmt.Sprint(i)
   613  			update, err := fetchObject(name)
   614  			if err != nil {
   615  				errs <- err
   616  				return
   617  			}
   618  			setNestedField(update, expectedLabelValue, "metadata", "labels", labelName)
   619  			if _, _, err := storage.CustomResource.Update(ctx, name, rest.DefaultUpdatedObjectInfo(nil, applyPatch(labelName, fmt.Sprint(i))), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   620  
   621  				errs <- fmt.Errorf("error updating custom resource label: %v", err)
   622  				return
   623  			}
   624  
   625  			gotObj, err := fetchObject(name)
   626  			if err != nil {
   627  				errs <- err
   628  				return
   629  			}
   630  			gotLabelValue, _, err := unstructured.NestedString(gotObj.Object, "metadata", "labels", labelName)
   631  			if err != nil {
   632  				errs <- fmt.Errorf("error getting label %s of custom resource %s: %v", labelName, name, err)
   633  				return
   634  			}
   635  			if gotLabelValue != expectedLabelValue {
   636  				errs <- fmt.Errorf("wrong label value: expected: %s, got: %s", expectedLabelValue, gotLabelValue)
   637  				return
   638  			}
   639  		}
   640  	}()
   641  
   642  	replicas := int32(0)
   643  	update := autoscalingv1.Scale{
   644  		ObjectMeta: metav1.ObjectMeta{
   645  			Name: name,
   646  		},
   647  	}
   648  	// continuously submits a scale update without a resourceVersion for a monotonically increasing replica value
   649  	// and verifies the scale update was effective
   650  	for i := 0; i < rounds; i++ {
   651  		select {
   652  		case err := <-errs:
   653  			t.Fatal(err)
   654  		default:
   655  			replicas++
   656  			update.Spec.Replicas = replicas
   657  			if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
   658  				t.Fatalf("error updating scale %v: %v", update, err)
   659  			}
   660  
   661  			obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   662  			if err != nil {
   663  				t.Fatalf("error fetching scale for %s: %v", name, err)
   664  			}
   665  			scale := obj.(*autoscalingv1.Scale)
   666  			if scale.Spec.Replicas != replicas {
   667  				t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
   668  			}
   669  		}
   670  	}
   671  }
   672  
   673  func TestScaleUpdateWithResourceVersionWithConflicts(t *testing.T) {
   674  	storage, server := newStorage(t)
   675  	defer server.Terminate(t)
   676  	defer storage.CustomResource.Store.DestroyFunc()
   677  
   678  	name := "foo"
   679  
   680  	var cr unstructured.Unstructured
   681  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
   682  	key := "/noxus/" + metav1.NamespaceDefault + "/" + name
   683  	if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
   684  		t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
   685  	}
   686  
   687  	obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
   688  	if err != nil {
   689  		t.Fatalf("error fetching scale for %s: %v", name, err)
   690  	}
   691  	scale, ok := obj.(*autoscalingv1.Scale)
   692  	if !ok {
   693  		t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
   694  	}
   695  
   696  	replicas := int32(12)
   697  	update := autoscalingv1.Scale{
   698  		ObjectMeta: scale.ObjectMeta,
   699  		Spec: autoscalingv1.ScaleSpec{
   700  			Replicas: replicas,
   701  		},
   702  	}
   703  	update.ResourceVersion = "1"
   704  
   705  	_, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
   706  	if err == nil {
   707  		t.Fatal("expecting an update conflict error")
   708  	}
   709  	if !errors.IsConflict(err) {
   710  		t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
   711  	}
   712  }
   713  
   714  func setSpecReplicas(u *unstructured.Unstructured, replicas int64) {
   715  	setNestedField(u, replicas, "spec", "replicas")
   716  }
   717  
   718  func getSpecReplicas(u *unstructured.Unstructured) int64 {
   719  	val, found, err := unstructured.NestedInt64(u.Object, "spec", "replicas")
   720  	if !found || err != nil {
   721  		return 0
   722  	}
   723  	return val
   724  }
   725  
   726  func setStatusReplicas(u *unstructured.Unstructured, replicas int64) {
   727  	setNestedField(u, replicas, "status", "replicas")
   728  }
   729  
   730  func getStatusReplicas(u *unstructured.Unstructured) int64 {
   731  	val, found, err := unstructured.NestedInt64(u.Object, "status", "replicas")
   732  	if !found || err != nil {
   733  		return 0
   734  	}
   735  	return val
   736  }
   737  
   738  func setNestedField(u *unstructured.Unstructured, value interface{}, fields ...string) {
   739  	if u.Object == nil {
   740  		u.Object = make(map[string]interface{})
   741  	}
   742  	unstructured.SetNestedField(u.Object, value, fields...)
   743  }
   744  

View as plain text