k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/apply.go (about) 1 /* 2 Copyright 2021 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 apimachinery 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "reflect" 24 25 appsv1 "k8s.io/api/apps/v1" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 apiextensionclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 28 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 "k8s.io/apimachinery/pkg/api/meta" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/client-go/dynamic" 35 clientset "k8s.io/client-go/kubernetes" 36 "k8s.io/kubernetes/test/e2e/framework" 37 e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment" 38 imageutils "k8s.io/kubernetes/test/utils/image" 39 admissionapi "k8s.io/pod-security-admission/api" 40 41 "github.com/onsi/ginkgo/v2" 42 43 // ensure libs have a chance to initialize 44 _ "github.com/stretchr/testify/assert" 45 ) 46 47 var _ = SIGDescribe("ServerSideApply", func() { 48 f := framework.NewDefaultFramework("apply") 49 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline 50 51 var client clientset.Interface 52 var ns string 53 54 ginkgo.BeforeEach(func() { 55 client = f.ClientSet 56 ns = f.Namespace.Name 57 }) 58 59 ginkgo.AfterEach(func(ctx context.Context) { 60 _ = client.AppsV1().Deployments(ns).Delete(ctx, "deployment", metav1.DeleteOptions{}) 61 _ = client.AppsV1().Deployments(ns).Delete(ctx, "deployment-shared-unset", metav1.DeleteOptions{}) 62 _ = client.AppsV1().Deployments(ns).Delete(ctx, "deployment-shared-map-item-removal", metav1.DeleteOptions{}) 63 _ = client.CoreV1().Pods(ns).Delete(ctx, "test-pod", metav1.DeleteOptions{}) 64 }) 65 66 /* 67 Release : v1.21 68 Testname: Server Side Apply, Create 69 Description: Apply an object. An apply on an object that does not exist MUST create the object. 70 */ 71 ginkgo.It("should create an applied object if it does not already exist", func(ctx context.Context) { 72 testCases := []struct { 73 resource string 74 name string 75 body string 76 managedFields string 77 }{ 78 { 79 resource: "pods", 80 name: "test-pod", 81 body: `{ 82 "apiVersion": "v1", 83 "kind": "Pod", 84 "metadata": { 85 "name": "test-pod" 86 }, 87 "spec": { 88 "containers": [{ 89 "name": "test-container", 90 "image": "test-image" 91 }] 92 } 93 }`, 94 managedFields: `{"f:spec":{"f:containers":{"k:{\"name\":\"test-container\"}":{".":{},"f:image":{},"f:name":{}}}}}`, 95 }, { 96 resource: "services", 97 name: "test-svc", 98 body: `{ 99 "apiVersion": "v1", 100 "kind": "Service", 101 "metadata": { 102 "name": "test-svc" 103 }, 104 "spec": { 105 "ports": [{ 106 "port": 8080, 107 "protocol": "UDP" 108 }] 109 } 110 }`, 111 managedFields: `{"f:spec":{"f:ports":{"k:{\"port\":8080,\"protocol\":\"UDP\"}":{".":{},"f:port":{},"f:protocol":{}}}}}`, 112 }, 113 } 114 115 for _, tc := range testCases { 116 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 117 Namespace(ns). 118 Resource(tc.resource). 119 Name(tc.name). 120 Param("fieldManager", "apply_test"). 121 Body([]byte(tc.body)). 122 Do(ctx). 123 Get() 124 if err != nil { 125 framework.Failf("Failed to create object using Apply patch: %v", err) 126 } 127 128 _, err = client.CoreV1().RESTClient().Get().Namespace(ns).Resource(tc.resource).Name(tc.name).Do(ctx).Get() 129 if err != nil { 130 framework.Failf("Failed to retrieve object: %v", err) 131 } 132 133 // Test that we can re apply with a different field manager and don't get conflicts 134 obj, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 135 Namespace(ns). 136 Resource(tc.resource). 137 Name(tc.name). 138 Param("fieldManager", "apply_test_2"). 139 Body([]byte(tc.body)). 140 Do(ctx). 141 Get() 142 if err != nil { 143 framework.Failf("Failed to re-apply object using Apply patch: %v", err) 144 } 145 146 // Verify that both appliers own the fields 147 accessor, err := meta.Accessor(obj) 148 framework.ExpectNoError(err, "getting ObjectMeta") 149 managedFields := accessor.GetManagedFields() 150 for _, entry := range managedFields { 151 if entry.Manager == "apply_test_2" || entry.Manager == "apply_test" { 152 if entry.FieldsV1.String() != tc.managedFields { 153 framework.Failf("Expected managed fields %s, got %s", tc.managedFields, entry.FieldsV1.String()) 154 } 155 } 156 } 157 } 158 }) 159 160 /* 161 Release : v1.21 162 Testname: Server Side Apply, Subresource 163 Description: Apply a resource and issue a subsequent apply on a subresource. The subresource MUST be updated with the applied object contents. 164 */ 165 ginkgo.It("should work for subresources", func(ctx context.Context) { 166 { 167 testCases := []struct { 168 resource string 169 name string 170 body string 171 statusPatch string 172 }{ 173 { 174 resource: "pods", 175 name: "test-pod", 176 body: `{ 177 "apiVersion": "v1", 178 "kind": "Pod", 179 "metadata": { 180 "name": "test-pod" 181 }, 182 "spec": { 183 "containers": [{ 184 "name": "nginx", 185 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 186 }] 187 } 188 }`, 189 statusPatch: `{ 190 "apiVersion": "v1", 191 "kind": "Pod", 192 "metadata": { 193 "name": "test-pod" 194 }, 195 "status": {"conditions": [{"type": "MyStatus", "status":"True"}]}}`, 196 }, 197 } 198 199 for _, tc := range testCases { 200 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 201 Namespace(ns). 202 Resource(tc.resource). 203 Name(tc.name). 204 Param("fieldManager", "apply_test"). 205 Body([]byte(tc.body)). 206 Do(ctx). 207 Get() 208 if err != nil { 209 framework.Failf("Failed to create object using Apply patch: %v", err) 210 } 211 212 _, err = client.CoreV1().RESTClient().Get().Namespace(ns).Resource(tc.resource).Name(tc.name).Do(ctx).Get() 213 if err != nil { 214 framework.Failf("Failed to retrieve object: %v", err) 215 } 216 217 // Test that apply does not update subresources unless directed at a subresource endpoint 218 _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 219 Namespace(ns). 220 Resource(tc.resource). 221 Name(tc.name). 222 Param("fieldManager", "apply_test2"). 223 Body([]byte(tc.statusPatch)). 224 Do(ctx). 225 Get() 226 if err != nil { 227 framework.Failf("Failed to Apply Status using Apply patch: %v", err) 228 } 229 pod, err := client.CoreV1().Pods(ns).Get(ctx, "test-pod", metav1.GetOptions{}) 230 framework.ExpectNoError(err, "retrieving test pod") 231 for _, c := range pod.Status.Conditions { 232 if c.Type == "MyStatus" { 233 framework.Failf("Apply should not update subresources unless the endpoint is specifically specified") 234 } 235 } 236 237 // Test that apply to subresource updates the subresource 238 _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 239 Namespace(ns). 240 Resource(tc.resource). 241 SubResource("status"). 242 Name(tc.name). 243 Param("fieldManager", "apply_test2"). 244 Body([]byte(tc.statusPatch)). 245 Do(ctx). 246 Get() 247 if err != nil { 248 framework.Failf("Failed to Apply Status using Apply patch: %v", err) 249 } 250 251 pod, err = client.CoreV1().Pods(ns).Get(ctx, "test-pod", metav1.GetOptions{}) 252 framework.ExpectNoError(err, "retrieving test pod") 253 254 myStatusFound := false 255 for _, c := range pod.Status.Conditions { 256 if c.Type == "MyStatus" { 257 myStatusFound = true 258 break 259 } 260 } 261 if myStatusFound == false { 262 framework.Failf("Expected pod to have applied status") 263 } 264 } 265 } 266 }) 267 268 /* 269 Release : v1.21 270 Testname: Server Side Apply, unset field 271 Description: Apply an object. Issue a subsequent apply that removes a field. The particular field MUST be removed. 272 */ 273 ginkgo.It("should remove a field if it is owned but removed in the apply request", func(ctx context.Context) { 274 obj := []byte(`{ 275 "apiVersion": "apps/v1", 276 "kind": "Deployment", 277 "metadata": { 278 "name": "deployment", 279 "labels": {"app": "nginx"} 280 }, 281 "spec": { 282 "replicas": 3, 283 "selector": { 284 "matchLabels": { 285 "app": "nginx" 286 } 287 }, 288 "template": { 289 "metadata": { 290 "labels": { 291 "app": "nginx" 292 } 293 }, 294 "spec": { 295 "containers": [{ 296 "name": "nginx", 297 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 298 "ports": [{ 299 "containerPort": 80, 300 "protocol": "TCP" 301 }] 302 }] 303 } 304 } 305 } 306 }`) 307 308 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 309 AbsPath("/apis/apps/v1"). 310 Namespace(ns). 311 Resource("deployments"). 312 Name("deployment"). 313 Param("fieldManager", "apply_test"). 314 Body(obj).Do(ctx).Get() 315 if err != nil { 316 framework.Failf("Failed to create object using Apply patch: %v", err) 317 } 318 319 obj = []byte(`{ 320 "apiVersion": "apps/v1", 321 "kind": "Deployment", 322 "metadata": { 323 "name": "deployment", 324 "labels": {"app": "nginx"} 325 }, 326 "spec": { 327 "replicas": 3, 328 "selector": { 329 "matchLabels": { 330 "app": "nginx" 331 } 332 }, 333 "template": { 334 "metadata": { 335 "labels": { 336 "app": "nginx" 337 } 338 }, 339 "spec": { 340 "containers": [{ 341 "name": "nginx", 342 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 343 }] 344 } 345 } 346 } 347 }`) 348 349 _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 350 AbsPath("/apis/apps/v1"). 351 Namespace(ns). 352 Resource("deployments"). 353 Name("deployment"). 354 Param("fieldManager", "apply_test"). 355 Body(obj).Do(ctx).Get() 356 if err != nil { 357 framework.Failf("Failed to remove container port using Apply patch: %v", err) 358 } 359 360 deployment, err := client.AppsV1().Deployments(ns).Get(ctx, "deployment", metav1.GetOptions{}) 361 if err != nil { 362 framework.Failf("Failed to retrieve object: %v", err) 363 } 364 365 if len(deployment.Spec.Template.Spec.Containers[0].Ports) > 0 { 366 framework.Failf("Expected no container ports but got: %v, object: \n%#v", deployment.Spec.Template.Spec.Containers[0].Ports, deployment) 367 } 368 369 }) 370 371 /* 372 Release : v1.21 373 Testname: Server Side Apply, unset field shared 374 Description: Apply an object. Unset ownership of a field that is also owned by other managers and make a subsequent apply request. The unset field MUST not be removed from the object. 375 */ 376 ginkgo.It("should not remove a field if an owner unsets the field but other managers still have ownership of the field", func(ctx context.Context) { 377 // spec.replicas is a optional, defaulted field 378 // spec.template.spec.hostname is an optional, non-defaulted field 379 apply := []byte(`{ 380 "apiVersion": "apps/v1", 381 "kind": "Deployment", 382 "metadata": { 383 "name": "deployment-shared-unset", 384 "labels": {"app": "nginx"} 385 }, 386 "spec": { 387 "replicas": 3, 388 "selector": { 389 "matchLabels": { 390 "app": "nginx" 391 } 392 }, 393 "template": { 394 "metadata": { 395 "labels": { 396 "app": "nginx" 397 } 398 }, 399 "spec": { 400 "hostname": "test-hostname", 401 "containers": [{ 402 "name": "nginx", 403 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 404 }] 405 } 406 } 407 } 408 }`) 409 410 for _, fieldManager := range []string{"shared_owner_1", "shared_owner_2"} { 411 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 412 AbsPath("/apis/apps/v1"). 413 Namespace(ns). 414 Resource("deployments"). 415 Name("deployment-shared-unset"). 416 Param("fieldManager", fieldManager). 417 Body(apply). 418 Do(ctx). 419 Get() 420 if err != nil { 421 framework.Failf("Failed to create object using Apply patch: %v", err) 422 } 423 } 424 425 // unset spec.replicas and spec.template.spec.hostname 426 apply = []byte(`{ 427 "apiVersion": "apps/v1", 428 "kind": "Deployment", 429 "metadata": { 430 "name": "deployment-shared-unset", 431 "labels": {"app": "nginx"} 432 }, 433 "spec": { 434 "selector": { 435 "matchLabels": { 436 "app": "nginx" 437 } 438 }, 439 "template": { 440 "metadata": { 441 "labels": { 442 "app": "nginx" 443 } 444 }, 445 "spec": { 446 "containers": [{ 447 "name": "nginx", 448 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 449 }] 450 } 451 } 452 } 453 }`) 454 455 patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 456 AbsPath("/apis/apps/v1"). 457 Namespace(ns). 458 Resource("deployments"). 459 Name("deployment-shared-unset"). 460 Param("fieldManager", "shared_owner_1"). 461 Body(apply). 462 Do(ctx). 463 Get() 464 if err != nil { 465 framework.Failf("Failed to create object using Apply patch: %v", err) 466 } 467 468 deployment, ok := patched.(*appsv1.Deployment) 469 if !ok { 470 framework.Failf("Failed to convert response object to Deployment") 471 } 472 if *deployment.Spec.Replicas != 3 { 473 framework.Failf("Expected deployment.spec.replicas to be 3, but got %d", deployment.Spec.Replicas) 474 } 475 if deployment.Spec.Template.Spec.Hostname != "test-hostname" { 476 framework.Failf("Expected deployment.spec.template.spec.hostname to be \"test-hostname\", but got %s", deployment.Spec.Template.Spec.Hostname) 477 } 478 }) 479 480 /* 481 Release : v1.21 482 Testname: Server Side Apply, Force Apply 483 Description: Apply an object. Force apply a modified version of the object such that a conflict will exist in the managed fields. The force apply MUST successfully update the object. 484 */ 485 ginkgo.It("should ignore conflict errors if force apply is used", func(ctx context.Context) { 486 obj := []byte(`{ 487 "apiVersion": "apps/v1", 488 "kind": "Deployment", 489 "metadata": { 490 "name": "deployment", 491 "labels": {"app": "nginx"} 492 }, 493 "spec": { 494 "replicas": 3, 495 "selector": { 496 "matchLabels": { 497 "app": "nginx" 498 } 499 }, 500 "template": { 501 "metadata": { 502 "labels": { 503 "app": "nginx" 504 } 505 }, 506 "spec": { 507 "containers": [{ 508 "name": "nginx", 509 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 510 }] 511 } 512 } 513 } 514 }`) 515 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 516 AbsPath("/apis/apps/v1"). 517 Namespace(ns). 518 Resource("deployments"). 519 Name("deployment"). 520 Param("fieldManager", "apply_test"). 521 Body(obj).Do(ctx).Get() 522 if err != nil { 523 framework.Failf("Failed to create object using Apply patch: %v", err) 524 } 525 526 _, err = client.CoreV1().RESTClient().Patch(types.MergePatchType). 527 AbsPath("/apis/apps/v1"). 528 Namespace(ns). 529 Resource("deployments"). 530 Name("deployment"). 531 Body([]byte(`{"spec":{"replicas": 5}}`)).Do(ctx).Get() 532 if err != nil { 533 framework.Failf("Failed to patch object: %v", err) 534 } 535 536 _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 537 AbsPath("/apis/apps/v1"). 538 Namespace(ns). 539 Resource("deployments"). 540 Name("deployment"). 541 Param("fieldManager", "apply_test"). 542 Body(obj).Do(ctx).Get() 543 if err == nil { 544 framework.Failf("Expecting to get conflicts when applying object") 545 } 546 status, ok := err.(*apierrors.StatusError) 547 if !(ok && apierrors.IsConflict(status)) { 548 framework.Failf("Expecting to get conflicts as API error") 549 } 550 if len(status.Status().Details.Causes) < 1 { 551 framework.Failf("Expecting to get at least one conflict when applying object, got: %v", status.Status().Details.Causes) 552 } 553 554 _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 555 AbsPath("/apis/apps/v1"). 556 Namespace(ns). 557 Resource("deployments"). 558 Name("deployment"). 559 Param("force", "true"). 560 Param("fieldManager", "apply_test"). 561 Body(obj).Do(ctx).Get() 562 if err != nil { 563 framework.Failf("Failed to apply object with force: %v", err) 564 } 565 }) 566 567 /* 568 Release : v1.21 569 Testname: Server Side Apply, CRD 570 Description: Create a CRD and apply a CRD resource. Subsequent apply requests that do not conflict with the previous ones should update the object. Apply requests that cause conflicts should fail. 571 */ 572 ginkgo.It("should work for CRDs", func(ctx context.Context) { 573 config, err := framework.LoadConfig() 574 if err != nil { 575 framework.Failf("%s", err) 576 } 577 apiExtensionClient, err := apiextensionclientset.NewForConfig(config) 578 if err != nil { 579 framework.Failf("%s", err) 580 } 581 dynamicClient, err := dynamic.NewForConfig(config) 582 if err != nil { 583 framework.Failf("%s", err) 584 } 585 586 noxuDefinition := fixtures.NewRandomNameMultipleVersionCustomResourceDefinition(apiextensionsv1.ClusterScoped) 587 588 var c apiextensionsv1.CustomResourceValidation 589 err = json.Unmarshal([]byte(`{ 590 "openAPIV3Schema": { 591 "type": "object", 592 "properties": { 593 "spec": { 594 "type": "object", 595 "x-kubernetes-preserve-unknown-fields": true, 596 "properties": { 597 "cronSpec": { 598 "type": "string", 599 "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" 600 }, 601 "ports": { 602 "type": "array", 603 "x-kubernetes-list-map-keys": [ 604 "containerPort", 605 "protocol" 606 ], 607 "x-kubernetes-list-type": "map", 608 "items": { 609 "properties": { 610 "containerPort": { 611 "format": "int32", 612 "type": "integer" 613 }, 614 "hostIP": { 615 "type": "string" 616 }, 617 "hostPort": { 618 "format": "int32", 619 "type": "integer" 620 }, 621 "name": { 622 "type": "string" 623 }, 624 "protocol": { 625 "type": "string" 626 } 627 }, 628 "required": [ 629 "containerPort", 630 "protocol" 631 ], 632 "type": "object" 633 } 634 } 635 } 636 } 637 } 638 } 639 }`), &c) 640 if err != nil { 641 framework.Failf("%s", err) 642 } 643 for i := range noxuDefinition.Spec.Versions { 644 noxuDefinition.Spec.Versions[i].Schema = &c 645 } 646 647 noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 648 if err != nil { 649 framework.Failf("cannot create crd %s", err) 650 } 651 652 defer func() { 653 err = fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient) 654 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 655 }() 656 657 kind := noxuDefinition.Spec.Names.Kind 658 apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[0].Name 659 name := "mytest" 660 661 rest := apiExtensionClient.Discovery().RESTClient() 662 yamlBody := []byte(fmt.Sprintf(` 663 apiVersion: %s 664 kind: %s 665 metadata: 666 name: %s 667 finalizers: 668 - test-finalizer 669 spec: 670 cronSpec: "* * * * */5" 671 replicas: 1 672 ports: 673 - name: x 674 containerPort: 80 675 protocol: TCP`, apiVersion, kind, name)) 676 result, err := rest.Patch(types.ApplyPatchType). 677 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 678 Name(name). 679 Param("fieldManager", "apply_test"). 680 Body(yamlBody). 681 DoRaw(ctx) 682 if err != nil { 683 framework.Failf("failed to create custom resource with apply: %v:\n%v", err, string(result)) 684 } 685 verifyNumFinalizers(result, 1) 686 verifyFinalizersIncludes(result, "test-finalizer") 687 verifyReplicas(result, 1) 688 verifyNumPorts(result, 1) 689 690 // Ensure that apply works with multiple resource versions 691 apiVersionBeta := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[1].Name 692 yamlBodyBeta := []byte(fmt.Sprintf(` 693 apiVersion: %s 694 kind: %s 695 metadata: 696 name: %s 697 spec: 698 cronSpec: "* * * * */5" 699 replicas: 1 700 ports: 701 - name: x 702 containerPort: 80 703 protocol: TCP`, apiVersionBeta, kind, name)) 704 result, err = rest.Patch(types.ApplyPatchType). 705 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[1].Name, noxuDefinition.Spec.Names.Plural). 706 Name(name). 707 Param("fieldManager", "apply_test"). 708 Body(yamlBodyBeta). 709 DoRaw(ctx) 710 if err != nil { 711 framework.Failf("failed to create custom resource with apply: %v:\n%v", err, string(result)) 712 } 713 verifyReplicas(result, 1) 714 verifyNumPorts(result, 1) 715 716 // Reset the finalizers after the test so the objects can be deleted 717 defer func() { 718 result, err = rest.Patch(types.MergePatchType). 719 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 720 Name(name). 721 Body([]byte(`{"metadata":{"finalizers":[]}}`)). 722 DoRaw(ctx) 723 if err != nil { 724 framework.Failf("failed to reset finalizers: %v:\n%v", err, string(result)) 725 } 726 }() 727 728 // Patch object to add another finalizer to the finalizers list 729 result, err = rest.Patch(types.MergePatchType). 730 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 731 Name(name). 732 Body([]byte(`{"metadata":{"finalizers":["test-finalizer","another-one"]}}`)). 733 DoRaw(ctx) 734 if err != nil { 735 framework.Failf("failed to add finalizer with merge patch: %v:\n%v", err, string(result)) 736 } 737 verifyNumFinalizers(result, 2) 738 verifyFinalizersIncludes(result, "test-finalizer") 739 verifyFinalizersIncludes(result, "another-one") 740 741 // Re-apply the same config, should work fine, since finalizers should have the list-type extension 'set'. 742 result, err = rest.Patch(types.ApplyPatchType). 743 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 744 Name(name). 745 Param("fieldManager", "apply_test"). 746 SetHeader("Accept", "application/json"). 747 Body(yamlBody). 748 DoRaw(ctx) 749 if err != nil { 750 framework.Failf("failed to apply same config after adding a finalizer: %v:\n%v", err, string(result)) 751 } 752 verifyNumFinalizers(result, 2) 753 verifyFinalizersIncludes(result, "test-finalizer") 754 verifyFinalizersIncludes(result, "another-one") 755 756 // Patch object to change the number of replicas 757 result, err = rest.Patch(types.MergePatchType). 758 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 759 Name(name). 760 Body([]byte(`{"spec":{"replicas": 5}}`)). 761 DoRaw(ctx) 762 if err != nil { 763 framework.Failf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result)) 764 } 765 verifyReplicas(result, 5) 766 767 // Re-apply, we should get conflicts now, since the number of replicas was changed. 768 result, err = rest.Patch(types.ApplyPatchType). 769 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 770 Name(name). 771 Param("fieldManager", "apply_test"). 772 Body(yamlBody). 773 DoRaw(ctx) 774 if err == nil { 775 framework.Failf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result) 776 } 777 status, ok := err.(*apierrors.StatusError) 778 if !ok { 779 framework.Failf("Expecting to get conflicts as API error") 780 } 781 if len(status.Status().Details.Causes) != 1 { 782 framework.Failf("Expecting to get one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes) 783 } 784 785 // Re-apply with force, should work fine. 786 result, err = rest.Patch(types.ApplyPatchType). 787 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 788 Name(name). 789 Param("force", "true"). 790 Param("fieldManager", "apply_test"). 791 Body(yamlBody). 792 DoRaw(ctx) 793 if err != nil { 794 framework.Failf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result)) 795 } 796 verifyReplicas(result, 1) 797 798 // New applier tries to edit an existing list item, we should get conflicts. 799 result, err = rest.Patch(types.ApplyPatchType). 800 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 801 Name(name). 802 Param("fieldManager", "apply_test_2"). 803 Body([]byte(fmt.Sprintf(` 804 apiVersion: %s 805 kind: %s 806 metadata: 807 name: %s 808 spec: 809 ports: 810 - name: "y" 811 containerPort: 80 812 protocol: TCP`, apiVersion, kind, name))). 813 DoRaw(ctx) 814 if err == nil { 815 framework.Failf("Expecting to get conflicts when a different applier updates existing list item, got no error: %s", result) 816 } 817 status, ok = err.(*apierrors.StatusError) 818 if !ok { 819 framework.Failf("Expecting to get conflicts as API error") 820 } 821 if len(status.Status().Details.Causes) != 1 { 822 framework.Failf("Expecting to get one conflict when a different applier updates existing list item, got: %v", status.Status().Details.Causes) 823 } 824 825 // New applier tries to add a new list item, should work fine. 826 result, err = rest.Patch(types.ApplyPatchType). 827 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 828 Name(name). 829 Param("fieldManager", "apply_test_2"). 830 Body([]byte(fmt.Sprintf(` 831 apiVersion: %s 832 kind: %s 833 metadata: 834 name: %s 835 spec: 836 ports: 837 - name: "y" 838 containerPort: 8080 839 protocol: TCP`, apiVersion, kind, name))). 840 SetHeader("Accept", "application/json"). 841 DoRaw(ctx) 842 if err != nil { 843 framework.Failf("failed to add a new list item to the object as a different applier: %v:\n%v", err, string(result)) 844 } 845 verifyNumPorts(result, 2) 846 847 // UpdateOnCreate 848 notExistingYAMLBody := []byte(fmt.Sprintf(` 849 { 850 "apiVersion": "%s", 851 "kind": "%s", 852 "metadata": { 853 "name": "%s", 854 "finalizers": [ 855 "test-finalizer" 856 ] 857 }, 858 "spec": { 859 "cronSpec": "* * * * */5", 860 "replicas": 1, 861 "ports": [ 862 { 863 "name": "x", 864 "containerPort": 80 865 } 866 ] 867 }, 868 "protocol": "TCP" 869 }`, apiVersion, kind, "should-not-exist")) 870 _, err = rest.Put(). 871 AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural). 872 Name("should-not-exist"). 873 Param("fieldManager", "apply_test"). 874 Body(notExistingYAMLBody). 875 DoRaw(ctx) 876 if !apierrors.IsNotFound(err) { 877 framework.Failf("create on update should fail with notFound, got %v", err) 878 } 879 880 // Create a CRD to test atomic lists 881 crd := fixtures.NewRandomNameV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) 882 err = json.Unmarshal([]byte(`{ 883 "openAPIV3Schema": { 884 "type": "object", 885 "properties": { 886 "spec": { 887 "type": "object", 888 "x-kubernetes-preserve-unknown-fields": true, 889 "properties": { 890 "atomicList": { 891 "type": "array", 892 "x-kubernetes-list-type": "atomic", 893 "items": { 894 "type": "string" 895 } 896 } 897 } 898 } 899 } 900 } 901 }`), &c) 902 if err != nil { 903 framework.Failf("%s", err) 904 } 905 for i := range crd.Spec.Versions { 906 crd.Spec.Versions[i].Schema = &c 907 } 908 909 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient) 910 if err != nil { 911 framework.Failf("cannot create crd %s", err) 912 } 913 914 defer func() { 915 err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient) 916 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 917 }() 918 919 crdKind := crd.Spec.Names.Kind 920 crdApiVersion := crd.Spec.Group + "/" + crd.Spec.Versions[0].Name 921 922 crdYamlBody := []byte(fmt.Sprintf(` 923 apiVersion: %s 924 kind: %s 925 metadata: 926 name: %s 927 spec: 928 atomicList: 929 - "item1"`, crdApiVersion, crdKind, name)) 930 result, err = rest.Patch(types.ApplyPatchType). 931 AbsPath("/apis", crd.Spec.Group, crd.Spec.Versions[0].Name, crd.Spec.Names.Plural). 932 Name(name). 933 Param("fieldManager", "apply_test"). 934 Body(crdYamlBody). 935 DoRaw(ctx) 936 if err != nil { 937 framework.Failf("failed to create custom resource with apply: %v:\n%v", err, string(result)) 938 } 939 940 verifyList(result, []interface{}{"item1"}) 941 942 crdYamlBody = []byte(fmt.Sprintf(` 943 apiVersion: %s 944 kind: %s 945 metadata: 946 name: %s 947 spec: 948 atomicList: 949 - "item2"`, crdApiVersion, crdKind, name)) 950 result, err = rest.Patch(types.ApplyPatchType). 951 AbsPath("/apis", crd.Spec.Group, crd.Spec.Versions[0].Name, crd.Spec.Names.Plural). 952 Name(name). 953 Param("fieldManager", "apply_test_2"). 954 Param("force", "true"). 955 Body(crdYamlBody). 956 DoRaw(ctx) 957 if err != nil { 958 framework.Failf("failed to create custom resource with apply: %v:\n%v", err, string(result)) 959 } 960 961 // Since the list is atomic the contents of the list must completely be replaced by the latest apply 962 verifyList(result, []interface{}{"item2"}) 963 }) 964 965 /* 966 Release : v1.21 967 Testname: Server Side Apply, Update take ownership 968 Description: Apply an object. Send an Update request which should take ownership of a field. The field should be owned by the new manager and a subsequent apply from the original manager MUST not change the field it does not have ownership of. 969 */ 970 ginkgo.It("should give up ownership of a field if forced applied by a controller", func(ctx context.Context) { 971 // Applier creates a deployment with replicas set to 3 972 apply := []byte(`{ 973 "apiVersion": "apps/v1", 974 "kind": "Deployment", 975 "metadata": { 976 "name": "deployment-shared-map-item-removal", 977 "labels": {"app": "nginx"} 978 }, 979 "spec": { 980 "replicas": 3, 981 "selector": { 982 "matchLabels": { 983 "app": "nginx" 984 } 985 }, 986 "template": { 987 "metadata": { 988 "labels": { 989 "app": "nginx" 990 } 991 }, 992 "spec": { 993 "containers": [{ 994 "name": "nginx", 995 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 996 }] 997 } 998 } 999 } 1000 }`) 1001 1002 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1003 AbsPath("/apis/apps/v1"). 1004 Namespace(ns). 1005 Resource("deployments"). 1006 Name("deployment-shared-map-item-removal"). 1007 Param("fieldManager", "test_applier"). 1008 Body(apply). 1009 Do(ctx). 1010 Get() 1011 if err != nil { 1012 framework.Failf("Failed to create object using Apply patch: %v", err) 1013 } 1014 1015 replicas := int32(4) 1016 _, err = e2edeployment.UpdateDeploymentWithRetries(client, ns, "deployment-shared-map-item-removal", func(update *appsv1.Deployment) { 1017 update.Spec.Replicas = &replicas 1018 }) 1019 framework.ExpectNoError(err) 1020 1021 // applier omits replicas 1022 apply = []byte(`{ 1023 "apiVersion": "apps/v1", 1024 "kind": "Deployment", 1025 "metadata": { 1026 "name": "deployment-shared-map-item-removal", 1027 "labels": {"app": "nginx"} 1028 }, 1029 "spec": { 1030 "selector": { 1031 "matchLabels": { 1032 "app": "nginx" 1033 } 1034 }, 1035 "template": { 1036 "metadata": { 1037 "labels": { 1038 "app": "nginx" 1039 } 1040 }, 1041 "spec": { 1042 "containers": [{ 1043 "name": "nginx", 1044 "image": "` + imageutils.GetE2EImage(imageutils.NginxNew) + `", 1045 }] 1046 } 1047 } 1048 } 1049 }`) 1050 1051 patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1052 AbsPath("/apis/apps/v1"). 1053 Namespace(ns). 1054 Resource("deployments"). 1055 Name("deployment-shared-map-item-removal"). 1056 Param("fieldManager", "test_applier"). 1057 Body(apply). 1058 Do(ctx). 1059 Get() 1060 if err != nil { 1061 framework.Failf("Failed to create object using Apply patch: %v", err) 1062 } 1063 1064 // ensure the container is deleted even though a controller updated a field of the container 1065 deployment, ok := patched.(*appsv1.Deployment) 1066 if !ok { 1067 framework.Failf("Failed to convert response object to Deployment") 1068 } 1069 if *deployment.Spec.Replicas != 4 { 1070 framework.Failf("Expected deployment.spec.replicas to be 4, but got %d", deployment.Spec.Replicas) 1071 } 1072 }) 1073 }) 1074 1075 // verifyNumFinalizers checks that len(.metadata.finalizers) == n 1076 func verifyNumFinalizers(b []byte, n int) { 1077 obj := unstructured.Unstructured{} 1078 err := obj.UnmarshalJSON(b) 1079 if err != nil { 1080 framework.Failf("failed to unmarshal response: %v", err) 1081 } 1082 if actual, expected := len(obj.GetFinalizers()), n; actual != expected { 1083 framework.Failf("expected %v finalizers but got %v:\n%v", expected, actual, string(b)) 1084 } 1085 } 1086 1087 // verifyFinalizersIncludes checks that .metadata.finalizers includes e 1088 func verifyFinalizersIncludes(b []byte, e string) { 1089 obj := unstructured.Unstructured{} 1090 err := obj.UnmarshalJSON(b) 1091 if err != nil { 1092 framework.Failf("failed to unmarshal response: %v", err) 1093 } 1094 for _, a := range obj.GetFinalizers() { 1095 if a == e { 1096 return 1097 } 1098 } 1099 framework.Failf("expected finalizers to include %q but got: %v", e, obj.GetFinalizers()) 1100 } 1101 1102 // verifyReplicas checks that .spec.replicas == r 1103 func verifyReplicas(b []byte, r int) { 1104 obj := unstructured.Unstructured{} 1105 err := obj.UnmarshalJSON(b) 1106 if err != nil { 1107 framework.Failf("failed to find replicas number in response: %v:\n%v", err, string(b)) 1108 } 1109 spec, ok := obj.Object["spec"] 1110 if !ok { 1111 framework.Failf("failed to find replicas number in response:\n%v", string(b)) 1112 } 1113 specMap, ok := spec.(map[string]interface{}) 1114 if !ok { 1115 framework.Failf("failed to find replicas number in response:\n%v", string(b)) 1116 } 1117 replicas, ok := specMap["replicas"] 1118 if !ok { 1119 framework.Failf("failed to find replicas number in response:\n%v", string(b)) 1120 } 1121 replicasNumber, ok := replicas.(int64) 1122 if !ok { 1123 framework.Failf("failed to find replicas number in response: expected int64 but got: %v", reflect.TypeOf(replicas)) 1124 } 1125 if actual, expected := replicasNumber, int64(r); actual != expected { 1126 framework.Failf("expected %v ports but got %v:\n%v", expected, actual, string(b)) 1127 } 1128 } 1129 1130 // verifyNumPorts checks that len(.spec.ports) == n 1131 func verifyNumPorts(b []byte, n int) { 1132 obj := unstructured.Unstructured{} 1133 err := obj.UnmarshalJSON(b) 1134 if err != nil { 1135 framework.Failf("failed to find ports list in response: %v:\n%v", err, string(b)) 1136 } 1137 spec, ok := obj.Object["spec"] 1138 if !ok { 1139 framework.Failf("failed to find ports list in response:\n%v", string(b)) 1140 } 1141 specMap, ok := spec.(map[string]interface{}) 1142 if !ok { 1143 framework.Failf("failed to find ports list in response:\n%v", string(b)) 1144 } 1145 ports, ok := specMap["ports"] 1146 if !ok { 1147 framework.Failf("failed to find ports list in response:\n%v", string(b)) 1148 } 1149 portsList, ok := ports.([]interface{}) 1150 if !ok { 1151 framework.Failf("failed to find ports list in response: expected array but got: %v", reflect.TypeOf(ports)) 1152 } 1153 if actual, expected := len(portsList), n; actual != expected { 1154 framework.Failf("expected %v ports but got %v:\n%v", expected, actual, string(b)) 1155 } 1156 } 1157 1158 // verifyList checks that .spec.atomicList is the exact same as the expectedList provided 1159 func verifyList(b []byte, expectedList []interface{}) { 1160 obj := unstructured.Unstructured{} 1161 err := obj.UnmarshalJSON(b) 1162 if err != nil { 1163 framework.Failf("failed to find atomicList in response: %v:\n%v", err, string(b)) 1164 } 1165 spec, ok := obj.Object["spec"] 1166 if !ok { 1167 framework.Failf("failed to find atomicList in response:\n%v", string(b)) 1168 } 1169 specMap, ok := spec.(map[string]interface{}) 1170 if !ok { 1171 framework.Failf("failed to find atomicList in response:\n%v", string(b)) 1172 } 1173 list, ok := specMap["atomicList"] 1174 if !ok { 1175 framework.Failf("failed to find atomicList in response:\n%v", string(b)) 1176 } 1177 listString, ok := list.([]interface{}) 1178 if !ok { 1179 framework.Failf("failed to find atomicList in response:\n%v", string(b)) 1180 } 1181 if !reflect.DeepEqual(listString, expectedList) { 1182 framework.Failf("Expected list %s, got %s", expectedList, listString) 1183 } 1184 }