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 }