k8s.io/client-go@v0.31.1/testing/fixture_test.go (about) 1 /* 2 Copyright 2015 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 testing 18 19 import ( 20 "fmt" 21 "math/rand" 22 "sigs.k8s.io/structured-merge-diff/v4/typed" 23 "strconv" 24 "sync" 25 "testing" 26 27 "github.com/stretchr/testify/assert" 28 29 v1 "k8s.io/api/core/v1" 30 "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 runtime "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 serializer "k8s.io/apimachinery/pkg/runtime/serializer" 37 "k8s.io/apimachinery/pkg/types" 38 "k8s.io/apimachinery/pkg/util/managedfields" 39 "k8s.io/apimachinery/pkg/watch" 40 "k8s.io/utils/ptr" 41 ) 42 43 func getArbitraryResource(s schema.GroupVersionResource, name, namespace string) *unstructured.Unstructured { 44 return &unstructured.Unstructured{ 45 Object: map[string]interface{}{ 46 "kind": s.Resource, 47 "apiVersion": s.Version, 48 "metadata": map[string]interface{}{ 49 "name": name, 50 "namespace": namespace, 51 "generateName": "test_generateName", 52 "uid": "test_uid", 53 "resourceVersion": "test_resourceVersion", 54 }, 55 "data": strconv.Itoa(rand.Int()), 56 }, 57 } 58 } 59 60 func TestWatchCallNonNamespace(t *testing.T) { 61 testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"} 62 testObj := getArbitraryResource(testResource, "test_name", "test_namespace") 63 accessor, err := meta.Accessor(testObj) 64 if err != nil { 65 t.Fatalf("unexpected error: %v", err) 66 } 67 ns := accessor.GetNamespace() 68 scheme := runtime.NewScheme() 69 codecs := serializer.NewCodecFactory(scheme) 70 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 71 watch, err := o.Watch(testResource, ns) 72 if err != nil { 73 t.Fatalf("test resource watch failed in %s: %v ", ns, err) 74 } 75 go func() { 76 err := o.Create(testResource, testObj, ns) 77 if err != nil { 78 t.Errorf("test resource creation failed: %v", err) 79 } 80 }() 81 out := <-watch.ResultChan() 82 assert.Equal(t, testObj, out.Object, "watched object mismatch") 83 } 84 85 func TestWatchCallAllNamespace(t *testing.T) { 86 testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"} 87 testObj := getArbitraryResource(testResource, "test_name", "test_namespace") 88 accessor, err := meta.Accessor(testObj) 89 if err != nil { 90 t.Fatalf("unexpected error: %v", err) 91 } 92 ns := accessor.GetNamespace() 93 scheme := runtime.NewScheme() 94 codecs := serializer.NewCodecFactory(scheme) 95 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 96 w, err := o.Watch(testResource, "test_namespace") 97 if err != nil { 98 t.Fatalf("test resource watch failed in test_namespace: %v", err) 99 } 100 wAll, err := o.Watch(testResource, "") 101 if err != nil { 102 t.Fatalf("test resource watch failed in all namespaces: %v", err) 103 } 104 go func() { 105 err := o.Create(testResource, testObj, ns) 106 assert.NoError(t, err, "test resource creation failed") 107 }() 108 out := <-w.ResultChan() 109 outAll := <-wAll.ResultChan() 110 assert.Equal(t, watch.Added, out.Type, "watch event mismatch") 111 assert.Equal(t, watch.Added, outAll.Type, "watch event mismatch") 112 assert.Equal(t, testObj, out.Object, "watched created object mismatch") 113 assert.Equal(t, testObj, outAll.Object, "watched created object mismatch") 114 go func() { 115 err := o.Update(testResource, testObj, ns) 116 assert.NoError(t, err, "test resource updating failed") 117 }() 118 out = <-w.ResultChan() 119 outAll = <-wAll.ResultChan() 120 assert.Equal(t, watch.Modified, out.Type, "watch event mismatch") 121 assert.Equal(t, watch.Modified, outAll.Type, "watch event mismatch") 122 assert.Equal(t, testObj, out.Object, "watched updated object mismatch") 123 assert.Equal(t, testObj, outAll.Object, "watched updated object mismatch") 124 go func() { 125 err := o.Delete(testResource, "test_namespace", "test_name") 126 assert.NoError(t, err, "test resource deletion failed") 127 }() 128 out = <-w.ResultChan() 129 outAll = <-wAll.ResultChan() 130 assert.Equal(t, watch.Deleted, out.Type, "watch event mismatch") 131 assert.Equal(t, watch.Deleted, outAll.Type, "watch event mismatch") 132 assert.Equal(t, testObj, out.Object, "watched deleted object mismatch") 133 assert.Equal(t, testObj, outAll.Object, "watched deleted object mismatch") 134 } 135 136 func TestWatchCallMultipleInvocation(t *testing.T) { 137 cases := []struct { 138 name string 139 op watch.EventType 140 ns string 141 }{ 142 { 143 "foo", 144 watch.Added, 145 "test_namespace", 146 }, 147 { 148 "bar", 149 watch.Added, 150 "test_namespace", 151 }, 152 { 153 "baz", 154 watch.Added, 155 "", 156 }, 157 { 158 "bar", 159 watch.Modified, 160 "test_namespace", 161 }, 162 { 163 "baz", 164 watch.Modified, 165 "", 166 }, 167 { 168 "foo", 169 watch.Deleted, 170 "test_namespace", 171 }, 172 { 173 "bar", 174 watch.Deleted, 175 "test_namespace", 176 }, 177 { 178 "baz", 179 watch.Deleted, 180 "", 181 }, 182 } 183 184 scheme := runtime.NewScheme() 185 codecs := serializer.NewCodecFactory(scheme) 186 testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"} 187 188 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 189 watchNamespaces := []string{ 190 "", 191 "", 192 "test_namespace", 193 "test_namespace", 194 } 195 var wg sync.WaitGroup 196 wg.Add(len(watchNamespaces)) 197 for idx, watchNamespace := range watchNamespaces { 198 i := idx 199 watchNamespace := watchNamespace 200 w, err := o.Watch(testResource, watchNamespace) 201 if err != nil { 202 t.Fatalf("test resource watch failed in %s: %v", watchNamespace, err) 203 } 204 go func() { 205 assert.NoError(t, err, "watch invocation failed") 206 for _, c := range cases { 207 if watchNamespace == "" || c.ns == watchNamespace { 208 fmt.Printf("%#v %#v\n", c, i) 209 event := <-w.ResultChan() 210 accessor, err := meta.Accessor(event.Object) 211 if err != nil { 212 t.Errorf("unexpected error: %v", err) 213 break 214 } 215 assert.Equal(t, c.op, event.Type, "watch event mismatched") 216 assert.Equal(t, c.name, accessor.GetName(), "watched object mismatch") 217 assert.Equal(t, c.ns, accessor.GetNamespace(), "watched object mismatch") 218 } 219 } 220 wg.Done() 221 }() 222 } 223 for _, c := range cases { 224 switch c.op { 225 case watch.Added: 226 obj := getArbitraryResource(testResource, c.name, c.ns) 227 o.Create(testResource, obj, c.ns) 228 case watch.Modified: 229 obj := getArbitraryResource(testResource, c.name, c.ns) 230 o.Update(testResource, obj, c.ns) 231 case watch.Deleted: 232 o.Delete(testResource, c.ns, c.name) 233 } 234 } 235 wg.Wait() 236 } 237 238 func TestWatchAddAfterStop(t *testing.T) { 239 testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"} 240 testObj := getArbitraryResource(testResource, "test_name", "test_namespace") 241 accessor, err := meta.Accessor(testObj) 242 if err != nil { 243 t.Fatalf("unexpected error: %v", err) 244 } 245 246 ns := accessor.GetNamespace() 247 scheme := runtime.NewScheme() 248 codecs := serializer.NewCodecFactory(scheme) 249 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 250 watch, err := o.Watch(testResource, ns) 251 if err != nil { 252 t.Errorf("watch creation failed: %v", err) 253 } 254 255 // When the watch is stopped it should ignore later events without panicking. 256 defer func() { 257 if r := recover(); r != nil { 258 t.Errorf("Watch panicked when it should have ignored create after stop: %v", r) 259 } 260 }() 261 262 watch.Stop() 263 err = o.Create(testResource, testObj, ns) 264 if err != nil { 265 t.Errorf("test resource creation failed: %v", err) 266 } 267 } 268 269 func TestPatchWithMissingObject(t *testing.T) { 270 nodesResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes"} 271 272 scheme := runtime.NewScheme() 273 codecs := serializer.NewCodecFactory(scheme) 274 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 275 reaction := ObjectReaction(o) 276 action := NewRootPatchSubresourceAction(nodesResource, "node-1", types.StrategicMergePatchType, []byte(`{}`)) 277 handled, node, err := reaction(action) 278 assert.True(t, handled) 279 assert.Nil(t, node) 280 assert.EqualError(t, err, `nodes "node-1" not found`) 281 } 282 283 func TestApplyCreate(t *testing.T) { 284 cmResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configMaps"} 285 scheme := runtime.NewScheme() 286 scheme.AddKnownTypes(cmResource.GroupVersion(), &v1.ConfigMap{}) 287 codecs := serializer.NewCodecFactory(scheme) 288 o := NewFieldManagedObjectTracker(scheme, codecs.UniversalDecoder(), configMapTypeConverter(scheme)) 289 290 reaction := ObjectReaction(o) 291 patch := []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k": "v"}}`) 292 action := NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 293 metav1.PatchOptions{FieldManager: "test-manager"}) 294 handled, configMap, err := reaction(action) 295 assert.True(t, handled) 296 if err != nil { 297 t.Errorf("Failed to create a resource with apply: %v", err) 298 } 299 cm := configMap.(*v1.ConfigMap) 300 assert.Equal(t, cm.Data, map[string]string{"k": "v"}) 301 } 302 303 func TestApplyUpdateMultipleFieldManagers(t *testing.T) { 304 cmResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configMaps"} 305 scheme := runtime.NewScheme() 306 scheme.AddKnownTypes(cmResource.GroupVersion(), &v1.ConfigMap{}) 307 codecs := serializer.NewCodecFactory(scheme) 308 o := NewFieldManagedObjectTracker(scheme, codecs.UniversalDecoder(), configMapTypeConverter(scheme)) 309 310 reaction := ObjectReaction(o) 311 action := NewCreateAction(cmResource, "default", &v1.ConfigMap{ 312 TypeMeta: metav1.TypeMeta{ 313 APIVersion: "v1", 314 Kind: "ConfigMap", 315 }, 316 ObjectMeta: metav1.ObjectMeta{ 317 Name: "cm-1", 318 }, 319 Data: map[string]string{ 320 "k0": "v0", 321 }, 322 }) 323 handled, _, err := reaction(action) 324 assert.True(t, handled) 325 if err != nil { 326 t.Errorf("Failed to create resource: %v", err) 327 } 328 329 // Apply with test-manager-1 330 // Expect data to be shared with initial create 331 patch := []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k1": "v1"}}`) 332 applyAction := NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 333 metav1.PatchOptions{FieldManager: "test-manager-1"}) 334 handled, configMap, err := reaction(applyAction) 335 assert.True(t, handled) 336 if err != nil { 337 t.Errorf("Failed to apply resource: %v", err) 338 } 339 cm := configMap.(*v1.ConfigMap) 340 assert.Equal(t, map[string]string{"k0": "v0", "k1": "v1"}, cm.Data) 341 342 // Apply conflicting with test-manager-2, expect apply to fail 343 patch = []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k1": "xyz"}}`) 344 applyAction = NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 345 metav1.PatchOptions{FieldManager: "test-manager-2"}) 346 handled, _, err = reaction(applyAction) 347 assert.True(t, handled) 348 if assert.Error(t, err) { 349 assert.Equal(t, "Apply failed with 1 conflict: conflict with \"test-manager-1\": .data.k1", err.Error()) 350 } 351 352 // Apply with test-manager-2 353 // Expect data to be shared with initial create and test-manager-1 354 patch = []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k2": "v2"}}`) 355 applyAction = NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 356 metav1.PatchOptions{FieldManager: "test-manager-2"}) 357 handled, configMap, err = reaction(applyAction) 358 assert.True(t, handled) 359 if err != nil { 360 t.Errorf("Failed to apply resource: %v", err) 361 } 362 cm = configMap.(*v1.ConfigMap) 363 assert.Equal(t, map[string]string{"k0": "v0", "k1": "v1", "k2": "v2"}, cm.Data) 364 365 // Apply with test-manager-1 366 // Expect owned data to be updated 367 patch = []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k1": "v101"}}`) 368 applyAction = NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 369 metav1.PatchOptions{FieldManager: "test-manager-1"}) 370 handled, configMap, err = reaction(applyAction) 371 assert.True(t, handled) 372 if err != nil { 373 t.Errorf("Failed to apply resource: %v", err) 374 } 375 cm = configMap.(*v1.ConfigMap) 376 assert.Equal(t, map[string]string{"k0": "v0", "k1": "v101", "k2": "v2"}, cm.Data) 377 378 // Force apply with test-manager-2 379 // Expect data owned by test-manager-1 to be updated, expect data already owned but not in apply configuration to be removed 380 patch = []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "cm-1"}, "data": {"k1": "v202"}}`) 381 applyAction = NewPatchActionWithOptions(cmResource, "default", "cm-1", types.ApplyPatchType, patch, 382 metav1.PatchOptions{FieldManager: "test-manager-2", Force: ptr.To(true)}) 383 handled, configMap, err = reaction(applyAction) 384 assert.True(t, handled) 385 if err != nil { 386 t.Errorf("Failed to apply resource: %v", err) 387 } 388 cm = configMap.(*v1.ConfigMap) 389 assert.Equal(t, map[string]string{"k0": "v0", "k1": "v202"}, cm.Data) 390 391 // Update with test-manager-1 to perform a force update of the entire resource 392 reaction = ObjectReaction(o) 393 updateAction := NewUpdateActionWithOptions(cmResource, "default", &v1.ConfigMap{ 394 TypeMeta: metav1.TypeMeta{ 395 APIVersion: "v1", 396 Kind: "ConfigMap", 397 }, 398 ObjectMeta: metav1.ObjectMeta{ 399 Name: "cm-1", 400 }, 401 Data: map[string]string{ 402 "k99": "v99", 403 }, 404 }, metav1.UpdateOptions{FieldManager: "test-manager-1"}) 405 handled, configMap, err = reaction(updateAction) 406 assert.True(t, handled) 407 if err != nil { 408 t.Errorf("Failed to apply resource: %v", err) 409 } 410 typedCm := configMap.(*v1.ConfigMap) 411 assert.Equal(t, map[string]string{"k99": "v99"}, typedCm.Data) 412 } 413 414 func TestGetWithExactMatch(t *testing.T) { 415 scheme := runtime.NewScheme() 416 codecs := serializer.NewCodecFactory(scheme) 417 418 constructObject := func(s schema.GroupVersionResource, name, namespace string) (*unstructured.Unstructured, schema.GroupVersionResource) { 419 obj := getArbitraryResource(s, name, namespace) 420 gvks, _, err := scheme.ObjectKinds(obj) 421 assert.NoError(t, err) 422 gvr, _ := meta.UnsafeGuessKindToResource(gvks[0]) 423 return obj, gvr 424 } 425 426 var err error 427 // Object with empty namespace 428 o := NewObjectTracker(scheme, codecs.UniversalDecoder()) 429 nodeResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "node"} 430 node, gvr := constructObject(nodeResource, "node", "") 431 432 assert.Nil(t, o.Add(node)) 433 434 // Exact match 435 _, err = o.Get(gvr, "", "node") 436 assert.NoError(t, err) 437 438 // Unexpected namespace provided 439 _, err = o.Get(gvr, "ns", "node") 440 assert.Error(t, err) 441 errNotFound := errors.NewNotFound(gvr.GroupResource(), "node") 442 assert.EqualError(t, err, errNotFound.Error()) 443 444 // Object with non-empty namespace 445 o = NewObjectTracker(scheme, codecs.UniversalDecoder()) 446 podResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"} 447 pod, gvr := constructObject(podResource, "pod", "default") 448 assert.Nil(t, o.Add(pod)) 449 450 // Exact match 451 _, err = o.Get(gvr, "default", "pod") 452 assert.NoError(t, err) 453 454 // Missing namespace 455 _, err = o.Get(gvr, "", "pod") 456 assert.Error(t, err) 457 errNotFound = errors.NewNotFound(gvr.GroupResource(), "pod") 458 assert.EqualError(t, err, errNotFound.Error()) 459 } 460 461 func Test_resourceCovers(t *testing.T) { 462 type args struct { 463 resource string 464 action Action 465 } 466 tests := []struct { 467 name string 468 args args 469 want bool 470 }{ 471 { 472 args: args{ 473 resource: "*", 474 action: ActionImpl{}, 475 }, 476 want: true, 477 }, 478 { 479 args: args{ 480 resource: "serviceaccounts", 481 action: ActionImpl{}, 482 }, 483 want: false, 484 }, 485 { 486 args: args{ 487 resource: "serviceaccounts", 488 action: ActionImpl{ 489 Resource: schema.GroupVersionResource{ 490 Resource: "serviceaccounts", 491 }, 492 }, 493 }, 494 want: true, 495 }, 496 { 497 args: args{ 498 resource: "serviceaccounts/token", 499 action: ActionImpl{ 500 Resource: schema.GroupVersionResource{}, 501 }, 502 }, 503 want: false, 504 }, 505 { 506 args: args{ 507 resource: "serviceaccounts/token", 508 action: ActionImpl{ 509 Resource: schema.GroupVersionResource{ 510 Resource: "serviceaccounts", 511 }, 512 }, 513 }, 514 want: false, 515 }, 516 { 517 args: args{ 518 resource: "serviceaccounts/token", 519 action: ActionImpl{ 520 Resource: schema.GroupVersionResource{}, 521 Subresource: "token", 522 }, 523 }, 524 want: false, 525 }, 526 { 527 args: args{ 528 resource: "serviceaccounts/token", 529 action: ActionImpl{ 530 Resource: schema.GroupVersionResource{ 531 Resource: "serviceaccounts", 532 }, 533 Subresource: "token", 534 }, 535 }, 536 want: true, 537 }, 538 } 539 for _, tt := range tests { 540 t.Run(tt.name, func(t *testing.T) { 541 if got := resourceCovers(tt.args.resource, tt.args.action); got != tt.want { 542 t.Errorf("resourceCovers() = %v, want %v", got, tt.want) 543 } 544 }) 545 } 546 } 547 548 func configMapTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { 549 parser, err := typed.NewParser(configMapTypedSchema) 550 if err != nil { 551 panic(fmt.Sprintf("Failed to parse schema: %v", err)) 552 } 553 554 return TypeConverter{Scheme: scheme, TypeResolver: parser} 555 } 556 557 var configMapTypedSchema = typed.YAMLObject(`types: 558 - name: io.k8s.api.core.v1.ConfigMap 559 map: 560 fields: 561 - name: apiVersion 562 type: 563 scalar: string 564 - name: data 565 type: 566 map: 567 elementType: 568 scalar: string 569 - name: kind 570 type: 571 scalar: string 572 - name: metadata 573 type: 574 namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta 575 default: {} 576 - name: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta 577 map: 578 fields: 579 - name: creationTimestamp 580 type: 581 namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time 582 - name: managedFields 583 type: 584 list: 585 elementType: 586 namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry 587 elementRelationship: atomic 588 - name: name 589 type: 590 scalar: string 591 - name: namespace 592 type: 593 scalar: string 594 - name: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry 595 map: 596 fields: 597 - name: apiVersion 598 type: 599 scalar: string 600 - name: fieldsType 601 type: 602 scalar: string 603 - name: fieldsV1 604 type: 605 namedType: io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 606 - name: manager 607 type: 608 scalar: string 609 - name: operation 610 type: 611 scalar: string 612 - name: subresource 613 type: 614 scalar: string 615 - name: time 616 type: 617 namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time 618 - name: io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 619 map: 620 elementType: 621 scalar: untyped 622 list: 623 elementType: 624 namedType: __untyped_atomic_ 625 elementRelationship: atomic 626 map: 627 elementType: 628 namedType: __untyped_deduced_ 629 elementRelationship: separable 630 - name: io.k8s.apimachinery.pkg.apis.meta.v1.Time 631 scalar: untyped 632 - name: __untyped_deduced_ 633 scalar: untyped 634 list: 635 elementType: 636 namedType: __untyped_atomic_ 637 elementRelationship: atomic 638 map: 639 elementType: 640 namedType: __untyped_deduced_ 641 elementRelationship: separable 642 `)