k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/rest_test.go (about) 1 /* 2 Copyright 2014 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 handlers 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "reflect" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/google/go-cmp/cmp" 31 fuzz "github.com/google/gofuzz" 32 jsonpatch "gopkg.in/evanphx/json-patch.v4" 33 apiequality "k8s.io/apimachinery/pkg/api/equality" 34 apierrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" 38 "k8s.io/apimachinery/pkg/runtime" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 "k8s.io/apimachinery/pkg/runtime/serializer" 41 "k8s.io/apimachinery/pkg/types" 42 "k8s.io/apimachinery/pkg/util/json" 43 "k8s.io/apimachinery/pkg/util/managedfields" 44 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 45 "k8s.io/apimachinery/pkg/util/strategicpatch" 46 "k8s.io/apimachinery/pkg/util/yaml" 47 "k8s.io/apiserver/pkg/admission" 48 "k8s.io/apiserver/pkg/apis/example" 49 examplev1 "k8s.io/apiserver/pkg/apis/example/v1" 50 "k8s.io/apiserver/pkg/endpoints/handlers/metrics" 51 "k8s.io/apiserver/pkg/endpoints/request" 52 "k8s.io/apiserver/pkg/registry/rest" 53 clientgoscheme "k8s.io/client-go/kubernetes/scheme" 54 "k8s.io/component-base/metrics/legacyregistry" 55 "k8s.io/component-base/metrics/testutil" 56 ) 57 58 var ( 59 scheme = runtime.NewScheme() 60 codecs = serializer.NewCodecFactory(scheme) 61 ) 62 63 func init() { 64 metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion) 65 utilruntime.Must(example.AddToScheme(scheme)) 66 utilruntime.Must(examplev1.AddToScheme(scheme)) 67 } 68 69 type testPatchType struct { 70 metav1.TypeMeta `json:",inline"` 71 72 TestPatchSubType `json:",inline"` 73 } 74 75 // We explicitly make it public as private types doesn't 76 // work correctly with json inlined types. 77 type TestPatchSubType struct { 78 StringField string `json:"theField"` 79 } 80 81 func (obj *testPatchType) DeepCopyObject() runtime.Object { 82 if obj == nil { 83 return nil 84 } 85 clone := *obj 86 return &clone 87 } 88 89 func TestPatchAnonymousField(t *testing.T) { 90 testGV := schema.GroupVersion{Group: "", Version: "v"} 91 scheme.AddKnownTypes(testGV, &testPatchType{}) 92 defaulter := runtime.ObjectDefaulter(scheme) 93 94 original := &testPatchType{ 95 TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"}, 96 TestPatchSubType: TestPatchSubType{StringField: "my-value"}, 97 } 98 patch := `{"theField": "changed!"}` 99 expected := &testPatchType{ 100 TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"}, 101 TestPatchSubType: TestPatchSubType{StringField: "changed!"}, 102 } 103 104 actual := &testPatchType{} 105 err := strategicPatchObject(context.TODO(), defaulter, original, []byte(patch), actual, &testPatchType{}, "") 106 if err != nil { 107 t.Fatalf("unexpected error: %v", err) 108 } 109 if !apiequality.Semantic.DeepEqual(actual, expected) { 110 t.Errorf("expected %#v, got %#v", expected, actual) 111 } 112 } 113 114 func TestLimitedReadBody(t *testing.T) { 115 defer legacyregistry.Reset() 116 legacyregistry.Register(metrics.RequestBodySizes) 117 118 testcases := []struct { 119 desc string 120 requestBody io.Reader 121 limit int64 122 expectedMetrics string 123 expectedErr bool 124 }{ 125 { 126 desc: "aaaa with limit 1", 127 requestBody: strings.NewReader("aaaa"), 128 limit: 1, 129 expectedMetrics: "", 130 expectedErr: true, 131 }, 132 { 133 desc: "aaaa with limit 5", 134 requestBody: strings.NewReader("aaaa"), 135 limit: 5, 136 expectedMetrics: ` 137 # HELP apiserver_request_body_size_bytes [ALPHA] Apiserver request body size in bytes broken out by resource and verb. 138 # TYPE apiserver_request_body_size_bytes histogram 139 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="50000"} 1 140 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="150000"} 1 141 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="250000"} 1 142 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="350000"} 1 143 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="450000"} 1 144 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="550000"} 1 145 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="650000"} 1 146 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="750000"} 1 147 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="850000"} 1 148 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="950000"} 1 149 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.05e+06"} 1 150 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.15e+06"} 1 151 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.25e+06"} 1 152 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.35e+06"} 1 153 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.45e+06"} 1 154 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.55e+06"} 1 155 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.65e+06"} 1 156 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.75e+06"} 1 157 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.85e+06"} 1 158 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="1.95e+06"} 1 159 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.05e+06"} 1 160 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.15e+06"} 1 161 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.25e+06"} 1 162 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.35e+06"} 1 163 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.45e+06"} 1 164 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.55e+06"} 1 165 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.65e+06"} 1 166 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.75e+06"} 1 167 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.85e+06"} 1 168 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="2.95e+06"} 1 169 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="3.05e+06"} 1 170 apiserver_request_body_size_bytes_bucket{resource="resource.group",verb="create",le="+Inf"} 1 171 apiserver_request_body_size_bytes_sum{resource="resource.group",verb="create"} 4 172 apiserver_request_body_size_bytes_count{resource="resource.group",verb="create"} 1 173 `, 174 expectedErr: false, 175 }, 176 } 177 178 for _, tc := range testcases { 179 t.Run(tc.desc, func(t *testing.T) { 180 // reset metrics 181 defer metrics.RequestBodySizes.Reset() 182 defer legacyregistry.Reset() 183 184 req, err := http.NewRequest("POST", "/", tc.requestBody) 185 if err != nil { 186 t.Errorf("err not expected: got %v", err) 187 } 188 _, err = limitedReadBodyWithRecordMetric(context.Background(), req, tc.limit, "resource.group", metrics.Create) 189 if tc.expectedErr { 190 if err == nil { 191 t.Errorf("err expected: got nil") 192 } 193 return 194 } 195 if err = testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedMetrics), "apiserver_request_body_size_bytes"); err != nil { 196 t.Errorf("unexpected err: %v", err) 197 } 198 }) 199 } 200 } 201 202 func TestStrategicMergePatchInvalid(t *testing.T) { 203 testGV := schema.GroupVersion{Group: "", Version: "v"} 204 scheme.AddKnownTypes(testGV, &testPatchType{}) 205 defaulter := runtime.ObjectDefaulter(scheme) 206 207 original := &testPatchType{ 208 TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"}, 209 TestPatchSubType: TestPatchSubType{StringField: "my-value"}, 210 } 211 patch := `barbaz` 212 expectedError := "invalid character 'b' looking for beginning of value" 213 214 actual := &testPatchType{} 215 err := strategicPatchObject(context.TODO(), defaulter, original, []byte(patch), actual, &testPatchType{}, "") 216 if !apierrors.IsBadRequest(err) { 217 t.Errorf("expected HTTP status: BadRequest, got: %#v", apierrors.ReasonForError(err)) 218 } 219 if !strings.Contains(err.Error(), expectedError) { 220 t.Errorf("expected %#v, got %#v", expectedError, err.Error()) 221 } 222 } 223 224 func TestJSONPatch(t *testing.T) { 225 for _, test := range []struct { 226 name string 227 patch string 228 expectedError string 229 expectedErrorType metav1.StatusReason 230 }{ 231 { 232 name: "valid", 233 patch: `[{"op": "test", "value": "podA", "path": "/metadata/name"}]`, 234 }, 235 { 236 name: "invalid-syntax", 237 patch: `invalid json patch`, 238 expectedError: "invalid character 'i' looking for beginning of value", 239 expectedErrorType: metav1.StatusReasonBadRequest, 240 }, 241 { 242 name: "invalid-semantics", 243 patch: `[{"op": "test", "value": "podA", "path": "/invalid/path"}]`, 244 expectedError: "the server rejected our request due to an error in our request", 245 expectedErrorType: metav1.StatusReasonInvalid, 246 }, 247 { 248 name: "valid-negative-index-patch", 249 patch: `[{"op": "test", "value": "foo", "path": "/metadata/finalizers/-1"}]`, 250 }, 251 } { 252 p := &patcher{ 253 patchType: types.JSONPatchType, 254 patchBytes: []byte(test.patch), 255 } 256 jp := jsonPatcher{patcher: p} 257 codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion) 258 pod := &examplev1.Pod{} 259 pod.Name = "podA" 260 pod.ObjectMeta.Finalizers = []string{"foo"} 261 versionedJS, err := runtime.Encode(codec, pod) 262 if err != nil { 263 t.Errorf("%s: unexpected error: %v", test.name, err) 264 continue 265 } 266 _, _, err = jp.applyJSPatch(versionedJS) 267 if err != nil { 268 if len(test.expectedError) == 0 { 269 t.Errorf("%s: expect no error when applying json patch, but got %v", test.name, err) 270 continue 271 } 272 if !strings.Contains(err.Error(), test.expectedError) { 273 t.Errorf("%s: expected error %v, but got %v", test.name, test.expectedError, err) 274 } 275 if test.expectedErrorType != apierrors.ReasonForError(err) { 276 t.Errorf("%s: expected error type %v, but got %v", test.name, test.expectedErrorType, apierrors.ReasonForError(err)) 277 } 278 } else if len(test.expectedError) > 0 { 279 t.Errorf("%s: expected err %s", test.name, test.expectedError) 280 } 281 } 282 } 283 284 func TestPatchCustomResource(t *testing.T) { 285 testGV := schema.GroupVersion{Group: "mygroup.example.com", Version: "v1beta1"} 286 scheme.AddKnownTypes(testGV, &unstructured.Unstructured{}) 287 defaulter := runtime.ObjectDefaulter(scheme) 288 289 original := &unstructured.Unstructured{ 290 Object: map[string]interface{}{ 291 "apiVersion": "mygroup.example.com/v1beta1", 292 "kind": "Noxu", 293 "metadata": map[string]interface{}{ 294 "namespace": "Namespaced", 295 "name": "foo", 296 }, 297 "spec": map[string]interface{}{ 298 "num": "10", 299 }, 300 }, 301 } 302 patch := `{"spec":{"num":"20"}}` 303 expectedError := "strategic merge patch format is not supported" 304 305 actual := &unstructured.Unstructured{} 306 err := strategicPatchObject(context.TODO(), defaulter, original, []byte(patch), actual, &unstructured.Unstructured{}, "") 307 if !apierrors.IsBadRequest(err) { 308 t.Errorf("expected HTTP status: BadRequest, got: %#v", apierrors.ReasonForError(err)) 309 } 310 if err.Error() != expectedError { 311 t.Errorf("expected %#v, got %#v", expectedError, err.Error()) 312 } 313 } 314 315 type testPatcher struct { 316 t *testing.T 317 318 // startingPod is used for the first Update 319 startingPod *example.Pod 320 321 // updatePod is the pod that is used for conflict comparison and used for subsequent Update calls 322 updatePod *example.Pod 323 324 numUpdates int 325 } 326 327 func (p *testPatcher) New() runtime.Object { 328 return &example.Pod{} 329 } 330 331 func (p *testPatcher) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { 332 // Simulate GuaranteedUpdate behavior (retries internally on etcd changes if the incoming resource doesn't pin resourceVersion) 333 for { 334 currentPod := p.startingPod 335 if p.numUpdates > 0 { 336 currentPod = p.updatePod 337 } 338 p.numUpdates++ 339 340 // Remember the current resource version 341 currentResourceVersion := currentPod.ResourceVersion 342 343 obj, err := objInfo.UpdatedObject(ctx, currentPod) 344 if err != nil { 345 return nil, false, err 346 } 347 inPod := obj.(*example.Pod) 348 if inPod.ResourceVersion == "" || inPod.ResourceVersion == "0" { 349 inPod.ResourceVersion = p.updatePod.ResourceVersion 350 } 351 if inPod.ResourceVersion != p.updatePod.ResourceVersion { 352 // If the patch didn't have an opinion on the resource version, retry like GuaranteedUpdate does 353 if inPod.ResourceVersion == currentResourceVersion { 354 continue 355 } 356 // If the patch changed the resource version and it mismatches, conflict 357 return nil, false, apierrors.NewConflict(example.Resource("pods"), inPod.Name, fmt.Errorf("existing %v, new %v", p.updatePod.ResourceVersion, inPod.ResourceVersion)) 358 } 359 360 if currentPod == nil { 361 if err := createValidation(ctx, currentPod); err != nil { 362 return nil, false, err 363 } 364 } else { 365 if err := updateValidation(ctx, currentPod, inPod); err != nil { 366 return nil, false, err 367 } 368 } 369 370 return inPod, false, nil 371 } 372 } 373 374 func (p *testPatcher) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { 375 p.t.Fatal("Unexpected call to testPatcher.Get") 376 return nil, errors.New("Unexpected call to testPatcher.Get") 377 } 378 379 type testNamer struct { 380 namespace string 381 name string 382 } 383 384 func (p *testNamer) Namespace(req *http.Request) (namespace string, err error) { 385 return p.namespace, nil 386 } 387 388 // Name returns the name from the request, and an optional namespace value if this is a namespace 389 // scoped call. An error is returned if the name is not available. 390 func (p *testNamer) Name(req *http.Request) (namespace, name string, err error) { 391 return p.namespace, p.name, nil 392 } 393 394 // ObjectName returns the namespace and name from an object if they exist, or an error if the object 395 // does not support names. 396 func (p *testNamer) ObjectName(obj runtime.Object) (namespace, name string, err error) { 397 return p.namespace, p.name, nil 398 } 399 400 type patchTestCase struct { 401 name string 402 403 // admission chain to use, nil is fine 404 admissionMutation mutateObjectUpdateFunc 405 admissionValidation rest.ValidateObjectUpdateFunc 406 407 // startingPod is used as the starting point for the first Update 408 startingPod *example.Pod 409 // changedPod can be set as the "destination" pod for the patch, and the test will compute a patch from the startingPod to the changedPod, 410 // or patches can be set directly using strategicMergePatch, mergePatch, and jsonPatch 411 changedPod *example.Pod 412 strategicMergePatch string 413 mergePatch string 414 jsonPatch string 415 416 // updatePod is the pod that is used for conflict comparison and as the starting point for the second Update 417 updatePod *example.Pod 418 419 // expectedPod is the pod that you expect to get back after the patch is complete 420 expectedPod *example.Pod 421 expectedError string 422 // if set, indicates the number of times patching was expected to be attempted 423 expectedTries int 424 } 425 426 func (tc *patchTestCase) Run(t *testing.T) { 427 t.Logf("Starting test %s", tc.name) 428 429 namespace := tc.startingPod.Namespace 430 name := tc.startingPod.Name 431 432 codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion) 433 434 admissionMutation := tc.admissionMutation 435 if admissionMutation == nil { 436 admissionMutation = func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 437 return nil 438 } 439 } 440 admissionValidation := tc.admissionValidation 441 if admissionValidation == nil { 442 admissionValidation = func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 443 return nil 444 } 445 } 446 447 ctx := request.NewDefaultContext() 448 ctx = request.WithNamespace(ctx, namespace) 449 450 namer := &testNamer{namespace, name} 451 creater := runtime.ObjectCreater(scheme) 452 defaulter := runtime.ObjectDefaulter(scheme) 453 convertor := runtime.UnsafeObjectConvertor(scheme) 454 objectInterfaces := admission.NewObjectInterfacesFromScheme(scheme) 455 kind := examplev1.SchemeGroupVersion.WithKind("Pod") 456 resource := examplev1.SchemeGroupVersion.WithResource("pods") 457 schemaReferenceObj := &examplev1.Pod{} 458 hubVersion := example.SchemeGroupVersion 459 460 fieldmanager, err := managedfields.NewDefaultFieldManager( 461 managedfields.NewDeducedTypeConverter(), 462 convertor, defaulter, creater, kind, hubVersion, "", nil) 463 464 if err != nil { 465 t.Fatalf("failed to create field manager: %v", err) 466 } 467 for _, patchType := range []types.PatchType{types.JSONPatchType, types.MergePatchType, types.StrategicMergePatchType} { 468 // This needs to be reset on each iteration. 469 testPatcher := &testPatcher{ 470 t: t, 471 startingPod: tc.startingPod, 472 updatePod: tc.updatePod, 473 } 474 475 t.Logf("Working with patchType %v", patchType) 476 477 patch := []byte{} 478 switch patchType { 479 case types.StrategicMergePatchType: 480 patch = []byte(tc.strategicMergePatch) 481 if len(patch) == 0 { 482 originalObjJS, err := runtime.Encode(codec, tc.startingPod) 483 if err != nil { 484 t.Errorf("%s: unexpected error: %v", tc.name, err) 485 continue 486 } 487 changedJS, err := runtime.Encode(codec, tc.changedPod) 488 if err != nil { 489 t.Errorf("%s: unexpected error: %v", tc.name, err) 490 continue 491 } 492 patch, err = strategicpatch.CreateTwoWayMergePatch(originalObjJS, changedJS, schemaReferenceObj) 493 if err != nil { 494 t.Errorf("%s: unexpected error: %v", tc.name, err) 495 continue 496 } 497 } 498 499 case types.MergePatchType: 500 patch = []byte(tc.mergePatch) 501 if len(patch) == 0 { 502 originalObjJS, err := runtime.Encode(codec, tc.startingPod) 503 if err != nil { 504 t.Errorf("%s: unexpected error: %v", tc.name, err) 505 continue 506 } 507 changedJS, err := runtime.Encode(codec, tc.changedPod) 508 if err != nil { 509 t.Errorf("%s: unexpected error: %v", tc.name, err) 510 continue 511 } 512 patch, err = jsonpatch.CreateMergePatch(originalObjJS, changedJS) 513 if err != nil { 514 t.Errorf("%s: unexpected error: %v", tc.name, err) 515 continue 516 } 517 } 518 519 case types.JSONPatchType: 520 patch = []byte(tc.jsonPatch) 521 if len(patch) == 0 { 522 // TODO SUPPORT THIS! 523 continue 524 } 525 526 default: 527 t.Error("unsupported patch type") 528 } 529 530 p := patcher{ 531 namer: namer, 532 creater: creater, 533 defaulter: defaulter, 534 unsafeConvertor: convertor, 535 kind: kind, 536 resource: resource, 537 538 objectInterfaces: objectInterfaces, 539 540 hubGroupVersion: hubVersion, 541 542 createValidation: rest.ValidateAllObjectFunc, 543 updateValidation: admissionValidation, 544 admissionCheck: admissionMutation, 545 546 codec: codec, 547 548 restPatcher: testPatcher, 549 name: name, 550 patchType: patchType, 551 patchBytes: patch, 552 options: &metav1.PatchOptions{ 553 FieldManager: "test-manager", 554 }, 555 } 556 557 ctx, cancel := context.WithTimeout(ctx, time.Second) 558 resultObj, _, err := p.patchResource(ctx, &RequestScope{ 559 FieldManager: fieldmanager, 560 }) 561 cancel() 562 563 if len(tc.expectedError) != 0 { 564 if err == nil || err.Error() != tc.expectedError { 565 t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err) 566 continue 567 } 568 } else { 569 if err != nil { 570 t.Errorf("%s: unexpected error: %v", tc.name, err) 571 continue 572 } 573 } 574 575 if tc.expectedTries > 0 { 576 if tc.expectedTries != testPatcher.numUpdates { 577 t.Errorf("%s: expected %d tries, got %d", tc.name, tc.expectedTries, testPatcher.numUpdates) 578 } 579 } 580 581 if tc.expectedPod == nil { 582 if resultObj != nil { 583 t.Errorf("%s: unexpected result: %v", tc.name, resultObj) 584 } 585 continue 586 } 587 588 resultPod := resultObj.(*example.Pod) 589 590 // roundtrip to get defaulting 591 expectedJS, err := runtime.Encode(codec, tc.expectedPod) 592 if err != nil { 593 t.Errorf("%s: unexpected error: %v", tc.name, err) 594 continue 595 } 596 expectedObj, err := runtime.Decode(codec, expectedJS) 597 if err != nil { 598 t.Errorf("%s: unexpected error: %v", tc.name, err) 599 continue 600 } 601 reallyExpectedPod := expectedObj.(*example.Pod) 602 603 if !reflect.DeepEqual(*reallyExpectedPod, *resultPod) { 604 t.Errorf("%s mismatch: %v\n", tc.name, cmp.Diff(reallyExpectedPod, resultPod)) 605 continue 606 } 607 } 608 609 } 610 611 func TestNumberConversion(t *testing.T) { 612 defaulter := runtime.ObjectDefaulter(scheme) 613 614 terminationGracePeriodSeconds := int64(42) 615 activeDeadlineSeconds := int64(42) 616 currentVersionedObject := &examplev1.Pod{ 617 TypeMeta: metav1.TypeMeta{Kind: "Example", APIVersion: examplev1.SchemeGroupVersion.String()}, 618 ObjectMeta: metav1.ObjectMeta{Name: "test-example"}, 619 Spec: examplev1.PodSpec{ 620 TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, 621 ActiveDeadlineSeconds: &activeDeadlineSeconds, 622 }, 623 } 624 versionedObjToUpdate := &examplev1.Pod{} 625 schemaReferenceObj := &examplev1.Pod{} 626 627 patchJS := []byte(`{"spec":{"terminationGracePeriodSeconds":42,"activeDeadlineSeconds":120}}`) 628 629 err := strategicPatchObject(context.TODO(), defaulter, currentVersionedObject, patchJS, versionedObjToUpdate, schemaReferenceObj, "") 630 if err != nil { 631 t.Fatal(err) 632 } 633 if versionedObjToUpdate.Spec.TerminationGracePeriodSeconds == nil || *versionedObjToUpdate.Spec.TerminationGracePeriodSeconds != 42 || 634 versionedObjToUpdate.Spec.ActiveDeadlineSeconds == nil || *versionedObjToUpdate.Spec.ActiveDeadlineSeconds != 120 { 635 t.Fatal(errors.New("Ports failed to merge because of number conversion issue")) 636 } 637 } 638 639 func TestPatchResourceNumberConversion(t *testing.T) { 640 namespace := "bar" 641 name := "foo" 642 uid := types.UID("uid") 643 fifteen := int64(15) 644 thirty := int64(30) 645 646 tc := &patchTestCase{ 647 name: "TestPatchResourceNumberConversion", 648 649 startingPod: &example.Pod{}, 650 changedPod: &example.Pod{}, 651 updatePod: &example.Pod{}, 652 653 expectedPod: &example.Pod{}, 654 } 655 656 setTcPod(tc.startingPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &fifteen, "") 657 658 // Patch tries to change to 30. 659 setTcPod(tc.changedPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &thirty, "") 660 661 // Someone else already changed it to 30. 662 // This should be fine since it's not a "meaningful conflict". 663 // Previously this was detected as a meaningful conflict because int64(30) != float64(30). 664 setTcPod(tc.updatePod, name, namespace, uid, "2", examplev1.SchemeGroupVersion.String(), &thirty, "anywhere") 665 666 setTcPod(tc.expectedPod, name, namespace, uid, "2", "", &thirty, "anywhere") 667 668 tc.Run(t) 669 } 670 671 func TestPatchResourceWithVersionConflict(t *testing.T) { 672 namespace := "bar" 673 name := "foo" 674 uid := types.UID("uid") 675 fifteen := int64(15) 676 thirty := int64(30) 677 678 tc := &patchTestCase{ 679 name: "TestPatchResourceWithVersionConflict", 680 681 startingPod: &example.Pod{}, 682 changedPod: &example.Pod{}, 683 updatePod: &example.Pod{}, 684 685 expectedPod: &example.Pod{}, 686 } 687 688 setTcPod(tc.startingPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &fifteen, "") 689 690 setTcPod(tc.changedPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &thirty, "") 691 692 setTcPod(tc.updatePod, name, namespace, uid, "2", examplev1.SchemeGroupVersion.String(), &fifteen, "anywhere") 693 694 setTcPod(tc.expectedPod, name, namespace, uid, "2", "", &thirty, "anywhere") 695 696 tc.Run(t) 697 } 698 699 func TestPatchResourceWithStaleVersionConflict(t *testing.T) { 700 namespace := "bar" 701 name := "foo" 702 uid := types.UID("uid") 703 704 tc := &patchTestCase{ 705 name: "TestPatchResourceWithStaleVersionConflict", 706 707 startingPod: &example.Pod{}, 708 updatePod: &example.Pod{}, 709 710 expectedError: `Operation cannot be fulfilled on pods.example.apiserver.k8s.io "foo": existing 2, new 1`, 711 expectedTries: 1, 712 } 713 714 // starting pod is at rv=2 715 tc.startingPod.Name = name 716 tc.startingPod.Namespace = namespace 717 tc.startingPod.UID = uid 718 tc.startingPod.ResourceVersion = "2" 719 tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String() 720 // same pod is still in place when attempting to persist the update 721 tc.updatePod = tc.startingPod 722 723 // patches are submitted with a stale rv=1 724 tc.mergePatch = `{"metadata":{"resourceVersion":"1"},"spec":{"nodeName":"foo"}}` 725 tc.strategicMergePatch = `{"metadata":{"resourceVersion":"1"},"spec":{"nodeName":"foo"}}` 726 727 tc.Run(t) 728 } 729 730 func TestPatchResourceWithRacingVersionConflict(t *testing.T) { 731 namespace := "bar" 732 name := "foo" 733 uid := types.UID("uid") 734 735 tc := &patchTestCase{ 736 name: "TestPatchResourceWithRacingVersionConflict", 737 738 startingPod: &example.Pod{}, 739 updatePod: &example.Pod{}, 740 741 expectedError: `Operation cannot be fulfilled on pods.example.apiserver.k8s.io "foo": existing 3, new 2`, 742 expectedTries: 2, 743 } 744 745 // starting pod is at rv=2 746 tc.startingPod.Name = name 747 tc.startingPod.Namespace = namespace 748 tc.startingPod.UID = uid 749 tc.startingPod.ResourceVersion = "2" 750 tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String() 751 752 // pod with rv=3 is found when attempting to persist the update 753 tc.updatePod.Name = name 754 tc.updatePod.Namespace = namespace 755 tc.updatePod.UID = uid 756 tc.updatePod.ResourceVersion = "3" 757 tc.updatePod.APIVersion = examplev1.SchemeGroupVersion.String() 758 759 // patches are submitted with a rv=2 760 tc.mergePatch = `{"metadata":{"resourceVersion":"2"},"spec":{"nodeName":"foo"}}` 761 tc.strategicMergePatch = `{"metadata":{"resourceVersion":"2"},"spec":{"nodeName":"foo"}}` 762 763 tc.Run(t) 764 } 765 766 func TestPatchResourceWithConflict(t *testing.T) { 767 namespace := "bar" 768 name := "foo" 769 uid := types.UID("uid") 770 771 tc := &patchTestCase{ 772 name: "TestPatchResourceWithConflict", 773 774 startingPod: &example.Pod{}, 775 changedPod: &example.Pod{}, 776 updatePod: &example.Pod{}, 777 expectedPod: &example.Pod{}, 778 } 779 780 // See issue #63104 for discussion of how much sense this makes. 781 782 setTcPod(tc.startingPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), nil, "here") 783 784 setTcPod(tc.changedPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), nil, "there") 785 786 setTcPod(tc.updatePod, name, namespace, uid, "2", examplev1.SchemeGroupVersion.String(), nil, "anywhere") 787 788 tc.expectedPod.Name = name 789 tc.expectedPod.Namespace = namespace 790 tc.expectedPod.UID = uid 791 tc.expectedPod.ResourceVersion = "2" 792 tc.expectedPod.APIVersion = examplev1.SchemeGroupVersion.String() 793 tc.expectedPod.Spec.NodeName = "there" 794 795 tc.Run(t) 796 } 797 798 func TestPatchWithAdmissionRejection(t *testing.T) { 799 namespace := "bar" 800 name := "foo" 801 uid := types.UID("uid") 802 fifteen := int64(15) 803 thirty := int64(30) 804 805 type Test struct { 806 name string 807 admissionMutation mutateObjectUpdateFunc 808 admissionValidation rest.ValidateObjectUpdateFunc 809 expectedError string 810 } 811 for _, test := range []Test{ 812 { 813 name: "TestPatchWithMutatingAdmissionRejection", 814 admissionMutation: func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 815 return errors.New("mutating admission failure") 816 }, 817 admissionValidation: rest.ValidateAllObjectUpdateFunc, 818 expectedError: "mutating admission failure", 819 }, 820 { 821 name: "TestPatchWithValidatingAdmissionRejection", 822 admissionMutation: rest.ValidateAllObjectUpdateFunc, 823 admissionValidation: func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 824 return errors.New("validating admission failure") 825 }, 826 expectedError: "validating admission failure", 827 }, 828 { 829 name: "TestPatchWithBothAdmissionRejections", 830 admissionMutation: func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 831 return errors.New("mutating admission failure") 832 }, 833 admissionValidation: func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 834 return errors.New("validating admission failure") 835 }, 836 expectedError: "mutating admission failure", 837 }, 838 } { 839 tc := &patchTestCase{ 840 name: test.name, 841 842 admissionMutation: test.admissionMutation, 843 admissionValidation: test.admissionValidation, 844 845 startingPod: &example.Pod{}, 846 changedPod: &example.Pod{}, 847 updatePod: &example.Pod{}, 848 849 expectedError: test.expectedError, 850 } 851 852 setTcPod(tc.startingPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &fifteen, "") 853 854 setTcPod(tc.changedPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &thirty, "") 855 856 setTcPod(tc.updatePod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &fifteen, "") 857 858 tc.Run(t) 859 } 860 } 861 862 func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) { 863 namespace := "bar" 864 name := "foo" 865 uid := types.UID("uid") 866 fifteen := int64(15) 867 thirty := int64(30) 868 seen := false 869 870 tc := &patchTestCase{ 871 name: "TestPatchWithVersionConflictThenAdmissionFailure", 872 873 admissionMutation: func(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object) error { 874 if seen { 875 return errors.New("admission failure") 876 } 877 878 seen = true 879 return nil 880 }, 881 882 startingPod: &example.Pod{}, 883 changedPod: &example.Pod{}, 884 updatePod: &example.Pod{}, 885 886 expectedError: "admission failure", 887 } 888 889 setTcPod(tc.startingPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &fifteen, "") 890 891 setTcPod(tc.changedPod, name, namespace, uid, "1", examplev1.SchemeGroupVersion.String(), &thirty, "") 892 893 setTcPod(tc.updatePod, name, namespace, uid, "2", examplev1.SchemeGroupVersion.String(), &fifteen, "anywhere") 894 895 tc.Run(t) 896 } 897 898 func TestHasUID(t *testing.T) { 899 testcases := []struct { 900 obj runtime.Object 901 hasUID bool 902 }{ 903 {obj: nil, hasUID: false}, 904 {obj: &example.Pod{}, hasUID: false}, 905 {obj: nil, hasUID: false}, 906 {obj: runtime.Object(nil), hasUID: false}, 907 {obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("A")}}, hasUID: true}, 908 } 909 for i, tc := range testcases { 910 actual, err := hasUID(tc.obj) 911 if err != nil { 912 t.Errorf("%d: unexpected error %v", i, err) 913 continue 914 } 915 if tc.hasUID != actual { 916 t.Errorf("%d: expected %v, got %v", i, tc.hasUID, actual) 917 } 918 } 919 } 920 921 func setTcPod(tcPod *example.Pod, name string, namespace string, uid types.UID, resourceVersion string, apiVersion string, activeDeadlineSeconds *int64, nodeName string) { 922 tcPod.Name = name 923 tcPod.Namespace = namespace 924 tcPod.UID = uid 925 tcPod.ResourceVersion = resourceVersion 926 if len(apiVersion) != 0 { 927 tcPod.APIVersion = apiVersion 928 } 929 if activeDeadlineSeconds != nil { 930 tcPod.Spec.ActiveDeadlineSeconds = activeDeadlineSeconds 931 } 932 if len(nodeName) != 0 { 933 tcPod.Spec.NodeName = nodeName 934 } 935 } 936 937 func (f mutateObjectUpdateFunc) Handles(operation admission.Operation) bool { 938 return true 939 } 940 941 func (f mutateObjectUpdateFunc) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { 942 return f(ctx, a.GetObject(), a.GetOldObject()) 943 } 944 945 func TestTransformDecodeErrorEnsuresBadRequestError(t *testing.T) { 946 testCases := []struct { 947 name string 948 typer runtime.ObjectTyper 949 decodedGVK *schema.GroupVersionKind 950 decodeIntoObject runtime.Object 951 baseErr error 952 expectedErr error 953 }{ 954 { 955 name: "decoding normal objects fails and returns a bad-request error", 956 typer: clientgoscheme.Scheme, 957 decodedGVK: &schema.GroupVersionKind{ 958 Group: testapigroupv1.GroupName, 959 Version: "v1", 960 Kind: "Carp", 961 }, 962 decodeIntoObject: &testapigroupv1.Carp{}, // which client-go's scheme doesn't recognize 963 baseErr: fmt.Errorf("plain error"), 964 }, 965 { 966 name: "decoding objects with unknown GVK fails and returns a bad-request error", 967 typer: alwaysErrorTyper{}, 968 decodedGVK: nil, 969 decodeIntoObject: &testapigroupv1.Carp{}, // which client-go's scheme doesn't recognize 970 baseErr: nil, 971 }, 972 } 973 for _, testCase := range testCases { 974 err := transformDecodeError(testCase.typer, testCase.baseErr, testCase.decodeIntoObject, testCase.decodedGVK, []byte(``)) 975 if apiStatus, ok := err.(apierrors.APIStatus); !ok || apiStatus.Status().Code != http.StatusBadRequest { 976 t.Errorf("expected bad request error but got: %v", err) 977 } 978 } 979 } 980 981 var _ runtime.ObjectTyper = alwaysErrorTyper{} 982 983 type alwaysErrorTyper struct{} 984 985 func (alwaysErrorTyper) ObjectKinds(runtime.Object) ([]schema.GroupVersionKind, bool, error) { 986 return nil, false, fmt.Errorf("always error") 987 } 988 989 func (alwaysErrorTyper) Recognizes(gvk schema.GroupVersionKind) bool { 990 return false 991 } 992 993 func TestUpdateToCreateOptions(t *testing.T) { 994 f := fuzz.New() 995 for i := 0; i < 100; i++ { 996 t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) { 997 update := &metav1.UpdateOptions{} 998 f.Fuzz(update) 999 create := updateToCreateOptions(update) 1000 1001 b, err := json.Marshal(create) 1002 if err != nil { 1003 t.Fatalf("failed to marshal CreateOptions (%v): %v", err, create) 1004 } 1005 got := &metav1.UpdateOptions{} 1006 err = json.Unmarshal(b, &got) 1007 if err != nil { 1008 t.Fatalf("failed to unmarshal UpdateOptions: %v", err) 1009 } 1010 got.TypeMeta = metav1.TypeMeta{} 1011 update.TypeMeta = metav1.TypeMeta{} 1012 if !reflect.DeepEqual(*update, *got) { 1013 t.Fatalf(`updateToCreateOptions round-trip failed: 1014 got: %#+v 1015 want: %#+v`, got, update) 1016 } 1017 1018 }) 1019 } 1020 } 1021 1022 func TestPatchToUpdateOptions(t *testing.T) { 1023 tests := []struct { 1024 name string 1025 converterFn func(po *metav1.PatchOptions) interface{} 1026 }{ 1027 { 1028 name: "patchToUpdateOptions", 1029 converterFn: func(patch *metav1.PatchOptions) interface{} { 1030 return patchToUpdateOptions(patch) 1031 }, 1032 }, 1033 { 1034 name: "patchToCreateOptions", 1035 converterFn: func(patch *metav1.PatchOptions) interface{} { 1036 return patchToCreateOptions(patch) 1037 }, 1038 }, 1039 } 1040 1041 f := fuzz.New() 1042 for _, test := range tests { 1043 t.Run(test.name, func(t *testing.T) { 1044 for i := 0; i < 100; i++ { 1045 t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) { 1046 patch := &metav1.PatchOptions{} 1047 f.Fuzz(patch) 1048 converted := test.converterFn(patch) 1049 1050 b, err := json.Marshal(converted) 1051 if err != nil { 1052 t.Fatalf("failed to marshal converted object (%v): %v", err, converted) 1053 } 1054 got := &metav1.PatchOptions{} 1055 err = json.Unmarshal(b, &got) 1056 if err != nil { 1057 t.Fatalf("failed to unmarshal converted object: %v", err) 1058 } 1059 1060 // Clear TypeMeta because we expect it to be different between the original and converted type 1061 got.TypeMeta = metav1.TypeMeta{} 1062 patch.TypeMeta = metav1.TypeMeta{} 1063 1064 // clear fields that we know belong in PatchOptions only 1065 patch.Force = nil 1066 1067 if !reflect.DeepEqual(*patch, *got) { 1068 t.Fatalf(`round-trip failed: 1069 got: %#+v 1070 want: %#+v`, got, converted) 1071 } 1072 1073 }) 1074 } 1075 }) 1076 } 1077 } 1078 1079 func TestDedupOwnerReferences(t *testing.T) { 1080 falseA := false 1081 falseB := false 1082 testCases := []struct { 1083 name string 1084 ownerReferences []metav1.OwnerReference 1085 expected []metav1.OwnerReference 1086 }{ 1087 { 1088 name: "simple multiple duplicates", 1089 ownerReferences: []metav1.OwnerReference{ 1090 { 1091 APIVersion: "customresourceVersion", 1092 Kind: "customresourceKind", 1093 Name: "name", 1094 UID: "1", 1095 }, 1096 { 1097 APIVersion: "customresourceVersion", 1098 Kind: "customresourceKind", 1099 Name: "name", 1100 UID: "2", 1101 }, 1102 { 1103 APIVersion: "customresourceVersion", 1104 Kind: "customresourceKind", 1105 Name: "name", 1106 UID: "1", 1107 }, 1108 { 1109 APIVersion: "customresourceVersion", 1110 Kind: "customresourceKind", 1111 Name: "name", 1112 UID: "1", 1113 }, 1114 { 1115 APIVersion: "customresourceVersion", 1116 Kind: "customresourceKind", 1117 Name: "name", 1118 UID: "2", 1119 }, 1120 }, 1121 expected: []metav1.OwnerReference{ 1122 { 1123 APIVersion: "customresourceVersion", 1124 Kind: "customresourceKind", 1125 Name: "name", 1126 UID: "1", 1127 }, 1128 { 1129 APIVersion: "customresourceVersion", 1130 Kind: "customresourceKind", 1131 Name: "name", 1132 UID: "2", 1133 }, 1134 }, 1135 }, 1136 { 1137 name: "don't dedup same uid different name entries", 1138 ownerReferences: []metav1.OwnerReference{ 1139 { 1140 APIVersion: "customresourceVersion", 1141 Kind: "customresourceKind", 1142 Name: "name1", 1143 UID: "1", 1144 }, 1145 { 1146 APIVersion: "customresourceVersion", 1147 Kind: "customresourceKind", 1148 Name: "name2", 1149 UID: "1", 1150 }, 1151 }, 1152 expected: []metav1.OwnerReference{ 1153 { 1154 APIVersion: "customresourceVersion", 1155 Kind: "customresourceKind", 1156 Name: "name1", 1157 UID: "1", 1158 }, 1159 { 1160 APIVersion: "customresourceVersion", 1161 Kind: "customresourceKind", 1162 Name: "name2", 1163 UID: "1", 1164 }, 1165 }, 1166 }, 1167 { 1168 name: "don't dedup same uid different API version entries", 1169 ownerReferences: []metav1.OwnerReference{ 1170 { 1171 APIVersion: "customresourceVersion1", 1172 Kind: "customresourceKind", 1173 Name: "name", 1174 UID: "1", 1175 }, 1176 { 1177 APIVersion: "customresourceVersion2", 1178 Kind: "customresourceKind", 1179 Name: "name", 1180 UID: "1", 1181 }, 1182 }, 1183 expected: []metav1.OwnerReference{ 1184 { 1185 APIVersion: "customresourceVersion1", 1186 Kind: "customresourceKind", 1187 Name: "name", 1188 UID: "1", 1189 }, 1190 { 1191 APIVersion: "customresourceVersion2", 1192 Kind: "customresourceKind", 1193 Name: "name", 1194 UID: "1", 1195 }, 1196 }, 1197 }, 1198 { 1199 name: "dedup memory-equal entries", 1200 ownerReferences: []metav1.OwnerReference{ 1201 { 1202 APIVersion: "customresourceVersion", 1203 Kind: "customresourceKind", 1204 Name: "name", 1205 UID: "1", 1206 Controller: &falseA, 1207 BlockOwnerDeletion: &falseA, 1208 }, 1209 { 1210 APIVersion: "customresourceVersion", 1211 Kind: "customresourceKind", 1212 Name: "name", 1213 UID: "1", 1214 Controller: &falseA, 1215 BlockOwnerDeletion: &falseA, 1216 }, 1217 }, 1218 expected: []metav1.OwnerReference{ 1219 { 1220 APIVersion: "customresourceVersion", 1221 Kind: "customresourceKind", 1222 Name: "name", 1223 UID: "1", 1224 Controller: &falseA, 1225 BlockOwnerDeletion: &falseA, 1226 }, 1227 }, 1228 }, 1229 { 1230 name: "dedup semantic-equal entries", 1231 ownerReferences: []metav1.OwnerReference{ 1232 { 1233 APIVersion: "customresourceVersion", 1234 Kind: "customresourceKind", 1235 Name: "name", 1236 UID: "1", 1237 Controller: &falseA, 1238 BlockOwnerDeletion: &falseA, 1239 }, 1240 { 1241 APIVersion: "customresourceVersion", 1242 Kind: "customresourceKind", 1243 Name: "name", 1244 UID: "1", 1245 Controller: &falseB, 1246 BlockOwnerDeletion: &falseB, 1247 }, 1248 }, 1249 expected: []metav1.OwnerReference{ 1250 { 1251 APIVersion: "customresourceVersion", 1252 Kind: "customresourceKind", 1253 Name: "name", 1254 UID: "1", 1255 Controller: &falseA, 1256 BlockOwnerDeletion: &falseA, 1257 }, 1258 }, 1259 }, 1260 { 1261 name: "don't dedup semantic-different entries", 1262 ownerReferences: []metav1.OwnerReference{ 1263 { 1264 APIVersion: "customresourceVersion", 1265 Kind: "customresourceKind", 1266 Name: "name", 1267 UID: "1", 1268 Controller: &falseA, 1269 BlockOwnerDeletion: &falseA, 1270 }, 1271 { 1272 APIVersion: "customresourceVersion", 1273 Kind: "customresourceKind", 1274 Name: "name", 1275 UID: "1", 1276 }, 1277 }, 1278 expected: []metav1.OwnerReference{ 1279 { 1280 APIVersion: "customresourceVersion", 1281 Kind: "customresourceKind", 1282 Name: "name", 1283 UID: "1", 1284 Controller: &falseA, 1285 BlockOwnerDeletion: &falseA, 1286 }, 1287 { 1288 APIVersion: "customresourceVersion", 1289 Kind: "customresourceKind", 1290 Name: "name", 1291 UID: "1", 1292 }, 1293 }, 1294 }, 1295 } 1296 for _, tc := range testCases { 1297 t.Run(tc.name, func(t *testing.T) { 1298 deduped, _ := dedupOwnerReferences(tc.ownerReferences) 1299 if !apiequality.Semantic.DeepEqual(deduped, tc.expected) { 1300 t.Errorf("diff: %v", cmp.Diff(deduped, tc.expected)) 1301 } 1302 }) 1303 } 1304 } 1305 1306 func TestParseYAMLWarnings(t *testing.T) { 1307 yamlNoErrs := `--- 1308 apiVersion: foo 1309 kind: bar 1310 metadata: 1311 name: no-errors 1312 spec: 1313 field1: val1 1314 field2: val2 1315 nested: 1316 - name: nestedName 1317 nestedField1: val1` 1318 yamlOneErr := `--- 1319 apiVersion: foo 1320 kind: bar 1321 metadata: 1322 name: no-errors 1323 spec: 1324 field1: val1 1325 field2: val2 1326 field2: val3 1327 nested: 1328 - name: nestedName 1329 nestedField1: val1` 1330 yamlManyErrs := `--- 1331 apiVersion: foo 1332 kind: bar 1333 metadata: 1334 name: no-errors 1335 spec: 1336 field1: val1 1337 field2: val2 1338 field2: val3 1339 nested: 1340 - name: nestedName 1341 nestedField1: val1 1342 nestedField2: val2 1343 nestedField2: val3` 1344 testCases := []struct { 1345 name string 1346 yaml string 1347 expected []string 1348 }{ 1349 { 1350 name: "no errors", 1351 yaml: yamlNoErrs, 1352 }, 1353 { 1354 name: "one error", 1355 yaml: yamlOneErr, 1356 expected: []string{`line 9: key "field2" already set in map`}, 1357 }, 1358 { 1359 name: "many errors", 1360 yaml: yamlManyErrs, 1361 expected: []string{`line 9: key "field2" already set in map`, `line 14: key "nestedField2" already set in map`}, 1362 }, 1363 } 1364 for _, tc := range testCases { 1365 t.Run(tc.name, func(t *testing.T) { 1366 obj := &unstructured.Unstructured{Object: map[string]interface{}{}} 1367 if err := yaml.UnmarshalStrict([]byte(tc.yaml), &obj.Object); err != nil { 1368 parsedErrs := parseYAMLWarnings(err.Error()) 1369 if !reflect.DeepEqual(tc.expected, parsedErrs) { 1370 t.Fatalf("expected: %v\n, got: %v\n", tc.expected, parsedErrs) 1371 } 1372 } 1373 }) 1374 } 1375 }