k8s.io/kubernetes@v1.29.3/pkg/api/v1/resource/helpers_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 resource 18 19 import ( 20 "testing" 21 22 "github.com/stretchr/testify/assert" 23 24 v1 "k8s.io/api/core/v1" 25 "k8s.io/apimachinery/pkg/api/equality" 26 "k8s.io/apimachinery/pkg/api/resource" 27 ) 28 29 func TestResourceHelpers(t *testing.T) { 30 cpuLimit := resource.MustParse("10") 31 memoryLimit := resource.MustParse("10G") 32 resourceSpec := v1.ResourceRequirements{ 33 Limits: v1.ResourceList{ 34 v1.ResourceCPU: cpuLimit, 35 v1.ResourceMemory: memoryLimit, 36 }, 37 } 38 if res := resourceSpec.Limits.Cpu(); res.Cmp(cpuLimit) != 0 { 39 t.Errorf("expected cpulimit %v, got %v", cpuLimit, res) 40 } 41 if res := resourceSpec.Limits.Memory(); res.Cmp(memoryLimit) != 0 { 42 t.Errorf("expected memorylimit %v, got %v", memoryLimit, res) 43 } 44 resourceSpec = v1.ResourceRequirements{ 45 Limits: v1.ResourceList{ 46 v1.ResourceMemory: memoryLimit, 47 }, 48 } 49 if res := resourceSpec.Limits.Cpu(); res.Value() != 0 { 50 t.Errorf("expected cpulimit %v, got %v", 0, res) 51 } 52 if res := resourceSpec.Limits.Memory(); res.Cmp(memoryLimit) != 0 { 53 t.Errorf("expected memorylimit %v, got %v", memoryLimit, res) 54 } 55 } 56 57 func TestDefaultResourceHelpers(t *testing.T) { 58 resourceList := v1.ResourceList{} 59 if resourceList.Cpu().Format != resource.DecimalSI { 60 t.Errorf("expected %v, actual %v", resource.DecimalSI, resourceList.Cpu().Format) 61 } 62 if resourceList.Memory().Format != resource.BinarySI { 63 t.Errorf("expected %v, actual %v", resource.BinarySI, resourceList.Memory().Format) 64 } 65 } 66 67 func TestGetResourceRequest(t *testing.T) { 68 cases := []struct { 69 pod *v1.Pod 70 cName string 71 resourceName v1.ResourceName 72 expectedValue int64 73 }{ 74 { 75 pod: getPod("foo", podResources{cpuRequest: "9"}), 76 resourceName: v1.ResourceCPU, 77 expectedValue: 9000, 78 }, 79 { 80 pod: getPod("foo", podResources{memoryRequest: "90Mi"}), 81 resourceName: v1.ResourceMemory, 82 expectedValue: 94371840, 83 }, 84 { 85 cName: "just-overhead for cpu", 86 pod: getPod("foo", podResources{cpuOverhead: "5", memoryOverhead: "5"}), 87 resourceName: v1.ResourceCPU, 88 expectedValue: 0, 89 }, 90 { 91 cName: "just-overhead for memory", 92 pod: getPod("foo", podResources{memoryOverhead: "5"}), 93 resourceName: v1.ResourceMemory, 94 expectedValue: 0, 95 }, 96 { 97 cName: "cpu overhead and req", 98 pod: getPod("foo", podResources{cpuRequest: "2", cpuOverhead: "5", memoryOverhead: "5"}), 99 resourceName: v1.ResourceCPU, 100 expectedValue: 7000, 101 }, 102 { 103 cName: "mem overhead and req", 104 pod: getPod("foo", podResources{cpuRequest: "2", memoryRequest: "1024", cpuOverhead: "5", memoryOverhead: "5"}), 105 resourceName: v1.ResourceMemory, 106 expectedValue: 1029, 107 }, 108 } 109 as := assert.New(t) 110 for idx, tc := range cases { 111 actual := GetResourceRequest(tc.pod, tc.resourceName) 112 as.Equal(actual, tc.expectedValue, "expected test case [%d] %v: to return %q; got %q instead", idx, tc.cName, tc.expectedValue, actual) 113 } 114 } 115 116 func TestExtractResourceValue(t *testing.T) { 117 cases := []struct { 118 fs *v1.ResourceFieldSelector 119 pod *v1.Pod 120 cName string 121 expectedValue string 122 expectedError error 123 }{ 124 { 125 fs: &v1.ResourceFieldSelector{ 126 Resource: "limits.cpu", 127 }, 128 cName: "foo", 129 pod: getPod("foo", podResources{cpuLimit: "9"}), 130 expectedValue: "9", 131 }, 132 { 133 fs: &v1.ResourceFieldSelector{ 134 Resource: "requests.cpu", 135 }, 136 cName: "foo", 137 pod: getPod("foo", podResources{}), 138 expectedValue: "0", 139 }, 140 { 141 fs: &v1.ResourceFieldSelector{ 142 Resource: "requests.cpu", 143 }, 144 cName: "foo", 145 pod: getPod("foo", podResources{cpuRequest: "8"}), 146 expectedValue: "8", 147 }, 148 { 149 fs: &v1.ResourceFieldSelector{ 150 Resource: "requests.cpu", 151 }, 152 cName: "foo", 153 pod: getPod("foo", podResources{cpuRequest: "100m"}), 154 expectedValue: "1", 155 }, 156 { 157 fs: &v1.ResourceFieldSelector{ 158 Resource: "requests.cpu", 159 Divisor: resource.MustParse("100m"), 160 }, 161 cName: "foo", 162 pod: getPod("foo", podResources{cpuRequest: "1200m"}), 163 expectedValue: "12", 164 }, 165 { 166 fs: &v1.ResourceFieldSelector{ 167 Resource: "requests.memory", 168 }, 169 cName: "foo", 170 pod: getPod("foo", podResources{memoryRequest: "100Mi"}), 171 expectedValue: "104857600", 172 }, 173 { 174 fs: &v1.ResourceFieldSelector{ 175 Resource: "requests.memory", 176 Divisor: resource.MustParse("1Mi"), 177 }, 178 cName: "foo", 179 pod: getPod("foo", podResources{memoryRequest: "100Mi", memoryLimit: "1Gi"}), 180 expectedValue: "100", 181 }, 182 { 183 fs: &v1.ResourceFieldSelector{ 184 Resource: "limits.memory", 185 }, 186 cName: "foo", 187 pod: getPod("foo", podResources{memoryRequest: "10Mi", memoryLimit: "100Mi"}), 188 expectedValue: "104857600", 189 }, 190 { 191 fs: &v1.ResourceFieldSelector{ 192 Resource: "limits.cpu", 193 }, 194 cName: "init-foo", 195 pod: getPod("foo", podResources{cpuLimit: "9"}), 196 expectedValue: "9", 197 }, 198 { 199 fs: &v1.ResourceFieldSelector{ 200 Resource: "requests.cpu", 201 }, 202 cName: "init-foo", 203 pod: getPod("foo", podResources{}), 204 expectedValue: "0", 205 }, 206 { 207 fs: &v1.ResourceFieldSelector{ 208 Resource: "requests.cpu", 209 }, 210 cName: "init-foo", 211 pod: getPod("foo", podResources{cpuRequest: "8"}), 212 expectedValue: "8", 213 }, 214 { 215 fs: &v1.ResourceFieldSelector{ 216 Resource: "requests.cpu", 217 }, 218 cName: "init-foo", 219 pod: getPod("foo", podResources{cpuRequest: "100m"}), 220 expectedValue: "1", 221 }, 222 { 223 fs: &v1.ResourceFieldSelector{ 224 Resource: "requests.cpu", 225 Divisor: resource.MustParse("100m"), 226 }, 227 cName: "init-foo", 228 pod: getPod("foo", podResources{cpuRequest: "1200m"}), 229 expectedValue: "12", 230 }, 231 { 232 fs: &v1.ResourceFieldSelector{ 233 Resource: "requests.memory", 234 }, 235 cName: "init-foo", 236 pod: getPod("foo", podResources{memoryRequest: "100Mi"}), 237 expectedValue: "104857600", 238 }, 239 { 240 fs: &v1.ResourceFieldSelector{ 241 Resource: "requests.memory", 242 Divisor: resource.MustParse("1Mi"), 243 }, 244 cName: "init-foo", 245 pod: getPod("foo", podResources{memoryRequest: "100Mi", memoryLimit: "1Gi"}), 246 expectedValue: "100", 247 }, 248 { 249 fs: &v1.ResourceFieldSelector{ 250 Resource: "limits.memory", 251 }, 252 cName: "init-foo", 253 pod: getPod("foo", podResources{memoryRequest: "10Mi", memoryLimit: "100Mi"}), 254 255 expectedValue: "104857600", 256 }, 257 } 258 as := assert.New(t) 259 for idx, tc := range cases { 260 actual, err := ExtractResourceValueByContainerName(tc.fs, tc.pod, tc.cName) 261 if tc.expectedError != nil { 262 as.Equal(tc.expectedError, err, "expected test case [%d] to fail with error %v; got %v", idx, tc.expectedError, err) 263 } else { 264 as.Nil(err, "expected test case [%d] to not return an error; got %v", idx, err) 265 as.Equal(tc.expectedValue, actual, "expected test case [%d] to return %q; got %q instead", idx, tc.expectedValue, actual) 266 } 267 } 268 } 269 270 func TestPodRequestsAndLimits(t *testing.T) { 271 cases := []struct { 272 pod *v1.Pod 273 cName string 274 expectedRequests v1.ResourceList 275 expectedLimits v1.ResourceList 276 }{ 277 { 278 cName: "just-limit-no-overhead", 279 pod: getPod("foo", podResources{cpuLimit: "9"}), 280 expectedRequests: v1.ResourceList{}, 281 expectedLimits: v1.ResourceList{ 282 v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), 283 }, 284 }, 285 { 286 cName: "just-overhead", 287 pod: getPod("foo", podResources{cpuOverhead: "5", memoryOverhead: "5"}), 288 expectedRequests: v1.ResourceList{ 289 v1.ResourceName(v1.ResourceCPU): resource.MustParse("5"), 290 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 291 }, 292 expectedLimits: v1.ResourceList{}, 293 }, 294 { 295 cName: "req-and-overhead", 296 pod: getPod("foo", podResources{cpuRequest: "1", memoryRequest: "10", cpuOverhead: "5", memoryOverhead: "5"}), 297 expectedRequests: v1.ResourceList{ 298 v1.ResourceName(v1.ResourceCPU): resource.MustParse("6"), 299 v1.ResourceName(v1.ResourceMemory): resource.MustParse("15"), 300 }, 301 expectedLimits: v1.ResourceList{}, 302 }, 303 { 304 cName: "all-req-lim-and-overhead", 305 pod: getPod("foo", podResources{cpuRequest: "1", cpuLimit: "2", memoryRequest: "10", memoryLimit: "12", cpuOverhead: "5", memoryOverhead: "5"}), 306 expectedRequests: v1.ResourceList{ 307 v1.ResourceName(v1.ResourceCPU): resource.MustParse("6"), 308 v1.ResourceName(v1.ResourceMemory): resource.MustParse("15"), 309 }, 310 expectedLimits: v1.ResourceList{ 311 v1.ResourceName(v1.ResourceCPU): resource.MustParse("7"), 312 v1.ResourceName(v1.ResourceMemory): resource.MustParse("17"), 313 }, 314 }, 315 { 316 cName: "req-some-lim-and-overhead", 317 pod: getPod("foo", podResources{cpuRequest: "1", cpuLimit: "2", memoryRequest: "10", cpuOverhead: "5", memoryOverhead: "5"}), 318 expectedRequests: v1.ResourceList{ 319 v1.ResourceName(v1.ResourceCPU): resource.MustParse("6"), 320 v1.ResourceName(v1.ResourceMemory): resource.MustParse("15"), 321 }, 322 expectedLimits: v1.ResourceList{ 323 v1.ResourceName(v1.ResourceCPU): resource.MustParse("7"), 324 }, 325 }, 326 } 327 for idx, tc := range cases { 328 resRequests := PodRequests(tc.pod, PodResourcesOptions{}) 329 resLimits := PodLimits(tc.pod, PodResourcesOptions{}) 330 331 if !equality.Semantic.DeepEqual(tc.expectedRequests, resRequests) { 332 t.Errorf("test case failure[%d]: %v, requests:\n expected:\t%v\ngot\t\t%v", idx, tc.cName, tc.expectedRequests, resRequests) 333 } 334 335 if !equality.Semantic.DeepEqual(tc.expectedLimits, resLimits) { 336 t.Errorf("test case failure[%d]: %v, limits:\n expected:\t%v\ngot\t\t%v", idx, tc.cName, tc.expectedLimits, resLimits) 337 } 338 } 339 } 340 341 func TestPodRequestsAndLimitsWithoutOverhead(t *testing.T) { 342 cases := []struct { 343 pod *v1.Pod 344 name string 345 expectedRequests v1.ResourceList 346 expectedLimits v1.ResourceList 347 }{ 348 { 349 name: "two container no overhead - should just be sum of containers", 350 pod: &v1.Pod{ 351 Spec: v1.PodSpec{ 352 Containers: []v1.Container{ 353 { 354 Name: "foobar", 355 Resources: v1.ResourceRequirements{ 356 Requests: v1.ResourceList{ 357 v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"), 358 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 359 }, 360 Limits: v1.ResourceList{ 361 v1.ResourceName(v1.ResourceCPU): resource.MustParse("2"), 362 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10"), 363 }, 364 }, 365 }, 366 { 367 Name: "foobar2", 368 Resources: v1.ResourceRequirements{ 369 Requests: v1.ResourceList{ 370 v1.ResourceName(v1.ResourceCPU): resource.MustParse("4"), 371 v1.ResourceName(v1.ResourceMemory): resource.MustParse("12"), 372 }, 373 Limits: v1.ResourceList{ 374 v1.ResourceName(v1.ResourceCPU): resource.MustParse("8"), 375 v1.ResourceName(v1.ResourceMemory): resource.MustParse("24"), 376 }, 377 }, 378 }, 379 }, 380 }, 381 }, 382 expectedRequests: v1.ResourceList{ 383 v1.ResourceName(v1.ResourceCPU): resource.MustParse("5"), 384 v1.ResourceName(v1.ResourceMemory): resource.MustParse("17"), 385 }, 386 expectedLimits: v1.ResourceList{ 387 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 388 v1.ResourceName(v1.ResourceMemory): resource.MustParse("34"), 389 }, 390 }, 391 { 392 name: "two container with overhead - shouldn't consider overhead", 393 pod: &v1.Pod{ 394 Spec: v1.PodSpec{ 395 Overhead: v1.ResourceList{ 396 v1.ResourceName(v1.ResourceCPU): resource.MustParse("3"), 397 v1.ResourceName(v1.ResourceMemory): resource.MustParse("8"), 398 }, 399 Containers: []v1.Container{ 400 { 401 Name: "foobar", 402 Resources: v1.ResourceRequirements{ 403 Requests: v1.ResourceList{ 404 v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"), 405 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 406 }, 407 Limits: v1.ResourceList{ 408 v1.ResourceName(v1.ResourceCPU): resource.MustParse("2"), 409 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10"), 410 }, 411 }, 412 }, 413 { 414 Name: "foobar2", 415 Resources: v1.ResourceRequirements{ 416 Requests: v1.ResourceList{ 417 v1.ResourceName(v1.ResourceCPU): resource.MustParse("4"), 418 v1.ResourceName(v1.ResourceMemory): resource.MustParse("12"), 419 }, 420 Limits: v1.ResourceList{ 421 v1.ResourceName(v1.ResourceCPU): resource.MustParse("8"), 422 v1.ResourceName(v1.ResourceMemory): resource.MustParse("24"), 423 }, 424 }, 425 }, 426 }, 427 }, 428 }, 429 expectedRequests: v1.ResourceList{ 430 v1.ResourceName(v1.ResourceCPU): resource.MustParse("5"), 431 v1.ResourceName(v1.ResourceMemory): resource.MustParse("17"), 432 }, 433 expectedLimits: v1.ResourceList{ 434 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 435 v1.ResourceName(v1.ResourceMemory): resource.MustParse("34"), 436 }, 437 }, 438 { 439 name: "two container with overhead, massive init - should just be the largest init", 440 pod: &v1.Pod{ 441 Spec: v1.PodSpec{ 442 Overhead: v1.ResourceList{ 443 v1.ResourceName(v1.ResourceCPU): resource.MustParse("3"), 444 v1.ResourceName(v1.ResourceMemory): resource.MustParse("8"), 445 }, 446 Containers: []v1.Container{ 447 { 448 Name: "foobar", 449 Resources: v1.ResourceRequirements{ 450 Requests: v1.ResourceList{ 451 v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"), 452 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 453 }, 454 Limits: v1.ResourceList{ 455 v1.ResourceName(v1.ResourceCPU): resource.MustParse("2"), 456 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10"), 457 }, 458 }, 459 }, 460 { 461 Name: "foobar2", 462 Resources: v1.ResourceRequirements{ 463 Requests: v1.ResourceList{ 464 v1.ResourceName(v1.ResourceCPU): resource.MustParse("4"), 465 v1.ResourceName(v1.ResourceMemory): resource.MustParse("12"), 466 }, 467 Limits: v1.ResourceList{ 468 v1.ResourceName(v1.ResourceCPU): resource.MustParse("8"), 469 v1.ResourceName(v1.ResourceMemory): resource.MustParse("24"), 470 }, 471 }, 472 }, 473 }, 474 InitContainers: []v1.Container{ 475 { 476 Name: "small-init", 477 Resources: v1.ResourceRequirements{ 478 Requests: v1.ResourceList{ 479 v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"), 480 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 481 }, 482 Limits: v1.ResourceList{ 483 v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"), 484 v1.ResourceName(v1.ResourceMemory): resource.MustParse("5"), 485 }, 486 }, 487 }, 488 { 489 Name: "big-init", 490 Resources: v1.ResourceRequirements{ 491 Requests: v1.ResourceList{ 492 v1.ResourceName(v1.ResourceCPU): resource.MustParse("40"), 493 v1.ResourceName(v1.ResourceMemory): resource.MustParse("120"), 494 }, 495 Limits: v1.ResourceList{ 496 v1.ResourceName(v1.ResourceCPU): resource.MustParse("80"), 497 v1.ResourceName(v1.ResourceMemory): resource.MustParse("240"), 498 }, 499 }, 500 }, 501 }, 502 }, 503 }, 504 expectedRequests: v1.ResourceList{ 505 v1.ResourceName(v1.ResourceCPU): resource.MustParse("40"), 506 v1.ResourceName(v1.ResourceMemory): resource.MustParse("120"), 507 }, 508 expectedLimits: v1.ResourceList{ 509 v1.ResourceName(v1.ResourceCPU): resource.MustParse("80"), 510 v1.ResourceName(v1.ResourceMemory): resource.MustParse("240"), 511 }, 512 }, 513 } 514 for idx, tc := range cases { 515 resRequests := PodRequests(tc.pod, PodResourcesOptions{ExcludeOverhead: true}) 516 resLimits := PodLimits(tc.pod, PodResourcesOptions{ExcludeOverhead: true}) 517 518 if !equality.Semantic.DeepEqual(tc.expectedRequests, resRequests) { 519 t.Errorf("test case failure[%d]: %v, requests:\n expected:\t%v\ngot\t\t%v", idx, tc.name, tc.expectedRequests, resRequests) 520 } 521 522 if !equality.Semantic.DeepEqual(tc.expectedLimits, resLimits) { 523 t.Errorf("test case failure[%d]: %v, limits:\n expected:\t%v\ngot\t\t%v", idx, tc.name, tc.expectedLimits, resLimits) 524 } 525 } 526 } 527 528 type podResources struct { 529 cpuRequest, cpuLimit, memoryRequest, memoryLimit, cpuOverhead, memoryOverhead string 530 } 531 532 func getPod(cname string, resources podResources) *v1.Pod { 533 r := v1.ResourceRequirements{ 534 Limits: make(v1.ResourceList), 535 Requests: make(v1.ResourceList), 536 } 537 538 overhead := make(v1.ResourceList) 539 540 if resources.cpuLimit != "" { 541 r.Limits[v1.ResourceCPU] = resource.MustParse(resources.cpuLimit) 542 } 543 if resources.memoryLimit != "" { 544 r.Limits[v1.ResourceMemory] = resource.MustParse(resources.memoryLimit) 545 } 546 if resources.cpuRequest != "" { 547 r.Requests[v1.ResourceCPU] = resource.MustParse(resources.cpuRequest) 548 } 549 if resources.memoryRequest != "" { 550 r.Requests[v1.ResourceMemory] = resource.MustParse(resources.memoryRequest) 551 } 552 if resources.cpuOverhead != "" { 553 overhead[v1.ResourceCPU] = resource.MustParse(resources.cpuOverhead) 554 } 555 if resources.memoryOverhead != "" { 556 overhead[v1.ResourceMemory] = resource.MustParse(resources.memoryOverhead) 557 } 558 559 return &v1.Pod{ 560 Spec: v1.PodSpec{ 561 Containers: []v1.Container{ 562 { 563 Name: cname, 564 Resources: r, 565 }, 566 }, 567 InitContainers: []v1.Container{ 568 { 569 Name: "init-" + cname, 570 Resources: r, 571 }, 572 }, 573 Overhead: overhead, 574 }, 575 } 576 } 577 578 func TestPodResourceRequests(t *testing.T) { 579 restartAlways := v1.ContainerRestartPolicyAlways 580 testCases := []struct { 581 description string 582 options PodResourcesOptions 583 overhead v1.ResourceList 584 podResizeStatus v1.PodResizeStatus 585 initContainers []v1.Container 586 containers []v1.Container 587 containerStatus []v1.ContainerStatus 588 expectedRequests v1.ResourceList 589 }{ 590 { 591 description: "nil options, larger init container", 592 expectedRequests: v1.ResourceList{ 593 v1.ResourceCPU: resource.MustParse("4"), 594 }, 595 initContainers: []v1.Container{ 596 { 597 Resources: v1.ResourceRequirements{ 598 Requests: v1.ResourceList{ 599 v1.ResourceCPU: resource.MustParse("4"), 600 }, 601 }, 602 }, 603 }, 604 containers: []v1.Container{ 605 { 606 Resources: v1.ResourceRequirements{ 607 Requests: v1.ResourceList{ 608 v1.ResourceCPU: resource.MustParse("1"), 609 }, 610 }, 611 }, 612 }, 613 }, 614 { 615 description: "nil options, larger containers", 616 expectedRequests: v1.ResourceList{ 617 v1.ResourceCPU: resource.MustParse("5"), 618 }, 619 initContainers: []v1.Container{ 620 { 621 Resources: v1.ResourceRequirements{ 622 Requests: v1.ResourceList{ 623 v1.ResourceCPU: resource.MustParse("2"), 624 }, 625 }, 626 }, 627 }, 628 containers: []v1.Container{ 629 { 630 Resources: v1.ResourceRequirements{ 631 Requests: v1.ResourceList{ 632 v1.ResourceCPU: resource.MustParse("2"), 633 }, 634 }, 635 }, 636 { 637 Resources: v1.ResourceRequirements{ 638 Requests: v1.ResourceList{ 639 v1.ResourceCPU: resource.MustParse("3"), 640 }, 641 }, 642 }, 643 }, 644 }, 645 { 646 description: "pod overhead excluded", 647 expectedRequests: v1.ResourceList{ 648 v1.ResourceCPU: resource.MustParse("5"), 649 }, 650 options: PodResourcesOptions{ 651 ExcludeOverhead: true, 652 }, 653 overhead: v1.ResourceList{ 654 v1.ResourceCPU: resource.MustParse("1"), 655 }, 656 initContainers: []v1.Container{ 657 { 658 Resources: v1.ResourceRequirements{ 659 Requests: v1.ResourceList{ 660 v1.ResourceCPU: resource.MustParse("2"), 661 }, 662 }, 663 }, 664 }, 665 containers: []v1.Container{ 666 { 667 Resources: v1.ResourceRequirements{ 668 Requests: v1.ResourceList{ 669 v1.ResourceCPU: resource.MustParse("2"), 670 }, 671 }, 672 }, 673 { 674 Resources: v1.ResourceRequirements{ 675 Requests: v1.ResourceList{ 676 v1.ResourceCPU: resource.MustParse("3"), 677 }, 678 }, 679 }, 680 }, 681 }, 682 { 683 description: "pod overhead included", 684 expectedRequests: v1.ResourceList{ 685 v1.ResourceCPU: resource.MustParse("6"), 686 v1.ResourceMemory: resource.MustParse("1Gi"), 687 }, 688 overhead: v1.ResourceList{ 689 v1.ResourceCPU: resource.MustParse("1"), 690 v1.ResourceMemory: resource.MustParse("1Gi"), 691 }, 692 initContainers: []v1.Container{ 693 { 694 Resources: v1.ResourceRequirements{ 695 Requests: v1.ResourceList{ 696 v1.ResourceCPU: resource.MustParse("2"), 697 }, 698 Limits: v1.ResourceList{ 699 v1.ResourceCPU: resource.MustParse("2"), 700 }, 701 }, 702 }, 703 }, 704 containers: []v1.Container{ 705 { 706 Resources: v1.ResourceRequirements{ 707 Requests: v1.ResourceList{ 708 v1.ResourceCPU: resource.MustParse("2"), 709 }, 710 }, 711 }, 712 { 713 Resources: v1.ResourceRequirements{ 714 Requests: v1.ResourceList{ 715 v1.ResourceCPU: resource.MustParse("3"), 716 }, 717 }, 718 }, 719 }, 720 }, 721 { 722 description: "resized, infeasible", 723 expectedRequests: v1.ResourceList{ 724 v1.ResourceCPU: resource.MustParse("2"), 725 }, 726 podResizeStatus: v1.PodResizeStatusInfeasible, 727 options: PodResourcesOptions{InPlacePodVerticalScalingEnabled: true}, 728 containers: []v1.Container{ 729 { 730 Name: "container-1", 731 Resources: v1.ResourceRequirements{ 732 Requests: v1.ResourceList{ 733 v1.ResourceCPU: resource.MustParse("4"), 734 }, 735 }, 736 }, 737 }, 738 containerStatus: []v1.ContainerStatus{ 739 { 740 Name: "container-1", 741 AllocatedResources: v1.ResourceList{ 742 v1.ResourceCPU: resource.MustParse("2"), 743 }, 744 }, 745 }, 746 }, 747 { 748 description: "resized, no resize status", 749 expectedRequests: v1.ResourceList{ 750 v1.ResourceCPU: resource.MustParse("4"), 751 }, 752 options: PodResourcesOptions{InPlacePodVerticalScalingEnabled: true}, 753 containers: []v1.Container{ 754 { 755 Name: "container-1", 756 Resources: v1.ResourceRequirements{ 757 Requests: v1.ResourceList{ 758 v1.ResourceCPU: resource.MustParse("4"), 759 }, 760 }, 761 }, 762 }, 763 containerStatus: []v1.ContainerStatus{ 764 { 765 Name: "container-1", 766 AllocatedResources: v1.ResourceList{ 767 v1.ResourceCPU: resource.MustParse("2"), 768 }, 769 }, 770 }, 771 }, 772 { 773 description: "resized, infeasible, feature gate disabled", 774 expectedRequests: v1.ResourceList{ 775 v1.ResourceCPU: resource.MustParse("4"), 776 }, 777 podResizeStatus: v1.PodResizeStatusInfeasible, 778 options: PodResourcesOptions{InPlacePodVerticalScalingEnabled: false}, 779 containers: []v1.Container{ 780 { 781 Name: "container-1", 782 Resources: v1.ResourceRequirements{ 783 Requests: v1.ResourceList{ 784 v1.ResourceCPU: resource.MustParse("4"), 785 }, 786 }, 787 }, 788 }, 789 containerStatus: []v1.ContainerStatus{ 790 { 791 Name: "container-1", 792 AllocatedResources: v1.ResourceList{ 793 v1.ResourceCPU: resource.MustParse("2"), 794 }, 795 }, 796 }, 797 }, 798 { 799 description: "restartable init container", 800 expectedRequests: v1.ResourceList{ 801 // restartable init + regular container 802 v1.ResourceCPU: resource.MustParse("2"), 803 }, 804 initContainers: []v1.Container{ 805 { 806 Name: "restartable-init-1", 807 RestartPolicy: &restartAlways, 808 Resources: v1.ResourceRequirements{ 809 Requests: v1.ResourceList{ 810 v1.ResourceCPU: resource.MustParse("1"), 811 }, 812 }, 813 }, 814 }, 815 containers: []v1.Container{ 816 { 817 Name: "container-1", 818 Resources: v1.ResourceRequirements{ 819 Requests: v1.ResourceList{ 820 v1.ResourceCPU: resource.MustParse("1"), 821 }, 822 }, 823 }, 824 }, 825 }, 826 { 827 description: "multiple restartable init containers", 828 expectedRequests: v1.ResourceList{ 829 // max(5, restartable init containers(3+2+1) + regular(1)) = 7 830 v1.ResourceCPU: resource.MustParse("7"), 831 }, 832 initContainers: []v1.Container{ 833 { 834 Name: "init-1", 835 Resources: v1.ResourceRequirements{ 836 Requests: v1.ResourceList{ 837 v1.ResourceCPU: resource.MustParse("5"), 838 }, 839 }, 840 }, 841 { 842 Name: "restartable-init-1", 843 RestartPolicy: &restartAlways, 844 Resources: v1.ResourceRequirements{ 845 Requests: v1.ResourceList{ 846 v1.ResourceCPU: resource.MustParse("1"), 847 }, 848 }, 849 }, 850 { 851 Name: "restartable-init-2", 852 RestartPolicy: &restartAlways, 853 Resources: v1.ResourceRequirements{ 854 Requests: v1.ResourceList{ 855 v1.ResourceCPU: resource.MustParse("2"), 856 }, 857 }, 858 }, 859 { 860 Name: "restartable-init-3", 861 RestartPolicy: &restartAlways, 862 Resources: v1.ResourceRequirements{ 863 Requests: v1.ResourceList{ 864 v1.ResourceCPU: resource.MustParse("3"), 865 }, 866 }, 867 }, 868 }, 869 containers: []v1.Container{ 870 { 871 Name: "container-1", 872 Resources: v1.ResourceRequirements{ 873 Requests: v1.ResourceList{ 874 v1.ResourceCPU: resource.MustParse("1"), 875 }, 876 }, 877 }, 878 }, 879 }, 880 { 881 description: "multiple restartable and regular init containers", 882 expectedRequests: v1.ResourceList{ 883 // init-2 requires 5 + the previously running restartable init 884 // containers(1+2) = 8, the restartable init container that starts 885 // after it doesn't count 886 v1.ResourceCPU: resource.MustParse("8"), 887 }, 888 initContainers: []v1.Container{ 889 { 890 Name: "init-1", 891 Resources: v1.ResourceRequirements{ 892 Requests: v1.ResourceList{ 893 v1.ResourceCPU: resource.MustParse("5"), 894 }, 895 }, 896 }, 897 { 898 Name: "restartable-init-1", 899 RestartPolicy: &restartAlways, 900 Resources: v1.ResourceRequirements{ 901 Requests: v1.ResourceList{ 902 v1.ResourceCPU: resource.MustParse("1"), 903 }, 904 }, 905 }, 906 { 907 Name: "restartable-init-2", 908 RestartPolicy: &restartAlways, 909 Resources: v1.ResourceRequirements{ 910 Requests: v1.ResourceList{ 911 v1.ResourceCPU: resource.MustParse("2"), 912 }, 913 }, 914 }, 915 { 916 Name: "init-2", 917 Resources: v1.ResourceRequirements{ 918 Requests: v1.ResourceList{ 919 v1.ResourceCPU: resource.MustParse("5"), 920 }, 921 }, 922 }, 923 { 924 Name: "restartable-init-3", 925 RestartPolicy: &restartAlways, 926 Resources: v1.ResourceRequirements{ 927 Requests: v1.ResourceList{ 928 v1.ResourceCPU: resource.MustParse("3"), 929 }, 930 }, 931 }, 932 }, 933 containers: []v1.Container{ 934 { 935 Name: "container-1", 936 Resources: v1.ResourceRequirements{ 937 Requests: v1.ResourceList{ 938 v1.ResourceCPU: resource.MustParse("1"), 939 }, 940 }, 941 }, 942 }, 943 }, 944 { 945 description: "restartable-init, init and regular", 946 expectedRequests: v1.ResourceList{ 947 v1.ResourceCPU: resource.MustParse("210"), 948 }, 949 initContainers: []v1.Container{ 950 { 951 Name: "restartable-init-1", 952 RestartPolicy: &restartAlways, 953 Resources: v1.ResourceRequirements{ 954 Requests: v1.ResourceList{ 955 v1.ResourceCPU: resource.MustParse("10"), 956 }, 957 }, 958 }, 959 { 960 Name: "init-1", 961 Resources: v1.ResourceRequirements{ 962 Requests: v1.ResourceList{ 963 v1.ResourceCPU: resource.MustParse("200"), 964 }, 965 }, 966 }, 967 }, 968 containers: []v1.Container{ 969 { 970 Name: "container-1", 971 Resources: v1.ResourceRequirements{ 972 Requests: v1.ResourceList{ 973 v1.ResourceCPU: resource.MustParse("100"), 974 }, 975 }, 976 }, 977 }, 978 }, 979 } 980 for _, tc := range testCases { 981 t.Run(tc.description, func(t *testing.T) { 982 p := &v1.Pod{ 983 Spec: v1.PodSpec{ 984 Containers: tc.containers, 985 InitContainers: tc.initContainers, 986 Overhead: tc.overhead, 987 }, 988 Status: v1.PodStatus{ 989 ContainerStatuses: tc.containerStatus, 990 Resize: tc.podResizeStatus, 991 }, 992 } 993 request := PodRequests(p, tc.options) 994 if !resourcesEqual(tc.expectedRequests, request) { 995 t.Errorf("[%s] expected requests = %v, got %v", tc.description, tc.expectedRequests, request) 996 } 997 }) 998 } 999 } 1000 1001 func TestPodResourceRequestsReuse(t *testing.T) { 1002 expectedRequests := v1.ResourceList{ 1003 v1.ResourceCPU: resource.MustParse("1"), 1004 } 1005 p := &v1.Pod{ 1006 Spec: v1.PodSpec{ 1007 Containers: []v1.Container{ 1008 { 1009 Resources: v1.ResourceRequirements{ 1010 Requests: expectedRequests, 1011 }, 1012 }, 1013 }, 1014 }, 1015 } 1016 1017 opts := PodResourcesOptions{ 1018 Reuse: v1.ResourceList{ 1019 v1.ResourceCPU: resource.MustParse("25"), 1020 }, 1021 } 1022 requests := PodRequests(p, opts) 1023 1024 if !resourcesEqual(expectedRequests, requests) { 1025 t.Errorf("expected requests = %v, got %v", expectedRequests, requests) 1026 } 1027 1028 // should re-use the maps we passed in 1029 if !resourcesEqual(expectedRequests, opts.Reuse) { 1030 t.Errorf("expected to re-use the requests") 1031 } 1032 } 1033 1034 func TestPodResourceLimits(t *testing.T) { 1035 restartAlways := v1.ContainerRestartPolicyAlways 1036 testCases := []struct { 1037 description string 1038 options PodResourcesOptions 1039 overhead v1.ResourceList 1040 initContainers []v1.Container 1041 containers []v1.Container 1042 expectedLimits v1.ResourceList 1043 }{ 1044 { 1045 description: "nil options, larger init container", 1046 expectedLimits: v1.ResourceList{ 1047 v1.ResourceCPU: resource.MustParse("4"), 1048 }, 1049 initContainers: []v1.Container{ 1050 { 1051 Resources: v1.ResourceRequirements{ 1052 Limits: v1.ResourceList{ 1053 v1.ResourceCPU: resource.MustParse("4"), 1054 }, 1055 }, 1056 }, 1057 }, 1058 containers: []v1.Container{ 1059 { 1060 Resources: v1.ResourceRequirements{ 1061 Limits: v1.ResourceList{ 1062 v1.ResourceCPU: resource.MustParse("1"), 1063 }, 1064 }, 1065 }, 1066 }, 1067 }, 1068 { 1069 description: "nil options, larger containers", 1070 expectedLimits: v1.ResourceList{ 1071 v1.ResourceCPU: resource.MustParse("5"), 1072 }, 1073 initContainers: []v1.Container{ 1074 { 1075 Resources: v1.ResourceRequirements{ 1076 Limits: v1.ResourceList{ 1077 v1.ResourceCPU: resource.MustParse("2"), 1078 }, 1079 }, 1080 }, 1081 }, 1082 containers: []v1.Container{ 1083 { 1084 Resources: v1.ResourceRequirements{ 1085 Limits: v1.ResourceList{ 1086 v1.ResourceCPU: resource.MustParse("2"), 1087 }, 1088 }, 1089 }, 1090 { 1091 Resources: v1.ResourceRequirements{ 1092 Limits: v1.ResourceList{ 1093 v1.ResourceCPU: resource.MustParse("3"), 1094 }, 1095 }, 1096 }, 1097 }, 1098 }, 1099 { 1100 description: "pod overhead excluded", 1101 expectedLimits: v1.ResourceList{ 1102 v1.ResourceCPU: resource.MustParse("5"), 1103 }, 1104 options: PodResourcesOptions{ 1105 ExcludeOverhead: true, 1106 }, 1107 overhead: v1.ResourceList{ 1108 v1.ResourceCPU: resource.MustParse("1"), 1109 }, 1110 initContainers: []v1.Container{ 1111 { 1112 Resources: v1.ResourceRequirements{ 1113 Limits: v1.ResourceList{ 1114 v1.ResourceCPU: resource.MustParse("2"), 1115 }, 1116 }, 1117 }, 1118 }, 1119 containers: []v1.Container{ 1120 { 1121 Resources: v1.ResourceRequirements{ 1122 Limits: v1.ResourceList{ 1123 v1.ResourceCPU: resource.MustParse("2"), 1124 }, 1125 }, 1126 }, 1127 { 1128 Resources: v1.ResourceRequirements{ 1129 Limits: v1.ResourceList{ 1130 v1.ResourceCPU: resource.MustParse("3"), 1131 }, 1132 }, 1133 }, 1134 }, 1135 }, 1136 { 1137 description: "pod overhead included", 1138 overhead: v1.ResourceList{ 1139 v1.ResourceCPU: resource.MustParse("1"), 1140 v1.ResourceMemory: resource.MustParse("1Gi"), 1141 }, 1142 expectedLimits: v1.ResourceList{ 1143 v1.ResourceCPU: resource.MustParse("6"), 1144 // overhead is only added to non-zero limits, so there will be no expected memory limit 1145 }, 1146 initContainers: []v1.Container{ 1147 { 1148 Resources: v1.ResourceRequirements{ 1149 Limits: v1.ResourceList{ 1150 v1.ResourceCPU: resource.MustParse("2"), 1151 }, 1152 }, 1153 }, 1154 }, 1155 containers: []v1.Container{ 1156 { 1157 Resources: v1.ResourceRequirements{ 1158 Limits: v1.ResourceList{ 1159 v1.ResourceCPU: resource.MustParse("2"), 1160 }, 1161 }, 1162 }, 1163 { 1164 Resources: v1.ResourceRequirements{ 1165 Limits: v1.ResourceList{ 1166 v1.ResourceCPU: resource.MustParse("3"), 1167 }, 1168 }, 1169 }, 1170 }, 1171 }, 1172 { 1173 description: "no limited containers should result in no limits for the pod", 1174 expectedLimits: v1.ResourceList{}, 1175 initContainers: []v1.Container{}, 1176 containers: []v1.Container{ 1177 { 1178 // Unlimited container 1179 }, 1180 }, 1181 }, 1182 { 1183 description: "one limited and one unlimited container should result in the limited container's limits for the pod", 1184 expectedLimits: v1.ResourceList{ 1185 v1.ResourceCPU: resource.MustParse("2"), 1186 v1.ResourceMemory: resource.MustParse("2Gi"), 1187 }, 1188 initContainers: []v1.Container{}, 1189 containers: []v1.Container{ 1190 { 1191 Resources: v1.ResourceRequirements{ 1192 Limits: v1.ResourceList{ 1193 v1.ResourceCPU: resource.MustParse("2"), 1194 v1.ResourceMemory: resource.MustParse("2Gi"), 1195 }, 1196 }, 1197 }, 1198 { 1199 // Unlimited container 1200 }, 1201 }, 1202 }, 1203 { 1204 description: "one limited and one unlimited init container should result in the limited init container's limits for the pod", 1205 expectedLimits: v1.ResourceList{ 1206 v1.ResourceCPU: resource.MustParse("2"), 1207 v1.ResourceMemory: resource.MustParse("2Gi"), 1208 }, 1209 initContainers: []v1.Container{ 1210 { 1211 Resources: v1.ResourceRequirements{ 1212 Limits: v1.ResourceList{ 1213 v1.ResourceCPU: resource.MustParse("2"), 1214 v1.ResourceMemory: resource.MustParse("2Gi"), 1215 }, 1216 }, 1217 }, 1218 { 1219 // Unlimited init container 1220 }, 1221 }, 1222 containers: []v1.Container{ 1223 { 1224 Resources: v1.ResourceRequirements{ 1225 Limits: v1.ResourceList{ 1226 v1.ResourceCPU: resource.MustParse("1"), 1227 v1.ResourceMemory: resource.MustParse("1Gi"), 1228 }, 1229 }, 1230 }, 1231 }, 1232 }, 1233 { 1234 description: "restartable init container", 1235 expectedLimits: v1.ResourceList{ 1236 // restartable init + regular container 1237 v1.ResourceCPU: resource.MustParse("2"), 1238 }, 1239 initContainers: []v1.Container{ 1240 { 1241 Name: "restartable-init-1", 1242 RestartPolicy: &restartAlways, 1243 Resources: v1.ResourceRequirements{ 1244 Limits: v1.ResourceList{ 1245 v1.ResourceCPU: resource.MustParse("1"), 1246 }, 1247 }, 1248 }, 1249 }, 1250 containers: []v1.Container{ 1251 { 1252 Name: "container-1", 1253 Resources: v1.ResourceRequirements{ 1254 Limits: v1.ResourceList{ 1255 v1.ResourceCPU: resource.MustParse("1"), 1256 }, 1257 }, 1258 }, 1259 }, 1260 }, 1261 { 1262 description: "multiple restartable init containers", 1263 expectedLimits: v1.ResourceList{ 1264 // max(5, restartable init containers(3+2+1) + regular(1)) = 7 1265 v1.ResourceCPU: resource.MustParse("7"), 1266 }, 1267 initContainers: []v1.Container{ 1268 { 1269 Name: "init-1", 1270 Resources: v1.ResourceRequirements{ 1271 Limits: v1.ResourceList{ 1272 v1.ResourceCPU: resource.MustParse("5"), 1273 }, 1274 }, 1275 }, 1276 { 1277 Name: "restartable-init-1", 1278 RestartPolicy: &restartAlways, 1279 Resources: v1.ResourceRequirements{ 1280 Limits: v1.ResourceList{ 1281 v1.ResourceCPU: resource.MustParse("1"), 1282 }, 1283 }, 1284 }, 1285 { 1286 Name: "restartable-init-2", 1287 RestartPolicy: &restartAlways, 1288 Resources: v1.ResourceRequirements{ 1289 Limits: v1.ResourceList{ 1290 v1.ResourceCPU: resource.MustParse("2"), 1291 }, 1292 }, 1293 }, 1294 { 1295 Name: "restartable-init-3", 1296 RestartPolicy: &restartAlways, 1297 Resources: v1.ResourceRequirements{ 1298 Limits: v1.ResourceList{ 1299 v1.ResourceCPU: resource.MustParse("3"), 1300 }, 1301 }, 1302 }, 1303 }, 1304 containers: []v1.Container{ 1305 { 1306 Name: "container-1", 1307 Resources: v1.ResourceRequirements{ 1308 Limits: v1.ResourceList{ 1309 v1.ResourceCPU: resource.MustParse("1"), 1310 }, 1311 }, 1312 }, 1313 }, 1314 }, 1315 { 1316 description: "multiple restartable and regular init containers", 1317 expectedLimits: v1.ResourceList{ 1318 // init-2 requires 5 + the previously running restartable init 1319 // containers(1+2) = 8, the restartable init container that starts 1320 // after it doesn't count 1321 v1.ResourceCPU: resource.MustParse("8"), 1322 }, 1323 initContainers: []v1.Container{ 1324 { 1325 Name: "init-1", 1326 Resources: v1.ResourceRequirements{ 1327 Limits: v1.ResourceList{ 1328 v1.ResourceCPU: resource.MustParse("5"), 1329 }, 1330 }, 1331 }, 1332 { 1333 Name: "restartable-init-1", 1334 RestartPolicy: &restartAlways, 1335 Resources: v1.ResourceRequirements{ 1336 Limits: v1.ResourceList{ 1337 v1.ResourceCPU: resource.MustParse("1"), 1338 }, 1339 }, 1340 }, 1341 { 1342 Name: "restartable-init-2", 1343 RestartPolicy: &restartAlways, 1344 Resources: v1.ResourceRequirements{ 1345 Limits: v1.ResourceList{ 1346 v1.ResourceCPU: resource.MustParse("2"), 1347 }, 1348 }, 1349 }, 1350 { 1351 Name: "init-2", 1352 Resources: v1.ResourceRequirements{ 1353 Limits: v1.ResourceList{ 1354 v1.ResourceCPU: resource.MustParse("5"), 1355 }, 1356 }, 1357 }, 1358 { 1359 Name: "restartable-init-3", 1360 RestartPolicy: &restartAlways, 1361 Resources: v1.ResourceRequirements{ 1362 Limits: v1.ResourceList{ 1363 v1.ResourceCPU: resource.MustParse("3"), 1364 }, 1365 }, 1366 }, 1367 }, 1368 containers: []v1.Container{ 1369 { 1370 Name: "container-1", 1371 Resources: v1.ResourceRequirements{ 1372 Limits: v1.ResourceList{ 1373 v1.ResourceCPU: resource.MustParse("1"), 1374 }, 1375 }, 1376 }, 1377 }, 1378 }, 1379 { 1380 description: "restartable-init, init and regular", 1381 expectedLimits: v1.ResourceList{ 1382 v1.ResourceCPU: resource.MustParse("210"), 1383 }, 1384 initContainers: []v1.Container{ 1385 { 1386 Name: "restartable-init-1", 1387 RestartPolicy: &restartAlways, 1388 Resources: v1.ResourceRequirements{ 1389 Limits: v1.ResourceList{ 1390 v1.ResourceCPU: resource.MustParse("10"), 1391 }, 1392 }, 1393 }, 1394 { 1395 Name: "init-1", 1396 Resources: v1.ResourceRequirements{ 1397 Limits: v1.ResourceList{ 1398 v1.ResourceCPU: resource.MustParse("200"), 1399 }, 1400 }, 1401 }, 1402 }, 1403 containers: []v1.Container{ 1404 { 1405 Name: "container-1", 1406 Resources: v1.ResourceRequirements{ 1407 Limits: v1.ResourceList{ 1408 v1.ResourceCPU: resource.MustParse("100"), 1409 }, 1410 }, 1411 }, 1412 }, 1413 }, 1414 } 1415 for _, tc := range testCases { 1416 t.Run(tc.description, func(t *testing.T) { 1417 p := &v1.Pod{ 1418 Spec: v1.PodSpec{ 1419 Containers: tc.containers, 1420 InitContainers: tc.initContainers, 1421 Overhead: tc.overhead, 1422 }, 1423 } 1424 limits := PodLimits(p, tc.options) 1425 if !resourcesEqual(tc.expectedLimits, limits) { 1426 t.Errorf("[%s] expected limits = %v, got %v", tc.description, tc.expectedLimits, limits) 1427 } 1428 }) 1429 } 1430 } 1431 1432 func resourcesEqual(lhs, rhs v1.ResourceList) bool { 1433 if len(lhs) != len(rhs) { 1434 return false 1435 } 1436 for name, lhsv := range lhs { 1437 rhsv, ok := rhs[name] 1438 if !ok { 1439 return false 1440 } 1441 if !lhsv.Equal(rhsv) { 1442 return false 1443 } 1444 } 1445 return true 1446 }