...

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

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

     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 integration
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"math"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	autoscaling "k8s.io/api/autoscaling/v1"
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/types"
    35  	"k8s.io/client-go/dynamic"
    36  
    37  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    38  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    39  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    40  )
    41  
    42  var labelSelectorPath = ".status.labelSelector"
    43  var anotherLabelSelectorPath = ".status.anotherLabelSelector"
    44  
    45  func NewNoxuSubresourcesCRDs(scope apiextensionsv1.ResourceScope) []*apiextensionsv1.CustomResourceDefinition {
    46  	return []*apiextensionsv1.CustomResourceDefinition{
    47  		// CRD that uses per-version subresources
    48  		{
    49  			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
    50  			Spec: apiextensionsv1.CustomResourceDefinitionSpec{
    51  				Group: "mygroup.example.com",
    52  				Names: apiextensionsv1.CustomResourceDefinitionNames{
    53  					Plural:     "noxus",
    54  					Singular:   "nonenglishnoxu",
    55  					Kind:       "WishIHadChosenNoxu",
    56  					ShortNames: []string{"foo", "bar", "abc", "def"},
    57  					ListKind:   "NoxuItemList",
    58  				},
    59  				Scope: scope,
    60  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
    61  					{
    62  						Name:    "v1beta1",
    63  						Served:  true,
    64  						Storage: true,
    65  						Subresources: &apiextensionsv1.CustomResourceSubresources{
    66  							Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
    67  							Scale: &apiextensionsv1.CustomResourceSubresourceScale{
    68  								SpecReplicasPath:   ".spec.replicas",
    69  								StatusReplicasPath: ".status.replicas",
    70  								LabelSelectorPath:  &labelSelectorPath,
    71  							},
    72  						},
    73  						Schema: fixtures.AllowAllSchema(),
    74  					},
    75  					{
    76  						Name:    "v1",
    77  						Served:  true,
    78  						Storage: false,
    79  						Subresources: &apiextensionsv1.CustomResourceSubresources{
    80  							Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
    81  							Scale: &apiextensionsv1.CustomResourceSubresourceScale{
    82  								SpecReplicasPath:   ".spec.replicas",
    83  								StatusReplicasPath: ".status.replicas",
    84  								LabelSelectorPath:  &anotherLabelSelectorPath,
    85  							},
    86  						},
    87  						Schema: fixtures.AllowAllSchema(),
    88  					},
    89  				},
    90  			},
    91  		},
    92  	}
    93  }
    94  
    95  func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.Unstructured {
    96  	return &unstructured.Unstructured{
    97  		Object: map[string]interface{}{
    98  			"apiVersion": fmt.Sprintf("mygroup.example.com/%s", version),
    99  			"kind":       "WishIHadChosenNoxu",
   100  			"metadata": map[string]interface{}{
   101  				"namespace": namespace,
   102  				"name":      name,
   103  			},
   104  			"spec": map[string]interface{}{
   105  				"num":      int64(10),
   106  				"replicas": int64(3),
   107  			},
   108  			"status": map[string]interface{}{
   109  				"replicas": int64(7),
   110  			},
   111  		},
   112  	}
   113  }
   114  
   115  func NewNoxuSubresourceInstanceWithReplicas(namespace, name, version, replicasField string) *unstructured.Unstructured {
   116  	return &unstructured.Unstructured{
   117  		Object: map[string]interface{}{
   118  			"apiVersion": fmt.Sprintf("mygroup.example.com/%s", version),
   119  			"kind":       "WishIHadChosenNoxu",
   120  			"metadata": map[string]interface{}{
   121  				"namespace": namespace,
   122  				"name":      name,
   123  			},
   124  			"spec": map[string]interface{}{
   125  				"num":         int64(10),
   126  				replicasField: int64(3),
   127  			},
   128  			"status": map[string]interface{}{
   129  				"replicas": int64(7),
   130  			},
   131  		},
   132  	}
   133  }
   134  
   135  func TestStatusSubresource(t *testing.T) {
   136  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   137  	if err != nil {
   138  		t.Fatal(err)
   139  	}
   140  	defer tearDown()
   141  
   142  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   143  	for _, noxuDefinition := range noxuDefinitions {
   144  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   145  		if err != nil {
   146  			t.Fatal(err)
   147  		}
   148  
   149  		ns := "not-the-default"
   150  		for _, v := range noxuDefinition.Spec.Versions {
   151  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   152  			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
   153  			if err != nil {
   154  				t.Fatalf("unable to create noxu instance: %v", err)
   155  			}
   156  			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   157  			if err != nil {
   158  				t.Fatal(err)
   159  			}
   160  			// status should not be set after creation
   161  			if val, ok := gottenNoxuInstance.Object["status"]; ok {
   162  				t.Fatalf("status should not be set after creation, got %v", val)
   163  			}
   164  
   165  			// .status.num = 20
   166  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
   167  			if err != nil {
   168  				t.Fatalf("unexpected error: %v", err)
   169  			}
   170  
   171  			// .spec.num = 20
   172  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
   173  			if err != nil {
   174  				t.Fatalf("unexpected error: %v", err)
   175  			}
   176  
   177  			// UpdateStatus should not update spec.
   178  			// Check that .spec.num = 10 and .status.num = 20
   179  			updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   180  			if err != nil {
   181  				t.Fatalf("unable to update status: %v", err)
   182  			}
   183  
   184  			specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
   185  			if !found || err != nil {
   186  				t.Fatalf("unable to get .spec.num")
   187  			}
   188  			if specNum != int64(10) {
   189  				t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
   190  			}
   191  
   192  			statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
   193  			if !found || err != nil {
   194  				t.Fatalf("unable to get .status.num")
   195  			}
   196  			if statusNum != int64(20) {
   197  				t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
   198  			}
   199  
   200  			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   201  			if err != nil {
   202  				t.Fatal(err)
   203  			}
   204  
   205  			// .status.num = 40
   206  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
   207  			if err != nil {
   208  				t.Fatalf("unexpected error: %v", err)
   209  			}
   210  
   211  			// .spec.num = 40
   212  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
   213  			if err != nil {
   214  				t.Fatalf("unexpected error: %v", err)
   215  			}
   216  
   217  			// Update should not update status.
   218  			// Check that .spec.num = 40 and .status.num = 20
   219  			updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   220  			if err != nil {
   221  				t.Fatalf("unable to update instance: %v", err)
   222  			}
   223  
   224  			specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
   225  			if !found || err != nil {
   226  				t.Fatalf("unable to get .spec.num")
   227  			}
   228  			if specNum != int64(40) {
   229  				t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
   230  			}
   231  
   232  			statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
   233  			if !found || err != nil {
   234  				t.Fatalf("unable to get .status.num")
   235  			}
   236  			if statusNum != int64(20) {
   237  				t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
   238  			}
   239  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   240  		}
   241  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   242  			t.Fatal(err)
   243  		}
   244  	}
   245  }
   246  
   247  func TestScaleSubresource(t *testing.T) {
   248  	groupResource := schema.GroupResource{
   249  		Group:    "mygroup.example.com",
   250  		Resource: "noxus",
   251  	}
   252  
   253  	tearDown, config, _, err := fixtures.StartDefaultServer(t)
   254  	if err != nil {
   255  		t.Fatal(err)
   256  	}
   257  	defer tearDown()
   258  
   259  	apiExtensionClient, err := clientset.NewForConfig(config)
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  	dynamicClient, err := dynamic.NewForConfig(config)
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   269  	for _, noxuDefinition := range noxuDefinitions {
   270  		for _, v := range noxuDefinition.Spec.Versions {
   271  			// Start with a new CRD, so that the object doesn't have resourceVersion
   272  			noxuDefinition := noxuDefinition.DeepCopy()
   273  
   274  			subresources, err := getSubresourcesForVersion(noxuDefinition, v.Name)
   275  			if err != nil {
   276  				t.Fatal(err)
   277  			}
   278  			// set invalid json path for specReplicasPath
   279  			subresources.Scale.SpecReplicasPath = "foo,bar"
   280  			_, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   281  			if err == nil {
   282  				t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
   283  			}
   284  
   285  			subresources.Scale.SpecReplicasPath = ".spec.replicas"
   286  			noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   287  			if err != nil {
   288  				t.Fatal(err)
   289  			}
   290  
   291  			ns := "not-the-default"
   292  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   293  			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
   294  			if err != nil {
   295  				t.Fatalf("unable to create noxu instance: %v", err)
   296  			}
   297  
   298  			scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
   299  			if err != nil {
   300  				t.Fatal(err)
   301  			}
   302  
   303  			// set .status.labelSelector = bar
   304  			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   305  			if err != nil {
   306  				t.Fatal(err)
   307  			}
   308  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
   309  			if err != nil {
   310  				t.Fatalf("unexpected error: %v", err)
   311  			}
   312  			_, err = noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   313  			if err != nil {
   314  				t.Fatalf("unable to update status: %v", err)
   315  			}
   316  
   317  			// get the scale object
   318  			gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{})
   319  			if err != nil {
   320  				t.Fatal(err)
   321  			}
   322  			if gottenScale.Spec.Replicas != 3 {
   323  				t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
   324  			}
   325  			if gottenScale.Status.Selector != "bar" {
   326  				t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
   327  			}
   328  
   329  			// update the scale object
   330  			// check that spec is updated, but status is not
   331  			gottenScale.Spec.Replicas = 5
   332  			gottenScale.Status.Selector = "baz"
   333  			updatedScale, err := scaleClient.Scales("not-the-default").Update(context.TODO(), groupResource, gottenScale, metav1.UpdateOptions{})
   334  			if err != nil {
   335  				t.Fatal(err)
   336  			}
   337  			if updatedScale.Spec.Replicas != 5 {
   338  				t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
   339  			}
   340  			if updatedScale.Status.Selector != "bar" {
   341  				t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
   342  			}
   343  
   344  			// check that .spec.replicas = 5, but status is not updated
   345  			updatedNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   346  			if err != nil {
   347  				t.Fatal(err)
   348  			}
   349  			specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
   350  			if !found || err != nil {
   351  				t.Fatalf("unable to get .spec.replicas")
   352  			}
   353  			if specReplicas != 5 {
   354  				t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
   355  			}
   356  			statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
   357  			if !found || err != nil {
   358  				t.Fatalf("unable to get %s", *subresources.Scale.LabelSelectorPath)
   359  			}
   360  			if statusLabelSelector != "bar" {
   361  				t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
   362  			}
   363  
   364  			// validate maximum value
   365  			// set .spec.replicas = math.MaxInt64
   366  			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   367  			if err != nil {
   368  				t.Fatal(err)
   369  			}
   370  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
   371  			if err != nil {
   372  				t.Fatalf("unexpected error: %v", err)
   373  			}
   374  			_, err = noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   375  			if err == nil {
   376  				t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
   377  			}
   378  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   379  			if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   380  				t.Fatal(err)
   381  			}
   382  		}
   383  	}
   384  }
   385  
   386  func TestApplyScaleSubresource(t *testing.T) {
   387  	tearDown, config, _, err := fixtures.StartDefaultServer(t)
   388  	if err != nil {
   389  		t.Fatal(err)
   390  	}
   391  	defer tearDown()
   392  
   393  	apiExtensionClient, err := clientset.NewForConfig(config)
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  	dynamicClient, err := dynamic.NewForConfig(config)
   398  	if err != nil {
   399  		t.Fatal(err)
   400  	}
   401  
   402  	noxuDefinition := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)[0]
   403  	subresources, err := getSubresourcesForVersion(noxuDefinition, "v1beta1")
   404  	if err != nil {
   405  		t.Fatal(err)
   406  	}
   407  	subresources.Scale.SpecReplicasPath = ".spec.replicas[0]"
   408  	noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   409  	if err != nil {
   410  		t.Fatal(err)
   411  	}
   412  
   413  	// Create a client for it.
   414  	ns := "not-the-default"
   415  	noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, "v1beta1")
   416  
   417  	obj := NewNoxuSubresourceInstanceWithReplicas(ns, "foo", "v1beta1", "replicas[0]")
   418  	obj, err = noxuResourceClient.Create(context.TODO(), obj, metav1.CreateOptions{})
   419  	if err != nil {
   420  		t.Logf("%#v", obj)
   421  		t.Fatalf("Failed to create CustomResource: %v", err)
   422  	}
   423  
   424  	noxuResourceClient = newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, "v1")
   425  	patch := `{"metadata": {"name": "foo"}, "kind": "WishIHadChosenNoxu", "apiVersion": "mygroup.example.com/v1", "spec": {"replicas": 3}}`
   426  	obj, err = noxuResourceClient.Patch(context.TODO(), "foo", types.ApplyPatchType, []byte(patch), metav1.PatchOptions{FieldManager: "applier"})
   427  	if err != nil {
   428  		t.Logf("%#v", obj)
   429  		t.Fatalf("Failed to Apply CustomResource: %v", err)
   430  	}
   431  
   432  	if got := len(obj.GetManagedFields()); got != 2 {
   433  		t.Fatalf("Expected 2 managed fields, got %v: %v", got, obj.GetManagedFields())
   434  	}
   435  
   436  	_, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{"spec": {"replicas": 5}}`), metav1.PatchOptions{FieldManager: "scaler"}, "scale")
   437  	if err != nil {
   438  		t.Fatal(err)
   439  	}
   440  
   441  	obj, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   442  	if err != nil {
   443  		t.Fatalf("Failed to Get CustomResource: %v", err)
   444  	}
   445  
   446  	// Managed fields should have 3 entries: one for scale, one for spec, and one for the rest of the fields
   447  	managedFields := obj.GetManagedFields()
   448  	if len(managedFields) != 3 {
   449  		t.Fatalf("Expected 3 managed fields, got %v: %v", len(managedFields), obj.GetManagedFields())
   450  	}
   451  	specEntry := managedFields[0]
   452  	if specEntry.Manager != "applier" || specEntry.APIVersion != "mygroup.example.com/v1" || specEntry.Operation != "Apply" || string(specEntry.FieldsV1.Raw) != `{"f:spec":{}}` || specEntry.Subresource != "" {
   453  		t.Fatalf("Unexpected entry: %v", specEntry)
   454  	}
   455  	scaleEntry := managedFields[1]
   456  	if scaleEntry.Manager != "scaler" || scaleEntry.APIVersion != "mygroup.example.com/v1" || scaleEntry.Operation != "Update" || string(scaleEntry.FieldsV1.Raw) != `{"f:spec":{"f:replicas":{}}}` || scaleEntry.Subresource != "scale" {
   457  		t.Fatalf("Unexpected entry: %v", scaleEntry)
   458  	}
   459  	restEntry := managedFields[2]
   460  	if restEntry.Manager != "integration.test" || restEntry.APIVersion != "mygroup.example.com/v1beta1" {
   461  		t.Fatalf("Unexpected entry: %v", restEntry)
   462  	}
   463  }
   464  
   465  func TestValidationSchemaWithStatus(t *testing.T) {
   466  	tearDown, config, _, err := fixtures.StartDefaultServer(t)
   467  	if err != nil {
   468  		t.Fatal(err)
   469  	}
   470  	defer tearDown()
   471  
   472  	apiExtensionClient, err := clientset.NewForConfig(config)
   473  	if err != nil {
   474  		t.Fatal(err)
   475  	}
   476  	dynamicClient, err := dynamic.NewForConfig(config)
   477  	if err != nil {
   478  		t.Fatal(err)
   479  	}
   480  
   481  	noxuDefinition := newNoxuValidationCRDs()[0]
   482  
   483  	// make sure we are not restricting fields to properties even in subschemas
   484  	noxuDefinition.Spec.Versions[0].Schema.OpenAPIV3Schema = &apiextensionsv1.JSONSchemaProps{
   485  		Type: "object",
   486  		Properties: map[string]apiextensionsv1.JSONSchemaProps{
   487  			"spec": {
   488  				Type:        "object",
   489  				Description: "Validation for spec",
   490  				Properties: map[string]apiextensionsv1.JSONSchemaProps{
   491  					"replicas": {
   492  						Type: "integer",
   493  					},
   494  				},
   495  			},
   496  		},
   497  		Required:    []string{"spec"},
   498  		Description: "This is a description at the root of the schema",
   499  	}
   500  	noxuDefinition.Spec.Versions[1].Schema.OpenAPIV3Schema = noxuDefinition.Spec.Versions[0].Schema.OpenAPIV3Schema
   501  
   502  	_, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   503  	if err != nil {
   504  		t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
   505  	}
   506  }
   507  
   508  func TestValidateOnlyStatus(t *testing.T) {
   509  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   510  	if err != nil {
   511  		t.Fatal(err)
   512  	}
   513  	defer tearDown()
   514  
   515  	// UpdateStatus should validate only status
   516  	// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
   517  	// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
   518  	// 3. update the spec of the cr with .spec.num = 15 (spec is invalid), expect no error
   519  	// 4. update the spec of the cr with .spec.num = 15 (spec is invalid), expect error
   520  
   521  	// max value of spec.num = 10 and status.num = 10
   522  	schema := &apiextensionsv1.JSONSchemaProps{
   523  		Type: "object",
   524  		Properties: map[string]apiextensionsv1.JSONSchemaProps{
   525  			"spec": {
   526  				Type: "object",
   527  				Properties: map[string]apiextensionsv1.JSONSchemaProps{
   528  					"num": {
   529  						Type:    "integer",
   530  						Maximum: float64Ptr(10),
   531  					},
   532  				},
   533  			},
   534  			"status": {
   535  				Type: "object",
   536  				Properties: map[string]apiextensionsv1.JSONSchemaProps{
   537  					"num": {
   538  						Type:    "integer",
   539  						Maximum: float64Ptr(10),
   540  					},
   541  				},
   542  			},
   543  		},
   544  	}
   545  
   546  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   547  	for _, noxuDefinition := range noxuDefinitions {
   548  		noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1.CustomResourceValidation{
   549  			OpenAPIV3Schema: schema.DeepCopy(),
   550  		}
   551  		noxuDefinition.Spec.Versions[1].Schema = &apiextensionsv1.CustomResourceValidation{
   552  			OpenAPIV3Schema: schema.DeepCopy(),
   553  		}
   554  
   555  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   556  		if err != nil {
   557  			t.Fatal(err)
   558  		}
   559  		ns := "not-the-default"
   560  		for _, v := range noxuDefinition.Spec.Versions {
   561  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   562  
   563  			// set .spec.num = 10 and .status.num = 10
   564  			noxuInstance := NewNoxuSubresourceInstance(ns, "foo", v.Name)
   565  			err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
   566  			if err != nil {
   567  				t.Fatalf("unexpected error: %v", err)
   568  			}
   569  
   570  			createdNoxuInstance, err := instantiateVersionedCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition, v.Name)
   571  			if err != nil {
   572  				t.Fatalf("unable to create noxu instance: %v", err)
   573  			}
   574  
   575  			// update the spec with .spec.num = 15, expecting no error
   576  			err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num")
   577  			if err != nil {
   578  				t.Fatalf("unexpected error setting .spec.num: %v", err)
   579  			}
   580  			createdNoxuInstance, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{})
   581  			if err != nil {
   582  				t.Fatalf("unexpected error: %v", err)
   583  			}
   584  
   585  			// update with .status.num = 15, expecting an error
   586  			err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num")
   587  			if err != nil {
   588  				t.Fatalf("unexpected error setting .status.num: %v", err)
   589  			}
   590  			_, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{})
   591  			if err == nil {
   592  				t.Fatal("expected error, but got none")
   593  			}
   594  			statusError, isStatus := err.(*apierrors.StatusError)
   595  			if !isStatus || statusError == nil {
   596  				t.Fatalf("expected status error, got %T: %v", err, err)
   597  			}
   598  			if !strings.Contains(statusError.Error(), "Invalid value") {
   599  				t.Fatalf("expected 'Invalid value' in error, got: %v", err)
   600  			}
   601  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   602  		}
   603  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   604  			t.Fatal(err)
   605  		}
   606  	}
   607  }
   608  
   609  func TestSubresourcesDiscovery(t *testing.T) {
   610  	tearDown, config, _, err := fixtures.StartDefaultServer(t)
   611  	if err != nil {
   612  		t.Fatal(err)
   613  	}
   614  	defer tearDown()
   615  
   616  	apiExtensionClient, err := clientset.NewForConfig(config)
   617  	if err != nil {
   618  		t.Fatal(err)
   619  	}
   620  	dynamicClient, err := dynamic.NewForConfig(config)
   621  	if err != nil {
   622  		t.Fatal(err)
   623  	}
   624  
   625  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   626  	for _, noxuDefinition := range noxuDefinitions {
   627  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   628  		if err != nil {
   629  			t.Fatal(err)
   630  		}
   631  
   632  		for _, v := range noxuDefinition.Spec.Versions {
   633  			group := "mygroup.example.com"
   634  			version := v.Name
   635  
   636  			resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
   637  			if err != nil {
   638  				t.Fatal(err)
   639  			}
   640  
   641  			if len(resources.APIResources) != 3 {
   642  				t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
   643  			}
   644  
   645  			// check discovery info for status
   646  			status := resources.APIResources[1]
   647  
   648  			if status.Name != "noxus/status" {
   649  				t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
   650  			}
   651  
   652  			if status.Namespaced != true {
   653  				t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
   654  			}
   655  
   656  			if status.Kind != "WishIHadChosenNoxu" {
   657  				t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
   658  			}
   659  
   660  			expectedVerbs := []string{"get", "patch", "update"}
   661  			sort.Strings(status.Verbs)
   662  			if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
   663  				t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
   664  			}
   665  
   666  			// check discovery info for scale
   667  			scale := resources.APIResources[2]
   668  
   669  			if scale.Group != autoscaling.GroupName {
   670  				t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
   671  			}
   672  
   673  			if scale.Version != "v1" {
   674  				t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
   675  			}
   676  
   677  			if scale.Name != "noxus/scale" {
   678  				t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
   679  			}
   680  
   681  			if scale.Namespaced != true {
   682  				t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
   683  			}
   684  
   685  			if scale.Kind != "Scale" {
   686  				t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
   687  			}
   688  
   689  			sort.Strings(scale.Verbs)
   690  			if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
   691  				t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
   692  			}
   693  		}
   694  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   695  			t.Fatal(err)
   696  		}
   697  	}
   698  }
   699  
   700  func TestGeneration(t *testing.T) {
   701  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   702  	if err != nil {
   703  		t.Fatal(err)
   704  	}
   705  	defer tearDown()
   706  
   707  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   708  	for _, noxuDefinition := range noxuDefinitions {
   709  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   710  		if err != nil {
   711  			t.Fatal(err)
   712  		}
   713  
   714  		ns := "not-the-default"
   715  		for _, v := range noxuDefinition.Spec.Versions {
   716  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   717  			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
   718  			if err != nil {
   719  				t.Fatalf("unable to create noxu instance: %v", err)
   720  			}
   721  
   722  			// .metadata.generation = 1
   723  			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   724  			if err != nil {
   725  				t.Fatal(err)
   726  			}
   727  			if gottenNoxuInstance.GetGeneration() != 1 {
   728  				t.Fatalf(".metadata.generation should be 1 after creation")
   729  			}
   730  
   731  			// .status.num = 20
   732  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
   733  			if err != nil {
   734  				t.Fatalf("unexpected error: %v", err)
   735  			}
   736  
   737  			// UpdateStatus does not increment generation
   738  			updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   739  			if err != nil {
   740  				t.Fatalf("unable to update status: %v", err)
   741  			}
   742  			if updatedStatusInstance.GetGeneration() != 1 {
   743  				t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
   744  			}
   745  
   746  			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   747  			if err != nil {
   748  				t.Fatal(err)
   749  			}
   750  
   751  			// .spec.num = 20
   752  			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
   753  			if err != nil {
   754  				t.Fatalf("unexpected error: %v", err)
   755  			}
   756  
   757  			// Update increments generation
   758  			updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   759  			if err != nil {
   760  				t.Fatalf("unable to update instance: %v", err)
   761  			}
   762  			if updatedInstance.GetGeneration() != 2 {
   763  				t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
   764  			}
   765  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   766  		}
   767  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   768  			t.Fatal(err)
   769  		}
   770  	}
   771  }
   772  
   773  func TestSubresourcePatch(t *testing.T) {
   774  	groupResource := schema.GroupResource{
   775  		Group:    "mygroup.example.com",
   776  		Resource: "noxus",
   777  	}
   778  
   779  	tearDown, config, _, err := fixtures.StartDefaultServer(t)
   780  	if err != nil {
   781  		t.Fatal(err)
   782  	}
   783  	defer tearDown()
   784  
   785  	apiExtensionClient, err := clientset.NewForConfig(config)
   786  	if err != nil {
   787  		t.Fatal(err)
   788  	}
   789  	dynamicClient, err := dynamic.NewForConfig(config)
   790  	if err != nil {
   791  		t.Fatal(err)
   792  	}
   793  
   794  	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1.NamespaceScoped)
   795  	for _, noxuDefinition := range noxuDefinitions {
   796  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   797  		if err != nil {
   798  			t.Fatal(err)
   799  		}
   800  
   801  		ns := "not-the-default"
   802  		for _, v := range noxuDefinition.Spec.Versions {
   803  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   804  
   805  			t.Logf("Creating foo")
   806  			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
   807  			if err != nil {
   808  				t.Fatalf("unable to create noxu instance: %v", err)
   809  			}
   810  
   811  			scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
   812  			if err != nil {
   813  				t.Fatal(err)
   814  			}
   815  
   816  			t.Logf("Patching .status.num to 999")
   817  			patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
   818  			patchedNoxuInstance, err := noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status")
   819  			if err != nil {
   820  				t.Fatalf("unexpected error: %v", err)
   821  			}
   822  
   823  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999
   824  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")    // .spec.num should remain 10
   825  
   826  			// server-side-apply increments resouceVersion if the resource is unchanged for 1 second after the previous write,
   827  			// and by waiting a second we ensure that resourceVersion will be updated if the no-op patch increments resourceVersion
   828  			time.Sleep(time.Second)
   829  			// no-op patch
   830  			rv := patchedNoxuInstance.GetResourceVersion()
   831  			found := false
   832  			t.Logf("Patching .status.num again to 999")
   833  			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status")
   834  			if err != nil {
   835  				t.Fatalf("unexpected error: %v", err)
   836  			}
   837  			// make sure no-op patch does not increment resourceVersion
   838  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
   839  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
   840  			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
   841  
   842  			// empty patch
   843  			t.Logf("Applying empty patch")
   844  			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "status")
   845  			if err != nil {
   846  				t.Fatalf("unexpected error: %v", err)
   847  			}
   848  
   849  			// an empty patch is a no-op patch. make sure it does not increment resourceVersion
   850  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
   851  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
   852  			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
   853  
   854  			t.Logf("Patching .spec.replicas to 7")
   855  			patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
   856  			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale")
   857  			if err != nil {
   858  				t.Fatalf("unexpected error: %v", err)
   859  			}
   860  
   861  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
   862  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0
   863  			rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
   864  			if err != nil {
   865  				t.Fatal(err)
   866  			}
   867  			if !found {
   868  				t.Fatalf("metadata.resourceVersion not found")
   869  			}
   870  
   871  			// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0
   872  			gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{})
   873  			if err != nil {
   874  				t.Fatal(err)
   875  			}
   876  			if gottenScale.Spec.Replicas != 7 {
   877  				t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
   878  			}
   879  			if gottenScale.Status.Replicas != 0 {
   880  				t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
   881  			}
   882  
   883  			// no-op patch
   884  			t.Logf("Patching .spec.replicas again to 7")
   885  			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale")
   886  			if err != nil {
   887  				t.Fatalf("unexpected error: %v", err)
   888  			}
   889  			// make sure no-op patch does not increment resourceVersion
   890  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
   891  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
   892  			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
   893  
   894  			// empty patch
   895  			t.Logf("Applying empty patch")
   896  			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "scale")
   897  			if err != nil {
   898  				t.Fatalf("unexpected error: %v", err)
   899  			}
   900  			// an empty patch is a no-op patch. make sure it does not increment resourceVersion
   901  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
   902  			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
   903  			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
   904  
   905  			// make sure strategic merge patch is not supported for both status and scale
   906  			_, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "status")
   907  			if err == nil {
   908  				t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
   909  			}
   910  
   911  			_, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "scale")
   912  			if err == nil {
   913  				t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
   914  			}
   915  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   916  		}
   917  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   918  			t.Fatal(err)
   919  		}
   920  	}
   921  }
   922  

View as plain text