k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/util/runtimeobjects/runtimeobjects_test.go (about) 1 /* 2 Copyright 2018 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 runtimeobjects_test 18 19 import ( 20 "fmt" 21 "reflect" 22 "strings" 23 "testing" 24 25 apps "k8s.io/api/apps/v1" 26 batch "k8s.io/api/batch/v1" 27 corev1 "k8s.io/api/core/v1" 28 v1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/api/resource" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/client-go/kubernetes/fake" 34 "k8s.io/client-go/kubernetes/scheme" 35 "k8s.io/perf-tests/clusterloader2/pkg/measurement/util/runtimeobjects" 36 ) 37 38 var ( 39 controllerName = "foobar" 40 testNamespace = "test-namespace" 41 defaultResourceVersion = "1" 42 defaultReplicas = int32(10) 43 daemonsetReplicas = int32(1) 44 45 daemonsetReplicasNoAffinity = int32(4) 46 daemonsetReplicasNoAffinityTolerateAll = int32(5) 47 ) 48 49 var ( 50 simpleLabel = map[string]string{"foo": "bar"} 51 affinityLabel = map[string]string{"foo": "bar", "affinity": "true"} 52 image = "gcr.io/some-project/some-image" 53 ) 54 55 var node1 = corev1.Node{ 56 ObjectMeta: metav1.ObjectMeta{ 57 Name: "node1", 58 Labels: simpleLabel, 59 }, 60 } 61 62 var node2 = corev1.Node{ 63 ObjectMeta: metav1.ObjectMeta{ 64 Name: "node2", 65 Labels: affinityLabel, 66 }, 67 } 68 69 var node3 = corev1.Node{ 70 ObjectMeta: metav1.ObjectMeta{ 71 Name: "node3", 72 Labels: simpleLabel, 73 }, 74 Spec: v1.NodeSpec{ 75 Unschedulable: true, 76 Taints: []v1.Taint{ 77 { 78 Key: v1.TaintNodeUnschedulable, 79 Effect: v1.TaintEffectNoSchedule, 80 }, 81 }, 82 }, 83 } 84 85 var node4 = corev1.Node{ 86 ObjectMeta: metav1.ObjectMeta{ 87 Name: "node4", 88 Labels: simpleLabel, 89 }, 90 Spec: v1.NodeSpec{ 91 Taints: []v1.Taint{ 92 { 93 Key: "something1", 94 Effect: v1.TaintEffectNoSchedule, 95 }, 96 }, 97 }, 98 } 99 100 var node5 = corev1.Node{ 101 ObjectMeta: metav1.ObjectMeta{ 102 Name: "node5", 103 Labels: simpleLabel, 104 }, 105 Spec: v1.NodeSpec{ 106 Taints: []v1.Taint{ 107 { 108 Key: "something2", 109 Effect: v1.TaintEffectPreferNoSchedule, 110 }, 111 }, 112 }, 113 } 114 115 var affinity = &corev1.Affinity{ 116 NodeAffinity: &corev1.NodeAffinity{ 117 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ 118 NodeSelectorTerms: []corev1.NodeSelectorTerm{ 119 { 120 MatchExpressions: []corev1.NodeSelectorRequirement{ 121 { 122 Key: "affinity", 123 Operator: v1.NodeSelectorOpIn, 124 Values: []string{"ok", "true"}, 125 }, 126 }, 127 }, 128 }, 129 }, 130 }, 131 } 132 133 var replicationcontroller = &corev1.ReplicationController{ 134 ObjectMeta: metav1.ObjectMeta{ 135 Name: controllerName, 136 Namespace: testNamespace, 137 ResourceVersion: defaultResourceVersion, 138 }, 139 Spec: corev1.ReplicationControllerSpec{ 140 Replicas: &defaultReplicas, 141 Selector: simpleLabel, 142 Template: &v1.PodTemplateSpec{ 143 ObjectMeta: metav1.ObjectMeta{ 144 Labels: simpleLabel, 145 }, 146 Spec: resourcePodSpec("", "50M", "0.5", nil, nil), 147 }, 148 }, 149 } 150 151 var replicaset = &apps.ReplicaSet{ 152 ObjectMeta: metav1.ObjectMeta{ 153 Name: controllerName, 154 Namespace: testNamespace, 155 ResourceVersion: defaultResourceVersion, 156 }, 157 Spec: apps.ReplicaSetSpec{ 158 Replicas: &defaultReplicas, 159 Selector: &metav1.LabelSelector{ 160 MatchLabels: simpleLabel, 161 }, 162 Template: v1.PodTemplateSpec{ 163 ObjectMeta: metav1.ObjectMeta{ 164 Labels: simpleLabel, 165 }, 166 Spec: resourcePodSpec("", "50M", "0.5", nil, nil), 167 }, 168 }, 169 } 170 171 var deployment = &apps.Deployment{ 172 ObjectMeta: metav1.ObjectMeta{ 173 Name: controllerName, 174 Namespace: testNamespace, 175 ResourceVersion: defaultResourceVersion, 176 }, 177 Spec: apps.DeploymentSpec{ 178 Replicas: &defaultReplicas, 179 Selector: &metav1.LabelSelector{ 180 MatchLabels: simpleLabel, 181 }, 182 Template: v1.PodTemplateSpec{ 183 ObjectMeta: metav1.ObjectMeta{ 184 Labels: simpleLabel, 185 }, 186 Spec: resourcePodSpec("", "50M", "0.5", nil, nil), 187 }, 188 }, 189 } 190 191 var daemonset = &apps.DaemonSet{ 192 ObjectMeta: metav1.ObjectMeta{ 193 Name: controllerName, 194 Namespace: testNamespace, 195 ResourceVersion: defaultResourceVersion, 196 }, 197 Spec: apps.DaemonSetSpec{ 198 Selector: &metav1.LabelSelector{ 199 MatchLabels: simpleLabel, 200 }, 201 Template: v1.PodTemplateSpec{ 202 ObjectMeta: metav1.ObjectMeta{ 203 Labels: simpleLabel, 204 }, 205 Spec: resourcePodSpec("", "50M", "0.5", simpleLabel, affinity), 206 }, 207 }, 208 } 209 210 var daemonsetNoAffinity = &apps.DaemonSet{ 211 ObjectMeta: metav1.ObjectMeta{ 212 Name: controllerName, 213 Namespace: testNamespace, 214 ResourceVersion: defaultResourceVersion, 215 }, 216 Spec: apps.DaemonSetSpec{ 217 Selector: &metav1.LabelSelector{ 218 MatchLabels: simpleLabel, 219 }, 220 Template: v1.PodTemplateSpec{ 221 ObjectMeta: metav1.ObjectMeta{ 222 Labels: simpleLabel, 223 }, 224 Spec: resourcePodSpec("", "50M", "0.5", simpleLabel, nil), 225 }, 226 }, 227 } 228 229 var daemonsetNoAffinityTolerateAll = &apps.DaemonSet{ 230 ObjectMeta: metav1.ObjectMeta{ 231 Name: controllerName, 232 Namespace: testNamespace, 233 ResourceVersion: defaultResourceVersion, 234 }, 235 Spec: apps.DaemonSetSpec{ 236 Selector: &metav1.LabelSelector{ 237 MatchLabels: simpleLabel, 238 }, 239 Template: v1.PodTemplateSpec{ 240 ObjectMeta: metav1.ObjectMeta{ 241 Labels: simpleLabel, 242 }, 243 Spec: func() v1.PodSpec { 244 podSpec := resourcePodSpec("", "50M", "0.5", simpleLabel, nil) 245 podSpec.Tolerations = []v1.Toleration{ 246 { 247 Effect: v1.TaintEffectNoSchedule, 248 }, 249 } 250 return podSpec 251 }(), 252 }, 253 }, 254 } 255 256 var job = &batch.Job{ 257 TypeMeta: metav1.TypeMeta{Kind: "Job"}, 258 ObjectMeta: metav1.ObjectMeta{ 259 Name: controllerName, 260 Namespace: testNamespace, 261 ResourceVersion: defaultResourceVersion, 262 }, 263 Spec: batch.JobSpec{ 264 Parallelism: &defaultReplicas, 265 Selector: &metav1.LabelSelector{ 266 MatchLabels: simpleLabel, 267 }, 268 Template: v1.PodTemplateSpec{ 269 ObjectMeta: metav1.ObjectMeta{ 270 Labels: simpleLabel, 271 }, 272 Spec: resourcePodSpec("", "50M", "0.5", nil, nil), 273 }, 274 }, 275 } 276 277 // pod is a sample pod that can be created for replicationcontroller, 278 // replicaset, deployment, job (NOT daemonset). 279 var pod = &corev1.Pod{ 280 ObjectMeta: metav1.ObjectMeta{ 281 Name: controllerName + "-abcd", 282 Namespace: testNamespace, 283 ResourceVersion: defaultResourceVersion, 284 }, 285 Spec: alterPodSpec(resourcePodSpec("", "50M", "0.5", nil, nil)), 286 } 287 288 var daemonsetPod = &corev1.Pod{ 289 ObjectMeta: metav1.ObjectMeta{ 290 Name: controllerName + "-abcd", 291 Namespace: testNamespace, 292 ResourceVersion: defaultResourceVersion, 293 }, 294 Spec: alterPodSpec(resourcePodSpec("", "50M", "0.5", simpleLabel, affinity)), 295 } 296 297 func resourcePodSpec(nodeName, memory, cpu string, nodeSelector map[string]string, affinity *v1.Affinity) v1.PodSpec { 298 return v1.PodSpec{ 299 NodeName: nodeName, 300 Containers: []v1.Container{{ 301 Resources: v1.ResourceRequirements{ 302 Requests: allocatableResources(memory, cpu), 303 }, 304 Image: image, 305 Env: []v1.EnvVar{ 306 { 307 Name: "env1", 308 Value: "val1", 309 }, 310 }, 311 }}, 312 NodeSelector: nodeSelector, 313 Affinity: affinity, 314 Tolerations: []v1.Toleration{ 315 { 316 Key: "default-toleration", 317 Value: "default-value", 318 Effect: v1.TaintEffectNoSchedule, 319 }, 320 }, 321 } 322 } 323 324 // alterPodSpec changees podSpec to simulate possible differences between template and final pod. 325 func alterPodSpec(in v1.PodSpec) v1.PodSpec { 326 out := in.DeepCopy() 327 // append some tolerations 328 out.Tolerations = append(out.Tolerations, v1.Toleration{ 329 Key: "test", 330 Value: "value", 331 Effect: v1.TaintEffectNoExecute, 332 }) 333 // set some defaults 334 i := int64(30) 335 out.TerminationGracePeriodSeconds = &i 336 out.ActiveDeadlineSeconds = &i 337 338 // Simulate schedule 339 if out.NodeName == "" { 340 out.NodeName = node1.Name 341 } 342 343 // Copy resources 344 for i := range out.Containers { 345 c := &out.Containers[i] 346 if c.Resources.Requests == nil { 347 c.Resources.Requests = c.Resources.Limits.DeepCopy() 348 } 349 } 350 return *out 351 } 352 353 func allocatableResources(memory, cpu string) v1.ResourceList { 354 return v1.ResourceList{ 355 v1.ResourceMemory: resource.MustParse(memory), 356 v1.ResourceCPU: resource.MustParse(cpu), 357 v1.ResourcePods: resource.MustParse("100"), 358 } 359 } 360 361 func TestGetResourceVersionFromRuntimeObject(t *testing.T) { 362 objects := []runtime.Object{ 363 replicationcontroller, 364 replicaset, 365 deployment, 366 job, 367 daemonset, 368 } 369 for _, obj := range objects { 370 unstructured := &unstructured.Unstructured{} 371 if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil { 372 t.Fatalf("error converting controller to unstructured: %v", err) 373 } 374 rv, err := runtimeobjects.GetResourceVersionFromRuntimeObject(unstructured) 375 if err != nil { 376 t.Fatalf("get resource version from runtime object failed: %v", err) 377 } 378 379 if defaultResourceVersion != fmt.Sprint(rv) { 380 t.Fatalf("Unexpected resource version from runtime object, expected: %s, actual: %v", defaultResourceVersion, rv) 381 } 382 } 383 } 384 385 func TestGetSpecFromRuntimeObject(t *testing.T) { 386 objects := []runtime.Object{ 387 replicationcontroller, 388 replicaset, 389 deployment, 390 job, 391 daemonset, 392 } 393 expected := []interface{}{ 394 replicationcontroller.Spec, 395 replicaset.Spec, 396 deployment.Spec, 397 job.Spec, 398 daemonset.Spec, 399 } 400 for i, obj := range objects { 401 unstructured := &unstructured.Unstructured{} 402 if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil { 403 t.Fatalf("error converting controller to unstructured: %v", err) 404 } 405 spec, err := runtimeobjects.GetSpecFromRuntimeObject(unstructured) 406 if err != nil { 407 t.Fatalf("get spec from runtime object failed: %v", err) 408 } 409 target, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&expected[i]) 410 if err != nil { 411 t.Fatalf("error converting target spec to unstructured: %v", err) 412 } 413 414 if !reflect.DeepEqual(target, spec) { 415 t.Fatalf("Unexpected spec from runtime object, expected: %v, actual: %v", expected[i], spec) 416 } 417 } 418 } 419 420 func changeImage(in *v1.Pod) *v1.Pod { 421 out := in.DeepCopy() 422 423 for i := range out.Spec.Containers { 424 c := &out.Spec.Containers[i] 425 c.Image = c.Image + "-diff" 426 } 427 428 return out 429 } 430 431 func changeEnv(in *v1.Pod) *v1.Pod { 432 out := in.DeepCopy() 433 434 for i := range out.Spec.Containers { 435 c := &out.Spec.Containers[i] 436 for j := range c.Env { 437 e := &c.Env[j] 438 e.Value = e.Value + "-diff" 439 } 440 } 441 442 return out 443 } 444 445 func noWhitespace(s string) string { 446 s = strings.ReplaceAll(s, " ", "") 447 s = strings.ReplaceAll(s, "\n", "") 448 s = strings.ReplaceAll(s, "\r", "") 449 s = strings.ReplaceAll(s, " ", "") //non-breaking space 450 return strings.ReplaceAll(s, "\t", "") 451 } 452 453 const fullCompareErrorDifferentEnv = `Not matching templates, diff: v1.PodSpec{ 454 Volumes: nil, 455 InitContainers: nil, 456 Containers: []v1.Container{ 457 { 458 ... // 5 identical fields 459 Ports: nil, 460 EnvFrom: nil, 461 Env: []v1.EnvVar{ 462 { 463 Name: "env1", 464 - Value: "val1", 465 + Value: "val1-diff", 466 ValueFrom: nil, 467 }, 468 }, 469 Resources: {Requests: {s"cpu": {i: {...}, Format: "DecimalSI"}, s"memory": {i: {...}, s: "50M", Format: "DecimalSI"}, s"pods": {i: {...}, s: "100", Format: "DecimalSI"}}}, 470 VolumeMounts: nil, 471 ... // 12 identical fields 472 }, 473 }, 474 EphemeralContainers: nil, 475 RestartPolicy: "", 476 - TerminationGracePeriodSeconds: nil, 477 + TerminationGracePeriodSeconds: &30, 478 - ActiveDeadlineSeconds: nil, 479 + ActiveDeadlineSeconds: &30, 480 DNSPolicy: "", 481 NodeSelector: nil, 482 ServiceAccountName: "", 483 DeprecatedServiceAccount: "", 484 AutomountServiceAccountToken: nil, 485 - NodeName: "", 486 + NodeName: "node1", 487 HostNetwork: false, 488 HostPID: false, 489 ... // 6 identical fields 490 Affinity: nil, 491 SchedulerName: "", 492 Tolerations: []v1.Toleration{ 493 {Key: "default-toleration", Value: "default-value", Effect: "NoSchedule"}, 494 + {Key: "test", Value: "value", Effect: "NoExecute"}, 495 }, 496 HostAliases: nil, 497 PriorityClassName: "", 498 ... // 9 identical fields 499 } 500 ` 501 502 const fullCompareErrorDifferentImage = `Not matching templates, diff: v1.PodSpec{ 503 Volumes: nil, 504 InitContainers: nil, 505 Containers: []v1.Container{ 506 { 507 Name: "", 508 - Image: "gcr.io/some-project/some-image", 509 + Image: "gcr.io/some-project/some-image-diff", 510 Command: nil, 511 Args: nil, 512 ... // 18 identical fields 513 }, 514 }, 515 EphemeralContainers: nil, 516 RestartPolicy: "", 517 - TerminationGracePeriodSeconds: nil, 518 + TerminationGracePeriodSeconds: &30, 519 - ActiveDeadlineSeconds: nil, 520 + ActiveDeadlineSeconds: &30, 521 DNSPolicy: "", 522 NodeSelector: nil, 523 ServiceAccountName: "", 524 DeprecatedServiceAccount: "", 525 AutomountServiceAccountToken: nil, 526 - NodeName: "", 527 + NodeName: "node1", 528 HostNetwork: false, 529 HostPID: false, 530 ... // 6 identical fields 531 Affinity: nil, 532 SchedulerName: "", 533 Tolerations: []v1.Toleration{ 534 {Key: "default-toleration", Value: "default-value", Effect: "NoSchedule"}, 535 + {Key: "test", Value: "value", Effect: "NoExecute"}, 536 }, 537 HostAliases: nil, 538 PriorityClassName: "", 539 ... // 9 identical fields 540 }` 541 542 func TestGetIsPodUpdatedPredicateFromRuntimeObject(t *testing.T) { 543 testCases := []struct { 544 name string 545 obj runtime.Object 546 pod *corev1.Pod 547 wantErr bool 548 wantfullCompareError string 549 }{ 550 { 551 name: "deployment, positive", 552 obj: deployment, 553 pod: pod, 554 }, 555 { 556 name: "deployment, different env", 557 obj: deployment, 558 pod: changeEnv(pod), 559 wantfullCompareError: fullCompareErrorDifferentEnv, 560 }, 561 { 562 name: "deployment, different image", 563 obj: deployment, 564 pod: changeImage(pod), 565 wantfullCompareError: fullCompareErrorDifferentImage, 566 }, 567 { 568 name: "replicaset, positive", 569 obj: replicaset, 570 pod: pod, 571 }, 572 { 573 name: "replicationcontroller, positive", 574 obj: replicationcontroller, 575 pod: pod, 576 }, 577 { 578 name: "daemonset, positive", 579 obj: daemonset, 580 pod: daemonsetPod, 581 }, 582 { 583 name: "job, positive", 584 obj: job, 585 pod: pod, 586 }, 587 { 588 name: "no spec.template", 589 obj: pod, // pod has no spec.template field. 590 pod: pod, 591 wantErr: true, 592 }, 593 } 594 595 for _, tc := range testCases { 596 t.Run(tc.name, func(t *testing.T) { 597 unstructured := &unstructured.Unstructured{} 598 if err := scheme.Scheme.Convert(tc.obj, unstructured, nil); err != nil { 599 t.Fatalf("error converting controller to unstructured: %v", err) 600 } 601 pred, err := runtimeobjects.GetIsPodUpdatedPredicateFromRuntimeObject(unstructured) 602 if (err != nil) != tc.wantErr { 603 t.Errorf("unexpected error; want: %v; got %v", tc.wantErr, err) 604 } 605 if err != nil { 606 return 607 } 608 gotErr := pred(tc.pod) 609 var gotErrString string 610 if gotErr != nil { 611 gotErrString = gotErr.Error() 612 } 613 if noWhitespace(gotErrString) != noWhitespace(tc.wantfullCompareError) { 614 t.Errorf("pred(tc.pod) = %v; want %v", noWhitespace(gotErrString), noWhitespace(tc.wantfullCompareError)) 615 } 616 }) 617 } 618 } 619 620 func TestGetReplicasFromRuntimeObject(t *testing.T) { 621 objects := []runtime.Object{ 622 replicationcontroller, 623 replicaset, 624 deployment, 625 job, 626 daemonset, 627 daemonsetNoAffinity, 628 daemonsetNoAffinityTolerateAll, 629 } 630 expected := []int32{ 631 defaultReplicas, 632 defaultReplicas, 633 defaultReplicas, 634 defaultReplicas, 635 daemonsetReplicas, 636 daemonsetReplicasNoAffinity, 637 daemonsetReplicasNoAffinityTolerateAll, 638 } 639 640 fakeClient := fake.NewSimpleClientset(&node1, &node2, &node3, &node4, &node5) 641 for i, obj := range objects { 642 unstructured := &unstructured.Unstructured{} 643 if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil { 644 t.Fatalf("error converting controller to unstructured: %v", err) 645 } 646 replicasWatcher, err := runtimeobjects.GetReplicasFromRuntimeObject(fakeClient, unstructured) 647 if err != nil { 648 t.Fatalf("get replicas from runtime object failed: %v", err) 649 } 650 651 replicas, err := runtimeobjects.GetReplicasOnce(replicasWatcher) 652 if err != nil { 653 t.Fatalf("got unexpected error while getting number of replicas: %v", err) 654 } 655 656 if int(expected[i]) != replicas { 657 t.Fatalf("unexpected replicas from runtime object, expected: %d, actual: %d", expected[i], replicas) 658 } 659 } 660 }