k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/field_validation_test.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 apiserver 18 19 import ( 20 "context" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "strings" 25 "testing" 26 "time" 27 28 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 29 apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 30 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 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 clientset "k8s.io/client-go/kubernetes" 37 "k8s.io/client-go/rest" 38 "k8s.io/klog/v2" 39 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 40 41 "k8s.io/kubernetes/test/integration/framework" 42 ) 43 44 var ( 45 invalidBodyJSON = ` 46 { 47 "apiVersion": "apps/v1", 48 "kind": "Deployment", 49 "metadata": { 50 "name": "dupename", 51 "name": "%s", 52 "labels": {"app": "nginx"}, 53 "unknownMeta": "metaVal" 54 }, 55 "spec": { 56 "unknown1": "val1", 57 "unknownDupe": "valDupe", 58 "unknownDupe": "valDupe2", 59 "paused": true, 60 "paused": false, 61 "selector": { 62 "matchLabels": { 63 "app": "nginx" 64 } 65 }, 66 "template": { 67 "metadata": { 68 "labels": { 69 "app": "nginx" 70 } 71 }, 72 "spec": { 73 "containers": [{ 74 "name": "nginx", 75 "image": "nginx:latest", 76 "unknownNested": "val1", 77 "imagePullPolicy": "Always", 78 "imagePullPolicy": "Never" 79 }] 80 } 81 } 82 } 83 } 84 ` 85 validBodyJSON = ` 86 { 87 "apiVersion": "apps/v1", 88 "kind": "Deployment", 89 "metadata": { 90 "name": "%s", 91 "labels": {"app": "nginx"}, 92 "annotations": {"a1": "foo", "a2": "bar"} 93 }, 94 "spec": { 95 "selector": { 96 "matchLabels": { 97 "app": "nginx" 98 } 99 }, 100 "template": { 101 "metadata": { 102 "labels": { 103 "app": "nginx" 104 } 105 }, 106 "spec": { 107 "containers": [{ 108 "name": "nginx", 109 "image": "nginx:latest", 110 "imagePullPolicy": "Always" 111 }] 112 } 113 }, 114 "replicas": 2 115 } 116 }` 117 118 invalidBodyYAML = `apiVersion: apps/v1 119 kind: Deployment 120 metadata: 121 name: dupename 122 name: %s 123 unknownMeta: metaVal 124 labels: 125 app: nginx 126 spec: 127 unknown1: val1 128 unknownDupe: valDupe 129 unknownDupe: valDupe2 130 paused: true 131 paused: false 132 selector: 133 matchLabels: 134 app: nginx 135 template: 136 metadata: 137 labels: 138 app: nginx 139 spec: 140 containers: 141 - name: nginx 142 image: nginx:latest 143 unknownNested: val1 144 imagePullPolicy: Always 145 imagePullPolicy: Never` 146 147 validBodyYAML = `apiVersion: apps/v1 148 kind: Deployment 149 metadata: 150 name: %s 151 labels: 152 app: nginx 153 annotations: 154 a1: foo 155 a2: bar 156 spec: 157 replicas: 2 158 paused: true 159 selector: 160 matchLabels: 161 app: nginx 162 template: 163 metadata: 164 labels: 165 app: nginx 166 spec: 167 containers: 168 - name: nginx 169 image: nginx:latest 170 imagePullPolicy: Always` 171 172 applyInvalidBody = `{ 173 "apiVersion": "apps/v1", 174 "kind": "Deployment", 175 "metadata": { 176 "name": "%s", 177 "labels": {"app": "nginx"} 178 }, 179 "spec": { 180 "paused": false, 181 "paused": true, 182 "selector": { 183 "matchLabels": { 184 "app": "nginx" 185 } 186 }, 187 "template": { 188 "metadata": { 189 "labels": { 190 "app": "nginx" 191 } 192 }, 193 "spec": { 194 "containers": [{ 195 "name": "nginx", 196 "image": "nginx:latest", 197 "imagePullPolicy": "Never", 198 "imagePullPolicy": "Always" 199 }] 200 } 201 } 202 } 203 }` 204 applyValidBody = ` 205 { 206 "apiVersion": "apps/v1", 207 "kind": "Deployment", 208 "metadata": { 209 "name": "%s", 210 "labels": {"app": "nginx"}, 211 "annotations": {"a1": "foo", "a2": "bar"} 212 }, 213 "spec": { 214 "selector": { 215 "matchLabels": { 216 "app": "nginx" 217 } 218 }, 219 "template": { 220 "metadata": { 221 "labels": { 222 "app": "nginx" 223 } 224 }, 225 "spec": { 226 "containers": [{ 227 "name": "nginx", 228 "image": "nginx:latest", 229 "imagePullPolicy": "Always" 230 }] 231 } 232 }, 233 "replicas": 3 234 } 235 }` 236 crdInvalidBody = ` 237 { 238 "apiVersion": "%s", 239 "kind": "%s", 240 "metadata": { 241 "name": "dupename", 242 "name": "%s", 243 "unknownMeta": "metaVal", 244 "resourceVersion": "%s" 245 }, 246 "spec": { 247 "unknown1": "val1", 248 "unknownDupe": "valDupe", 249 "unknownDupe": "valDupe2", 250 "knownField1": "val1", 251 "knownField1": "val2", 252 "ports": [{ 253 "name": "portName", 254 "containerPort": 8080, 255 "protocol": "TCP", 256 "hostPort": 8081, 257 "hostPort": 8082, 258 "unknownNested": "val" 259 }], 260 "embeddedObj": { 261 "apiVersion": "v1", 262 "kind": "ConfigMap", 263 "metadata": { 264 "name": "my-cm", 265 "namespace": "my-ns", 266 "unknownEmbeddedMeta": "foo" 267 } 268 } 269 } 270 }` 271 272 crdValidBody = ` 273 { 274 "apiVersion": "%s", 275 "kind": "%s", 276 "metadata": { 277 "name": "%s", 278 "resourceVersion": "%s" 279 }, 280 "spec": { 281 "knownField1": "val1", 282 "ports": [{ 283 "name": "portName", 284 "containerPort": 8080, 285 "protocol": "TCP", 286 "hostPort": 8081 287 }], 288 "embeddedObj": { 289 "apiVersion": "v1", 290 "kind": "ConfigMap", 291 "metadata": { 292 "name": "my-cm" 293 } 294 } 295 } 296 } 297 ` 298 299 crdInvalidBodyYAML = ` 300 apiVersion: "%s" 301 kind: "%s" 302 metadata: 303 name: dupename 304 name: "%s" 305 resourceVersion: "%s" 306 unknownMeta: metaVal 307 spec: 308 unknown1: val1 309 unknownDupe: valDupe 310 unknownDupe: valDupe2 311 knownField1: val1 312 knownField1: val2 313 ports: 314 - name: portName 315 containerPort: 8080 316 protocol: TCP 317 hostPort: 8081 318 hostPort: 8082 319 unknownNested: val 320 embeddedObj: 321 apiVersion: v1 322 kind: ConfigMap 323 metadata: 324 name: my-cm 325 namespace: my-ns 326 unknownEmbeddedMeta: foo` 327 328 crdValidBodyYAML = ` 329 apiVersion: "%s" 330 kind: "%s" 331 metadata: 332 name: "%s" 333 resourceVersion: "%s" 334 spec: 335 knownField1: val1 336 ports: 337 - name: portName 338 containerPort: 8080 339 protocol: TCP 340 hostPort: 8081 341 embeddedObj: 342 apiVersion: v1 343 kind: ConfigMap 344 metadata: 345 name: my-cm 346 namespace: my-ns` 347 348 crdApplyInvalidBody = ` 349 { 350 "apiVersion": "%s", 351 "kind": "%s", 352 "metadata": { 353 "name": "%s" 354 }, 355 "spec": { 356 "knownField1": "val1", 357 "knownField1": "val2", 358 "ports": [{ 359 "name": "portName", 360 "containerPort": 8080, 361 "protocol": "TCP", 362 "hostPort": 8081, 363 "hostPort": 8082 364 }], 365 "embeddedObj": { 366 "apiVersion": "v1", 367 "kind": "ConfigMap", 368 "metadata": { 369 "name": "my-cm", 370 "namespace": "my-ns" 371 } 372 } 373 } 374 }` 375 376 crdApplyValidBody = ` 377 { 378 "apiVersion": "%s", 379 "kind": "%s", 380 "metadata": { 381 "name": "%s" 382 }, 383 "spec": { 384 "knownField1": "val1", 385 "ports": [{ 386 "name": "portName", 387 "containerPort": 8080, 388 "protocol": "TCP", 389 "hostPort": 8082 390 }], 391 "embeddedObj": { 392 "apiVersion": "v1", 393 "kind": "ConfigMap", 394 "metadata": { 395 "name": "my-cm", 396 "namespace": "my-ns" 397 } 398 } 399 } 400 }` 401 402 crdApplyValidBody2 = ` 403 { 404 "apiVersion": "%s", 405 "kind": "%s", 406 "metadata": { 407 "name": "%s" 408 }, 409 "spec": { 410 "knownField1": "val2", 411 "ports": [{ 412 "name": "portName", 413 "containerPort": 8080, 414 "protocol": "TCP", 415 "hostPort": 8083 416 }], 417 "embeddedObj": { 418 "apiVersion": "v1", 419 "kind": "ConfigMap", 420 "metadata": { 421 "name": "my-cm", 422 "namespace": "my-ns" 423 } 424 } 425 } 426 }` 427 428 crdApplyFinalizerBody = ` 429 { 430 "apiVersion": "%s", 431 "kind": "%s", 432 "metadata": { 433 "name": "%s", 434 "finalizers": %s 435 }, 436 "spec": { 437 "knownField1": "val1", 438 "ports": [{ 439 "name": "portName", 440 "containerPort": 8080, 441 "protocol": "TCP", 442 "hostPort": 8082 443 }], 444 "embeddedObj": { 445 "apiVersion": "v1", 446 "kind": "ConfigMap", 447 "metadata": { 448 "name": "my-cm", 449 "namespace": "my-ns" 450 } 451 } 452 } 453 }` 454 455 patchYAMLBody = ` 456 apiVersion: %s 457 kind: %s 458 metadata: 459 name: %s 460 finalizers: 461 - test/finalizer 462 spec: 463 cronSpec: "* * * * */5" 464 ports: 465 - name: x 466 containerPort: 80 467 protocol: TCP 468 ` 469 470 crdSchemaBase = ` 471 { 472 "openAPIV3Schema": { 473 "type": "object", 474 "properties": { 475 "spec": { 476 "type": "object", 477 %s 478 "properties": { 479 "cronSpec": { 480 "type": "string", 481 "pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$" 482 }, 483 "knownField1": { 484 "type": "string" 485 }, 486 "embeddedObj": { 487 "x-kubernetes-embedded-resource": true, 488 "type": "object", 489 "properties": { 490 "apiversion": { 491 "type": "string" 492 }, 493 "kind": { 494 "type": "string" 495 }, 496 "metadata": { 497 "type": "object" 498 } 499 } 500 }, 501 "ports": { 502 "type": "array", 503 "x-kubernetes-list-map-keys": [ 504 "containerPort", 505 "protocol" 506 ], 507 "x-kubernetes-list-type": "map", 508 "items": { 509 "properties": { 510 "containerPort": { 511 "format": "int32", 512 "type": "integer" 513 }, 514 "hostIP": { 515 "type": "string" 516 }, 517 "hostPort": { 518 "format": "int32", 519 "type": "integer" 520 }, 521 "name": { 522 "type": "string" 523 }, 524 "protocol": { 525 "type": "string" 526 } 527 }, 528 "required": [ 529 "containerPort", 530 "protocol" 531 ], 532 "type": "object" 533 } 534 } 535 } 536 } 537 } 538 } 539 } 540 ` 541 ) 542 543 func TestFieldValidation(t *testing.T) { 544 server, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) 545 if err != nil { 546 t.Fatal(err) 547 } 548 config := server.ClientConfig 549 defer server.TearDownFn() 550 551 // don't log warnings, tests inspect them in the responses directly 552 config.WarningHandler = rest.NoWarnings{} 553 554 schemaCRD := setupCRD(t, config, "schema.example.com", false) 555 schemaGVR := schema.GroupVersionResource{ 556 Group: schemaCRD.Spec.Group, 557 Version: schemaCRD.Spec.Versions[0].Name, 558 Resource: schemaCRD.Spec.Names.Plural, 559 } 560 schemaGVK := schema.GroupVersionKind{ 561 Group: schemaCRD.Spec.Group, 562 Version: schemaCRD.Spec.Versions[0].Name, 563 Kind: schemaCRD.Spec.Names.Kind, 564 } 565 566 schemalessCRD := setupCRD(t, config, "schemaless.example.com", true) 567 schemalessGVR := schema.GroupVersionResource{ 568 Group: schemalessCRD.Spec.Group, 569 Version: schemalessCRD.Spec.Versions[0].Name, 570 Resource: schemalessCRD.Spec.Names.Plural, 571 } 572 schemalessGVK := schema.GroupVersionKind{ 573 Group: schemalessCRD.Spec.Group, 574 Version: schemalessCRD.Spec.Versions[0].Name, 575 Kind: schemalessCRD.Spec.Names.Kind, 576 } 577 578 client := clientset.NewForConfigOrDie(config) 579 rest := client.Discovery().RESTClient() 580 581 t.Run("Post", func(t *testing.T) { testFieldValidationPost(t, client) }) 582 t.Run("Put", func(t *testing.T) { testFieldValidationPut(t, client) }) 583 t.Run("PatchTyped", func(t *testing.T) { testFieldValidationPatchTyped(t, client) }) 584 t.Run("SMP", func(t *testing.T) { testFieldValidationSMP(t, client) }) 585 t.Run("ApplyCreate", func(t *testing.T) { testFieldValidationApplyCreate(t, client) }) 586 t.Run("ApplyUpdate", func(t *testing.T) { testFieldValidationApplyUpdate(t, client) }) 587 588 t.Run("PostCRD", func(t *testing.T) { testFieldValidationPostCRD(t, rest, schemaGVK, schemaGVR) }) 589 t.Run("PutCRD", func(t *testing.T) { testFieldValidationPutCRD(t, rest, schemaGVK, schemaGVR) }) 590 t.Run("PatchCRD", func(t *testing.T) { testFieldValidationPatchCRD(t, rest, schemaGVK, schemaGVR) }) 591 t.Run("ApplyCreateCRD", func(t *testing.T) { testFieldValidationApplyCreateCRD(t, rest, schemaGVK, schemaGVR) }) 592 t.Run("ApplyUpdateCRD", func(t *testing.T) { testFieldValidationApplyUpdateCRD(t, rest, schemaGVK, schemaGVR) }) 593 594 t.Run("PostCRDSchemaless", func(t *testing.T) { testFieldValidationPostCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) }) 595 t.Run("PutCRDSchemaless", func(t *testing.T) { testFieldValidationPutCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) }) 596 t.Run("PatchCRDSchemaless", func(t *testing.T) { testFieldValidationPatchCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) }) 597 t.Run("ApplyCreateCRDSchemaless", func(t *testing.T) { testFieldValidationApplyCreateCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) }) 598 t.Run("ApplyUpdateCRDSchemaless", func(t *testing.T) { testFieldValidationApplyUpdateCRDSchemaless(t, rest, schemalessGVK, schemalessGVR) }) 599 t.Run("testFinalizerValidationApplyCreateCRD", func(t *testing.T) { 600 testFinalizerValidationApplyCreateAndUpdateCRD(t, rest, schemalessGVK, schemalessGVR) 601 }) 602 } 603 604 // testFieldValidationPost tests POST requests containing unknown fields with 605 // strict and non-strict field validation. 606 func testFieldValidationPost(t *testing.T, client clientset.Interface) { 607 var testcases = []struct { 608 name string 609 bodyBase string 610 opts metav1.CreateOptions 611 contentType string 612 strictDecodingError string 613 strictDecodingWarnings []string 614 }{ 615 { 616 name: "post-strict-validation", 617 opts: metav1.CreateOptions{ 618 FieldValidation: "Strict", 619 }, 620 bodyBase: invalidBodyJSON, 621 strictDecodingError: `strict decoding error: duplicate field "metadata.name", unknown field "metadata.unknownMeta", unknown field "spec.unknown1", unknown field "spec.unknownDupe", duplicate field "spec.paused", unknown field "spec.template.spec.containers[0].unknownNested", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 622 }, 623 { 624 name: "post-warn-validation", 625 opts: metav1.CreateOptions{ 626 FieldValidation: "Warn", 627 }, 628 bodyBase: invalidBodyJSON, 629 strictDecodingWarnings: []string{ 630 `duplicate field "metadata.name"`, 631 `unknown field "metadata.unknownMeta"`, 632 `unknown field "spec.unknown1"`, 633 `unknown field "spec.unknownDupe"`, 634 `duplicate field "spec.paused"`, 635 // note: fields that are both unknown 636 // and duplicated will only be detected 637 // as unknown for typed resources. 638 `unknown field "spec.template.spec.containers[0].unknownNested"`, 639 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 640 }, 641 }, 642 { 643 name: "post-ignore-validation", 644 opts: metav1.CreateOptions{ 645 FieldValidation: "Ignore", 646 }, 647 bodyBase: invalidBodyJSON, 648 }, 649 { 650 name: "post-no-validation", 651 bodyBase: invalidBodyJSON, 652 strictDecodingWarnings: []string{ 653 `duplicate field "metadata.name"`, 654 `unknown field "metadata.unknownMeta"`, 655 `unknown field "spec.unknown1"`, 656 `unknown field "spec.unknownDupe"`, 657 `duplicate field "spec.paused"`, 658 // note: fields that are both unknown 659 // and duplicated will only be detected 660 // as unknown for typed resources. 661 `unknown field "spec.template.spec.containers[0].unknownNested"`, 662 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 663 }, 664 }, 665 { 666 name: "post-strict-validation-yaml", 667 opts: metav1.CreateOptions{ 668 FieldValidation: "Strict", 669 }, 670 bodyBase: invalidBodyYAML, 671 contentType: "application/yaml", 672 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 673 line 5: key "name" already set in map 674 line 12: key "unknownDupe" already set in map 675 line 14: key "paused" already set in map 676 line 28: key "imagePullPolicy" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`, 677 }, 678 { 679 name: "post-warn-validation-yaml", 680 opts: metav1.CreateOptions{ 681 FieldValidation: "Warn", 682 }, 683 bodyBase: invalidBodyYAML, 684 contentType: "application/yaml", 685 strictDecodingWarnings: []string{ 686 `line 5: key "name" already set in map`, 687 `line 12: key "unknownDupe" already set in map`, 688 `line 14: key "paused" already set in map`, 689 `line 28: key "imagePullPolicy" already set in map`, 690 `unknown field "metadata.unknownMeta"`, 691 `unknown field "spec.template.spec.containers[0].unknownNested"`, 692 `unknown field "spec.unknown1"`, 693 `unknown field "spec.unknownDupe"`, 694 }, 695 }, 696 { 697 name: "post-ignore-validation-yaml", 698 opts: metav1.CreateOptions{ 699 FieldValidation: "Ignore", 700 }, 701 bodyBase: invalidBodyYAML, 702 contentType: "application/yaml", 703 }, 704 { 705 name: "post-no-validation-yaml", 706 bodyBase: invalidBodyYAML, 707 contentType: "application/yaml", 708 strictDecodingWarnings: []string{ 709 `line 5: key "name" already set in map`, 710 `line 12: key "unknownDupe" already set in map`, 711 `line 14: key "paused" already set in map`, 712 `line 28: key "imagePullPolicy" already set in map`, 713 `unknown field "metadata.unknownMeta"`, 714 `unknown field "spec.template.spec.containers[0].unknownNested"`, 715 `unknown field "spec.unknown1"`, 716 `unknown field "spec.unknownDupe"`, 717 }, 718 }, 719 } 720 721 for _, tc := range testcases { 722 t.Run(tc.name, func(t *testing.T) { 723 klog.Warningf("running tc named: %s", tc.name) 724 body := []byte(fmt.Sprintf(tc.bodyBase, fmt.Sprintf("test-deployment-%s", tc.name))) 725 req := client.CoreV1().RESTClient().Post(). 726 AbsPath("/apis/apps/v1"). 727 Namespace("default"). 728 Resource("deployments"). 729 SetHeader("Content-Type", tc.contentType). 730 VersionedParams(&tc.opts, metav1.ParameterCodec) 731 result := req.Body(body).Do(context.TODO()) 732 if result.Error() == nil && tc.strictDecodingError != "" { 733 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 734 } 735 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 736 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 737 } 738 739 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 740 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 741 } 742 for i, strictWarn := range tc.strictDecodingWarnings { 743 if strictWarn != result.Warnings()[i].Text { 744 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 745 } 746 747 } 748 }) 749 } 750 } 751 752 // testFieldValidationPut tests PUT requests 753 // that update existing objects with unknown fields 754 // for both strict and non-strict field validation. 755 func testFieldValidationPut(t *testing.T, client clientset.Interface) { 756 deployName := "test-deployment-put" 757 postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) 758 759 if _, err := client.CoreV1().RESTClient().Post(). 760 AbsPath("/apis/apps/v1"). 761 Namespace("default"). 762 Resource("deployments"). 763 Body(postBody). 764 DoRaw(context.TODO()); err != nil { 765 t.Fatalf("failed to create initial deployment: %v", err) 766 } 767 768 var testcases = []struct { 769 name string 770 opts metav1.UpdateOptions 771 putBodyBase string 772 contentType string 773 strictDecodingError string 774 strictDecodingWarnings []string 775 }{ 776 { 777 name: "put-strict-validation", 778 opts: metav1.UpdateOptions{ 779 FieldValidation: "Strict", 780 }, 781 putBodyBase: invalidBodyJSON, 782 strictDecodingError: `strict decoding error: duplicate field "metadata.name", unknown field "metadata.unknownMeta", unknown field "spec.unknown1", unknown field "spec.unknownDupe", duplicate field "spec.paused", unknown field "spec.template.spec.containers[0].unknownNested", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 783 }, 784 { 785 name: "put-warn-validation", 786 opts: metav1.UpdateOptions{ 787 FieldValidation: "Warn", 788 }, 789 putBodyBase: invalidBodyJSON, 790 strictDecodingWarnings: []string{ 791 `duplicate field "metadata.name"`, 792 `unknown field "metadata.unknownMeta"`, 793 `unknown field "spec.unknown1"`, 794 `unknown field "spec.unknownDupe"`, 795 `duplicate field "spec.paused"`, 796 // note: fields that are both unknown 797 // and duplicated will only be detected 798 // as unknown for typed resources. 799 `unknown field "spec.template.spec.containers[0].unknownNested"`, 800 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 801 }, 802 }, 803 { 804 name: "put-ignore-validation", 805 opts: metav1.UpdateOptions{ 806 FieldValidation: "Ignore", 807 }, 808 putBodyBase: invalidBodyJSON, 809 }, 810 { 811 name: "put-no-validation", 812 putBodyBase: invalidBodyJSON, 813 strictDecodingWarnings: []string{ 814 `duplicate field "metadata.name"`, 815 `unknown field "metadata.unknownMeta"`, 816 `unknown field "spec.unknown1"`, 817 `unknown field "spec.unknownDupe"`, 818 `duplicate field "spec.paused"`, 819 // note: fields that are both unknown 820 // and duplicated will only be detected 821 // as unknown for typed resources. 822 `unknown field "spec.template.spec.containers[0].unknownNested"`, 823 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 824 }, 825 }, 826 { 827 name: "put-strict-validation-yaml", 828 opts: metav1.UpdateOptions{ 829 FieldValidation: "Strict", 830 }, 831 putBodyBase: invalidBodyYAML, 832 contentType: "application/yaml", 833 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 834 line 5: key "name" already set in map 835 line 12: key "unknownDupe" already set in map 836 line 14: key "paused" already set in map 837 line 28: key "imagePullPolicy" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`, 838 }, 839 { 840 name: "put-warn-validation-yaml", 841 opts: metav1.UpdateOptions{ 842 FieldValidation: "Warn", 843 }, 844 putBodyBase: invalidBodyYAML, 845 contentType: "application/yaml", 846 strictDecodingWarnings: []string{ 847 `line 5: key "name" already set in map`, 848 `line 12: key "unknownDupe" already set in map`, 849 `line 14: key "paused" already set in map`, 850 `line 28: key "imagePullPolicy" already set in map`, 851 `unknown field "metadata.unknownMeta"`, 852 `unknown field "spec.template.spec.containers[0].unknownNested"`, 853 `unknown field "spec.unknown1"`, 854 `unknown field "spec.unknownDupe"`, 855 }, 856 }, 857 { 858 name: "put-ignore-validation-yaml", 859 opts: metav1.UpdateOptions{ 860 FieldValidation: "Ignore", 861 }, 862 putBodyBase: invalidBodyYAML, 863 contentType: "application/yaml", 864 }, 865 { 866 name: "put-no-validation-yaml", 867 putBodyBase: invalidBodyYAML, 868 contentType: "application/yaml", 869 strictDecodingWarnings: []string{ 870 `line 5: key "name" already set in map`, 871 `line 12: key "unknownDupe" already set in map`, 872 `line 14: key "paused" already set in map`, 873 `line 28: key "imagePullPolicy" already set in map`, 874 `unknown field "metadata.unknownMeta"`, 875 `unknown field "spec.template.spec.containers[0].unknownNested"`, 876 `unknown field "spec.unknown1"`, 877 `unknown field "spec.unknownDupe"`, 878 }, 879 }, 880 } 881 882 for _, tc := range testcases { 883 t.Run(tc.name, func(t *testing.T) { 884 putBody := []byte(fmt.Sprintf(string(tc.putBodyBase), deployName)) 885 req := client.CoreV1().RESTClient().Put(). 886 AbsPath("/apis/apps/v1"). 887 Namespace("default"). 888 Resource("deployments"). 889 SetHeader("Content-Type", tc.contentType). 890 Name(deployName). 891 VersionedParams(&tc.opts, metav1.ParameterCodec) 892 result := req.Body([]byte(putBody)).Do(context.TODO()) 893 if result.Error() == nil && tc.strictDecodingError != "" { 894 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 895 } 896 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 897 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 898 } 899 900 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 901 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 902 } 903 for i, strictWarn := range tc.strictDecodingWarnings { 904 if strictWarn != result.Warnings()[i].Text { 905 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 906 } 907 908 } 909 }) 910 } 911 } 912 913 // testFieldValidationPatchTyped tests merge-patch and json-patch requests containing unknown fields with 914 // strict and non-strict field validation for typed objects. 915 func testFieldValidationPatchTyped(t *testing.T, client clientset.Interface) { 916 deployName := "test-deployment-patch-typed" 917 postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) 918 919 if _, err := client.CoreV1().RESTClient().Post(). 920 AbsPath("/apis/apps/v1"). 921 Namespace("default"). 922 Resource("deployments"). 923 Body(postBody). 924 DoRaw(context.TODO()); err != nil { 925 t.Fatalf("failed to create initial deployment: %v", err) 926 } 927 928 mergePatchBody := ` 929 { 930 "spec": { 931 "unknown1": "val1", 932 "unknownDupe": "valDupe", 933 "unknownDupe": "valDupe2", 934 "paused": true, 935 "paused": false, 936 "template": { 937 "spec": { 938 "containers": [{ 939 "name": "nginx", 940 "image": "nginx:latest", 941 "unknownNested": "val1", 942 "imagePullPolicy": "Always", 943 "imagePullPolicy": "Never" 944 }] 945 } 946 } 947 } 948 } 949 ` 950 jsonPatchBody := ` 951 [ 952 {"op": "add", "path": "/spec/unknown1", "value": "val1", "foo":"bar"}, 953 {"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val1"}, 954 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"}, 955 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"}, 956 {"op": "add", "path": "/spec/paused", "value": true}, 957 {"op": "add", "path": "/spec/paused", "value": false}, 958 {"op": "add", "path": "/spec/template/spec/containers/0/unknownNested", "value": "val1"}, 959 {"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Always"}, 960 {"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"} 961 ] 962 ` 963 // non-conflicting mergePatch has issues with the patch (duplicate fields), 964 // but doesn't conflict with the existing object it's being patched to 965 nonconflictingMergePatchBody := ` 966 { 967 "spec": { 968 "paused": true, 969 "paused": false, 970 "template": { 971 "spec": { 972 "containers": [{ 973 "name": "nginx", 974 "image": "nginx:latest", 975 "imagePullPolicy": "Always", 976 "imagePullPolicy": "Never" 977 }] 978 } 979 } 980 } 981 } 982 ` 983 var testcases = []struct { 984 name string 985 opts metav1.PatchOptions 986 patchType types.PatchType 987 body string 988 strictDecodingError string 989 strictDecodingWarnings []string 990 }{ 991 { 992 name: "merge-patch-strict-validation", 993 opts: metav1.PatchOptions{ 994 FieldValidation: "Strict", 995 }, 996 patchType: types.MergePatchType, 997 body: mergePatchBody, 998 strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`, 999 }, 1000 { 1001 name: "merge-patch-warn-validation", 1002 opts: metav1.PatchOptions{ 1003 FieldValidation: "Warn", 1004 }, 1005 patchType: types.MergePatchType, 1006 body: mergePatchBody, 1007 strictDecodingWarnings: []string{ 1008 `duplicate field "spec.unknownDupe"`, 1009 `duplicate field "spec.paused"`, 1010 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1011 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1012 `unknown field "spec.unknown1"`, 1013 `unknown field "spec.unknownDupe"`, 1014 }, 1015 }, 1016 { 1017 name: "merge-patch-ignore-validation", 1018 opts: metav1.PatchOptions{ 1019 FieldValidation: "Ignore", 1020 }, 1021 patchType: types.MergePatchType, 1022 body: mergePatchBody, 1023 }, 1024 { 1025 name: "merge-patch-no-validation", 1026 patchType: types.MergePatchType, 1027 body: mergePatchBody, 1028 strictDecodingWarnings: []string{ 1029 `duplicate field "spec.unknownDupe"`, 1030 `duplicate field "spec.paused"`, 1031 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1032 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1033 `unknown field "spec.unknown1"`, 1034 `unknown field "spec.unknownDupe"`, 1035 }, 1036 }, 1037 { 1038 name: "json-patch-strict-validation", 1039 patchType: types.JSONPatchType, 1040 opts: metav1.PatchOptions{ 1041 FieldValidation: "Strict", 1042 }, 1043 body: jsonPatchBody, 1044 strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknown3", unknown field "spec.unknownDupe"`, 1045 }, 1046 { 1047 name: "json-patch-warn-validation", 1048 patchType: types.JSONPatchType, 1049 opts: metav1.PatchOptions{ 1050 FieldValidation: "Warn", 1051 }, 1052 body: jsonPatchBody, 1053 strictDecodingWarnings: []string{ 1054 // note: duplicate fields in the patch itself 1055 // are dropped by the 1056 // evanphx/json-patch library and is expected. 1057 // Duplicate fields in the json patch ops 1058 // themselves can be detected though 1059 `json patch unknown field "[0].foo"`, 1060 `json patch duplicate field "[1].path"`, 1061 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1062 `unknown field "spec.unknown1"`, 1063 `unknown field "spec.unknown3"`, 1064 `unknown field "spec.unknownDupe"`, 1065 }, 1066 }, 1067 { 1068 name: "json-patch-ignore-validation", 1069 patchType: types.JSONPatchType, 1070 opts: metav1.PatchOptions{ 1071 FieldValidation: "Ignore", 1072 }, 1073 body: jsonPatchBody, 1074 }, 1075 { 1076 name: "json-patch-no-validation", 1077 patchType: types.JSONPatchType, 1078 body: jsonPatchBody, 1079 strictDecodingWarnings: []string{ 1080 // note: duplicate fields in the patch itself 1081 // are dropped by the 1082 // evanphx/json-patch library and is expected. 1083 // Duplicate fields in the json patch ops 1084 // themselves can be detected though 1085 `json patch unknown field "[0].foo"`, 1086 `json patch duplicate field "[1].path"`, 1087 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1088 `unknown field "spec.unknown1"`, 1089 `unknown field "spec.unknown3"`, 1090 `unknown field "spec.unknownDupe"`, 1091 }, 1092 }, 1093 { 1094 name: "nonconflicting-merge-patch-strict-validation", 1095 opts: metav1.PatchOptions{ 1096 FieldValidation: "Strict", 1097 }, 1098 patchType: types.MergePatchType, 1099 body: nonconflictingMergePatchBody, 1100 strictDecodingError: `strict decoding error: duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1101 }, 1102 { 1103 name: "nonconflicting-merge-patch-warn-validation", 1104 opts: metav1.PatchOptions{ 1105 FieldValidation: "Warn", 1106 }, 1107 patchType: types.MergePatchType, 1108 body: nonconflictingMergePatchBody, 1109 strictDecodingWarnings: []string{ 1110 `duplicate field "spec.paused"`, 1111 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1112 }, 1113 }, 1114 { 1115 name: "nonconflicting-merge-patch-ignore-validation", 1116 opts: metav1.PatchOptions{ 1117 FieldValidation: "Ignore", 1118 }, 1119 patchType: types.MergePatchType, 1120 body: nonconflictingMergePatchBody, 1121 }, 1122 { 1123 name: "nonconflicting-merge-patch-no-validation", 1124 patchType: types.MergePatchType, 1125 body: nonconflictingMergePatchBody, 1126 strictDecodingWarnings: []string{ 1127 `duplicate field "spec.paused"`, 1128 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1129 }, 1130 }, 1131 } 1132 1133 for _, tc := range testcases { 1134 t.Run(tc.name, func(t *testing.T) { 1135 req := client.CoreV1().RESTClient().Patch(tc.patchType). 1136 AbsPath("/apis/apps/v1"). 1137 Namespace("default"). 1138 Resource("deployments"). 1139 Name(deployName). 1140 VersionedParams(&tc.opts, metav1.ParameterCodec) 1141 result := req.Body([]byte(tc.body)).Do(context.TODO()) 1142 if result.Error() == nil && tc.strictDecodingError != "" { 1143 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1144 } 1145 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1146 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1147 } 1148 1149 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1150 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1151 } 1152 for i, strictWarn := range tc.strictDecodingWarnings { 1153 if strictWarn != result.Warnings()[i].Text { 1154 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1155 } 1156 1157 } 1158 }) 1159 } 1160 } 1161 1162 // testFieldValidationSMP tests that attempting a strategic-merge-patch 1163 // with unknown fields errors out when fieldValidation is strict, 1164 // but succeeds when fieldValidation is ignored. 1165 func testFieldValidationSMP(t *testing.T, client clientset.Interface) { 1166 // non-conflicting SMP has issues with the patch (duplicate fields), 1167 // but doesn't conflict with the existing object it's being patched to 1168 nonconflictingSMPBody := ` 1169 { 1170 "spec": { 1171 "paused": true, 1172 "paused": false, 1173 "selector": { 1174 "matchLabels": { 1175 "app": "nginx" 1176 } 1177 }, 1178 "template": { 1179 "metadata": { 1180 "labels": { 1181 "app": "nginx" 1182 } 1183 }, 1184 "spec": { 1185 "containers": [{ 1186 "name": "nginx", 1187 "imagePullPolicy": "Always", 1188 "imagePullPolicy": "Never" 1189 }] 1190 } 1191 } 1192 } 1193 } 1194 ` 1195 1196 smpBody := ` 1197 { 1198 "spec": { 1199 "unknown1": "val1", 1200 "unknownDupe": "valDupe", 1201 "unknownDupe": "valDupe2", 1202 "paused": true, 1203 "paused": false, 1204 "selector": { 1205 "matchLabels": { 1206 "app": "nginx" 1207 } 1208 }, 1209 "template": { 1210 "metadata": { 1211 "labels": { 1212 "app": "nginx" 1213 } 1214 }, 1215 "spec": { 1216 "containers": [{ 1217 "name": "nginx", 1218 "unknownNested": "val1", 1219 "imagePullPolicy": "Always", 1220 "imagePullPolicy": "Never" 1221 }] 1222 } 1223 } 1224 } 1225 } 1226 ` 1227 var testcases = []struct { 1228 name string 1229 opts metav1.PatchOptions 1230 body string 1231 strictDecodingError string 1232 strictDecodingWarnings []string 1233 }{ 1234 { 1235 name: "smp-strict-validation", 1236 opts: metav1.PatchOptions{ 1237 FieldValidation: "Strict", 1238 }, 1239 body: smpBody, 1240 strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy", unknown field "spec.template.spec.containers[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`, 1241 }, 1242 { 1243 name: "smp-warn-validation", 1244 opts: metav1.PatchOptions{ 1245 FieldValidation: "Warn", 1246 }, 1247 body: smpBody, 1248 strictDecodingWarnings: []string{ 1249 `duplicate field "spec.unknownDupe"`, 1250 `duplicate field "spec.paused"`, 1251 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1252 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1253 `unknown field "spec.unknown1"`, 1254 `unknown field "spec.unknownDupe"`, 1255 }, 1256 }, 1257 { 1258 name: "smp-ignore-validation", 1259 opts: metav1.PatchOptions{ 1260 FieldValidation: "Ignore", 1261 }, 1262 body: smpBody, 1263 }, 1264 { 1265 name: "smp-no-validation", 1266 body: smpBody, 1267 strictDecodingWarnings: []string{ 1268 `duplicate field "spec.unknownDupe"`, 1269 `duplicate field "spec.paused"`, 1270 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1271 `unknown field "spec.template.spec.containers[0].unknownNested"`, 1272 `unknown field "spec.unknown1"`, 1273 `unknown field "spec.unknownDupe"`, 1274 }, 1275 }, 1276 { 1277 name: "nonconflicting-smp-strict-validation", 1278 opts: metav1.PatchOptions{ 1279 FieldValidation: "Strict", 1280 }, 1281 body: nonconflictingSMPBody, 1282 strictDecodingError: `strict decoding error: duplicate field "spec.paused", duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1283 }, 1284 { 1285 name: "nonconflicting-smp-warn-validation", 1286 opts: metav1.PatchOptions{ 1287 FieldValidation: "Warn", 1288 }, 1289 body: nonconflictingSMPBody, 1290 strictDecodingWarnings: []string{ 1291 `duplicate field "spec.paused"`, 1292 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1293 }, 1294 }, 1295 { 1296 name: "nonconflicting-smp-ignore-validation", 1297 opts: metav1.PatchOptions{ 1298 FieldValidation: "Ignore", 1299 }, 1300 body: nonconflictingSMPBody, 1301 }, 1302 { 1303 name: "nonconflicting-smp-no-validation", 1304 body: nonconflictingSMPBody, 1305 strictDecodingWarnings: []string{ 1306 `duplicate field "spec.paused"`, 1307 `duplicate field "spec.template.spec.containers[0].imagePullPolicy"`, 1308 }, 1309 }, 1310 } 1311 1312 for _, tc := range testcases { 1313 t.Run(tc.name, func(t *testing.T) { 1314 body := []byte(fmt.Sprintf(validBodyJSON, tc.name)) 1315 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1316 AbsPath("/apis/apps/v1"). 1317 Namespace("default"). 1318 Resource("deployments"). 1319 Name(tc.name). 1320 Param("fieldManager", "apply_test"). 1321 Body(body). 1322 Do(context.TODO()). 1323 Get() 1324 if err != nil { 1325 t.Fatalf("Failed to create object using Apply patch: %v", err) 1326 } 1327 1328 req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). 1329 AbsPath("/apis/apps/v1"). 1330 Namespace("default"). 1331 Resource("deployments"). 1332 Name(tc.name). 1333 VersionedParams(&tc.opts, metav1.ParameterCodec) 1334 result := req.Body([]byte(tc.body)).Do(context.TODO()) 1335 if result.Error() == nil && tc.strictDecodingError != "" { 1336 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1337 } 1338 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1339 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1340 } 1341 1342 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1343 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1344 } 1345 1346 for i, strictWarn := range tc.strictDecodingWarnings { 1347 if strictWarn != result.Warnings()[i].Text { 1348 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1349 } 1350 1351 } 1352 }) 1353 } 1354 } 1355 1356 // testFieldValidationApplyCreate tests apply patch requests containing unknown fields 1357 // on newly created objects, with strict and non-strict field validation. 1358 func testFieldValidationApplyCreate(t *testing.T, client clientset.Interface) { 1359 var testcases = []struct { 1360 name string 1361 opts metav1.PatchOptions 1362 strictDecodingError string 1363 strictDecodingWarnings []string 1364 }{ 1365 { 1366 name: "strict-validation", 1367 opts: metav1.PatchOptions{ 1368 FieldValidation: "Strict", 1369 FieldManager: "mgr", 1370 }, 1371 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 1372 line 10: key "paused" already set in map 1373 line 27: key "imagePullPolicy" already set in map`, 1374 }, 1375 { 1376 name: "warn-validation", 1377 opts: metav1.PatchOptions{ 1378 FieldValidation: "Warn", 1379 FieldManager: "mgr", 1380 }, 1381 strictDecodingWarnings: []string{ 1382 `line 10: key "paused" already set in map`, 1383 `line 27: key "imagePullPolicy" already set in map`, 1384 }, 1385 }, 1386 { 1387 name: "ignore-validation", 1388 opts: metav1.PatchOptions{ 1389 FieldValidation: "Ignore", 1390 FieldManager: "mgr", 1391 }, 1392 }, 1393 { 1394 name: "no-validation", 1395 opts: metav1.PatchOptions{ 1396 FieldManager: "mgr", 1397 }, 1398 strictDecodingWarnings: []string{ 1399 `line 10: key "paused" already set in map`, 1400 `line 27: key "imagePullPolicy" already set in map`, 1401 }, 1402 }, 1403 } 1404 1405 for _, tc := range testcases { 1406 t.Run(tc.name, func(t *testing.T) { 1407 name := fmt.Sprintf("apply-create-deployment-%s", tc.name) 1408 body := []byte(fmt.Sprintf(applyInvalidBody, name)) 1409 req := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1410 AbsPath("/apis/apps/v1"). 1411 Namespace("default"). 1412 Resource("deployments"). 1413 Name(name). 1414 VersionedParams(&tc.opts, metav1.ParameterCodec) 1415 result := req.Body(body).Do(context.TODO()) 1416 if result.Error() == nil && tc.strictDecodingError != "" { 1417 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1418 } 1419 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1420 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1421 } 1422 1423 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1424 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1425 } 1426 for i, strictWarn := range tc.strictDecodingWarnings { 1427 if strictWarn != result.Warnings()[i].Text { 1428 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1429 } 1430 1431 } 1432 }) 1433 } 1434 } 1435 1436 // testFieldValidationApplyUpdate tests apply patch requests containing unknown fields 1437 // on apply requests to existing objects, with strict and non-strict field validation. 1438 func testFieldValidationApplyUpdate(t *testing.T, client clientset.Interface) { 1439 var testcases = []struct { 1440 name string 1441 opts metav1.PatchOptions 1442 strictDecodingError string 1443 strictDecodingWarnings []string 1444 }{ 1445 { 1446 name: "strict-validation", 1447 opts: metav1.PatchOptions{ 1448 FieldValidation: "Strict", 1449 FieldManager: "mgr", 1450 }, 1451 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 1452 line 10: key "paused" already set in map 1453 line 27: key "imagePullPolicy" already set in map`, 1454 }, 1455 { 1456 name: "warn-validation", 1457 opts: metav1.PatchOptions{ 1458 FieldValidation: "Warn", 1459 FieldManager: "mgr", 1460 }, 1461 strictDecodingWarnings: []string{ 1462 `line 10: key "paused" already set in map`, 1463 `line 27: key "imagePullPolicy" already set in map`, 1464 }, 1465 }, 1466 { 1467 name: "ignore-validation", 1468 opts: metav1.PatchOptions{ 1469 FieldValidation: "Ignore", 1470 FieldManager: "mgr", 1471 }, 1472 }, 1473 { 1474 name: "no-validation", 1475 opts: metav1.PatchOptions{ 1476 FieldManager: "mgr", 1477 }, 1478 strictDecodingWarnings: []string{ 1479 `line 10: key "paused" already set in map`, 1480 `line 27: key "imagePullPolicy" already set in map`, 1481 }, 1482 }, 1483 } 1484 1485 for _, tc := range testcases { 1486 t.Run(tc.name, func(t *testing.T) { 1487 name := fmt.Sprintf("apply-update-deployment-%s", tc.name) 1488 createBody := []byte(fmt.Sprintf(validBodyJSON, name)) 1489 createReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1490 AbsPath("/apis/apps/v1"). 1491 Namespace("default"). 1492 Resource("deployments"). 1493 Name(name). 1494 VersionedParams(&tc.opts, metav1.ParameterCodec) 1495 createResult := createReq.Body(createBody).Do(context.TODO()) 1496 if createResult.Error() != nil { 1497 t.Fatalf("unexpected apply create err: %v", createResult.Error()) 1498 } 1499 1500 updateBody := []byte(fmt.Sprintf(applyInvalidBody, name)) 1501 updateReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 1502 AbsPath("/apis/apps/v1"). 1503 Namespace("default"). 1504 Resource("deployments"). 1505 Name(name). 1506 VersionedParams(&tc.opts, metav1.ParameterCodec) 1507 result := updateReq.Body(updateBody).Do(context.TODO()) 1508 if result.Error() == nil && tc.strictDecodingError != "" { 1509 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1510 } 1511 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1512 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1513 } 1514 1515 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1516 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1517 } 1518 for i, strictWarn := range tc.strictDecodingWarnings { 1519 if strictWarn != result.Warnings()[i].Text { 1520 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1521 } 1522 1523 } 1524 }) 1525 } 1526 } 1527 1528 // testFieldValidationPostCRD tests that server-side schema validation 1529 // works for CRD create requests for CRDs with schemas 1530 func testFieldValidationPostCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 1531 var testcases = []struct { 1532 name string 1533 opts metav1.PatchOptions 1534 body string 1535 contentType string 1536 strictDecodingError string 1537 strictDecodingWarnings []string 1538 }{ 1539 { 1540 name: "crd-post-strict-validation", 1541 opts: metav1.PatchOptions{ 1542 FieldValidation: "Strict", 1543 }, 1544 body: crdInvalidBody, 1545 strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1546 }, 1547 { 1548 name: "crd-post-warn-validation", 1549 opts: metav1.PatchOptions{ 1550 FieldValidation: "Warn", 1551 }, 1552 body: crdInvalidBody, 1553 strictDecodingWarnings: []string{ 1554 `duplicate field "metadata.name"`, 1555 `duplicate field "spec.unknownDupe"`, 1556 `duplicate field "spec.knownField1"`, 1557 `duplicate field "spec.ports[0].hostPort"`, 1558 `unknown field "metadata.unknownMeta"`, 1559 `unknown field "spec.ports[0].unknownNested"`, 1560 `unknown field "spec.unknown1"`, 1561 `unknown field "spec.unknownDupe"`, 1562 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1563 }, 1564 }, 1565 { 1566 name: "crd-post-ignore-validation", 1567 opts: metav1.PatchOptions{ 1568 FieldValidation: "Ignore", 1569 }, 1570 body: crdInvalidBody, 1571 }, 1572 { 1573 name: "crd-post-no-validation", 1574 body: crdInvalidBody, 1575 strictDecodingWarnings: []string{ 1576 `duplicate field "metadata.name"`, 1577 `duplicate field "spec.unknownDupe"`, 1578 `duplicate field "spec.knownField1"`, 1579 `duplicate field "spec.ports[0].hostPort"`, 1580 `unknown field "metadata.unknownMeta"`, 1581 `unknown field "spec.ports[0].unknownNested"`, 1582 `unknown field "spec.unknown1"`, 1583 `unknown field "spec.unknownDupe"`, 1584 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1585 }, 1586 }, 1587 { 1588 name: "crd-post-strict-validation-yaml", 1589 opts: metav1.PatchOptions{ 1590 FieldValidation: "Strict", 1591 }, 1592 body: crdInvalidBodyYAML, 1593 contentType: "application/yaml", 1594 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 1595 line 6: key "name" already set in map 1596 line 12: key "unknownDupe" already set in map 1597 line 14: key "knownField1" already set in map 1598 line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1599 }, 1600 { 1601 name: "crd-post-warn-validation-yaml", 1602 opts: metav1.PatchOptions{ 1603 FieldValidation: "Warn", 1604 }, 1605 body: crdInvalidBodyYAML, 1606 contentType: "application/yaml", 1607 strictDecodingWarnings: []string{ 1608 `line 6: key "name" already set in map`, 1609 `line 12: key "unknownDupe" already set in map`, 1610 `line 14: key "knownField1" already set in map`, 1611 `line 20: key "hostPort" already set in map`, 1612 `unknown field "metadata.unknownMeta"`, 1613 `unknown field "spec.ports[0].unknownNested"`, 1614 `unknown field "spec.unknown1"`, 1615 `unknown field "spec.unknownDupe"`, 1616 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1617 }, 1618 }, 1619 { 1620 name: "crd-post-ignore-validation-yaml", 1621 opts: metav1.PatchOptions{ 1622 FieldValidation: "Ignore", 1623 }, 1624 body: crdInvalidBodyYAML, 1625 contentType: "application/yaml", 1626 }, 1627 { 1628 name: "crd-post-no-validation-yaml", 1629 body: crdInvalidBodyYAML, 1630 contentType: "application/yaml", 1631 strictDecodingWarnings: []string{ 1632 `line 6: key "name" already set in map`, 1633 `line 12: key "unknownDupe" already set in map`, 1634 `line 14: key "knownField1" already set in map`, 1635 `line 20: key "hostPort" already set in map`, 1636 `unknown field "metadata.unknownMeta"`, 1637 `unknown field "spec.ports[0].unknownNested"`, 1638 `unknown field "spec.unknown1"`, 1639 `unknown field "spec.unknownDupe"`, 1640 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1641 }, 1642 }, 1643 } 1644 for _, tc := range testcases { 1645 t.Run(tc.name, func(t *testing.T) { 1646 klog.Warningf("running tc named: %s", tc.name) 1647 kind := gvk.Kind 1648 apiVersion := gvk.Group + "/" + gvk.Version 1649 1650 // create the CR as specified by the test case 1651 jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, tc.name)) 1652 req := rest.Post(). 1653 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 1654 SetHeader("Content-Type", tc.contentType). 1655 VersionedParams(&tc.opts, metav1.ParameterCodec) 1656 result := req.Body([]byte(jsonBody)).Do(context.TODO()) 1657 if result.Error() == nil && tc.strictDecodingError != "" { 1658 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1659 } 1660 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1661 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1662 } 1663 1664 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1665 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1666 } 1667 1668 for i, strictWarn := range tc.strictDecodingWarnings { 1669 if strictWarn != result.Warnings()[i].Text { 1670 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1671 } 1672 1673 } 1674 }) 1675 } 1676 } 1677 1678 // testFieldValidationPostCRDSchemaless tests that server-side schema validation 1679 // works for CRD create requests for CRDs that have schemas 1680 // with x-kubernetes-preserve-unknown-field set 1681 func testFieldValidationPostCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 1682 var testcases = []struct { 1683 name string 1684 opts metav1.PatchOptions 1685 body string 1686 contentType string 1687 strictDecodingError string 1688 strictDecodingWarnings []string 1689 }{ 1690 { 1691 name: "schemaless-crd-post-strict-validation", 1692 opts: metav1.PatchOptions{ 1693 FieldValidation: "Strict", 1694 }, 1695 body: crdInvalidBody, 1696 strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1697 }, 1698 { 1699 name: "schemaless-crd-post-warn-validation", 1700 opts: metav1.PatchOptions{ 1701 FieldValidation: "Warn", 1702 }, 1703 body: crdInvalidBody, 1704 strictDecodingWarnings: []string{ 1705 `duplicate field "metadata.name"`, 1706 `duplicate field "spec.unknownDupe"`, 1707 `duplicate field "spec.knownField1"`, 1708 `duplicate field "spec.ports[0].hostPort"`, 1709 `unknown field "metadata.unknownMeta"`, 1710 `unknown field "spec.ports[0].unknownNested"`, 1711 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1712 }, 1713 }, 1714 { 1715 name: "schemaless-crd-post-ignore-validation", 1716 opts: metav1.PatchOptions{ 1717 FieldValidation: "Ignore", 1718 }, 1719 body: crdInvalidBody, 1720 }, 1721 { 1722 name: "schemaless-crd-post-no-validation", 1723 body: crdInvalidBody, 1724 strictDecodingWarnings: []string{ 1725 `duplicate field "metadata.name"`, 1726 `duplicate field "spec.unknownDupe"`, 1727 `duplicate field "spec.knownField1"`, 1728 `duplicate field "spec.ports[0].hostPort"`, 1729 `unknown field "metadata.unknownMeta"`, 1730 `unknown field "spec.ports[0].unknownNested"`, 1731 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1732 }, 1733 }, 1734 { 1735 name: "schemaless-crd-post-strict-validation-yaml", 1736 opts: metav1.PatchOptions{ 1737 FieldValidation: "Strict", 1738 }, 1739 body: crdInvalidBodyYAML, 1740 contentType: "application/yaml", 1741 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 1742 line 6: key "name" already set in map 1743 line 12: key "unknownDupe" already set in map 1744 line 14: key "knownField1" already set in map 1745 line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1746 }, 1747 { 1748 name: "schemaless-crd-post-warn-validation-yaml", 1749 opts: metav1.PatchOptions{ 1750 FieldValidation: "Warn", 1751 }, 1752 body: crdInvalidBodyYAML, 1753 contentType: "application/yaml", 1754 strictDecodingWarnings: []string{ 1755 `line 6: key "name" already set in map`, 1756 `line 12: key "unknownDupe" already set in map`, 1757 `line 14: key "knownField1" already set in map`, 1758 `line 20: key "hostPort" already set in map`, 1759 `unknown field "metadata.unknownMeta"`, 1760 `unknown field "spec.ports[0].unknownNested"`, 1761 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1762 }, 1763 }, 1764 { 1765 name: "schemaless-crd-post-ignore-validation-yaml", 1766 opts: metav1.PatchOptions{ 1767 FieldValidation: "Ignore", 1768 }, 1769 body: crdInvalidBodyYAML, 1770 contentType: "application/yaml", 1771 }, 1772 { 1773 name: "schemaless-crd-post-no-validation-yaml", 1774 body: crdInvalidBodyYAML, 1775 contentType: "application/yaml", 1776 strictDecodingWarnings: []string{ 1777 `line 6: key "name" already set in map`, 1778 `line 12: key "unknownDupe" already set in map`, 1779 `line 14: key "knownField1" already set in map`, 1780 `line 20: key "hostPort" already set in map`, 1781 `unknown field "metadata.unknownMeta"`, 1782 `unknown field "spec.ports[0].unknownNested"`, 1783 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1784 }, 1785 }, 1786 } 1787 for _, tc := range testcases { 1788 t.Run(tc.name, func(t *testing.T) { 1789 1790 kind := gvk.Kind 1791 apiVersion := gvk.Group + "/" + gvk.Version 1792 1793 // create the CR as specified by the test case 1794 jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, tc.name)) 1795 req := rest.Post(). 1796 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 1797 SetHeader("Content-Type", tc.contentType). 1798 VersionedParams(&tc.opts, metav1.ParameterCodec) 1799 result := req.Body([]byte(jsonBody)).Do(context.TODO()) 1800 if result.Error() == nil && tc.strictDecodingError != "" { 1801 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1802 } 1803 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1804 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1805 } 1806 1807 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1808 t.Logf("expected:") 1809 for _, w := range tc.strictDecodingWarnings { 1810 t.Logf("\t%v", w) 1811 } 1812 t.Logf("got:") 1813 for _, w := range result.Warnings() { 1814 t.Logf("\t%v", w.Text) 1815 } 1816 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1817 } 1818 1819 for i, strictWarn := range tc.strictDecodingWarnings { 1820 if strictWarn != result.Warnings()[i].Text { 1821 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1822 } 1823 1824 } 1825 }) 1826 } 1827 } 1828 1829 // testFieldValidationPutCRD tests that server-side schema validation 1830 // works for CRD update requests for CRDs with schemas. 1831 func testFieldValidationPutCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 1832 var testcases = []struct { 1833 name string 1834 opts metav1.PatchOptions 1835 putBody string 1836 contentType string 1837 strictDecodingError string 1838 strictDecodingWarnings []string 1839 }{ 1840 { 1841 name: "crd-put-strict-validation", 1842 opts: metav1.PatchOptions{ 1843 FieldValidation: "Strict", 1844 }, 1845 putBody: crdInvalidBody, 1846 strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1847 }, 1848 { 1849 name: "crd-put-warn-validation", 1850 opts: metav1.PatchOptions{ 1851 FieldValidation: "Warn", 1852 }, 1853 putBody: crdInvalidBody, 1854 strictDecodingWarnings: []string{ 1855 `duplicate field "metadata.name"`, 1856 `duplicate field "spec.unknownDupe"`, 1857 `duplicate field "spec.knownField1"`, 1858 `duplicate field "spec.ports[0].hostPort"`, 1859 `unknown field "metadata.unknownMeta"`, 1860 `unknown field "spec.ports[0].unknownNested"`, 1861 `unknown field "spec.unknown1"`, 1862 `unknown field "spec.unknownDupe"`, 1863 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1864 }, 1865 }, 1866 { 1867 name: "crd-put-ignore-validation", 1868 opts: metav1.PatchOptions{ 1869 FieldValidation: "Ignore", 1870 }, 1871 putBody: crdInvalidBody, 1872 }, 1873 { 1874 name: "crd-put-no-validation", 1875 putBody: crdInvalidBody, 1876 strictDecodingWarnings: []string{ 1877 `duplicate field "metadata.name"`, 1878 `duplicate field "spec.unknownDupe"`, 1879 `duplicate field "spec.knownField1"`, 1880 `duplicate field "spec.ports[0].hostPort"`, 1881 `unknown field "metadata.unknownMeta"`, 1882 `unknown field "spec.ports[0].unknownNested"`, 1883 `unknown field "spec.unknown1"`, 1884 `unknown field "spec.unknownDupe"`, 1885 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1886 }, 1887 }, 1888 { 1889 name: "crd-put-strict-validation-yaml", 1890 opts: metav1.PatchOptions{ 1891 FieldValidation: "Strict", 1892 }, 1893 putBody: crdInvalidBodyYAML, 1894 contentType: "application/yaml", 1895 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 1896 line 6: key "name" already set in map 1897 line 12: key "unknownDupe" already set in map 1898 line 14: key "knownField1" already set in map 1899 line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1900 }, 1901 { 1902 name: "crd-put-warn-validation-yaml", 1903 opts: metav1.PatchOptions{ 1904 FieldValidation: "Warn", 1905 }, 1906 putBody: crdInvalidBodyYAML, 1907 contentType: "application/yaml", 1908 strictDecodingWarnings: []string{ 1909 `line 6: key "name" already set in map`, 1910 `line 12: key "unknownDupe" already set in map`, 1911 `line 14: key "knownField1" already set in map`, 1912 `line 20: key "hostPort" already set in map`, 1913 `unknown field "metadata.unknownMeta"`, 1914 `unknown field "spec.ports[0].unknownNested"`, 1915 `unknown field "spec.unknown1"`, 1916 `unknown field "spec.unknownDupe"`, 1917 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1918 }, 1919 }, 1920 { 1921 name: "crd-put-ignore-validation-yaml", 1922 opts: metav1.PatchOptions{ 1923 FieldValidation: "Ignore", 1924 }, 1925 putBody: crdInvalidBodyYAML, 1926 contentType: "application/yaml", 1927 }, 1928 { 1929 name: "crd-put-no-validation-yaml", 1930 putBody: crdInvalidBodyYAML, 1931 contentType: "application/yaml", 1932 strictDecodingWarnings: []string{ 1933 `line 6: key "name" already set in map`, 1934 `line 12: key "unknownDupe" already set in map`, 1935 `line 14: key "knownField1" already set in map`, 1936 `line 20: key "hostPort" already set in map`, 1937 `unknown field "metadata.unknownMeta"`, 1938 `unknown field "spec.ports[0].unknownNested"`, 1939 `unknown field "spec.unknown1"`, 1940 `unknown field "spec.unknownDupe"`, 1941 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 1942 }, 1943 }, 1944 } 1945 for _, tc := range testcases { 1946 t.Run(tc.name, func(t *testing.T) { 1947 kind := gvk.Kind 1948 apiVersion := gvk.Group + "/" + gvk.Version 1949 1950 // create the CR as specified by the test case 1951 jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, tc.name)) 1952 postReq := rest.Post(). 1953 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 1954 VersionedParams(&tc.opts, metav1.ParameterCodec) 1955 postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw() 1956 if err != nil { 1957 t.Fatalf("unexpeted error on CR creation: %v", err) 1958 } 1959 postUnstructured := &unstructured.Unstructured{} 1960 if err := postUnstructured.UnmarshalJSON(postResult); err != nil { 1961 t.Fatalf("unexpeted error unmarshalling created CR: %v", err) 1962 } 1963 1964 // update the CR as specified by the test case 1965 putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, tc.name, postUnstructured.GetResourceVersion())) 1966 putReq := rest.Put(). 1967 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 1968 Name(tc.name). 1969 SetHeader("Content-Type", tc.contentType). 1970 VersionedParams(&tc.opts, metav1.ParameterCodec) 1971 result := putReq.Body([]byte(putBody)).Do(context.TODO()) 1972 if result.Error() == nil && tc.strictDecodingError != "" { 1973 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 1974 } 1975 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 1976 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 1977 } 1978 1979 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 1980 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 1981 } 1982 1983 for i, strictWarn := range tc.strictDecodingWarnings { 1984 if strictWarn != result.Warnings()[i].Text { 1985 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 1986 } 1987 1988 } 1989 }) 1990 } 1991 } 1992 1993 // testFieldValidationPutCRDSchemaless tests that server-side schema validation 1994 // works for CRD update requests for CRDs that have schemas 1995 // with x-kubernetes-preserve-unknown-field set 1996 func testFieldValidationPutCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 1997 var testcases = []struct { 1998 name string 1999 opts metav1.PatchOptions 2000 putBody string 2001 contentType string 2002 strictDecodingError string 2003 strictDecodingWarnings []string 2004 }{ 2005 { 2006 name: "schemaless-crd-put-strict-validation", 2007 opts: metav1.PatchOptions{ 2008 FieldValidation: "Strict", 2009 }, 2010 putBody: crdInvalidBody, 2011 strictDecodingError: `strict decoding error: duplicate field "metadata.name", duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2012 }, 2013 { 2014 name: "schemaless-crd-put-warn-validation", 2015 opts: metav1.PatchOptions{ 2016 FieldValidation: "Warn", 2017 }, 2018 putBody: crdInvalidBody, 2019 strictDecodingWarnings: []string{ 2020 `duplicate field "metadata.name"`, 2021 `duplicate field "spec.unknownDupe"`, 2022 `duplicate field "spec.knownField1"`, 2023 `duplicate field "spec.ports[0].hostPort"`, 2024 `unknown field "metadata.unknownMeta"`, 2025 `unknown field "spec.ports[0].unknownNested"`, 2026 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2027 }, 2028 }, 2029 { 2030 name: "schemaless-crd-put-ignore-validation", 2031 opts: metav1.PatchOptions{ 2032 FieldValidation: "Ignore", 2033 }, 2034 putBody: crdInvalidBody, 2035 }, 2036 { 2037 name: "schemaless-crd-put-no-validation", 2038 putBody: crdInvalidBody, 2039 strictDecodingWarnings: []string{ 2040 `duplicate field "metadata.name"`, 2041 `duplicate field "spec.unknownDupe"`, 2042 `duplicate field "spec.knownField1"`, 2043 `duplicate field "spec.ports[0].hostPort"`, 2044 `unknown field "metadata.unknownMeta"`, 2045 `unknown field "spec.ports[0].unknownNested"`, 2046 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2047 }, 2048 }, 2049 { 2050 name: "schemaless-crd-put-strict-validation-yaml", 2051 opts: metav1.PatchOptions{ 2052 FieldValidation: "Strict", 2053 }, 2054 putBody: crdInvalidBodyYAML, 2055 contentType: "application/yaml", 2056 strictDecodingError: `strict decoding error: yaml: unmarshal errors: 2057 line 6: key "name" already set in map 2058 line 12: key "unknownDupe" already set in map 2059 line 14: key "knownField1" already set in map 2060 line 20: key "hostPort" already set in map, unknown field "metadata.unknownMeta", unknown field "spec.ports[0].unknownNested", unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2061 }, 2062 { 2063 name: "schemaless-crd-put-warn-validation-yaml", 2064 opts: metav1.PatchOptions{ 2065 FieldValidation: "Warn", 2066 }, 2067 putBody: crdInvalidBodyYAML, 2068 contentType: "application/yaml", 2069 strictDecodingWarnings: []string{ 2070 `line 6: key "name" already set in map`, 2071 `line 12: key "unknownDupe" already set in map`, 2072 `line 14: key "knownField1" already set in map`, 2073 `line 20: key "hostPort" already set in map`, 2074 `unknown field "metadata.unknownMeta"`, 2075 `unknown field "spec.ports[0].unknownNested"`, 2076 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2077 }, 2078 }, 2079 { 2080 name: "schemaless-crd-put-ignore-validation-yaml", 2081 opts: metav1.PatchOptions{ 2082 FieldValidation: "Ignore", 2083 }, 2084 putBody: crdInvalidBodyYAML, 2085 contentType: "application/yaml", 2086 }, 2087 { 2088 name: "schemaless-crd-put-no-validation-yaml", 2089 putBody: crdInvalidBodyYAML, 2090 contentType: "application/yaml", 2091 strictDecodingWarnings: []string{ 2092 `line 6: key "name" already set in map`, 2093 `line 12: key "unknownDupe" already set in map`, 2094 `line 14: key "knownField1" already set in map`, 2095 `line 20: key "hostPort" already set in map`, 2096 `unknown field "metadata.unknownMeta"`, 2097 `unknown field "spec.ports[0].unknownNested"`, 2098 `unknown field "spec.embeddedObj.metadata.unknownEmbeddedMeta"`, 2099 }, 2100 }, 2101 } 2102 for _, tc := range testcases { 2103 t.Run(tc.name, func(t *testing.T) { 2104 kind := gvk.Kind 2105 apiVersion := gvk.Group + "/" + gvk.Version 2106 2107 // create the CR as specified by the test case 2108 jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, tc.name)) 2109 postReq := rest.Post(). 2110 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2111 VersionedParams(&tc.opts, metav1.ParameterCodec) 2112 postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw() 2113 if err != nil { 2114 t.Fatalf("unexpeted error on CR creation: %v", err) 2115 } 2116 postUnstructured := &unstructured.Unstructured{} 2117 if err := postUnstructured.UnmarshalJSON(postResult); err != nil { 2118 t.Fatalf("unexpeted error unmarshalling created CR: %v", err) 2119 } 2120 2121 // update the CR as specified by the test case 2122 putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, tc.name, postUnstructured.GetResourceVersion())) 2123 putReq := rest.Put(). 2124 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2125 Name(tc.name). 2126 SetHeader("Content-Type", tc.contentType). 2127 VersionedParams(&tc.opts, metav1.ParameterCodec) 2128 result := putReq.Body([]byte(putBody)).Do(context.TODO()) 2129 if result.Error() == nil && tc.strictDecodingError != "" { 2130 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2131 } 2132 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2133 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2134 } 2135 2136 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2137 t.Logf("expected:") 2138 for _, w := range tc.strictDecodingWarnings { 2139 t.Logf("\t%v", w) 2140 } 2141 t.Logf("got:") 2142 for _, w := range result.Warnings() { 2143 t.Logf("\t%v", w.Text) 2144 } 2145 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2146 } 2147 2148 for i, strictWarn := range tc.strictDecodingWarnings { 2149 if strictWarn != result.Warnings()[i].Text { 2150 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2151 } 2152 2153 } 2154 }) 2155 } 2156 } 2157 2158 // testFieldValidationPatchCRD tests that server-side schema validation 2159 // works for jsonpatch and mergepatch requests 2160 // for custom resources that have schemas. 2161 func testFieldValidationPatchCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2162 patchYAMLBody := ` 2163 apiVersion: %s 2164 kind: %s 2165 metadata: 2166 name: %s 2167 finalizers: 2168 - test/finalizer 2169 spec: 2170 cronSpec: "* * * * */5" 2171 ports: 2172 - name: x 2173 containerPort: 80 2174 protocol: TCP` 2175 2176 mergePatchBody := ` 2177 { 2178 "spec": { 2179 "unknown1": "val1", 2180 "unknownDupe": "valDupe", 2181 "unknownDupe": "valDupe2", 2182 "knownField1": "val1", 2183 "knownField1": "val2", 2184 "ports": [{ 2185 "name": "portName", 2186 "containerPort": 8080, 2187 "protocol": "TCP", 2188 "hostPort": 8081, 2189 "hostPort": 8082, 2190 "unknownNested": "val" 2191 }] 2192 } 2193 } 2194 ` 2195 jsonPatchBody := ` 2196 [ 2197 {"op": "add", "path": "/spec/unknown1", "value": "val1", "foo": "bar"}, 2198 {"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val2"}, 2199 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"}, 2200 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"}, 2201 {"op": "add", "path": "/spec/knownField1", "value": "val1"}, 2202 {"op": "add", "path": "/spec/knownField1", "value": "val2"}, 2203 {"op": "add", "path": "/spec/ports/0/name", "value": "portName"}, 2204 {"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080}, 2205 {"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"}, 2206 {"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081}, 2207 {"op": "add", "path": "/spec/ports/0/hostPort", "value": 8082}, 2208 {"op": "add", "path": "/spec/ports/0/unknownNested", "value": "val"} 2209 ] 2210 ` 2211 var testcases = []struct { 2212 name string 2213 patchType types.PatchType 2214 opts metav1.PatchOptions 2215 body string 2216 strictDecodingError string 2217 strictDecodingWarnings []string 2218 }{ 2219 { 2220 name: "crd-merge-patch-strict-validation", 2221 patchType: types.MergePatchType, 2222 opts: metav1.PatchOptions{ 2223 FieldValidation: "Strict", 2224 }, 2225 body: mergePatchBody, 2226 strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknownDupe"`, 2227 }, 2228 { 2229 name: "crd-merge-patch-warn-validation", 2230 patchType: types.MergePatchType, 2231 opts: metav1.PatchOptions{ 2232 FieldValidation: "Warn", 2233 }, 2234 body: mergePatchBody, 2235 strictDecodingWarnings: []string{ 2236 `duplicate field "spec.unknownDupe"`, 2237 `duplicate field "spec.knownField1"`, 2238 `duplicate field "spec.ports[0].hostPort"`, 2239 `unknown field "spec.ports[0].unknownNested"`, 2240 `unknown field "spec.unknown1"`, 2241 `unknown field "spec.unknownDupe"`, 2242 }, 2243 }, 2244 { 2245 name: "crd-merge-patch-ignore-validation", 2246 patchType: types.MergePatchType, 2247 opts: metav1.PatchOptions{ 2248 FieldValidation: "Ignore", 2249 }, 2250 body: mergePatchBody, 2251 }, 2252 { 2253 name: "crd-merge-patch-no-validation", 2254 patchType: types.MergePatchType, 2255 body: mergePatchBody, 2256 strictDecodingWarnings: []string{ 2257 `duplicate field "spec.unknownDupe"`, 2258 `duplicate field "spec.knownField1"`, 2259 `duplicate field "spec.ports[0].hostPort"`, 2260 `unknown field "spec.ports[0].unknownNested"`, 2261 `unknown field "spec.unknown1"`, 2262 `unknown field "spec.unknownDupe"`, 2263 }, 2264 }, 2265 { 2266 name: "crd-json-patch-strict-validation", 2267 patchType: types.JSONPatchType, 2268 opts: metav1.PatchOptions{ 2269 FieldValidation: "Strict", 2270 }, 2271 body: jsonPatchBody, 2272 // note: duplicate fields in the patch itself 2273 // are dropped by the 2274 // evanphx/json-patch library and is expected. 2275 // Duplicate fields in the json patch ops 2276 // themselves can be detected though 2277 strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.ports[0].unknownNested", unknown field "spec.unknown1", unknown field "spec.unknown3", unknown field "spec.unknownDupe"`, 2278 }, 2279 { 2280 name: "crd-json-patch-warn-validation", 2281 patchType: types.JSONPatchType, 2282 opts: metav1.PatchOptions{ 2283 FieldValidation: "Warn", 2284 }, 2285 body: jsonPatchBody, 2286 strictDecodingWarnings: []string{ 2287 // note: duplicate fields in the patch itself 2288 // are dropped by the 2289 // evanphx/json-patch library and is expected. 2290 // Duplicate fields in the json patch ops 2291 // themselves can be detected though 2292 `json patch unknown field "[0].foo"`, 2293 `json patch duplicate field "[1].path"`, 2294 `unknown field "spec.ports[0].unknownNested"`, 2295 `unknown field "spec.unknown1"`, 2296 `unknown field "spec.unknown3"`, 2297 `unknown field "spec.unknownDupe"`, 2298 }, 2299 }, 2300 { 2301 name: "crd-json-patch-ignore-validation", 2302 patchType: types.JSONPatchType, 2303 opts: metav1.PatchOptions{ 2304 FieldValidation: "Ignore", 2305 }, 2306 body: jsonPatchBody, 2307 }, 2308 { 2309 name: "crd-json-patch-no-validation", 2310 patchType: types.JSONPatchType, 2311 body: jsonPatchBody, 2312 strictDecodingWarnings: []string{ 2313 // note: duplicate fields in the patch itself 2314 // are dropped by the 2315 // evanphx/json-patch library and is expected. 2316 // Duplicate fields in the json patch ops 2317 // themselves can be detected though 2318 `json patch unknown field "[0].foo"`, 2319 `json patch duplicate field "[1].path"`, 2320 `unknown field "spec.ports[0].unknownNested"`, 2321 `unknown field "spec.unknown1"`, 2322 `unknown field "spec.unknown3"`, 2323 `unknown field "spec.unknownDupe"`, 2324 }, 2325 }, 2326 } 2327 for _, tc := range testcases { 2328 t.Run(tc.name, func(t *testing.T) { 2329 kind := gvk.Kind 2330 apiVersion := gvk.Group + "/" + gvk.Version 2331 // create a CR 2332 yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, tc.name)) 2333 createResult, err := rest.Patch(types.ApplyPatchType). 2334 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2335 Name(tc.name). 2336 Param("fieldManager", "apply_test"). 2337 Body(yamlBody). 2338 DoRaw(context.TODO()) 2339 if err != nil { 2340 t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult)) 2341 } 2342 2343 // patch the CR as specified by the test case 2344 req := rest.Patch(tc.patchType). 2345 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2346 Name(tc.name). 2347 VersionedParams(&tc.opts, metav1.ParameterCodec) 2348 result := req.Body([]byte(tc.body)).Do(context.TODO()) 2349 if result.Error() == nil && tc.strictDecodingError != "" { 2350 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2351 } 2352 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2353 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2354 } 2355 2356 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2357 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2358 } 2359 2360 for i, strictWarn := range tc.strictDecodingWarnings { 2361 if strictWarn != result.Warnings()[i].Text { 2362 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2363 } 2364 2365 } 2366 }) 2367 } 2368 } 2369 2370 // testFieldValidationPatchCRDSchemaless tests that server-side schema validation 2371 // works for jsonpatch and mergepatch requests 2372 // for custom resources that have schemas 2373 // with x-kubernetes-preserve-unknown-field set 2374 func testFieldValidationPatchCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2375 mergePatchBody := ` 2376 { 2377 "spec": { 2378 "unknown1": "val1", 2379 "unknownDupe": "valDupe", 2380 "unknownDupe": "valDupe2", 2381 "knownField1": "val1", 2382 "knownField1": "val2", 2383 "ports": [{ 2384 "name": "portName", 2385 "containerPort": 8080, 2386 "protocol": "TCP", 2387 "hostPort": 8081, 2388 "hostPort": 8082, 2389 "unknownNested": "val" 2390 }] 2391 } 2392 } 2393 ` 2394 jsonPatchBody := ` 2395 [ 2396 {"op": "add", "path": "/spec/unknown1", "value": "val1", "foo": "bar"}, 2397 {"op": "add", "path": "/spec/unknown2", "path": "/spec/unknown3", "value": "val2"}, 2398 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe"}, 2399 {"op": "add", "path": "/spec/unknownDupe", "value": "valDupe2"}, 2400 {"op": "add", "path": "/spec/knownField1", "value": "val1"}, 2401 {"op": "add", "path": "/spec/knownField1", "value": "val2"}, 2402 {"op": "add", "path": "/spec/ports/0/name", "value": "portName"}, 2403 {"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080}, 2404 {"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"}, 2405 {"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081}, 2406 {"op": "add", "path": "/spec/ports/0/hostPort", "value": 8082}, 2407 {"op": "add", "path": "/spec/ports/0/unknownNested", "value": "val"} 2408 ] 2409 ` 2410 var testcases = []struct { 2411 name string 2412 patchType types.PatchType 2413 opts metav1.PatchOptions 2414 body string 2415 strictDecodingError string 2416 strictDecodingWarnings []string 2417 }{ 2418 { 2419 name: "schemaless-crd-merge-patch-strict-validation", 2420 patchType: types.MergePatchType, 2421 opts: metav1.PatchOptions{ 2422 FieldValidation: "Strict", 2423 }, 2424 body: mergePatchBody, 2425 strictDecodingError: `strict decoding error: duplicate field "spec.unknownDupe", duplicate field "spec.knownField1", duplicate field "spec.ports[0].hostPort", unknown field "spec.ports[0].unknownNested"`, 2426 }, 2427 { 2428 name: "schemaless-crd-merge-patch-warn-validation", 2429 patchType: types.MergePatchType, 2430 opts: metav1.PatchOptions{ 2431 FieldValidation: "Warn", 2432 }, 2433 body: mergePatchBody, 2434 strictDecodingWarnings: []string{ 2435 `duplicate field "spec.unknownDupe"`, 2436 `duplicate field "spec.knownField1"`, 2437 `duplicate field "spec.ports[0].hostPort"`, 2438 `unknown field "spec.ports[0].unknownNested"`, 2439 }, 2440 }, 2441 { 2442 name: "schemaless-crd-merge-patch-ignore-validation", 2443 patchType: types.MergePatchType, 2444 opts: metav1.PatchOptions{ 2445 FieldValidation: "Ignore", 2446 }, 2447 body: mergePatchBody, 2448 }, 2449 { 2450 name: "schemaless-crd-merge-patch-no-validation", 2451 patchType: types.MergePatchType, 2452 body: mergePatchBody, 2453 strictDecodingWarnings: []string{ 2454 `duplicate field "spec.unknownDupe"`, 2455 `duplicate field "spec.knownField1"`, 2456 `duplicate field "spec.ports[0].hostPort"`, 2457 `unknown field "spec.ports[0].unknownNested"`, 2458 }, 2459 }, 2460 { 2461 name: "schemaless-crd-json-patch-strict-validation", 2462 patchType: types.JSONPatchType, 2463 opts: metav1.PatchOptions{ 2464 FieldValidation: "Strict", 2465 }, 2466 body: jsonPatchBody, 2467 // note: duplicate fields in the patch itself 2468 // are dropped by the 2469 // evanphx/json-patch library and is expected. 2470 // Duplicate fields in the json patch ops 2471 // themselves can be detected though 2472 strictDecodingError: `strict decoding error: json patch unknown field "[0].foo", json patch duplicate field "[1].path", unknown field "spec.ports[0].unknownNested"`, 2473 }, 2474 { 2475 name: "schemaless-crd-json-patch-warn-validation", 2476 patchType: types.JSONPatchType, 2477 opts: metav1.PatchOptions{ 2478 FieldValidation: "Warn", 2479 }, 2480 body: jsonPatchBody, 2481 strictDecodingWarnings: []string{ 2482 // note: duplicate fields in the patch itself 2483 // are dropped by the 2484 // evanphx/json-patch library and is expected. 2485 // Duplicate fields in the json patch ops 2486 // themselves can be detected though 2487 `json patch unknown field "[0].foo"`, 2488 `json patch duplicate field "[1].path"`, 2489 `unknown field "spec.ports[0].unknownNested"`, 2490 }, 2491 }, 2492 { 2493 name: "schemaless-crd-json-patch-ignore-validation", 2494 patchType: types.JSONPatchType, 2495 opts: metav1.PatchOptions{ 2496 FieldValidation: "Ignore", 2497 }, 2498 body: jsonPatchBody, 2499 }, 2500 { 2501 name: "schemaless-crd-json-patch-no-validation", 2502 patchType: types.JSONPatchType, 2503 body: jsonPatchBody, 2504 strictDecodingWarnings: []string{ 2505 // note: duplicate fields in the patch itself 2506 // are dropped by the 2507 // evanphx/json-patch library and is expected. 2508 // Duplicate fields in the json patch ops 2509 // themselves can be detected though 2510 `json patch unknown field "[0].foo"`, 2511 `json patch duplicate field "[1].path"`, 2512 `unknown field "spec.ports[0].unknownNested"`, 2513 }, 2514 }, 2515 } 2516 for _, tc := range testcases { 2517 t.Run(tc.name, func(t *testing.T) { 2518 kind := gvk.Kind 2519 apiVersion := gvk.Group + "/" + gvk.Version 2520 // create a CR 2521 yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, tc.name)) 2522 createResult, err := rest.Patch(types.ApplyPatchType). 2523 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2524 Name(tc.name). 2525 Param("fieldManager", "apply_test"). 2526 Body(yamlBody). 2527 DoRaw(context.TODO()) 2528 if err != nil { 2529 t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult)) 2530 } 2531 2532 // patch the CR as specified by the test case 2533 req := rest.Patch(tc.patchType). 2534 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2535 Name(tc.name). 2536 VersionedParams(&tc.opts, metav1.ParameterCodec) 2537 result := req.Body([]byte(tc.body)).Do(context.TODO()) 2538 if result.Error() == nil && tc.strictDecodingError != "" { 2539 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2540 } 2541 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2542 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2543 } 2544 2545 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2546 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2547 } 2548 2549 for i, strictWarn := range tc.strictDecodingWarnings { 2550 if strictWarn != result.Warnings()[i].Text { 2551 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2552 } 2553 2554 } 2555 }) 2556 } 2557 } 2558 2559 // testFieldValidationApplyCreateCRD tests apply patch requests containing duplicate fields 2560 // on newly created objects, for CRDs that have schemas 2561 // Note that even prior to server-side validation, unknown fields were treated as 2562 // errors in apply-patch and are not tested here. 2563 func testFieldValidationApplyCreateCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2564 var testcases = []struct { 2565 name string 2566 opts metav1.PatchOptions 2567 strictDecodingError string 2568 strictDecodingWarnings []string 2569 }{ 2570 { 2571 name: "strict-validation", 2572 opts: metav1.PatchOptions{ 2573 FieldValidation: "Strict", 2574 FieldManager: "mgr", 2575 }, 2576 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 2577 line 10: key "knownField1" already set in map 2578 line 16: key "hostPort" already set in map`, 2579 }, 2580 { 2581 name: "warn-validation", 2582 opts: metav1.PatchOptions{ 2583 FieldValidation: "Warn", 2584 FieldManager: "mgr", 2585 }, 2586 strictDecodingWarnings: []string{ 2587 `line 10: key "knownField1" already set in map`, 2588 `line 16: key "hostPort" already set in map`, 2589 }, 2590 }, 2591 { 2592 name: "ignore-validation", 2593 opts: metav1.PatchOptions{ 2594 FieldValidation: "Ignore", 2595 FieldManager: "mgr", 2596 }, 2597 }, 2598 { 2599 name: "no-validation", 2600 opts: metav1.PatchOptions{ 2601 FieldManager: "mgr", 2602 }, 2603 strictDecodingWarnings: []string{ 2604 `line 10: key "knownField1" already set in map`, 2605 `line 16: key "hostPort" already set in map`, 2606 }, 2607 }, 2608 } 2609 2610 for _, tc := range testcases { 2611 t.Run(tc.name, func(t *testing.T) { 2612 kind := gvk.Kind 2613 apiVersion := gvk.Group + "/" + gvk.Version 2614 2615 // create the CR as specified by the test case 2616 name := fmt.Sprintf("apply-create-crd-%s", tc.name) 2617 applyCreateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name)) 2618 2619 req := rest.Patch(types.ApplyPatchType). 2620 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2621 Name(name). 2622 VersionedParams(&tc.opts, metav1.ParameterCodec) 2623 result := req.Body(applyCreateBody).Do(context.TODO()) 2624 if result.Error() == nil && tc.strictDecodingError != "" { 2625 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2626 } 2627 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2628 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2629 } 2630 2631 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2632 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2633 } 2634 for i, strictWarn := range tc.strictDecodingWarnings { 2635 if strictWarn != result.Warnings()[i].Text { 2636 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2637 } 2638 2639 } 2640 }) 2641 } 2642 } 2643 2644 // testFieldValidationApplyCreateCRDSchemaless tests apply patch requests containing duplicate fields 2645 // on newly created objects, for CRDs that have schemas 2646 // with x-kubernetes-preserve-unknown-field set 2647 // Note that even prior to server-side validation, unknown fields were treated as 2648 // errors in apply-patch and are not tested here. 2649 func testFieldValidationApplyCreateCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2650 var testcases = []struct { 2651 name string 2652 opts metav1.PatchOptions 2653 strictDecodingError string 2654 strictDecodingWarnings []string 2655 }{ 2656 { 2657 name: "schemaless-strict-validation", 2658 opts: metav1.PatchOptions{ 2659 FieldValidation: "Strict", 2660 FieldManager: "mgr", 2661 }, 2662 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 2663 line 10: key "knownField1" already set in map 2664 line 16: key "hostPort" already set in map`, 2665 }, 2666 { 2667 name: "schemaless-warn-validation", 2668 opts: metav1.PatchOptions{ 2669 FieldValidation: "Warn", 2670 FieldManager: "mgr", 2671 }, 2672 strictDecodingWarnings: []string{ 2673 `line 10: key "knownField1" already set in map`, 2674 `line 16: key "hostPort" already set in map`, 2675 }, 2676 }, 2677 { 2678 name: "schemaless-ignore-validation", 2679 opts: metav1.PatchOptions{ 2680 FieldValidation: "Ignore", 2681 FieldManager: "mgr", 2682 }, 2683 }, 2684 { 2685 name: "schemaless-no-validation", 2686 opts: metav1.PatchOptions{ 2687 FieldManager: "mgr", 2688 }, 2689 strictDecodingWarnings: []string{ 2690 `line 10: key "knownField1" already set in map`, 2691 `line 16: key "hostPort" already set in map`, 2692 }, 2693 }, 2694 } 2695 2696 for _, tc := range testcases { 2697 t.Run(tc.name, func(t *testing.T) { 2698 kind := gvk.Kind 2699 apiVersion := gvk.Group + "/" + gvk.Version 2700 2701 // create the CR as specified by the test case 2702 name := fmt.Sprintf("apply-create-crd-schemaless-%s", tc.name) 2703 applyCreateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name)) 2704 2705 req := rest.Patch(types.ApplyPatchType). 2706 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2707 Name(name). 2708 VersionedParams(&tc.opts, metav1.ParameterCodec) 2709 result := req.Body(applyCreateBody).Do(context.TODO()) 2710 if result.Error() == nil && tc.strictDecodingError != "" { 2711 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2712 } 2713 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2714 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2715 } 2716 2717 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2718 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2719 } 2720 for i, strictWarn := range tc.strictDecodingWarnings { 2721 if strictWarn != result.Warnings()[i].Text { 2722 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2723 } 2724 2725 } 2726 }) 2727 } 2728 } 2729 2730 // testFieldValidationApplyUpdateCRD tests apply patch requests containing duplicate fields 2731 // on existing objects, for CRDs with schemas 2732 // Note that even prior to server-side validation, unknown fields were treated as 2733 // errors in apply-patch and are not tested here. 2734 func testFieldValidationApplyUpdateCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2735 var testcases = []struct { 2736 name string 2737 opts metav1.PatchOptions 2738 strictDecodingError string 2739 strictDecodingWarnings []string 2740 }{ 2741 { 2742 name: "strict-validation", 2743 opts: metav1.PatchOptions{ 2744 FieldValidation: "Strict", 2745 FieldManager: "mgr", 2746 }, 2747 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 2748 line 10: key "knownField1" already set in map 2749 line 16: key "hostPort" already set in map`, 2750 }, 2751 { 2752 name: "warn-validation", 2753 opts: metav1.PatchOptions{ 2754 FieldValidation: "Warn", 2755 FieldManager: "mgr", 2756 }, 2757 strictDecodingWarnings: []string{ 2758 `line 10: key "knownField1" already set in map`, 2759 `line 16: key "hostPort" already set in map`, 2760 }, 2761 }, 2762 { 2763 name: "ignore-validation", 2764 opts: metav1.PatchOptions{ 2765 FieldValidation: "Ignore", 2766 FieldManager: "mgr", 2767 }, 2768 }, 2769 { 2770 name: "no-validation", 2771 opts: metav1.PatchOptions{ 2772 FieldManager: "mgr", 2773 }, 2774 strictDecodingWarnings: []string{ 2775 `line 10: key "knownField1" already set in map`, 2776 `line 16: key "hostPort" already set in map`, 2777 }, 2778 }, 2779 } 2780 2781 for _, tc := range testcases { 2782 t.Run(tc.name, func(t *testing.T) { 2783 kind := gvk.Kind 2784 apiVersion := gvk.Group + "/" + gvk.Version 2785 2786 // create the CR as specified by the test case 2787 name := fmt.Sprintf("apply-update-crd-%s", tc.name) 2788 applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name)) 2789 createReq := rest.Patch(types.ApplyPatchType). 2790 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2791 Name(name). 2792 VersionedParams(&tc.opts, metav1.ParameterCodec) 2793 createResult := createReq.Body(applyCreateBody).Do(context.TODO()) 2794 if createResult.Error() != nil { 2795 t.Fatalf("unexpected apply create err: %v", createResult.Error()) 2796 } 2797 2798 applyUpdateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name)) 2799 updateReq := rest.Patch(types.ApplyPatchType). 2800 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2801 Name(name). 2802 VersionedParams(&tc.opts, metav1.ParameterCodec) 2803 result := updateReq.Body(applyUpdateBody).Do(context.TODO()) 2804 if result.Error() == nil && tc.strictDecodingError != "" { 2805 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2806 } 2807 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2808 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2809 } 2810 2811 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2812 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2813 } 2814 for i, strictWarn := range tc.strictDecodingWarnings { 2815 if strictWarn != result.Warnings()[i].Text { 2816 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2817 } 2818 2819 } 2820 }) 2821 } 2822 } 2823 2824 // testFieldValidationApplyUpdateCRDSchemaless tests apply patch requests containing duplicate fields 2825 // on existing objects, for CRDs with schemas 2826 // with x-kubernetes-preserve-unknown-field set 2827 // Note that even prior to server-side validation, unknown fields were treated as 2828 // errors in apply-patch and are not tested here. 2829 func testFieldValidationApplyUpdateCRDSchemaless(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2830 var testcases = []struct { 2831 name string 2832 opts metav1.PatchOptions 2833 strictDecodingError string 2834 strictDecodingWarnings []string 2835 }{ 2836 { 2837 name: "schemaless-strict-validation", 2838 opts: metav1.PatchOptions{ 2839 FieldValidation: "Strict", 2840 FieldManager: "mgr", 2841 }, 2842 strictDecodingError: `error strict decoding YAML: error converting YAML to JSON: yaml: unmarshal errors: 2843 line 10: key "knownField1" already set in map 2844 line 16: key "hostPort" already set in map`, 2845 }, 2846 { 2847 name: "schemaless-warn-validation", 2848 opts: metav1.PatchOptions{ 2849 FieldValidation: "Warn", 2850 FieldManager: "mgr", 2851 }, 2852 strictDecodingWarnings: []string{ 2853 `line 10: key "knownField1" already set in map`, 2854 `line 16: key "hostPort" already set in map`, 2855 }, 2856 }, 2857 { 2858 name: "schemaless-ignore-validation", 2859 opts: metav1.PatchOptions{ 2860 FieldValidation: "Ignore", 2861 FieldManager: "mgr", 2862 }, 2863 }, 2864 { 2865 name: "schemaless-no-validation", 2866 opts: metav1.PatchOptions{ 2867 FieldManager: "mgr", 2868 }, 2869 strictDecodingWarnings: []string{ 2870 `line 10: key "knownField1" already set in map`, 2871 `line 16: key "hostPort" already set in map`, 2872 }, 2873 }, 2874 } 2875 2876 for _, tc := range testcases { 2877 t.Run(tc.name, func(t *testing.T) { 2878 kind := gvk.Kind 2879 apiVersion := gvk.Group + "/" + gvk.Version 2880 2881 // create the CR as specified by the test case 2882 name := fmt.Sprintf("apply-update-crd-schemaless-%s", tc.name) 2883 applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name)) 2884 createReq := rest.Patch(types.ApplyPatchType). 2885 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2886 Name(name). 2887 VersionedParams(&tc.opts, metav1.ParameterCodec) 2888 createResult := createReq.Body(applyCreateBody).Do(context.TODO()) 2889 if createResult.Error() != nil { 2890 t.Fatalf("unexpected apply create err: %v", createResult.Error()) 2891 } 2892 2893 applyUpdateBody := []byte(fmt.Sprintf(crdApplyInvalidBody, apiVersion, kind, name)) 2894 updateReq := rest.Patch(types.ApplyPatchType). 2895 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2896 Name(name). 2897 VersionedParams(&tc.opts, metav1.ParameterCodec) 2898 result := updateReq.Body(applyUpdateBody).Do(context.TODO()) 2899 2900 if result.Error() == nil && tc.strictDecodingError != "" { 2901 t.Fatalf("received nil error when expecting: %q", tc.strictDecodingError) 2902 } 2903 if result.Error() != nil && (tc.strictDecodingError == "" || !strings.HasSuffix(result.Error().Error(), tc.strictDecodingError)) { 2904 t.Fatalf("expected error: %q, got: %v", tc.strictDecodingError, result.Error()) 2905 } 2906 2907 if len(result.Warnings()) != len(tc.strictDecodingWarnings) { 2908 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.strictDecodingWarnings), len(result.Warnings())) 2909 } 2910 for i, strictWarn := range tc.strictDecodingWarnings { 2911 if strictWarn != result.Warnings()[i].Text { 2912 t.Fatalf("expected warning: %s, got warning: %s", strictWarn, result.Warnings()[i].Text) 2913 } 2914 2915 } 2916 }) 2917 } 2918 } 2919 2920 func testFinalizerValidationApplyCreateAndUpdateCRD(t *testing.T, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 2921 var testcases = []struct { 2922 name string 2923 finalizer []string 2924 updatedFinalizer []string 2925 opts metav1.PatchOptions 2926 expectUpdateWarnings []string 2927 expectCreateWarnings []string 2928 }{ 2929 { 2930 name: "create-crd-with-invalid-finalizer", 2931 finalizer: []string{"invalid-finalizer"}, 2932 expectCreateWarnings: []string{ 2933 `metadata.finalizers: "invalid-finalizer": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, 2934 }, 2935 }, 2936 { 2937 name: "create-crd-with-valid-finalizer", 2938 finalizer: []string{"kubernetes.io/valid-finalizer"}, 2939 }, 2940 { 2941 name: "update-crd-with-invalid-finalizer", 2942 finalizer: []string{"invalid-finalizer"}, 2943 updatedFinalizer: []string{"another-invalid-finalizer"}, 2944 expectCreateWarnings: []string{ 2945 `metadata.finalizers: "invalid-finalizer": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, 2946 }, 2947 expectUpdateWarnings: []string{ 2948 `metadata.finalizers: "another-invalid-finalizer": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, 2949 }, 2950 }, 2951 { 2952 name: "update-crd-with-valid-finalizer", 2953 finalizer: []string{"kubernetes.io/valid-finalizer"}, 2954 updatedFinalizer: []string{"kubernetes.io/another-valid-finalizer"}, 2955 }, 2956 { 2957 name: "update-crd-with-valid-finalizer-leaving-an-existing-invalid-finalizer", 2958 finalizer: []string{"invalid-finalizer"}, 2959 updatedFinalizer: []string{"kubernetes.io/another-valid-finalizer"}, 2960 expectCreateWarnings: []string{ 2961 `metadata.finalizers: "invalid-finalizer": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, 2962 }, 2963 }, 2964 } 2965 2966 for _, tc := range testcases { 2967 t.Run(tc.name, func(t *testing.T) { 2968 kind := gvk.Kind 2969 apiVersion := gvk.Group + "/" + gvk.Version 2970 2971 // create the CR as specified by the test case 2972 name := fmt.Sprintf("apply-create-crd-%s", tc.name) 2973 finalizerVal, _ := json.Marshal(tc.finalizer) 2974 applyCreateBody := []byte(fmt.Sprintf(crdApplyFinalizerBody, apiVersion, kind, name, finalizerVal)) 2975 2976 req := rest.Patch(types.ApplyPatchType). 2977 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 2978 Name(name). 2979 Param("fieldManager", "apply_test"). 2980 VersionedParams(&tc.opts, metav1.ParameterCodec) 2981 result := req.Body(applyCreateBody).Do(context.TODO()) 2982 if result.Error() != nil { 2983 t.Fatalf("unexpected error: %v", result.Error()) 2984 } 2985 2986 if len(result.Warnings()) != len(tc.expectCreateWarnings) { 2987 for _, r := range result.Warnings() { 2988 t.Logf("received warning: %v", r) 2989 } 2990 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.expectCreateWarnings), len(result.Warnings())) 2991 } 2992 for i, expectedWarning := range tc.expectCreateWarnings { 2993 if expectedWarning != result.Warnings()[i].Text { 2994 t.Fatalf("expected warning: %s, got warning: %s", expectedWarning, result.Warnings()[i].Text) 2995 } 2996 } 2997 2998 if len(tc.updatedFinalizer) != 0 { 2999 finalizerVal, _ := json.Marshal(tc.updatedFinalizer) 3000 applyUpdateBody := []byte(fmt.Sprintf(crdApplyFinalizerBody, apiVersion, kind, name, finalizerVal)) 3001 updateReq := rest.Patch(types.ApplyPatchType). 3002 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3003 Name(name). 3004 Param("fieldManager", "apply_test"). 3005 VersionedParams(&tc.opts, metav1.ParameterCodec) 3006 result = updateReq.Body(applyUpdateBody).Do(context.TODO()) 3007 3008 if result.Error() != nil { 3009 t.Fatalf("unexpected error: %v", result.Error()) 3010 } 3011 3012 if len(result.Warnings()) != len(tc.expectUpdateWarnings) { 3013 t.Fatalf("unexpected number of warnings, expected: %d, got: %d", len(tc.expectUpdateWarnings), len(result.Warnings())) 3014 } 3015 for i, expectedWarning := range tc.expectUpdateWarnings { 3016 if expectedWarning != result.Warnings()[i].Text { 3017 t.Fatalf("expected warning: %s, got warning: %s", expectedWarning, result.Warnings()[i].Text) 3018 } 3019 } 3020 } 3021 }) 3022 } 3023 } 3024 3025 func setupCRD(t testing.TB, config *rest.Config, apiGroup string, schemaless bool) *apiextensionsv1.CustomResourceDefinition { 3026 apiExtensionClient, err := apiextensionsclient.NewForConfig(config) 3027 if err != nil { 3028 t.Fatal(err) 3029 } 3030 dynamicClient, err := dynamic.NewForConfig(config) 3031 if err != nil { 3032 t.Fatal(err) 3033 } 3034 3035 preserveUnknownFields := "" 3036 if schemaless { 3037 preserveUnknownFields = `"x-kubernetes-preserve-unknown-fields": true,` 3038 } 3039 crdSchema := fmt.Sprintf(crdSchemaBase, preserveUnknownFields) 3040 3041 // create the CRD 3042 crd := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) 3043 3044 // adjust the API group 3045 crd.Name = crd.Spec.Names.Plural + "." + apiGroup 3046 crd.Spec.Group = apiGroup 3047 3048 var c apiextensionsv1.CustomResourceValidation 3049 err = json.Unmarshal([]byte(crdSchema), &c) 3050 if err != nil { 3051 t.Fatal(err) 3052 } 3053 //crd.Spec.PreserveUnknownFields = false 3054 for i := range crd.Spec.Versions { 3055 crd.Spec.Versions[i].Schema = &c 3056 } 3057 // install the CRD 3058 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient) 3059 if err != nil { 3060 t.Fatal(err) 3061 } 3062 3063 return crd 3064 } 3065 3066 func BenchmarkFieldValidation(b *testing.B) { 3067 flag.Lookup("v").Value.Set("0") 3068 server, err := kubeapiservertesting.StartTestServer(b, kubeapiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) 3069 if err != nil { 3070 b.Fatal(err) 3071 } 3072 config := server.ClientConfig 3073 defer server.TearDownFn() 3074 3075 // don't log warnings, tests inspect them in the responses directly 3076 config.WarningHandler = rest.NoWarnings{} 3077 3078 client := clientset.NewForConfigOrDie(config) 3079 3080 schemaCRD := setupCRD(b, config, "schema.example.com", false) 3081 schemaGVR := schema.GroupVersionResource{ 3082 Group: schemaCRD.Spec.Group, 3083 Version: schemaCRD.Spec.Versions[0].Name, 3084 Resource: schemaCRD.Spec.Names.Plural, 3085 } 3086 schemaGVK := schema.GroupVersionKind{ 3087 Group: schemaCRD.Spec.Group, 3088 Version: schemaCRD.Spec.Versions[0].Name, 3089 Kind: schemaCRD.Spec.Names.Kind, 3090 } 3091 3092 schemalessCRD := setupCRD(b, config, "schemaless.example.com", true) 3093 schemalessGVR := schema.GroupVersionResource{ 3094 Group: schemalessCRD.Spec.Group, 3095 Version: schemalessCRD.Spec.Versions[0].Name, 3096 Resource: schemalessCRD.Spec.Names.Plural, 3097 } 3098 schemalessGVK := schema.GroupVersionKind{ 3099 Group: schemalessCRD.Spec.Group, 3100 Version: schemalessCRD.Spec.Versions[0].Name, 3101 Kind: schemalessCRD.Spec.Names.Kind, 3102 } 3103 3104 rest := client.Discovery().RESTClient() 3105 3106 b.Run("Post", func(b *testing.B) { benchFieldValidationPost(b, client) }) 3107 b.Run("Put", func(b *testing.B) { benchFieldValidationPut(b, client) }) 3108 b.Run("PatchTyped", func(b *testing.B) { benchFieldValidationPatchTyped(b, client) }) 3109 b.Run("SMP", func(b *testing.B) { benchFieldValidationSMP(b, client) }) 3110 b.Run("ApplyCreate", func(b *testing.B) { benchFieldValidationApplyCreate(b, client) }) 3111 b.Run("ApplyUpdate", func(b *testing.B) { benchFieldValidationApplyUpdate(b, client) }) 3112 3113 b.Run("PostCRD", func(b *testing.B) { benchFieldValidationPostCRD(b, rest, schemaGVK, schemaGVR) }) 3114 b.Run("PutCRD", func(b *testing.B) { benchFieldValidationPutCRD(b, rest, schemaGVK, schemaGVR) }) 3115 b.Run("PatchCRD", func(b *testing.B) { benchFieldValidationPatchCRD(b, rest, schemaGVK, schemaGVR) }) 3116 b.Run("ApplyCreateCRD", func(b *testing.B) { benchFieldValidationApplyCreateCRD(b, rest, schemaGVK, schemaGVR) }) 3117 b.Run("ApplyUpdateCRD", func(b *testing.B) { benchFieldValidationApplyUpdateCRD(b, rest, schemaGVK, schemaGVR) }) 3118 3119 b.Run("PostCRDSchemaless", func(b *testing.B) { benchFieldValidationPostCRD(b, rest, schemalessGVK, schemalessGVR) }) 3120 b.Run("PutCRDSchemaless", func(b *testing.B) { benchFieldValidationPutCRD(b, rest, schemalessGVK, schemalessGVR) }) 3121 b.Run("PatchCRDSchemaless", func(b *testing.B) { benchFieldValidationPatchCRD(b, rest, schemalessGVK, schemalessGVR) }) 3122 b.Run("ApplyCreateCRDSchemaless", func(b *testing.B) { benchFieldValidationApplyCreateCRD(b, rest, schemalessGVK, schemalessGVR) }) 3123 b.Run("ApplyUpdateCRDSchemaless", func(b *testing.B) { benchFieldValidationApplyUpdateCRD(b, rest, schemalessGVK, schemalessGVR) }) 3124 3125 } 3126 3127 func benchFieldValidationPost(b *testing.B, client clientset.Interface) { 3128 var benchmarks = []struct { 3129 name string 3130 bodyBase string 3131 opts metav1.CreateOptions 3132 contentType string 3133 }{ 3134 { 3135 name: "post-strict-validation", 3136 opts: metav1.CreateOptions{ 3137 FieldValidation: "Strict", 3138 }, 3139 bodyBase: validBodyJSON, 3140 }, 3141 { 3142 name: "post-warn-validation", 3143 opts: metav1.CreateOptions{ 3144 FieldValidation: "Warn", 3145 }, 3146 bodyBase: validBodyJSON, 3147 }, 3148 { 3149 name: "post-ignore-validation", 3150 opts: metav1.CreateOptions{ 3151 FieldValidation: "Ignore", 3152 }, 3153 bodyBase: validBodyJSON, 3154 }, 3155 { 3156 name: "post-strict-validation-yaml", 3157 opts: metav1.CreateOptions{ 3158 FieldValidation: "Strict", 3159 }, 3160 bodyBase: validBodyYAML, 3161 contentType: "application/yaml", 3162 }, 3163 { 3164 name: "post-warn-validation-yaml", 3165 opts: metav1.CreateOptions{ 3166 FieldValidation: "Warn", 3167 }, 3168 bodyBase: validBodyYAML, 3169 contentType: "application/yaml", 3170 }, 3171 { 3172 name: "post-ignore-validation-yaml", 3173 opts: metav1.CreateOptions{ 3174 FieldValidation: "Ignore", 3175 }, 3176 bodyBase: validBodyYAML, 3177 contentType: "application/yaml", 3178 }, 3179 } 3180 3181 for _, bm := range benchmarks { 3182 b.Run(bm.name, func(b *testing.B) { 3183 b.ResetTimer() 3184 b.ReportAllocs() 3185 for n := 0; n < b.N; n++ { 3186 body := []byte(fmt.Sprintf(bm.bodyBase, fmt.Sprintf("test-deployment-%s-%d-%d-%d", bm.name, n, b.N, time.Now().UnixNano()))) 3187 req := client.CoreV1().RESTClient().Post(). 3188 AbsPath("/apis/apps/v1"). 3189 Namespace("default"). 3190 Resource("deployments"). 3191 SetHeader("Content-Type", bm.contentType). 3192 VersionedParams(&bm.opts, metav1.ParameterCodec) 3193 result := req.Body(body).Do(context.TODO()) 3194 if result.Error() != nil { 3195 b.Fatalf("unexpected request err: %v", result.Error()) 3196 } 3197 } 3198 }) 3199 } 3200 } 3201 3202 func benchFieldValidationPut(b *testing.B, client clientset.Interface) { 3203 var testcases = []struct { 3204 name string 3205 opts metav1.UpdateOptions 3206 putBodyBase string 3207 contentType string 3208 }{ 3209 { 3210 name: "put-strict-validation", 3211 opts: metav1.UpdateOptions{ 3212 FieldValidation: "Strict", 3213 }, 3214 putBodyBase: validBodyJSON, 3215 }, 3216 { 3217 name: "put-warn-validation", 3218 opts: metav1.UpdateOptions{ 3219 FieldValidation: "Warn", 3220 }, 3221 putBodyBase: validBodyJSON, 3222 }, 3223 { 3224 name: "put-ignore-validation", 3225 opts: metav1.UpdateOptions{ 3226 FieldValidation: "Ignore", 3227 }, 3228 putBodyBase: validBodyJSON, 3229 }, 3230 { 3231 name: "put-strict-validation-yaml", 3232 opts: metav1.UpdateOptions{ 3233 FieldValidation: "Strict", 3234 }, 3235 putBodyBase: validBodyYAML, 3236 contentType: "application/yaml", 3237 }, 3238 { 3239 name: "put-warn-validation-yaml", 3240 opts: metav1.UpdateOptions{ 3241 FieldValidation: "Warn", 3242 }, 3243 putBodyBase: validBodyYAML, 3244 contentType: "application/yaml", 3245 }, 3246 { 3247 name: "put-ignore-validation-yaml", 3248 opts: metav1.UpdateOptions{ 3249 FieldValidation: "Ignore", 3250 }, 3251 putBodyBase: validBodyYAML, 3252 contentType: "application/yaml", 3253 }, 3254 } 3255 3256 for _, tc := range testcases { 3257 b.Run(tc.name, func(b *testing.B) { 3258 names := make([]string, b.N) 3259 for n := 0; n < b.N; n++ { 3260 deployName := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3261 names[n] = deployName 3262 postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) 3263 3264 if _, err := client.CoreV1().RESTClient().Post(). 3265 AbsPath("/apis/apps/v1"). 3266 Namespace("default"). 3267 Resource("deployments"). 3268 Body(postBody). 3269 DoRaw(context.TODO()); err != nil { 3270 b.Fatalf("failed to create initial deployment: %v", err) 3271 } 3272 3273 } 3274 b.ResetTimer() 3275 b.ReportAllocs() 3276 for n := 0; n < b.N; n++ { 3277 deployName := names[n] 3278 putBody := []byte(fmt.Sprintf(string(tc.putBodyBase), deployName)) 3279 req := client.CoreV1().RESTClient().Put(). 3280 AbsPath("/apis/apps/v1"). 3281 Namespace("default"). 3282 Resource("deployments"). 3283 SetHeader("Content-Type", tc.contentType). 3284 Name(deployName). 3285 VersionedParams(&tc.opts, metav1.ParameterCodec) 3286 result := req.Body([]byte(putBody)).Do(context.TODO()) 3287 if result.Error() != nil { 3288 b.Fatalf("unexpected request err: %v", result.Error()) 3289 } 3290 } 3291 }) 3292 } 3293 } 3294 3295 func benchFieldValidationPatchTyped(b *testing.B, client clientset.Interface) { 3296 mergePatchBodyValid := ` 3297 { 3298 "spec": { 3299 "paused": false, 3300 "template": { 3301 "spec": { 3302 "containers": [{ 3303 "name": "nginx", 3304 "image": "nginx:latest", 3305 "imagePullPolicy": "Always" 3306 }] 3307 } 3308 }, 3309 "replicas": 2 3310 } 3311 } 3312 ` 3313 3314 jsonPatchBodyValid := ` 3315 [ 3316 {"op": "add", "path": "/spec/paused", "value": true}, 3317 {"op": "add", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"}, 3318 {"op": "add", "path": "/spec/replicas", "value": 2} 3319 ] 3320 ` 3321 3322 var testcases = []struct { 3323 name string 3324 opts metav1.PatchOptions 3325 patchType types.PatchType 3326 body string 3327 }{ 3328 { 3329 name: "merge-patch-strict-validation", 3330 opts: metav1.PatchOptions{ 3331 FieldValidation: "Strict", 3332 }, 3333 patchType: types.MergePatchType, 3334 body: mergePatchBodyValid, 3335 }, 3336 { 3337 name: "merge-patch-warn-validation", 3338 opts: metav1.PatchOptions{ 3339 FieldValidation: "Warn", 3340 }, 3341 patchType: types.MergePatchType, 3342 body: mergePatchBodyValid, 3343 }, 3344 { 3345 name: "merge-patch-ignore-validation", 3346 opts: metav1.PatchOptions{ 3347 FieldValidation: "Ignore", 3348 }, 3349 patchType: types.MergePatchType, 3350 body: mergePatchBodyValid, 3351 }, 3352 { 3353 name: "json-patch-strict-validation", 3354 patchType: types.JSONPatchType, 3355 opts: metav1.PatchOptions{ 3356 FieldValidation: "Strict", 3357 }, 3358 body: jsonPatchBodyValid, 3359 }, 3360 { 3361 name: "json-patch-warn-validation", 3362 patchType: types.JSONPatchType, 3363 opts: metav1.PatchOptions{ 3364 FieldValidation: "Warn", 3365 }, 3366 body: jsonPatchBodyValid, 3367 }, 3368 { 3369 name: "json-patch-ignore-validation", 3370 patchType: types.JSONPatchType, 3371 opts: metav1.PatchOptions{ 3372 FieldValidation: "Ignore", 3373 }, 3374 body: jsonPatchBodyValid, 3375 }, 3376 } 3377 3378 for _, tc := range testcases { 3379 b.Run(tc.name, func(b *testing.B) { 3380 names := make([]string, b.N) 3381 for n := 0; n < b.N; n++ { 3382 deployName := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3383 names[n] = deployName 3384 postBody := []byte(fmt.Sprintf(string(validBodyJSON), deployName)) 3385 3386 if _, err := client.CoreV1().RESTClient().Post(). 3387 AbsPath("/apis/apps/v1"). 3388 Namespace("default"). 3389 Resource("deployments"). 3390 Body(postBody). 3391 DoRaw(context.TODO()); err != nil { 3392 b.Fatalf("failed to create initial deployment: %v", err) 3393 } 3394 } 3395 b.ResetTimer() 3396 b.ReportAllocs() 3397 for n := 0; n < b.N; n++ { 3398 deployName := names[n] 3399 req := client.CoreV1().RESTClient().Patch(tc.patchType). 3400 AbsPath("/apis/apps/v1"). 3401 Namespace("default"). 3402 Resource("deployments"). 3403 Name(deployName). 3404 VersionedParams(&tc.opts, metav1.ParameterCodec) 3405 result := req.Body([]byte(tc.body)).Do(context.TODO()) 3406 if result.Error() != nil { 3407 b.Fatalf("unexpected request err: %v", result.Error()) 3408 } 3409 } 3410 3411 }) 3412 } 3413 } 3414 3415 func benchFieldValidationSMP(b *testing.B, client clientset.Interface) { 3416 smpBodyValid := ` 3417 { 3418 "spec": { 3419 "replicas": 3, 3420 "paused": false, 3421 "selector": { 3422 "matchLabels": { 3423 "app": "nginx" 3424 } 3425 }, 3426 "template": { 3427 "metadata": { 3428 "labels": { 3429 "app": "nginx" 3430 } 3431 }, 3432 "spec": { 3433 "containers": [{ 3434 "name": "nginx", 3435 "imagePullPolicy": "Never" 3436 }] 3437 } 3438 } 3439 } 3440 } 3441 ` 3442 var testcases = []struct { 3443 name string 3444 opts metav1.PatchOptions 3445 body string 3446 }{ 3447 { 3448 name: "smp-strict-validation", 3449 opts: metav1.PatchOptions{ 3450 FieldValidation: "Strict", 3451 }, 3452 body: smpBodyValid, 3453 }, 3454 { 3455 name: "smp-warn-validation", 3456 opts: metav1.PatchOptions{ 3457 FieldValidation: "Warn", 3458 }, 3459 body: smpBodyValid, 3460 }, 3461 { 3462 name: "smp-ignore-validation", 3463 opts: metav1.PatchOptions{ 3464 FieldValidation: "Ignore", 3465 }, 3466 body: smpBodyValid, 3467 }, 3468 } 3469 3470 for _, tc := range testcases { 3471 b.Run(tc.name, func(b *testing.B) { 3472 names := make([]string, b.N) 3473 for n := 0; n < b.N; n++ { 3474 name := fmt.Sprintf("%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3475 names[n] = name 3476 body := []byte(fmt.Sprintf(validBodyJSON, name)) 3477 _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 3478 AbsPath("/apis/apps/v1"). 3479 Namespace("default"). 3480 Resource("deployments"). 3481 Name(name). 3482 Param("fieldManager", "apply_test"). 3483 Body(body). 3484 Do(context.TODO()). 3485 Get() 3486 if err != nil { 3487 b.Fatalf("Failed to create object using Apply patch: %v", err) 3488 } 3489 } 3490 b.ResetTimer() 3491 b.ReportAllocs() 3492 for n := 0; n < b.N; n++ { 3493 name := names[n] 3494 req := client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType). 3495 AbsPath("/apis/apps/v1"). 3496 Namespace("default"). 3497 Resource("deployments"). 3498 Name(name). 3499 VersionedParams(&tc.opts, metav1.ParameterCodec) 3500 result := req.Body([]byte(tc.body)).Do(context.TODO()) 3501 if result.Error() != nil { 3502 b.Fatalf("unexpected request err: %v", result.Error()) 3503 } 3504 } 3505 }) 3506 } 3507 3508 } 3509 3510 func benchFieldValidationApplyCreate(b *testing.B, client clientset.Interface) { 3511 var testcases = []struct { 3512 name string 3513 opts metav1.PatchOptions 3514 }{ 3515 { 3516 name: "strict-validation", 3517 opts: metav1.PatchOptions{ 3518 FieldValidation: "Strict", 3519 FieldManager: "mgr", 3520 }, 3521 }, 3522 { 3523 name: "warn-validation", 3524 opts: metav1.PatchOptions{ 3525 FieldValidation: "Warn", 3526 FieldManager: "mgr", 3527 }, 3528 }, 3529 { 3530 name: "ignore-validation", 3531 opts: metav1.PatchOptions{ 3532 FieldValidation: "Ignore", 3533 FieldManager: "mgr", 3534 }, 3535 }, 3536 } 3537 3538 for _, tc := range testcases { 3539 b.Run(tc.name, func(b *testing.B) { 3540 b.ResetTimer() 3541 b.ReportAllocs() 3542 for n := 0; n < b.N; n++ { 3543 name := fmt.Sprintf("apply-create-deployment-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3544 body := []byte(fmt.Sprintf(validBodyJSON, name)) 3545 req := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 3546 AbsPath("/apis/apps/v1"). 3547 Namespace("default"). 3548 Resource("deployments"). 3549 Name(name). 3550 VersionedParams(&tc.opts, metav1.ParameterCodec) 3551 result := req.Body(body).Do(context.TODO()) 3552 if result.Error() != nil { 3553 b.Fatalf("unexpected request err: %v", result.Error()) 3554 } 3555 } 3556 }) 3557 } 3558 } 3559 3560 func benchFieldValidationApplyUpdate(b *testing.B, client clientset.Interface) { 3561 var testcases = []struct { 3562 name string 3563 opts metav1.PatchOptions 3564 }{ 3565 { 3566 name: "strict-validation", 3567 opts: metav1.PatchOptions{ 3568 FieldValidation: "Strict", 3569 FieldManager: "mgr", 3570 }, 3571 }, 3572 { 3573 name: "warn-validation", 3574 opts: metav1.PatchOptions{ 3575 FieldValidation: "Warn", 3576 FieldManager: "mgr", 3577 }, 3578 }, 3579 { 3580 name: "ignore-validation", 3581 opts: metav1.PatchOptions{ 3582 FieldValidation: "Ignore", 3583 FieldManager: "mgr", 3584 }, 3585 }, 3586 } 3587 3588 for _, tc := range testcases { 3589 b.Run(tc.name, func(b *testing.B) { 3590 names := make([]string, b.N) 3591 for n := 0; n < b.N; n++ { 3592 name := fmt.Sprintf("apply-update-deployment-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3593 names[n] = name 3594 createBody := []byte(fmt.Sprintf(validBodyJSON, name)) 3595 createReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 3596 AbsPath("/apis/apps/v1"). 3597 Namespace("default"). 3598 Resource("deployments"). 3599 Name(name). 3600 VersionedParams(&tc.opts, metav1.ParameterCodec) 3601 createResult := createReq.Body(createBody).Do(context.TODO()) 3602 if createResult.Error() != nil { 3603 b.Fatalf("unexpected apply create err: %v", createResult.Error()) 3604 } 3605 } 3606 b.ResetTimer() 3607 b.ReportAllocs() 3608 for n := 0; n < b.N; n++ { 3609 name := names[n] 3610 updateBody := []byte(fmt.Sprintf(applyValidBody, name)) 3611 updateReq := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). 3612 AbsPath("/apis/apps/v1"). 3613 Namespace("default"). 3614 Resource("deployments"). 3615 Name(name). 3616 VersionedParams(&tc.opts, metav1.ParameterCodec) 3617 result := updateReq.Body(updateBody).Do(context.TODO()) 3618 if result.Error() != nil { 3619 b.Fatalf("unexpected request err: %v", result.Error()) 3620 } 3621 } 3622 }) 3623 } 3624 } 3625 3626 func benchFieldValidationPostCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 3627 var testcases = []struct { 3628 name string 3629 opts metav1.PatchOptions 3630 body string 3631 contentType string 3632 }{ 3633 { 3634 name: "crd-post-strict-validation", 3635 opts: metav1.PatchOptions{ 3636 FieldValidation: "Strict", 3637 }, 3638 body: crdValidBody, 3639 }, 3640 { 3641 name: "crd-post-warn-validation", 3642 opts: metav1.PatchOptions{ 3643 FieldValidation: "Warn", 3644 }, 3645 body: crdValidBody, 3646 }, 3647 { 3648 name: "crd-post-ignore-validation", 3649 opts: metav1.PatchOptions{ 3650 FieldValidation: "Ignore", 3651 }, 3652 body: crdValidBody, 3653 }, 3654 { 3655 name: "crd-post-no-validation", 3656 body: crdValidBody, 3657 }, 3658 { 3659 name: "crd-post-strict-validation-yaml", 3660 opts: metav1.PatchOptions{ 3661 FieldValidation: "Strict", 3662 }, 3663 body: crdValidBodyYAML, 3664 contentType: "application/yaml", 3665 }, 3666 { 3667 name: "crd-post-warn-validation-yaml", 3668 opts: metav1.PatchOptions{ 3669 FieldValidation: "Warn", 3670 }, 3671 body: crdValidBodyYAML, 3672 contentType: "application/yaml", 3673 }, 3674 { 3675 name: "crd-post-ignore-validation-yaml", 3676 opts: metav1.PatchOptions{ 3677 FieldValidation: "Ignore", 3678 }, 3679 body: crdValidBodyYAML, 3680 contentType: "application/yaml", 3681 }, 3682 { 3683 name: "crd-post-no-validation-yaml", 3684 body: crdValidBodyYAML, 3685 contentType: "application/yaml", 3686 }, 3687 } 3688 for _, tc := range testcases { 3689 b.Run(tc.name, func(b *testing.B) { 3690 b.ResetTimer() 3691 b.ReportAllocs() 3692 for n := 0; n < b.N; n++ { 3693 kind := gvk.Kind 3694 apiVersion := gvk.Group + "/" + gvk.Version 3695 3696 // create the CR as specified by the test case 3697 jsonBody := []byte(fmt.Sprintf(tc.body, apiVersion, kind, fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()))) 3698 req := rest.Post(). 3699 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3700 SetHeader("Content-Type", tc.contentType). 3701 VersionedParams(&tc.opts, metav1.ParameterCodec) 3702 result := req.Body([]byte(jsonBody)).Do(context.TODO()) 3703 3704 if result.Error() != nil { 3705 b.Fatalf("unexpected post err: %v", result.Error()) 3706 } 3707 } 3708 }) 3709 } 3710 } 3711 3712 func benchFieldValidationPutCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 3713 var testcases = []struct { 3714 name string 3715 opts metav1.PatchOptions 3716 putBody string 3717 contentType string 3718 }{ 3719 { 3720 name: "crd-put-strict-validation", 3721 opts: metav1.PatchOptions{ 3722 FieldValidation: "Strict", 3723 }, 3724 putBody: crdValidBody, 3725 }, 3726 { 3727 name: "crd-put-warn-validation", 3728 opts: metav1.PatchOptions{ 3729 FieldValidation: "Warn", 3730 }, 3731 putBody: crdValidBody, 3732 }, 3733 { 3734 name: "crd-put-ignore-validation", 3735 opts: metav1.PatchOptions{ 3736 FieldValidation: "Ignore", 3737 }, 3738 putBody: crdValidBody, 3739 }, 3740 { 3741 name: "crd-put-no-validation", 3742 putBody: crdValidBody, 3743 }, 3744 { 3745 name: "crd-put-strict-validation-yaml", 3746 opts: metav1.PatchOptions{ 3747 FieldValidation: "Strict", 3748 }, 3749 putBody: crdValidBodyYAML, 3750 contentType: "application/yaml", 3751 }, 3752 { 3753 name: "crd-put-warn-validation-yaml", 3754 opts: metav1.PatchOptions{ 3755 FieldValidation: "Warn", 3756 }, 3757 putBody: crdValidBodyYAML, 3758 contentType: "application/yaml", 3759 }, 3760 { 3761 name: "crd-put-ignore-validation-yaml", 3762 opts: metav1.PatchOptions{ 3763 FieldValidation: "Ignore", 3764 }, 3765 putBody: crdValidBodyYAML, 3766 contentType: "application/yaml", 3767 }, 3768 { 3769 name: "crd-put-no-validation-yaml", 3770 putBody: crdValidBodyYAML, 3771 contentType: "application/yaml", 3772 }, 3773 } 3774 for _, tc := range testcases { 3775 b.Run(tc.name, func(b *testing.B) { 3776 kind := gvk.Kind 3777 apiVersion := gvk.Group + "/" + gvk.Version 3778 names := make([]string, b.N) 3779 resourceVersions := make([]string, b.N) 3780 for n := 0; n < b.N; n++ { 3781 deployName := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3782 names[n] = deployName 3783 3784 // create the CR as specified by the test case 3785 jsonPostBody := []byte(fmt.Sprintf(crdValidBody, apiVersion, kind, deployName)) 3786 postReq := rest.Post(). 3787 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3788 VersionedParams(&tc.opts, metav1.ParameterCodec) 3789 postResult, err := postReq.Body([]byte(jsonPostBody)).Do(context.TODO()).Raw() 3790 if err != nil { 3791 b.Fatalf("unexpeted error on CR creation: %v", err) 3792 } 3793 postUnstructured := &unstructured.Unstructured{} 3794 if err := postUnstructured.UnmarshalJSON(postResult); err != nil { 3795 b.Fatalf("unexpeted error unmarshalling created CR: %v", err) 3796 } 3797 resourceVersions[n] = postUnstructured.GetResourceVersion() 3798 } 3799 b.ResetTimer() 3800 b.ReportAllocs() 3801 for n := 0; n < b.N; n++ { 3802 // update the CR as specified by the test case 3803 putBody := []byte(fmt.Sprintf(tc.putBody, apiVersion, kind, names[n], resourceVersions[n])) 3804 putReq := rest.Put(). 3805 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3806 Name(names[n]). 3807 SetHeader("Content-Type", tc.contentType). 3808 VersionedParams(&tc.opts, metav1.ParameterCodec) 3809 result := putReq.Body([]byte(putBody)).Do(context.TODO()) 3810 if result.Error() != nil { 3811 b.Fatalf("unexpected put err: %v", result.Error()) 3812 } 3813 } 3814 }) 3815 } 3816 } 3817 3818 func benchFieldValidationPatchCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 3819 patchYAMLBody := ` 3820 apiVersion: %s 3821 kind: %s 3822 metadata: 3823 name: %s 3824 finalizers: 3825 - test/finalizer 3826 spec: 3827 cronSpec: "* * * * */5" 3828 ports: 3829 - name: x 3830 containerPort: 80 3831 protocol: TCP` 3832 3833 mergePatchBody := ` 3834 { 3835 "spec": { 3836 "knownField1": "val1", 3837 "ports": [{ 3838 "name": "portName", 3839 "containerPort": 8080, 3840 "protocol": "TCP", 3841 "hostPort": 8081 3842 }] 3843 } 3844 } 3845 ` 3846 jsonPatchBody := ` 3847 [ 3848 {"op": "add", "path": "/spec/knownField1", "value": "val1"}, 3849 {"op": "add", "path": "/spec/ports/0/name", "value": "portName"}, 3850 {"op": "add", "path": "/spec/ports/0/containerPort", "value": 8080}, 3851 {"op": "add", "path": "/spec/ports/0/protocol", "value": "TCP"}, 3852 {"op": "add", "path": "/spec/ports/0/hostPort", "value": 8081} 3853 ] 3854 ` 3855 var testcases = []struct { 3856 name string 3857 patchType types.PatchType 3858 opts metav1.PatchOptions 3859 body string 3860 }{ 3861 { 3862 name: "crd-merge-patch-strict-validation", 3863 patchType: types.MergePatchType, 3864 opts: metav1.PatchOptions{ 3865 FieldValidation: "Strict", 3866 }, 3867 body: mergePatchBody, 3868 }, 3869 { 3870 name: "crd-merge-patch-warn-validation", 3871 patchType: types.MergePatchType, 3872 opts: metav1.PatchOptions{ 3873 FieldValidation: "Warn", 3874 }, 3875 body: mergePatchBody, 3876 }, 3877 { 3878 name: "crd-merge-patch-ignore-validation", 3879 patchType: types.MergePatchType, 3880 opts: metav1.PatchOptions{ 3881 FieldValidation: "Ignore", 3882 }, 3883 body: mergePatchBody, 3884 }, 3885 { 3886 name: "crd-merge-patch-no-validation", 3887 patchType: types.MergePatchType, 3888 body: mergePatchBody, 3889 }, 3890 { 3891 name: "crd-json-patch-strict-validation", 3892 patchType: types.JSONPatchType, 3893 opts: metav1.PatchOptions{ 3894 FieldValidation: "Strict", 3895 }, 3896 body: jsonPatchBody, 3897 }, 3898 { 3899 name: "crd-json-patch-warn-validation", 3900 patchType: types.JSONPatchType, 3901 opts: metav1.PatchOptions{ 3902 FieldValidation: "Warn", 3903 }, 3904 body: jsonPatchBody, 3905 }, 3906 { 3907 name: "crd-json-patch-ignore-validation", 3908 patchType: types.JSONPatchType, 3909 opts: metav1.PatchOptions{ 3910 FieldValidation: "Ignore", 3911 }, 3912 body: jsonPatchBody, 3913 }, 3914 { 3915 name: "crd-json-patch-no-validation", 3916 patchType: types.JSONPatchType, 3917 body: jsonPatchBody, 3918 }, 3919 } 3920 for _, tc := range testcases { 3921 b.Run(tc.name, func(b *testing.B) { 3922 kind := gvk.Kind 3923 apiVersion := gvk.Group + "/" + gvk.Version 3924 names := make([]string, b.N) 3925 for n := 0; n < b.N; n++ { 3926 deployName := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 3927 names[n] = deployName 3928 3929 // create a CR 3930 yamlBody := []byte(fmt.Sprintf(string(patchYAMLBody), apiVersion, kind, deployName)) 3931 createResult, err := rest.Patch(types.ApplyPatchType). 3932 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3933 Name(deployName). 3934 Param("fieldManager", "apply_test"). 3935 Body(yamlBody). 3936 DoRaw(context.TODO()) 3937 if err != nil { 3938 b.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(createResult)) 3939 } 3940 } 3941 b.ResetTimer() 3942 b.ReportAllocs() 3943 for n := 0; n < b.N; n++ { 3944 // patch the CR as specified by the test case 3945 req := rest.Patch(tc.patchType). 3946 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 3947 Name(names[n]). 3948 VersionedParams(&tc.opts, metav1.ParameterCodec) 3949 result := req.Body([]byte(tc.body)).Do(context.TODO()) 3950 if result.Error() != nil { 3951 b.Fatalf("unexpected patch err: %v", result.Error()) 3952 } 3953 } 3954 3955 }) 3956 } 3957 } 3958 3959 func benchFieldValidationApplyCreateCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 3960 var testcases = []struct { 3961 name string 3962 opts metav1.PatchOptions 3963 }{ 3964 { 3965 name: "strict-validation", 3966 opts: metav1.PatchOptions{ 3967 FieldValidation: "Strict", 3968 FieldManager: "mgr", 3969 }, 3970 }, 3971 { 3972 name: "warn-validation", 3973 opts: metav1.PatchOptions{ 3974 FieldValidation: "Warn", 3975 FieldManager: "mgr", 3976 }, 3977 }, 3978 { 3979 name: "ignore-validation", 3980 opts: metav1.PatchOptions{ 3981 FieldValidation: "Ignore", 3982 FieldManager: "mgr", 3983 }, 3984 }, 3985 { 3986 name: "no-validation", 3987 opts: metav1.PatchOptions{ 3988 FieldManager: "mgr", 3989 }, 3990 }, 3991 } 3992 3993 for _, tc := range testcases { 3994 b.Run(tc.name, func(b *testing.B) { 3995 b.ResetTimer() 3996 b.ReportAllocs() 3997 for n := 0; n < b.N; n++ { 3998 kind := gvk.Kind 3999 apiVersion := gvk.Group + "/" + gvk.Version 4000 name := fmt.Sprintf("test-dep-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 4001 4002 // create the CR as specified by the test case 4003 applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, name)) 4004 4005 req := rest.Patch(types.ApplyPatchType). 4006 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 4007 Name(name). 4008 VersionedParams(&tc.opts, metav1.ParameterCodec) 4009 result := req.Body(applyCreateBody).Do(context.TODO()) 4010 if result.Error() != nil { 4011 b.Fatalf("unexpected apply err: %v", result.Error()) 4012 } 4013 4014 } 4015 }) 4016 } 4017 } 4018 4019 func benchFieldValidationApplyUpdateCRD(b *testing.B, rest rest.Interface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) { 4020 var testcases = []struct { 4021 name string 4022 opts metav1.PatchOptions 4023 }{ 4024 { 4025 name: "strict-validation", 4026 opts: metav1.PatchOptions{ 4027 FieldValidation: "Strict", 4028 FieldManager: "mgr", 4029 }, 4030 }, 4031 { 4032 name: "warn-validation", 4033 opts: metav1.PatchOptions{ 4034 FieldValidation: "Warn", 4035 FieldManager: "mgr", 4036 }, 4037 }, 4038 { 4039 name: "ignore-validation", 4040 opts: metav1.PatchOptions{ 4041 FieldValidation: "Ignore", 4042 FieldManager: "mgr", 4043 }, 4044 }, 4045 { 4046 name: "no-validation", 4047 opts: metav1.PatchOptions{ 4048 FieldManager: "mgr", 4049 }, 4050 }, 4051 } 4052 4053 for _, tc := range testcases { 4054 b.Run(tc.name, func(b *testing.B) { 4055 kind := gvk.Kind 4056 apiVersion := gvk.Group + "/" + gvk.Version 4057 names := make([]string, b.N) 4058 4059 for n := 0; n < b.N; n++ { 4060 names[n] = fmt.Sprintf("apply-update-crd-%s-%d-%d-%d", tc.name, n, b.N, time.Now().UnixNano()) 4061 applyCreateBody := []byte(fmt.Sprintf(crdApplyValidBody, apiVersion, kind, names[n])) 4062 createReq := rest.Patch(types.ApplyPatchType). 4063 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 4064 Name(names[n]). 4065 VersionedParams(&tc.opts, metav1.ParameterCodec) 4066 createResult := createReq.Body(applyCreateBody).Do(context.TODO()) 4067 if createResult.Error() != nil { 4068 b.Fatalf("unexpected apply create err: %v", createResult.Error()) 4069 } 4070 } 4071 b.ResetTimer() 4072 b.ReportAllocs() 4073 for n := 0; n < b.N; n++ { 4074 applyUpdateBody := []byte(fmt.Sprintf(crdApplyValidBody2, apiVersion, kind, names[n])) 4075 updateReq := rest.Patch(types.ApplyPatchType). 4076 AbsPath("/apis", gvr.Group, gvr.Version, gvr.Resource). 4077 Name(names[n]). 4078 VersionedParams(&tc.opts, metav1.ParameterCodec) 4079 result := updateReq.Body(applyUpdateBody).Do(context.TODO()) 4080 4081 if result.Error() != nil { 4082 b.Fatalf("unexpected apply err: %v", result.Error()) 4083 } 4084 } 4085 4086 }) 4087 } 4088 }