k8s.io/kubernetes@v1.29.3/pkg/controller/testutil/test_utils.go (about) 1 /* 2 Copyright 2016 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 testutil 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "reflect" 25 "sync" 26 "testing" 27 "time" 28 29 v1 "k8s.io/api/core/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/resource" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/sets" 37 "k8s.io/apimachinery/pkg/util/strategicpatch" 38 "k8s.io/apimachinery/pkg/watch" 39 v1apply "k8s.io/client-go/applyconfigurations/core/v1" 40 "k8s.io/client-go/kubernetes/fake" 41 v1core "k8s.io/client-go/kubernetes/typed/core/v1" 42 "k8s.io/client-go/tools/cache" 43 ref "k8s.io/client-go/tools/reference" 44 utilnode "k8s.io/component-helpers/node/topology" 45 "k8s.io/klog/v2" 46 "k8s.io/kubernetes/pkg/api/legacyscheme" 47 api "k8s.io/kubernetes/pkg/apis/core" 48 "k8s.io/utils/clock" 49 testingclock "k8s.io/utils/clock/testing" 50 51 jsonpatch "github.com/evanphx/json-patch" 52 ) 53 54 var ( 55 keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc 56 ) 57 58 // FakeNodeHandler is a fake implementation of NodesInterface and NodeInterface. It 59 // allows test cases to have fine-grained control over mock behaviors. We also need 60 // PodsInterface and PodInterface to test list & delete pods, which is implemented in 61 // the embedded client.Fake field. 62 type FakeNodeHandler struct { 63 *fake.Clientset 64 65 // Input: Hooks determine if request is valid or not 66 CreateHook func(*FakeNodeHandler, *v1.Node) bool 67 Existing []*v1.Node 68 AsyncCalls []func(*FakeNodeHandler) 69 70 // Output 71 CreatedNodes []*v1.Node 72 DeletedNodes []*v1.Node 73 UpdatedNodes []*v1.Node 74 UpdatedNodeStatuses []*v1.Node 75 RequestCount int 76 77 // Synchronization 78 lock sync.Mutex 79 DeleteWaitChan chan struct{} 80 PatchWaitChan chan struct{} 81 } 82 83 // FakeLegacyHandler is a fake implementation of CoreV1Interface. 84 type FakeLegacyHandler struct { 85 v1core.CoreV1Interface 86 n *FakeNodeHandler 87 } 88 89 // GetUpdatedNodesCopy returns a slice of Nodes with updates applied. 90 func (m *FakeNodeHandler) GetUpdatedNodesCopy() []*v1.Node { 91 m.lock.Lock() 92 defer m.lock.Unlock() 93 updatedNodesCopy := make([]*v1.Node, len(m.UpdatedNodes), len(m.UpdatedNodes)) 94 for i, ptr := range m.UpdatedNodes { 95 updatedNodesCopy[i] = ptr 96 } 97 return updatedNodesCopy 98 } 99 100 // Core returns fake CoreInterface. 101 func (m *FakeNodeHandler) Core() v1core.CoreV1Interface { 102 return &FakeLegacyHandler{m.Clientset.CoreV1(), m} 103 } 104 105 // CoreV1 returns fake CoreV1Interface 106 func (m *FakeNodeHandler) CoreV1() v1core.CoreV1Interface { 107 return &FakeLegacyHandler{m.Clientset.CoreV1(), m} 108 } 109 110 // Nodes return fake NodeInterfaces. 111 func (m *FakeLegacyHandler) Nodes() v1core.NodeInterface { 112 return m.n 113 } 114 115 // Create adds a new Node to the fake store. 116 func (m *FakeNodeHandler) Create(_ context.Context, node *v1.Node, _ metav1.CreateOptions) (*v1.Node, error) { 117 m.lock.Lock() 118 defer func() { 119 m.RequestCount++ 120 m.lock.Unlock() 121 }() 122 for _, n := range m.Existing { 123 if n.Name == node.Name { 124 return nil, apierrors.NewAlreadyExists(api.Resource("nodes"), node.Name) 125 } 126 } 127 if m.CreateHook == nil || m.CreateHook(m, node) { 128 nodeCopy := *node 129 m.CreatedNodes = append(m.CreatedNodes, &nodeCopy) 130 return node, nil 131 } 132 return nil, errors.New("create error") 133 } 134 135 // Get returns a Node from the fake store. 136 func (m *FakeNodeHandler) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Node, error) { 137 m.lock.Lock() 138 defer func() { 139 m.RequestCount++ 140 m.runAsyncCalls() 141 m.lock.Unlock() 142 }() 143 for i := range m.UpdatedNodes { 144 if m.UpdatedNodes[i].Name == name { 145 nodeCopy := *m.UpdatedNodes[i] 146 return &nodeCopy, nil 147 } 148 } 149 for i := range m.Existing { 150 if m.Existing[i].Name == name { 151 nodeCopy := *m.Existing[i] 152 return &nodeCopy, nil 153 } 154 } 155 return nil, apierrors.NewNotFound(schema.GroupResource{Resource: "nodes"}, name) 156 } 157 158 func (m *FakeNodeHandler) runAsyncCalls() { 159 for _, a := range m.AsyncCalls { 160 a(m) 161 } 162 } 163 164 // List returns a list of Nodes from the fake store. 165 func (m *FakeNodeHandler) List(_ context.Context, opts metav1.ListOptions) (*v1.NodeList, error) { 166 m.lock.Lock() 167 defer func() { 168 m.RequestCount++ 169 m.lock.Unlock() 170 }() 171 var nodes []*v1.Node 172 for i := 0; i < len(m.UpdatedNodes); i++ { 173 if !contains(m.UpdatedNodes[i], m.DeletedNodes) { 174 nodes = append(nodes, m.UpdatedNodes[i]) 175 } 176 } 177 for i := 0; i < len(m.Existing); i++ { 178 if !contains(m.Existing[i], m.DeletedNodes) && !contains(m.Existing[i], nodes) { 179 nodes = append(nodes, m.Existing[i]) 180 } 181 } 182 for i := 0; i < len(m.CreatedNodes); i++ { 183 if !contains(m.CreatedNodes[i], m.DeletedNodes) && !contains(m.CreatedNodes[i], nodes) { 184 nodes = append(nodes, m.CreatedNodes[i]) 185 } 186 } 187 nodeList := &v1.NodeList{} 188 for _, node := range nodes { 189 nodeList.Items = append(nodeList.Items, *node) 190 } 191 return nodeList, nil 192 } 193 194 // Delete deletes a Node from the fake store. 195 func (m *FakeNodeHandler) Delete(_ context.Context, id string, opt metav1.DeleteOptions) error { 196 m.lock.Lock() 197 defer func() { 198 m.RequestCount++ 199 if m.DeleteWaitChan != nil { 200 m.DeleteWaitChan <- struct{}{} 201 } 202 m.lock.Unlock() 203 }() 204 m.DeletedNodes = append(m.DeletedNodes, NewNode(id)) 205 return nil 206 } 207 208 // DeleteCollection deletes a collection of Nodes from the fake store. 209 func (m *FakeNodeHandler) DeleteCollection(_ context.Context, opt metav1.DeleteOptions, listOpts metav1.ListOptions) error { 210 return nil 211 } 212 213 // Update updates a Node in the fake store. 214 func (m *FakeNodeHandler) Update(_ context.Context, node *v1.Node, _ metav1.UpdateOptions) (*v1.Node, error) { 215 m.lock.Lock() 216 defer func() { 217 m.RequestCount++ 218 m.lock.Unlock() 219 }() 220 221 nodeCopy := *node 222 for i, updateNode := range m.UpdatedNodes { 223 if updateNode.Name == nodeCopy.Name { 224 if updateNode.GetObjectMeta().GetResourceVersion() != nodeCopy.GetObjectMeta().GetResourceVersion() { 225 return nil, apierrors.NewConflict(schema.GroupResource{}, "fake conflict", nil) 226 } 227 m.UpdatedNodes[i] = &nodeCopy 228 return node, nil 229 } 230 } 231 m.UpdatedNodes = append(m.UpdatedNodes, &nodeCopy) 232 return node, nil 233 } 234 235 // UpdateStatus updates a status of a Node in the fake store. 236 func (m *FakeNodeHandler) UpdateStatus(_ context.Context, node *v1.Node, _ metav1.UpdateOptions) (*v1.Node, error) { 237 m.lock.Lock() 238 defer func() { 239 m.RequestCount++ 240 m.lock.Unlock() 241 }() 242 243 var origNodeCopy v1.Node 244 found := false 245 for i := range m.Existing { 246 if m.Existing[i].Name == node.Name { 247 origNodeCopy = *m.Existing[i] 248 found = true 249 break 250 } 251 } 252 updatedNodeIndex := -1 253 for i := range m.UpdatedNodes { 254 if m.UpdatedNodes[i].Name == node.Name { 255 origNodeCopy = *m.UpdatedNodes[i] 256 updatedNodeIndex = i 257 found = true 258 break 259 } 260 } 261 262 if !found { 263 return nil, fmt.Errorf("not found node %v", node) 264 } 265 266 origNodeCopy.Status = node.Status 267 if updatedNodeIndex < 0 { 268 m.UpdatedNodes = append(m.UpdatedNodes, &origNodeCopy) 269 } else { 270 m.UpdatedNodes[updatedNodeIndex] = &origNodeCopy 271 } 272 273 nodeCopy := *node 274 m.UpdatedNodeStatuses = append(m.UpdatedNodeStatuses, &nodeCopy) 275 return node, nil 276 } 277 278 // PatchStatus patches a status of a Node in the fake store. 279 func (m *FakeNodeHandler) PatchStatus(ctx context.Context, nodeName string, data []byte) (*v1.Node, error) { 280 m.RequestCount++ 281 return m.Patch(ctx, nodeName, types.StrategicMergePatchType, data, metav1.PatchOptions{}, "status") 282 } 283 284 // Watch watches Nodes in a fake store. 285 func (m *FakeNodeHandler) Watch(_ context.Context, opts metav1.ListOptions) (watch.Interface, error) { 286 return watch.NewFake(), nil 287 } 288 289 // Patch patches a Node in the fake store. 290 func (m *FakeNodeHandler) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, _ metav1.PatchOptions, subresources ...string) (*v1.Node, error) { 291 m.lock.Lock() 292 defer func() { 293 m.RequestCount++ 294 if m.PatchWaitChan != nil { 295 m.PatchWaitChan <- struct{}{} 296 } 297 m.lock.Unlock() 298 }() 299 var nodeCopy v1.Node 300 for i := range m.Existing { 301 if m.Existing[i].Name == name { 302 nodeCopy = *m.Existing[i] 303 } 304 } 305 updatedNodeIndex := -1 306 for i := range m.UpdatedNodes { 307 if m.UpdatedNodes[i].Name == name { 308 nodeCopy = *m.UpdatedNodes[i] 309 updatedNodeIndex = i 310 } 311 } 312 313 originalObjJS, err := json.Marshal(nodeCopy) 314 if err != nil { 315 klog.FromContext(ctx).Error(nil, "Failed to marshal", "node", klog.KObj(&nodeCopy)) 316 return nil, nil 317 } 318 var originalNode v1.Node 319 if err = json.Unmarshal(originalObjJS, &originalNode); err != nil { 320 klog.FromContext(ctx).Error(err, "Failed to unmarshal original object") 321 return nil, nil 322 } 323 324 var patchedObjJS []byte 325 switch pt { 326 case types.JSONPatchType: 327 patchObj, err := jsonpatch.DecodePatch(data) 328 if err != nil { 329 klog.FromContext(ctx).Error(err, "") 330 return nil, nil 331 } 332 if patchedObjJS, err = patchObj.Apply(originalObjJS); err != nil { 333 klog.FromContext(ctx).Error(err, "") 334 return nil, nil 335 } 336 case types.MergePatchType: 337 if patchedObjJS, err = jsonpatch.MergePatch(originalObjJS, data); err != nil { 338 klog.FromContext(ctx).Error(err, "") 339 return nil, nil 340 } 341 case types.StrategicMergePatchType: 342 if patchedObjJS, err = strategicpatch.StrategicMergePatch(originalObjJS, data, originalNode); err != nil { 343 klog.FromContext(ctx).Error(err, "") 344 return nil, nil 345 } 346 default: 347 klog.FromContext(ctx).Error(nil, "Unknown Content-Type header", "patch", pt) 348 return nil, nil 349 } 350 351 var updatedNode v1.Node 352 if err = json.Unmarshal(patchedObjJS, &updatedNode); err != nil { 353 klog.FromContext(ctx).Error(err, "Failed to unmarshal patched object") 354 return nil, nil 355 } 356 357 if updatedNodeIndex < 0 { 358 m.UpdatedNodes = append(m.UpdatedNodes, &updatedNode) 359 } else { 360 if updatedNode.GetObjectMeta().GetResourceVersion() != m.UpdatedNodes[updatedNodeIndex].GetObjectMeta().GetResourceVersion() { 361 return nil, apierrors.NewConflict(schema.GroupResource{}, "fake conflict", nil) 362 } 363 m.UpdatedNodes[updatedNodeIndex] = &updatedNode 364 } 365 366 return &updatedNode, nil 367 } 368 369 // Apply applies a NodeApplyConfiguration to a Node in the fake store. 370 func (m *FakeNodeHandler) Apply(ctx context.Context, node *v1apply.NodeApplyConfiguration, opts metav1.ApplyOptions) (*v1.Node, error) { 371 patchOpts := opts.ToPatchOptions() 372 data, err := json.Marshal(node) 373 if err != nil { 374 return nil, err 375 } 376 name := node.Name 377 if name == nil { 378 return nil, fmt.Errorf("deployment.Name must be provided to Apply") 379 } 380 381 return m.Patch(ctx, *name, types.ApplyPatchType, data, patchOpts) 382 } 383 384 // ApplyStatus applies a status of a Node in the fake store. 385 func (m *FakeNodeHandler) ApplyStatus(ctx context.Context, node *v1apply.NodeApplyConfiguration, opts metav1.ApplyOptions) (*v1.Node, error) { 386 patchOpts := opts.ToPatchOptions() 387 data, err := json.Marshal(node) 388 if err != nil { 389 return nil, err 390 } 391 name := node.Name 392 if name == nil { 393 return nil, fmt.Errorf("deployment.Name must be provided to Apply") 394 } 395 396 return m.Patch(ctx, *name, types.ApplyPatchType, data, patchOpts, "status") 397 } 398 399 // FakeRecorder is used as a fake during testing. 400 type FakeRecorder struct { 401 sync.Mutex 402 source v1.EventSource 403 Events []*v1.Event 404 clock clock.Clock 405 } 406 407 // Event emits a fake event to the fake recorder 408 func (f *FakeRecorder) Event(obj runtime.Object, eventtype, reason, message string) { 409 f.generateEvent(obj, metav1.Now(), eventtype, reason, message) 410 } 411 412 // Eventf emits a fake formatted event to the fake recorder 413 func (f *FakeRecorder) Eventf(obj runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { 414 f.Event(obj, eventtype, reason, fmt.Sprintf(messageFmt, args...)) 415 } 416 417 // AnnotatedEventf emits a fake formatted event to the fake recorder 418 func (f *FakeRecorder) AnnotatedEventf(obj runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { 419 f.Eventf(obj, eventtype, reason, messageFmt, args...) 420 } 421 422 func (f *FakeRecorder) generateEvent(obj runtime.Object, timestamp metav1.Time, eventtype, reason, message string) { 423 f.Lock() 424 defer f.Unlock() 425 ctx := context.TODO() 426 ref, err := ref.GetReference(legacyscheme.Scheme, obj) 427 if err != nil { 428 klog.FromContext(ctx).Error(err, "Encountered error while getting reference") 429 return 430 } 431 event := f.makeEvent(ref, eventtype, reason, message) 432 event.Source = f.source 433 if f.Events != nil { 434 f.Events = append(f.Events, event) 435 } 436 } 437 438 func (f *FakeRecorder) makeEvent(ref *v1.ObjectReference, eventtype, reason, message string) *v1.Event { 439 t := metav1.Time{Time: f.clock.Now()} 440 namespace := ref.Namespace 441 if namespace == "" { 442 namespace = metav1.NamespaceDefault 443 } 444 445 clientref := v1.ObjectReference{ 446 Kind: ref.Kind, 447 Namespace: ref.Namespace, 448 Name: ref.Name, 449 UID: ref.UID, 450 APIVersion: ref.APIVersion, 451 ResourceVersion: ref.ResourceVersion, 452 FieldPath: ref.FieldPath, 453 } 454 455 return &v1.Event{ 456 ObjectMeta: metav1.ObjectMeta{ 457 Name: fmt.Sprintf("%v.%x", ref.Name, t.UnixNano()), 458 Namespace: namespace, 459 }, 460 InvolvedObject: clientref, 461 Reason: reason, 462 Message: message, 463 FirstTimestamp: t, 464 LastTimestamp: t, 465 Count: 1, 466 Type: eventtype, 467 } 468 } 469 470 // NewFakeRecorder returns a pointer to a newly constructed FakeRecorder. 471 func NewFakeRecorder() *FakeRecorder { 472 return &FakeRecorder{ 473 source: v1.EventSource{Component: "nodeControllerTest"}, 474 Events: []*v1.Event{}, 475 clock: testingclock.NewFakeClock(time.Now()), 476 } 477 } 478 479 // NewNode is a helper function for creating Nodes for testing. 480 func NewNode(name string) *v1.Node { 481 return &v1.Node{ 482 ObjectMeta: metav1.ObjectMeta{Name: name}, 483 Status: v1.NodeStatus{ 484 Capacity: v1.ResourceList{ 485 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 486 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 487 }, 488 }, 489 } 490 } 491 492 // NewPod is a helper function for creating Pods for testing. 493 func NewPod(name, host string) *v1.Pod { 494 pod := &v1.Pod{ 495 ObjectMeta: metav1.ObjectMeta{ 496 Namespace: "default", 497 Name: name, 498 }, 499 Spec: v1.PodSpec{ 500 NodeName: host, 501 }, 502 Status: v1.PodStatus{ 503 Conditions: []v1.PodCondition{ 504 { 505 Type: v1.PodReady, 506 Status: v1.ConditionTrue, 507 }, 508 }, 509 }, 510 } 511 512 return pod 513 } 514 515 func contains(node *v1.Node, nodes []*v1.Node) bool { 516 for i := 0; i < len(nodes); i++ { 517 if node.Name == nodes[i].Name { 518 return true 519 } 520 } 521 return false 522 } 523 524 // GetZones returns list of zones for all Nodes stored in FakeNodeHandler 525 func GetZones(nodeHandler *FakeNodeHandler) []string { 526 nodes, _ := nodeHandler.List(context.TODO(), metav1.ListOptions{}) 527 zones := sets.NewString() 528 for _, node := range nodes.Items { 529 zones.Insert(utilnode.GetZoneKey(&node)) 530 } 531 return zones.List() 532 } 533 534 // CreateZoneID returns a single zoneID for a given region and zone. 535 func CreateZoneID(region, zone string) string { 536 return region + ":\x00:" + zone 537 } 538 539 // GetKey is a helper function used by controllers unit tests to get the 540 // key for a given kubernetes resource. 541 func GetKey(obj interface{}, t *testing.T) string { 542 t.Helper() 543 tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 544 if ok { 545 // if tombstone , try getting the value from tombstone.Obj 546 obj = tombstone.Obj 547 } 548 val := reflect.ValueOf(obj).Elem() 549 name := val.FieldByName("Name").String() 550 if len(name) == 0 { 551 t.Errorf("Unexpected object %v", obj) 552 } 553 554 key, err := keyFunc(obj) 555 if err != nil { 556 t.Errorf("Unexpected error getting key for %T %v: %v", val.Interface(), name, err) 557 return "" 558 } 559 return key 560 }