k8s.io/kubernetes@v1.29.3/test/integration/apiserver/apply/apply_crd_test.go (about)

     1  /*
     2  Copyright 2019 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 apiserver
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"path"
    24  	"reflect"
    25  	"testing"
    26  	"time"
    27  
    28  	"k8s.io/apimachinery/pkg/util/wait"
    29  
    30  	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    31  
    32  	"go.etcd.io/etcd/client/pkg/v3/transport"
    33  	clientv3 "go.etcd.io/etcd/client/v3"
    34  	"google.golang.org/grpc"
    35  
    36  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    37  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    38  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    39  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    40  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    41  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    42  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    43  	"k8s.io/apimachinery/pkg/types"
    44  	"k8s.io/client-go/dynamic"
    45  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    46  	"k8s.io/kubernetes/test/integration/framework"
    47  )
    48  
    49  // TestApplyCRDStructuralSchema tests that when a CRD has a structural schema in its validation field,
    50  // it will be used to construct the CR schema used by apply.
    51  func TestApplyCRDStructuralSchema(t *testing.T) {
    52  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
    53  	if err != nil {
    54  		t.Fatal(err)
    55  	}
    56  	defer server.TearDownFn()
    57  	config := server.ClientConfig
    58  
    59  	apiExtensionClient, err := clientset.NewForConfig(config)
    60  	if err != nil {
    61  		t.Fatal(err)
    62  	}
    63  	dynamicClient, err := dynamic.NewForConfig(config)
    64  	if err != nil {
    65  		t.Fatal(err)
    66  	}
    67  
    68  	noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1.ClusterScoped)
    69  
    70  	var c apiextensionsv1.CustomResourceValidation
    71  	err = json.Unmarshal([]byte(`{
    72  		"openAPIV3Schema": {
    73  			"type": "object",
    74  			"properties": {
    75  				"spec": {
    76  					"type": "object",
    77  					"x-kubernetes-preserve-unknown-fields": true,
    78  					"properties": {
    79  						"cronSpec": {
    80  							"type": "string",
    81  							"pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$"
    82  						},
    83  						"ports": {
    84  							"type": "array",
    85  							"x-kubernetes-list-map-keys": [
    86  								"containerPort",
    87  								"protocol"
    88  							],
    89  							"x-kubernetes-list-type": "map",
    90  							"items": {
    91  								"properties": {
    92  									"containerPort": {
    93  										"format": "int32",
    94  										"type": "integer"
    95  									},
    96  									"hostIP": {
    97  										"type": "string"
    98  									},
    99  									"hostPort": {
   100  										"format": "int32",
   101  										"type": "integer"
   102  									},
   103  									"name": {
   104  										"type": "string"
   105  									},
   106  									"protocol": {
   107  										"type": "string"
   108  									}
   109  								},
   110  								"required": [
   111  									"containerPort",
   112  									"protocol"
   113  								],
   114  								"type": "object"
   115  							}
   116  						}
   117  					}
   118  				}
   119  			}
   120  		}
   121  	}`), &c)
   122  	if err != nil {
   123  		t.Fatal(err)
   124  	}
   125  	for i := range noxuDefinition.Spec.Versions {
   126  		noxuDefinition.Spec.Versions[i].Schema = &c
   127  	}
   128  
   129  	noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   130  	if err != nil {
   131  		t.Fatal(err)
   132  	}
   133  
   134  	kind := noxuDefinition.Spec.Names.Kind
   135  	apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name
   136  	name := "mytest"
   137  
   138  	rest := apiExtensionClient.Discovery().RESTClient()
   139  	yamlBody := []byte(fmt.Sprintf(`
   140  apiVersion: %s
   141  kind: %s
   142  metadata:
   143    name: %s
   144    finalizers:
   145    - test-finalizer
   146  spec:
   147    cronSpec: "* * * * */5"
   148    replicas: 1
   149    ports:
   150    - name: x
   151      containerPort: 80
   152      protocol: TCP`, apiVersion, kind, name))
   153  	result, err := rest.Patch(types.ApplyPatchType).
   154  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   155  		Name(name).
   156  		Param("fieldManager", "apply_test").
   157  		Body(yamlBody).
   158  		DoRaw(context.TODO())
   159  	if err != nil {
   160  		t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
   161  	}
   162  	verifyNumFinalizers(t, result, 1)
   163  	verifyFinalizersIncludes(t, result, "test-finalizer")
   164  	verifyReplicas(t, result, 1)
   165  	verifyNumPorts(t, result, 1)
   166  
   167  	// Patch object to add another finalizer to the finalizers list
   168  	result, err = rest.Patch(types.MergePatchType).
   169  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   170  		Name(name).
   171  		Body([]byte(`{"metadata":{"finalizers":["test-finalizer","another-one"]}}`)).
   172  		DoRaw(context.TODO())
   173  	if err != nil {
   174  		t.Fatalf("failed to add finalizer with merge patch: %v:\n%v", err, string(result))
   175  	}
   176  	verifyNumFinalizers(t, result, 2)
   177  	verifyFinalizersIncludes(t, result, "test-finalizer")
   178  	verifyFinalizersIncludes(t, result, "another-one")
   179  
   180  	// Re-apply the same config, should work fine, since finalizers should have the list-type extension 'set'.
   181  	result, err = rest.Patch(types.ApplyPatchType).
   182  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   183  		Name(name).
   184  		Param("fieldManager", "apply_test").
   185  		SetHeader("Accept", "application/json").
   186  		Body(yamlBody).
   187  		DoRaw(context.TODO())
   188  	if err != nil {
   189  		t.Fatalf("failed to apply same config after adding a finalizer: %v:\n%v", err, string(result))
   190  	}
   191  	verifyNumFinalizers(t, result, 2)
   192  	verifyFinalizersIncludes(t, result, "test-finalizer")
   193  	verifyFinalizersIncludes(t, result, "another-one")
   194  
   195  	// Patch object to change the number of replicas
   196  	result, err = rest.Patch(types.MergePatchType).
   197  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   198  		Name(name).
   199  		Body([]byte(`{"spec":{"replicas": 5}}`)).
   200  		DoRaw(context.TODO())
   201  	if err != nil {
   202  		t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
   203  	}
   204  	verifyReplicas(t, result, 5)
   205  
   206  	// Re-apply, we should get conflicts now, since the number of replicas was changed.
   207  	result, err = rest.Patch(types.ApplyPatchType).
   208  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   209  		Name(name).
   210  		Param("fieldManager", "apply_test").
   211  		Body(yamlBody).
   212  		DoRaw(context.TODO())
   213  	if err == nil {
   214  		t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
   215  	}
   216  	status, ok := err.(*apierrors.StatusError)
   217  	if !ok {
   218  		t.Fatalf("Expecting to get conflicts as API error")
   219  	}
   220  	if len(status.Status().Details.Causes) != 1 {
   221  		t.Fatalf("Expecting to get one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
   222  	}
   223  
   224  	// Re-apply with force, should work fine.
   225  	result, err = rest.Patch(types.ApplyPatchType).
   226  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   227  		Name(name).
   228  		Param("force", "true").
   229  		Param("fieldManager", "apply_test").
   230  		Body(yamlBody).
   231  		DoRaw(context.TODO())
   232  	if err != nil {
   233  		t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
   234  	}
   235  	verifyReplicas(t, result, 1)
   236  
   237  	// New applier tries to edit an existing list item, we should get conflicts.
   238  	result, err = rest.Patch(types.ApplyPatchType).
   239  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   240  		Name(name).
   241  		Param("fieldManager", "apply_test_2").
   242  		Body([]byte(fmt.Sprintf(`
   243  apiVersion: %s
   244  kind: %s
   245  metadata:
   246    name: %s
   247  spec:
   248    ports:
   249    - name: "y"
   250      containerPort: 80
   251      protocol: TCP`, apiVersion, kind, name))).
   252  		DoRaw(context.TODO())
   253  	if err == nil {
   254  		t.Fatalf("Expecting to get conflicts when a different applier updates existing list item, got no error: %s", result)
   255  	}
   256  	status, ok = err.(*apierrors.StatusError)
   257  	if !ok {
   258  		t.Fatalf("Expecting to get conflicts as API error")
   259  	}
   260  	if len(status.Status().Details.Causes) != 1 {
   261  		t.Fatalf("Expecting to get one conflict when a different applier updates existing list item, got: %v", status.Status().Details.Causes)
   262  	}
   263  
   264  	// New applier tries to add a new list item, should work fine.
   265  	result, err = rest.Patch(types.ApplyPatchType).
   266  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   267  		Name(name).
   268  		Param("fieldManager", "apply_test_2").
   269  		Body([]byte(fmt.Sprintf(`
   270  apiVersion: %s
   271  kind: %s
   272  metadata:
   273    name: %s
   274  spec:
   275    ports:
   276    - name: "y"
   277      containerPort: 8080
   278      protocol: TCP`, apiVersion, kind, name))).
   279  		SetHeader("Accept", "application/json").
   280  		DoRaw(context.TODO())
   281  	if err != nil {
   282  		t.Fatalf("failed to add a new list item to the object as a different applier: %v:\n%v", err, string(result))
   283  	}
   284  	verifyNumPorts(t, result, 2)
   285  
   286  	// UpdateOnCreate
   287  	notExistingYAMLBody := []byte(fmt.Sprintf(`
   288  	{
   289  		"apiVersion": "%s",
   290  		"kind": "%s",
   291  		"metadata": {
   292  		  "name": "%s",
   293  		  "finalizers": [
   294  			"test-finalizer"
   295  		  ]
   296  		},
   297  		"spec": {
   298  		  "cronSpec": "* * * * */5",
   299  		  "replicas": 1,
   300  		  "ports": [
   301  			{
   302  			  "name": "x",
   303  			  "containerPort": 80
   304  			}
   305  		  ]
   306  		},
   307  		"protocol": "TCP"
   308  	}`, apiVersion, kind, "should-not-exist"))
   309  	_, err = rest.Put().
   310  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   311  		Name("should-not-exist").
   312  		Param("fieldManager", "apply_test").
   313  		Body(notExistingYAMLBody).
   314  		DoRaw(context.TODO())
   315  	if !apierrors.IsNotFound(err) {
   316  		t.Fatalf("create on update should fail with notFound, got %v", err)
   317  	}
   318  }
   319  
   320  // verifyNumFinalizers checks that len(.metadata.finalizers) == n
   321  func verifyNumFinalizers(t *testing.T, b []byte, n int) {
   322  	obj := unstructured.Unstructured{}
   323  	err := obj.UnmarshalJSON(b)
   324  	if err != nil {
   325  		t.Fatalf("failed to unmarshal response: %v", err)
   326  	}
   327  	if actual, expected := len(obj.GetFinalizers()), n; actual != expected {
   328  		t.Fatalf("expected %v finalizers but got %v:\n%v", expected, actual, string(b))
   329  	}
   330  }
   331  
   332  // verifyFinalizersIncludes checks that .metadata.finalizers includes e
   333  func verifyFinalizersIncludes(t *testing.T, b []byte, e string) {
   334  	obj := unstructured.Unstructured{}
   335  	err := obj.UnmarshalJSON(b)
   336  	if err != nil {
   337  		t.Fatalf("failed to unmarshal response: %v", err)
   338  	}
   339  	for _, a := range obj.GetFinalizers() {
   340  		if a == e {
   341  			return
   342  		}
   343  	}
   344  	t.Fatalf("expected finalizers to include %q but got: %v", e, obj.GetFinalizers())
   345  }
   346  
   347  // verifyReplicas checks that .spec.replicas == r
   348  func verifyReplicas(t *testing.T, b []byte, r int) {
   349  	obj := unstructured.Unstructured{}
   350  	err := obj.UnmarshalJSON(b)
   351  	if err != nil {
   352  		t.Fatalf("failed to find replicas number in response: %v:\n%v", err, string(b))
   353  	}
   354  	spec, ok := obj.Object["spec"]
   355  	if !ok {
   356  		t.Fatalf("failed to find replicas number in response:\n%v", string(b))
   357  	}
   358  	specMap, ok := spec.(map[string]interface{})
   359  	if !ok {
   360  		t.Fatalf("failed to find replicas number in response:\n%v", string(b))
   361  	}
   362  	replicas, ok := specMap["replicas"]
   363  	if !ok {
   364  		t.Fatalf("failed to find replicas number in response:\n%v", string(b))
   365  	}
   366  	replicasNumber, ok := replicas.(int64)
   367  	if !ok {
   368  		t.Fatalf("failed to find replicas number in response: expected int64 but got: %v", reflect.TypeOf(replicas))
   369  	}
   370  	if actual, expected := replicasNumber, int64(r); actual != expected {
   371  		t.Fatalf("expected %v ports but got %v:\n%v", expected, actual, string(b))
   372  	}
   373  }
   374  
   375  // verifyNumPorts checks that len(.spec.ports) == n
   376  func verifyNumPorts(t *testing.T, b []byte, n int) {
   377  	obj := unstructured.Unstructured{}
   378  	err := obj.UnmarshalJSON(b)
   379  	if err != nil {
   380  		t.Fatalf("failed to find ports list in response: %v:\n%v", err, string(b))
   381  	}
   382  	spec, ok := obj.Object["spec"]
   383  	if !ok {
   384  		t.Fatalf("failed to find ports list in response:\n%v", string(b))
   385  	}
   386  	specMap, ok := spec.(map[string]interface{})
   387  	if !ok {
   388  		t.Fatalf("failed to find ports list in response:\n%v", string(b))
   389  	}
   390  	ports, ok := specMap["ports"]
   391  	if !ok {
   392  		t.Fatalf("failed to find ports list in response:\n%v", string(b))
   393  	}
   394  	portsList, ok := ports.([]interface{})
   395  	if !ok {
   396  		t.Fatalf("failed to find ports list in response: expected array but got: %v", reflect.TypeOf(ports))
   397  	}
   398  	if actual, expected := len(portsList), n; actual != expected {
   399  		t.Fatalf("expected %v ports but got %v:\n%v", expected, actual, string(b))
   400  	}
   401  }
   402  
   403  func findCRDCondition(crd *apiextensionsv1.CustomResourceDefinition, conditionType apiextensionsv1.CustomResourceDefinitionConditionType) *apiextensionsv1.CustomResourceDefinitionCondition {
   404  	for i := range crd.Status.Conditions {
   405  		if crd.Status.Conditions[i].Type == conditionType {
   406  			return &crd.Status.Conditions[i]
   407  		}
   408  	}
   409  
   410  	return nil
   411  }
   412  
   413  // TestApplyCRDUnhandledSchema tests that when a CRD has a schema that kube-openapi ToProtoModels cannot handle correctly,
   414  // apply falls back to non-schema behavior
   415  func TestApplyCRDUnhandledSchema(t *testing.T) {
   416  	storageConfig := framework.SharedEtcd()
   417  	tlsInfo := transport.TLSInfo{
   418  		CertFile:      storageConfig.Transport.CertFile,
   419  		KeyFile:       storageConfig.Transport.KeyFile,
   420  		TrustedCAFile: storageConfig.Transport.TrustedCAFile,
   421  	}
   422  	tlsConfig, err := tlsInfo.ClientConfig()
   423  	if err != nil {
   424  		t.Fatal(err)
   425  	}
   426  	etcdConfig := clientv3.Config{
   427  		Endpoints:   storageConfig.Transport.ServerList,
   428  		DialTimeout: 20 * time.Second,
   429  		DialOptions: []grpc.DialOption{
   430  			grpc.WithBlock(), // block until the underlying connection is up
   431  		},
   432  		TLS: tlsConfig,
   433  	}
   434  	etcdclient, err := clientv3.New(etcdConfig)
   435  	if err != nil {
   436  		t.Fatal(err)
   437  	}
   438  	defer etcdclient.Close()
   439  
   440  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, storageConfig)
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	defer server.TearDownFn()
   445  	config := server.ClientConfig
   446  
   447  	apiExtensionClient, err := clientset.NewForConfig(config)
   448  	if err != nil {
   449  		t.Fatal(err)
   450  	}
   451  
   452  	// this has to be v1beta1, so we can have an item with validation that does not match.  v1 validation prevents this.
   453  
   454  	noxuBetaDefinition := &apiextensionsv1beta1.CustomResourceDefinition{
   455  		TypeMeta: metav1.TypeMeta{
   456  			Kind:       "CustomResourceDefinition",
   457  			APIVersion: "apiextensions.k8s.io/v1beta1",
   458  		},
   459  		ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
   460  		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   461  			Group: "mygroup.example.com",
   462  			Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{{
   463  				Name:    "v1beta1",
   464  				Served:  true,
   465  				Storage: true,
   466  			}},
   467  			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   468  				Plural:     "noxus",
   469  				Singular:   "nonenglishnoxu",
   470  				Kind:       "WishIHadChosenNoxu",
   471  				ShortNames: []string{"foo", "bar", "abc", "def"},
   472  				ListKind:   "NoxuItemList",
   473  				Categories: []string{"all"},
   474  			},
   475  			Scope: apiextensionsv1beta1.ClusterScoped,
   476  		},
   477  	}
   478  
   479  	// This is a schema that kube-openapi ToProtoModels does not handle correctly.
   480  	// https://github.com/kubernetes/kubernetes/blob/38752f7f99869ed65fb44378360a517649dc2f83/vendor/k8s.io/kube-openapi/pkg/util/proto/document.go#L184
   481  	var c apiextensionsv1beta1.CustomResourceValidation
   482  	err = json.Unmarshal([]byte(`{
   483  		"openAPIV3Schema": {
   484  			"properties": {
   485  				"TypeFooBar": {
   486  					"type": "array"
   487  				}
   488  			}
   489  		}
   490  	}`), &c)
   491  	if err != nil {
   492  		t.Fatal(err)
   493  	}
   494  	noxuBetaDefinition.Spec.Validation = &c
   495  
   496  	betaBytes, err := json.Marshal(noxuBetaDefinition)
   497  	if err != nil {
   498  		t.Fatal(err)
   499  	}
   500  	t.Log(string(betaBytes))
   501  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceNone)
   502  	key := path.Join("/", storageConfig.Prefix, "apiextensions.k8s.io", "customresourcedefinitions", noxuBetaDefinition.Name)
   503  	if _, err := etcdclient.Put(ctx, key, string(betaBytes)); err != nil {
   504  		t.Fatalf("unexpected error: %v", err)
   505  	}
   506  
   507  	noxuDefinition, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), noxuBetaDefinition.Name, metav1.GetOptions{})
   508  	if err != nil {
   509  		t.Fatal(err)
   510  	}
   511  	// wait until the CRD is established
   512  	err = wait.Poll(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   513  		localCrd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), noxuBetaDefinition.Name, metav1.GetOptions{})
   514  		if err != nil {
   515  			return false, err
   516  		}
   517  		condition := findCRDCondition(localCrd, apiextensionsv1.Established)
   518  		if condition == nil {
   519  			return false, nil
   520  		}
   521  		if condition.Status == apiextensionsv1.ConditionTrue {
   522  			return true, nil
   523  		}
   524  		return false, nil
   525  	})
   526  	if err != nil {
   527  		t.Fatal(err)
   528  	}
   529  
   530  	kind := noxuDefinition.Spec.Names.Kind
   531  	apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name
   532  	name := "mytest"
   533  
   534  	rest := apiExtensionClient.Discovery().RESTClient()
   535  	yamlBody := []byte(fmt.Sprintf(`
   536  apiVersion: %s
   537  kind: %s
   538  metadata:
   539    name: %s
   540  spec:
   541    replicas: 1`, apiVersion, kind, name))
   542  	result, err := rest.Patch(types.ApplyPatchType).
   543  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   544  		Name(name).
   545  		Param("fieldManager", "apply_test").
   546  		Body(yamlBody).
   547  		DoRaw(context.TODO())
   548  	if err != nil {
   549  		t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
   550  	}
   551  	verifyReplicas(t, result, 1)
   552  
   553  	// Patch object to change the number of replicas
   554  	result, err = rest.Patch(types.MergePatchType).
   555  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   556  		Name(name).
   557  		Body([]byte(`{"spec":{"replicas": 5}}`)).
   558  		DoRaw(context.TODO())
   559  	if err != nil {
   560  		t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
   561  	}
   562  	verifyReplicas(t, result, 5)
   563  
   564  	// Re-apply, we should get conflicts now, since the number of replicas was changed.
   565  	result, err = rest.Patch(types.ApplyPatchType).
   566  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   567  		Name(name).
   568  		Param("fieldManager", "apply_test").
   569  		Body(yamlBody).
   570  		DoRaw(context.TODO())
   571  	if err == nil {
   572  		t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
   573  	}
   574  	status, ok := err.(*apierrors.StatusError)
   575  	if !ok {
   576  		t.Fatalf("Expecting to get conflicts as API error")
   577  	}
   578  	if len(status.Status().Details.Causes) != 1 {
   579  		t.Fatalf("Expecting to get one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
   580  	}
   581  
   582  	// Re-apply with force, should work fine.
   583  	result, err = rest.Patch(types.ApplyPatchType).
   584  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   585  		Name(name).
   586  		Param("force", "true").
   587  		Param("fieldManager", "apply_test").
   588  		Body(yamlBody).
   589  		DoRaw(context.TODO())
   590  	if err != nil {
   591  		t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
   592  	}
   593  	verifyReplicas(t, result, 1)
   594  }
   595  
   596  func getManagedFields(rawResponse []byte) ([]metav1.ManagedFieldsEntry, error) {
   597  	obj := unstructured.Unstructured{}
   598  	if err := obj.UnmarshalJSON(rawResponse); err != nil {
   599  		return nil, err
   600  	}
   601  	return obj.GetManagedFields(), nil
   602  }
   603  
   604  func TestDefaultMissingKeyCRD(t *testing.T) {
   605  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
   606  	if err != nil {
   607  		t.Fatal(err)
   608  	}
   609  	defer server.TearDownFn()
   610  	config := server.ClientConfig
   611  
   612  	apiExtensionClient, err := clientset.NewForConfig(config)
   613  	if err != nil {
   614  		t.Fatal(err)
   615  	}
   616  	dynamicClient, err := dynamic.NewForConfig(config)
   617  	if err != nil {
   618  		t.Fatal(err)
   619  	}
   620  
   621  	noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped)
   622  	err = json.Unmarshal([]byte(`{
   623  		"openAPIV3Schema": {
   624  			"type": "object",
   625  			"properties": {
   626  				"spec": {
   627  					"type": "object",
   628  					"x-kubernetes-preserve-unknown-fields": true,
   629  					"properties": {
   630  						"cronSpec": {
   631  							"type": "string",
   632  							"pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$"
   633  						},
   634  						"ports": {
   635  							"type": "array",
   636  							"x-kubernetes-list-map-keys": [
   637  								"containerPort",
   638  								"protocol"
   639  							],
   640  							"x-kubernetes-list-type": "map",
   641  							"items": {
   642  								"properties": {
   643  									"containerPort": {
   644  										"format": "int32",
   645  										"type": "integer"
   646  									},
   647  									"hostIP": {
   648  										"type": "string"
   649  									},
   650  									"hostPort": {
   651  										"format": "int32",
   652  										"type": "integer"
   653  									},
   654  									"name": {
   655  										"type": "string"
   656  									},
   657  									"protocol": {
   658  										"default": "TCP",
   659  										"type": "string"
   660  									}
   661  								},
   662  								"required": [
   663  									"containerPort"
   664  								],
   665  								"type": "object"
   666  							}
   667  						}
   668  					}
   669  				}
   670  			}
   671  		}
   672  	}`), &noxuDefinition.Spec.Versions[0].Schema)
   673  	if err != nil {
   674  		t.Fatal(err)
   675  	}
   676  	noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   677  	if err != nil {
   678  		t.Fatal(err)
   679  	}
   680  
   681  	kind := noxuDefinition.Spec.Names.Kind
   682  	apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name
   683  	name := "mytest"
   684  
   685  	rest := apiExtensionClient.Discovery().RESTClient()
   686  	yamlBody := []byte(fmt.Sprintf(`
   687  apiVersion: %s
   688  kind: %s
   689  metadata:
   690    name: %s
   691    finalizers:
   692    - test-finalizer
   693  spec:
   694    cronSpec: "* * * * */5"
   695    replicas: 1
   696    ports:
   697    - name: x
   698      containerPort: 80`, apiVersion, kind, name))
   699  	result, err := rest.Patch(types.ApplyPatchType).
   700  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   701  		Name(name).
   702  		Param("fieldManager", "apply_test").
   703  		Body(yamlBody).
   704  		DoRaw(context.TODO())
   705  	if err != nil {
   706  		t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
   707  	}
   708  
   709  	// New applier tries to edit an existing list item, we should get conflicts.
   710  	result, err = rest.Patch(types.ApplyPatchType).
   711  		AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
   712  		Name(name).
   713  		Param("fieldManager", "apply_test_2").
   714  		Body([]byte(fmt.Sprintf(`
   715  apiVersion: %s
   716  kind: %s
   717  metadata:
   718    name: %s
   719  spec:
   720    ports:
   721    - name: "y"
   722      containerPort: 80
   723      protocol: TCP`, apiVersion, kind, name))).
   724  		DoRaw(context.TODO())
   725  	if err == nil {
   726  		t.Fatalf("Expecting to get conflicts when a different applier updates existing list item, got no error: %s", result)
   727  	}
   728  	status, ok := err.(*apierrors.StatusError)
   729  	if !ok {
   730  		t.Fatalf("Expecting to get conflicts as API error")
   731  	}
   732  	if len(status.Status().Details.Causes) != 1 {
   733  		t.Fatalf("Expecting to get one conflict when a different applier updates existing list item, got: %v", status.Status().Details.Causes)
   734  	}
   735  }