k8s.io/kubernetes@v1.29.3/pkg/registry/core/pod/strategy_test.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package pod 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "net/url" 24 "reflect" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/google/go-cmp/cmp/cmpopts" 29 apiv1 "k8s.io/api/core/v1" 30 "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/fields" 34 "k8s.io/apimachinery/pkg/labels" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/types" 37 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 38 utilfeature "k8s.io/apiserver/pkg/util/feature" 39 "k8s.io/client-go/tools/cache" 40 featuregatetesting "k8s.io/component-base/featuregate/testing" 41 utilpointer "k8s.io/utils/pointer" 42 43 apitesting "k8s.io/kubernetes/pkg/api/testing" 44 api "k8s.io/kubernetes/pkg/apis/core" 45 "k8s.io/kubernetes/pkg/features" 46 "k8s.io/kubernetes/pkg/kubelet/client" 47 48 // ensure types are installed 49 _ "k8s.io/kubernetes/pkg/apis/core/install" 50 ) 51 52 func TestMatchPod(t *testing.T) { 53 testCases := []struct { 54 in *api.Pod 55 fieldSelector fields.Selector 56 expectMatch bool 57 }{ 58 { 59 in: &api.Pod{ 60 Spec: api.PodSpec{NodeName: "nodeA"}, 61 }, 62 fieldSelector: fields.ParseSelectorOrDie("spec.nodeName=nodeA"), 63 expectMatch: true, 64 }, 65 { 66 in: &api.Pod{ 67 Spec: api.PodSpec{NodeName: "nodeB"}, 68 }, 69 fieldSelector: fields.ParseSelectorOrDie("spec.nodeName=nodeA"), 70 expectMatch: false, 71 }, 72 { 73 in: &api.Pod{ 74 Spec: api.PodSpec{RestartPolicy: api.RestartPolicyAlways}, 75 }, 76 fieldSelector: fields.ParseSelectorOrDie("spec.restartPolicy=Always"), 77 expectMatch: true, 78 }, 79 { 80 in: &api.Pod{ 81 Spec: api.PodSpec{RestartPolicy: api.RestartPolicyAlways}, 82 }, 83 fieldSelector: fields.ParseSelectorOrDie("spec.restartPolicy=Never"), 84 expectMatch: false, 85 }, 86 { 87 in: &api.Pod{ 88 Spec: api.PodSpec{SchedulerName: "scheduler1"}, 89 }, 90 fieldSelector: fields.ParseSelectorOrDie("spec.schedulerName=scheduler1"), 91 expectMatch: true, 92 }, 93 { 94 in: &api.Pod{ 95 Spec: api.PodSpec{SchedulerName: "scheduler1"}, 96 }, 97 fieldSelector: fields.ParseSelectorOrDie("spec.schedulerName=scheduler2"), 98 expectMatch: false, 99 }, 100 { 101 in: &api.Pod{ 102 Spec: api.PodSpec{ServiceAccountName: "serviceAccount1"}, 103 }, 104 fieldSelector: fields.ParseSelectorOrDie("spec.serviceAccountName=serviceAccount1"), 105 expectMatch: true, 106 }, 107 { 108 in: &api.Pod{ 109 Spec: api.PodSpec{SchedulerName: "serviceAccount1"}, 110 }, 111 fieldSelector: fields.ParseSelectorOrDie("spec.serviceAccountName=serviceAccount2"), 112 expectMatch: false, 113 }, 114 { 115 in: &api.Pod{ 116 Status: api.PodStatus{Phase: api.PodRunning}, 117 }, 118 fieldSelector: fields.ParseSelectorOrDie("status.phase=Running"), 119 expectMatch: true, 120 }, 121 { 122 in: &api.Pod{ 123 Status: api.PodStatus{Phase: api.PodRunning}, 124 }, 125 fieldSelector: fields.ParseSelectorOrDie("status.phase=Pending"), 126 expectMatch: false, 127 }, 128 { 129 in: &api.Pod{ 130 Status: api.PodStatus{ 131 PodIPs: []api.PodIP{ 132 {IP: "1.2.3.4"}, 133 }, 134 }, 135 }, 136 fieldSelector: fields.ParseSelectorOrDie("status.podIP=1.2.3.4"), 137 expectMatch: true, 138 }, 139 { 140 in: &api.Pod{ 141 Status: api.PodStatus{ 142 PodIPs: []api.PodIP{ 143 {IP: "1.2.3.4"}, 144 }, 145 }, 146 }, 147 fieldSelector: fields.ParseSelectorOrDie("status.podIP=4.3.2.1"), 148 expectMatch: false, 149 }, 150 { 151 in: &api.Pod{ 152 Status: api.PodStatus{NominatedNodeName: "node1"}, 153 }, 154 fieldSelector: fields.ParseSelectorOrDie("status.nominatedNodeName=node1"), 155 expectMatch: true, 156 }, 157 { 158 in: &api.Pod{ 159 Status: api.PodStatus{NominatedNodeName: "node1"}, 160 }, 161 fieldSelector: fields.ParseSelectorOrDie("status.nominatedNodeName=node2"), 162 expectMatch: false, 163 }, 164 { 165 in: &api.Pod{ 166 Status: api.PodStatus{ 167 PodIPs: []api.PodIP{ 168 {IP: "2001:db8::"}, 169 }, 170 }, 171 }, 172 fieldSelector: fields.ParseSelectorOrDie("status.podIP=2001:db8::"), 173 expectMatch: true, 174 }, 175 { 176 in: &api.Pod{ 177 Status: api.PodStatus{ 178 PodIPs: []api.PodIP{ 179 {IP: "2001:db8::"}, 180 }, 181 }, 182 }, 183 fieldSelector: fields.ParseSelectorOrDie("status.podIP=2001:db7::"), 184 expectMatch: false, 185 }, 186 { 187 in: &api.Pod{ 188 Spec: api.PodSpec{ 189 SecurityContext: &api.PodSecurityContext{ 190 HostNetwork: true, 191 }, 192 }, 193 }, 194 fieldSelector: fields.ParseSelectorOrDie("spec.hostNetwork=true"), 195 expectMatch: true, 196 }, 197 { 198 in: &api.Pod{ 199 Spec: api.PodSpec{ 200 SecurityContext: &api.PodSecurityContext{ 201 HostNetwork: true, 202 }, 203 }, 204 }, 205 fieldSelector: fields.ParseSelectorOrDie("spec.hostNetwork=false"), 206 expectMatch: false, 207 }, 208 { 209 in: &api.Pod{ 210 Spec: api.PodSpec{ 211 SecurityContext: &api.PodSecurityContext{ 212 HostNetwork: false, 213 }, 214 }, 215 }, 216 fieldSelector: fields.ParseSelectorOrDie("spec.hostNetwork=false"), 217 expectMatch: true, 218 }, 219 { 220 in: &api.Pod{ 221 Spec: api.PodSpec{}, 222 }, 223 fieldSelector: fields.ParseSelectorOrDie("spec.hostNetwork=false"), 224 expectMatch: true, 225 }, 226 { 227 in: &api.Pod{ 228 Spec: api.PodSpec{}, 229 }, 230 fieldSelector: fields.ParseSelectorOrDie("spec.hostNetwork=true"), 231 expectMatch: false, 232 }, 233 } 234 for _, testCase := range testCases { 235 m := MatchPod(labels.Everything(), testCase.fieldSelector) 236 result, err := m.Matches(testCase.in) 237 if err != nil { 238 t.Errorf("Unexpected error %v", err) 239 } 240 if result != testCase.expectMatch { 241 t.Errorf("Result %v, Expected %v, Selector: %v, Pod: %v", result, testCase.expectMatch, testCase.fieldSelector.String(), testCase.in) 242 } 243 } 244 } 245 246 func getResourceList(cpu, memory string) api.ResourceList { 247 res := api.ResourceList{} 248 if cpu != "" { 249 res[api.ResourceCPU] = resource.MustParse(cpu) 250 } 251 if memory != "" { 252 res[api.ResourceMemory] = resource.MustParse(memory) 253 } 254 return res 255 } 256 257 func getResourceRequirements(requests, limits api.ResourceList) api.ResourceRequirements { 258 res := api.ResourceRequirements{} 259 res.Requests = requests 260 res.Limits = limits 261 return res 262 } 263 264 func newContainer(name string, requests api.ResourceList, limits api.ResourceList) api.Container { 265 return api.Container{ 266 Name: name, 267 Resources: getResourceRequirements(requests, limits), 268 } 269 } 270 271 func newPod(name string, containers []api.Container) *api.Pod { 272 return &api.Pod{ 273 ObjectMeta: metav1.ObjectMeta{ 274 Name: name, 275 }, 276 Spec: api.PodSpec{ 277 Containers: containers, 278 }, 279 } 280 } 281 282 func TestGetPodQOS(t *testing.T) { 283 testCases := []struct { 284 pod *api.Pod 285 expected api.PodQOSClass 286 }{ 287 { 288 pod: newPod("guaranteed", []api.Container{ 289 newContainer("guaranteed", getResourceList("100m", "100Mi"), getResourceList("100m", "100Mi")), 290 }), 291 expected: api.PodQOSGuaranteed, 292 }, 293 { 294 pod: newPod("best-effort", []api.Container{ 295 newContainer("best-effort", getResourceList("", ""), getResourceList("", "")), 296 }), 297 expected: api.PodQOSBestEffort, 298 }, 299 { 300 pod: newPod("burstable", []api.Container{ 301 newContainer("burstable", getResourceList("100m", "100Mi"), getResourceList("", "")), 302 }), 303 expected: api.PodQOSBurstable, 304 }, 305 } 306 for id, testCase := range testCases { 307 Strategy.PrepareForCreate(genericapirequest.NewContext(), testCase.pod) 308 actual := testCase.pod.Status.QOSClass 309 if actual != testCase.expected { 310 t.Errorf("[%d]: invalid qos pod %s, expected: %s, actual: %s", id, testCase.pod.Name, testCase.expected, actual) 311 } 312 } 313 } 314 315 func TestWaitingForGatesCondition(t *testing.T) { 316 tests := []struct { 317 name string 318 pod *api.Pod 319 featureEnabled bool 320 want api.PodCondition 321 }{ 322 { 323 name: "pod without .spec.schedulingGates, feature disabled", 324 pod: &api.Pod{}, 325 featureEnabled: false, 326 want: api.PodCondition{}, 327 }, 328 { 329 name: "pod without .spec.schedulingGates, feature enabled", 330 pod: &api.Pod{}, 331 featureEnabled: true, 332 want: api.PodCondition{}, 333 }, 334 { 335 name: "pod with .spec.schedulingGates, feature disabled", 336 pod: &api.Pod{ 337 Spec: api.PodSpec{ 338 SchedulingGates: []api.PodSchedulingGate{{Name: "foo"}}, 339 }, 340 }, 341 featureEnabled: false, 342 want: api.PodCondition{}, 343 }, 344 { 345 name: "pod with .spec.schedulingGates, feature enabled", 346 pod: &api.Pod{ 347 Spec: api.PodSpec{ 348 SchedulingGates: []api.PodSchedulingGate{{Name: "foo"}}, 349 }, 350 }, 351 featureEnabled: true, 352 want: api.PodCondition{ 353 Type: api.PodScheduled, 354 Status: api.ConditionFalse, 355 Reason: apiv1.PodReasonSchedulingGated, 356 Message: "Scheduling is blocked due to non-empty scheduling gates", 357 }, 358 }, 359 } 360 361 for _, tt := range tests { 362 t.Run(tt.name, func(t *testing.T) { 363 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodSchedulingReadiness, tt.featureEnabled)() 364 365 Strategy.PrepareForCreate(genericapirequest.NewContext(), tt.pod) 366 var got api.PodCondition 367 for _, condition := range tt.pod.Status.Conditions { 368 if condition.Type == api.PodScheduled { 369 got = condition 370 break 371 } 372 } 373 374 if diff := cmp.Diff(tt.want, got); diff != "" { 375 t.Errorf("unexpected field errors (-want, +got):\n%s", diff) 376 } 377 }) 378 } 379 } 380 381 func TestCheckGracefulDelete(t *testing.T) { 382 defaultGracePeriod := int64(30) 383 tcs := []struct { 384 name string 385 pod *api.Pod 386 deleteGracePeriod *int64 387 gracePeriod int64 388 }{ 389 { 390 name: "in pending phase with has node name", 391 pod: &api.Pod{ 392 Spec: api.PodSpec{NodeName: "something"}, 393 Status: api.PodStatus{Phase: api.PodPending}, 394 }, 395 deleteGracePeriod: &defaultGracePeriod, 396 gracePeriod: defaultGracePeriod, 397 }, 398 { 399 name: "in failed phase with has node name", 400 pod: &api.Pod{ 401 Spec: api.PodSpec{NodeName: "something"}, 402 Status: api.PodStatus{Phase: api.PodFailed}, 403 }, 404 deleteGracePeriod: &defaultGracePeriod, 405 gracePeriod: 0, 406 }, 407 { 408 name: "in failed phase", 409 pod: &api.Pod{ 410 Spec: api.PodSpec{}, 411 Status: api.PodStatus{Phase: api.PodPending}, 412 }, 413 deleteGracePeriod: &defaultGracePeriod, 414 gracePeriod: 0, 415 }, 416 { 417 name: "in succeeded phase", 418 pod: &api.Pod{ 419 Spec: api.PodSpec{}, 420 Status: api.PodStatus{Phase: api.PodSucceeded}, 421 }, 422 deleteGracePeriod: &defaultGracePeriod, 423 gracePeriod: 0, 424 }, 425 { 426 name: "no phase", 427 pod: &api.Pod{ 428 Spec: api.PodSpec{}, 429 Status: api.PodStatus{}, 430 }, 431 deleteGracePeriod: &defaultGracePeriod, 432 gracePeriod: 0, 433 }, 434 { 435 name: "has negative grace period", 436 pod: &api.Pod{ 437 Spec: api.PodSpec{ 438 NodeName: "something", 439 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 440 }, 441 Status: api.PodStatus{}, 442 }, 443 gracePeriod: 1, 444 }, 445 } 446 for _, tc := range tcs { 447 t.Run(tc.name, func(t *testing.T) { 448 out := &metav1.DeleteOptions{} 449 if tc.deleteGracePeriod != nil { 450 out.GracePeriodSeconds = utilpointer.Int64(*tc.deleteGracePeriod) 451 } 452 Strategy.CheckGracefulDelete(genericapirequest.NewContext(), tc.pod, out) 453 if out.GracePeriodSeconds == nil { 454 t.Errorf("out grace period was nil but supposed to be %v", tc.gracePeriod) 455 } 456 if *(out.GracePeriodSeconds) != tc.gracePeriod { 457 t.Errorf("out grace period was %v but was expected to be %v", *out, tc.gracePeriod) 458 } 459 }) 460 } 461 } 462 463 type mockPodGetter struct { 464 pod *api.Pod 465 } 466 467 func (g mockPodGetter) Get(context.Context, string, *metav1.GetOptions) (runtime.Object, error) { 468 return g.pod, nil 469 } 470 471 func TestCheckLogLocation(t *testing.T) { 472 ctx := genericapirequest.NewDefaultContext() 473 fakePodName := "test" 474 tcs := []struct { 475 name string 476 in *api.Pod 477 opts *api.PodLogOptions 478 expectedErr error 479 expectedTransport http.RoundTripper 480 }{ 481 { 482 name: "simple", 483 in: &api.Pod{ 484 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 485 Spec: api.PodSpec{ 486 Containers: []api.Container{ 487 {Name: "mycontainer"}, 488 }, 489 NodeName: "foo", 490 }, 491 Status: api.PodStatus{}, 492 }, 493 opts: &api.PodLogOptions{}, 494 expectedErr: nil, 495 expectedTransport: fakeSecureRoundTripper, 496 }, 497 { 498 name: "insecure", 499 in: &api.Pod{ 500 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 501 Spec: api.PodSpec{ 502 Containers: []api.Container{ 503 {Name: "mycontainer"}, 504 }, 505 NodeName: "foo", 506 }, 507 Status: api.PodStatus{}, 508 }, 509 opts: &api.PodLogOptions{ 510 InsecureSkipTLSVerifyBackend: true, 511 }, 512 expectedErr: nil, 513 expectedTransport: fakeInsecureRoundTripper, 514 }, 515 { 516 name: "missing container", 517 in: &api.Pod{ 518 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 519 Spec: api.PodSpec{}, 520 Status: api.PodStatus{}, 521 }, 522 opts: &api.PodLogOptions{}, 523 expectedErr: errors.NewBadRequest("a container name must be specified for pod test"), 524 expectedTransport: nil, 525 }, 526 { 527 name: "choice of two containers", 528 in: &api.Pod{ 529 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 530 Spec: api.PodSpec{ 531 Containers: []api.Container{ 532 {Name: "container1"}, 533 {Name: "container2"}, 534 }, 535 }, 536 Status: api.PodStatus{}, 537 }, 538 opts: &api.PodLogOptions{}, 539 expectedErr: errors.NewBadRequest("a container name must be specified for pod test, choose one of: [container1 container2]"), 540 expectedTransport: nil, 541 }, 542 { 543 name: "initcontainers", 544 in: &api.Pod{ 545 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 546 Spec: api.PodSpec{ 547 Containers: []api.Container{ 548 {Name: "container1"}, 549 {Name: "container2"}, 550 }, 551 InitContainers: []api.Container{ 552 {Name: "initcontainer1"}, 553 }, 554 }, 555 Status: api.PodStatus{}, 556 }, 557 opts: &api.PodLogOptions{}, 558 expectedErr: errors.NewBadRequest("a container name must be specified for pod test, choose one of: [initcontainer1 container1 container2]"), 559 expectedTransport: nil, 560 }, 561 { 562 in: &api.Pod{ 563 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 564 Spec: api.PodSpec{ 565 Containers: []api.Container{ 566 {Name: "container1"}, 567 {Name: "container2"}, 568 }, 569 InitContainers: []api.Container{ 570 {Name: "initcontainer1"}, 571 }, 572 EphemeralContainers: []api.EphemeralContainer{ 573 {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "debugger"}}, 574 }, 575 }, 576 Status: api.PodStatus{}, 577 }, 578 opts: &api.PodLogOptions{}, 579 expectedErr: errors.NewBadRequest("a container name must be specified for pod test, choose one of: [initcontainer1 container1 container2 debugger]"), 580 expectedTransport: nil, 581 }, 582 { 583 name: "bad container", 584 in: &api.Pod{ 585 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 586 Spec: api.PodSpec{ 587 Containers: []api.Container{ 588 {Name: "container1"}, 589 {Name: "container2"}, 590 }, 591 }, 592 Status: api.PodStatus{}, 593 }, 594 opts: &api.PodLogOptions{ 595 Container: "unknown", 596 }, 597 expectedErr: errors.NewBadRequest("container unknown is not valid for pod test"), 598 expectedTransport: nil, 599 }, 600 { 601 name: "good with two containers", 602 in: &api.Pod{ 603 ObjectMeta: metav1.ObjectMeta{Name: fakePodName}, 604 Spec: api.PodSpec{ 605 Containers: []api.Container{ 606 {Name: "container1"}, 607 {Name: "container2"}, 608 }, 609 NodeName: "foo", 610 }, 611 Status: api.PodStatus{}, 612 }, 613 opts: &api.PodLogOptions{ 614 Container: "container2", 615 }, 616 expectedErr: nil, 617 expectedTransport: fakeSecureRoundTripper, 618 }, 619 } 620 for _, tc := range tcs { 621 t.Run(tc.name, func(t *testing.T) { 622 getter := &mockPodGetter{tc.in} 623 connectionGetter := &mockConnectionInfoGetter{&client.ConnectionInfo{ 624 Transport: fakeSecureRoundTripper, 625 InsecureSkipTLSVerifyTransport: fakeInsecureRoundTripper, 626 }} 627 628 _, actualTransport, err := LogLocation(ctx, getter, connectionGetter, fakePodName, tc.opts) 629 if !reflect.DeepEqual(err, tc.expectedErr) { 630 t.Errorf("expected %q, got %q", tc.expectedErr, err) 631 } 632 if actualTransport != tc.expectedTransport { 633 t.Errorf("expected %q, got %q", tc.expectedTransport, actualTransport) 634 } 635 }) 636 } 637 } 638 639 func TestSelectableFieldLabelConversions(t *testing.T) { 640 apitesting.TestSelectableFieldLabelConversionsOfKind(t, 641 "v1", 642 "Pod", 643 ToSelectableFields(&api.Pod{}), 644 nil, 645 ) 646 } 647 648 type mockConnectionInfoGetter struct { 649 info *client.ConnectionInfo 650 } 651 652 func (g mockConnectionInfoGetter) GetConnectionInfo(ctx context.Context, nodeName types.NodeName) (*client.ConnectionInfo, error) { 653 return g.info, nil 654 } 655 656 func TestPortForwardLocation(t *testing.T) { 657 ctx := genericapirequest.NewDefaultContext() 658 tcs := []struct { 659 in *api.Pod 660 info *client.ConnectionInfo 661 opts *api.PodPortForwardOptions 662 expectedErr error 663 expectedURL *url.URL 664 }{ 665 { 666 in: &api.Pod{ 667 Spec: api.PodSpec{}, 668 }, 669 opts: &api.PodPortForwardOptions{}, 670 expectedErr: errors.NewBadRequest("pod test does not have a host assigned"), 671 }, 672 { 673 in: &api.Pod{ 674 ObjectMeta: metav1.ObjectMeta{ 675 Namespace: "ns", 676 Name: "pod1", 677 }, 678 Spec: api.PodSpec{ 679 NodeName: "node1", 680 }, 681 }, 682 info: &client.ConnectionInfo{}, 683 opts: &api.PodPortForwardOptions{}, 684 expectedURL: &url.URL{Host: ":", Path: "/portForward/ns/pod1"}, 685 }, 686 { 687 in: &api.Pod{ 688 ObjectMeta: metav1.ObjectMeta{ 689 Namespace: "ns", 690 Name: "pod1", 691 }, 692 Spec: api.PodSpec{ 693 NodeName: "node1", 694 }, 695 }, 696 info: &client.ConnectionInfo{}, 697 opts: &api.PodPortForwardOptions{Ports: []int32{80}}, 698 expectedURL: &url.URL{Host: ":", Path: "/portForward/ns/pod1", RawQuery: "port=80"}, 699 }, 700 } 701 for _, tc := range tcs { 702 getter := &mockPodGetter{tc.in} 703 connectionGetter := &mockConnectionInfoGetter{tc.info} 704 loc, _, err := PortForwardLocation(ctx, getter, connectionGetter, "test", tc.opts) 705 if !reflect.DeepEqual(err, tc.expectedErr) { 706 t.Errorf("expected %v, got %v", tc.expectedErr, err) 707 } 708 if !reflect.DeepEqual(loc, tc.expectedURL) { 709 t.Errorf("expected %v, got %v", tc.expectedURL, loc) 710 } 711 } 712 } 713 714 func TestGetPodIP(t *testing.T) { 715 testCases := []struct { 716 name string 717 pod *api.Pod 718 expectedIP string 719 }{ 720 { 721 name: "nil pod", 722 pod: nil, 723 expectedIP: "", 724 }, 725 { 726 name: "no status object", 727 pod: &api.Pod{ 728 ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod1"}, 729 Spec: api.PodSpec{}, 730 }, 731 expectedIP: "", 732 }, 733 { 734 name: "no pod ips", 735 pod: &api.Pod{ 736 ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod1"}, 737 Spec: api.PodSpec{}, 738 Status: api.PodStatus{}, 739 }, 740 expectedIP: "", 741 }, 742 { 743 name: "empty list", 744 pod: &api.Pod{ 745 ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod1"}, 746 Spec: api.PodSpec{}, 747 Status: api.PodStatus{ 748 PodIPs: []api.PodIP{}, 749 }, 750 }, 751 expectedIP: "", 752 }, 753 { 754 name: "1 ip", 755 pod: &api.Pod{ 756 ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod1"}, 757 Spec: api.PodSpec{}, 758 Status: api.PodStatus{ 759 PodIPs: []api.PodIP{ 760 {IP: "10.0.0.10"}, 761 }, 762 }, 763 }, 764 expectedIP: "10.0.0.10", 765 }, 766 { 767 name: "multiple ips", 768 pod: &api.Pod{ 769 ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod1"}, 770 Spec: api.PodSpec{}, 771 Status: api.PodStatus{ 772 PodIPs: []api.PodIP{ 773 {IP: "10.0.0.10"}, 774 {IP: "10.0.0.20"}, 775 }, 776 }, 777 }, 778 expectedIP: "10.0.0.10", 779 }, 780 } 781 for _, tc := range testCases { 782 t.Run(tc.name, func(t *testing.T) { 783 podIP := getPodIP(tc.pod) 784 if podIP != tc.expectedIP { 785 t.Errorf("expected pod ip:%v does not match actual %v", tc.expectedIP, podIP) 786 } 787 }) 788 } 789 } 790 791 type fakeTransport struct { 792 val string 793 } 794 795 func (f fakeTransport) RoundTrip(*http.Request) (*http.Response, error) { 796 return nil, nil 797 } 798 799 var ( 800 fakeSecureRoundTripper = fakeTransport{val: "secure"} 801 fakeInsecureRoundTripper = fakeTransport{val: "insecure"} 802 ) 803 804 func TestPodIndexFunc(t *testing.T) { 805 tcs := []struct { 806 name string 807 indexFunc cache.IndexFunc 808 pod interface{} 809 expectedValue string 810 expectedErr error 811 }{ 812 { 813 name: "node name index", 814 indexFunc: NodeNameIndexFunc, 815 pod: &api.Pod{ 816 Spec: api.PodSpec{ 817 NodeName: "test-pod", 818 }, 819 }, 820 expectedValue: "test-pod", 821 expectedErr: nil, 822 }, 823 { 824 name: "not a pod failed", 825 indexFunc: NodeNameIndexFunc, 826 pod: "not a pod object", 827 expectedValue: "test-pod", 828 expectedErr: fmt.Errorf("not a pod"), 829 }, 830 } 831 832 for _, tc := range tcs { 833 indexValues, err := tc.indexFunc(tc.pod) 834 if !reflect.DeepEqual(err, tc.expectedErr) { 835 t.Errorf("name %v, expected %v, got %v", tc.name, tc.expectedErr, err) 836 } 837 if err == nil && len(indexValues) != 1 && indexValues[0] != tc.expectedValue { 838 t.Errorf("name %v, expected %v, got %v", tc.name, tc.expectedValue, indexValues) 839 } 840 841 } 842 } 843 844 func newPodWithHugePageValue(resourceName api.ResourceName, value resource.Quantity) *api.Pod { 845 return &api.Pod{ 846 ObjectMeta: metav1.ObjectMeta{ 847 Namespace: "default", 848 Name: "foo", 849 ResourceVersion: "1", 850 }, 851 Spec: api.PodSpec{ 852 RestartPolicy: api.RestartPolicyAlways, 853 DNSPolicy: api.DNSDefault, 854 Containers: []api.Container{{ 855 Name: "foo", 856 Image: "image", 857 ImagePullPolicy: "IfNotPresent", 858 TerminationMessagePolicy: "File", 859 Resources: api.ResourceRequirements{ 860 Requests: api.ResourceList{ 861 api.ResourceCPU: resource.MustParse("10"), 862 resourceName: value, 863 }, 864 Limits: api.ResourceList{ 865 api.ResourceCPU: resource.MustParse("10"), 866 resourceName: value, 867 }, 868 }}, 869 }, 870 }, 871 } 872 } 873 874 func TestPodStrategyValidate(t *testing.T) { 875 const containerName = "container" 876 877 tests := []struct { 878 name string 879 pod *api.Pod 880 wantErr bool 881 }{ 882 { 883 name: "a new pod setting container with indivisible hugepages values", 884 pod: newPodWithHugePageValue(api.ResourceHugePagesPrefix+"1Mi", resource.MustParse("1.1Mi")), 885 wantErr: true, 886 }, 887 { 888 name: "a new pod setting init-container with indivisible hugepages values", 889 pod: &api.Pod{ 890 ObjectMeta: metav1.ObjectMeta{ 891 Namespace: "default", 892 Name: "foo", 893 }, 894 Spec: api.PodSpec{ 895 RestartPolicy: api.RestartPolicyAlways, 896 DNSPolicy: api.DNSDefault, 897 InitContainers: []api.Container{{ 898 Name: containerName, 899 Image: "image", 900 ImagePullPolicy: "IfNotPresent", 901 TerminationMessagePolicy: "File", 902 Resources: api.ResourceRequirements{ 903 Requests: api.ResourceList{ 904 api.ResourceName(api.ResourceHugePagesPrefix + "64Ki"): resource.MustParse("127Ki"), 905 }, 906 Limits: api.ResourceList{ 907 api.ResourceName(api.ResourceHugePagesPrefix + "64Ki"): resource.MustParse("127Ki"), 908 }, 909 }}, 910 }, 911 }, 912 }, 913 wantErr: true, 914 }, 915 { 916 name: "a new pod setting init-container with indivisible hugepages values while container with divisible hugepages values", 917 pod: &api.Pod{ 918 ObjectMeta: metav1.ObjectMeta{ 919 Namespace: "default", 920 Name: "foo", 921 }, 922 Spec: api.PodSpec{ 923 RestartPolicy: api.RestartPolicyAlways, 924 DNSPolicy: api.DNSDefault, 925 InitContainers: []api.Container{{ 926 Name: containerName, 927 Image: "image", 928 ImagePullPolicy: "IfNotPresent", 929 TerminationMessagePolicy: "File", 930 Resources: api.ResourceRequirements{ 931 Requests: api.ResourceList{ 932 api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("5.1Mi"), 933 }, 934 Limits: api.ResourceList{ 935 api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("5.1Mi"), 936 }, 937 }}, 938 }, 939 Containers: []api.Container{{ 940 Name: containerName, 941 Image: "image", 942 ImagePullPolicy: "IfNotPresent", 943 TerminationMessagePolicy: "File", 944 Resources: api.ResourceRequirements{ 945 Requests: api.ResourceList{ 946 api.ResourceName(api.ResourceHugePagesPrefix + "1Gi"): resource.MustParse("2Gi"), 947 }, 948 Limits: api.ResourceList{ 949 api.ResourceName(api.ResourceHugePagesPrefix + "1Gi"): resource.MustParse("2Gi"), 950 }, 951 }}, 952 }, 953 }, 954 }, 955 wantErr: true, 956 }, 957 { 958 name: "a new pod setting container with divisible hugepages values", 959 pod: &api.Pod{ 960 ObjectMeta: metav1.ObjectMeta{ 961 Namespace: "default", 962 Name: "foo", 963 }, 964 Spec: api.PodSpec{ 965 RestartPolicy: api.RestartPolicyAlways, 966 DNSPolicy: api.DNSDefault, 967 Containers: []api.Container{{ 968 Name: containerName, 969 Image: "image", 970 ImagePullPolicy: "IfNotPresent", 971 TerminationMessagePolicy: "File", 972 Resources: api.ResourceRequirements{ 973 Requests: api.ResourceList{ 974 api.ResourceName(api.ResourceCPU): resource.MustParse("10"), 975 api.ResourceName(api.ResourceHugePagesPrefix + "1Mi"): resource.MustParse("2Mi"), 976 }, 977 Limits: api.ResourceList{ 978 api.ResourceName(api.ResourceCPU): resource.MustParse("10"), 979 api.ResourceName(api.ResourceHugePagesPrefix + "1Mi"): resource.MustParse("2Mi"), 980 }, 981 }}, 982 }, 983 }, 984 }, 985 }, 986 } 987 988 for _, tc := range tests { 989 t.Run(tc.name, func(t *testing.T) { 990 errs := Strategy.Validate(genericapirequest.NewContext(), tc.pod) 991 if tc.wantErr && len(errs) == 0 { 992 t.Errorf("expected errors but got none") 993 } 994 if !tc.wantErr && len(errs) != 0 { 995 t.Errorf("unexpected errors: %v", errs.ToAggregate()) 996 } 997 }) 998 } 999 } 1000 1001 func TestEphemeralContainerStrategyValidateUpdate(t *testing.T) { 1002 1003 test := []struct { 1004 name string 1005 newPod *api.Pod 1006 oldPod *api.Pod 1007 }{ 1008 { 1009 name: "add ephemeral container to regular pod and expect success", 1010 oldPod: &api.Pod{ 1011 ObjectMeta: metav1.ObjectMeta{ 1012 Name: "test-pod", 1013 Namespace: "test-ns", 1014 ResourceVersion: "1", 1015 }, 1016 Spec: api.PodSpec{ 1017 RestartPolicy: api.RestartPolicyAlways, 1018 DNSPolicy: api.DNSDefault, 1019 Containers: []api.Container{ 1020 { 1021 Name: "container", 1022 Image: "image", 1023 ImagePullPolicy: "IfNotPresent", 1024 TerminationMessagePolicy: "File", 1025 }, 1026 }, 1027 }, 1028 }, 1029 newPod: &api.Pod{ 1030 ObjectMeta: metav1.ObjectMeta{ 1031 Name: "test-pod", 1032 Namespace: "test-ns", 1033 ResourceVersion: "1", 1034 }, 1035 Spec: api.PodSpec{ 1036 RestartPolicy: api.RestartPolicyAlways, 1037 DNSPolicy: api.DNSDefault, 1038 Containers: []api.Container{ 1039 { 1040 Name: "container", 1041 Image: "image", 1042 ImagePullPolicy: "IfNotPresent", 1043 TerminationMessagePolicy: "File", 1044 }, 1045 }, 1046 EphemeralContainers: []api.EphemeralContainer{ 1047 { 1048 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1049 Name: "debugger", 1050 Image: "image", 1051 ImagePullPolicy: "IfNotPresent", 1052 TerminationMessagePolicy: "File", 1053 }, 1054 }, 1055 }, 1056 }, 1057 }, 1058 }, 1059 } 1060 1061 // expect no errors 1062 for _, tc := range test { 1063 t.Run(tc.name, func(t *testing.T) { 1064 if errs := EphemeralContainersStrategy.ValidateUpdate(genericapirequest.NewContext(), tc.newPod, tc.oldPod); len(errs) != 0 { 1065 t.Errorf("unexpected error:%v", errs) 1066 } 1067 }) 1068 } 1069 1070 test = []struct { 1071 name string 1072 newPod *api.Pod 1073 oldPod *api.Pod 1074 }{ 1075 { 1076 name: "add ephemeral container to static pod and expect failure", 1077 oldPod: &api.Pod{ 1078 ObjectMeta: metav1.ObjectMeta{ 1079 Name: "test-pod", 1080 Namespace: "test-ns", 1081 ResourceVersion: "1", 1082 Annotations: map[string]string{api.MirrorPodAnnotationKey: "someVal"}, 1083 }, 1084 Spec: api.PodSpec{ 1085 RestartPolicy: api.RestartPolicyAlways, 1086 DNSPolicy: api.DNSDefault, 1087 Containers: []api.Container{ 1088 { 1089 Name: "container", 1090 Image: "image", 1091 ImagePullPolicy: "IfNotPresent", 1092 TerminationMessagePolicy: "File", 1093 }, 1094 }, 1095 NodeName: "example.com", 1096 }, 1097 }, 1098 newPod: &api.Pod{ 1099 ObjectMeta: metav1.ObjectMeta{ 1100 Name: "test-pod", 1101 Namespace: "test-ns", 1102 ResourceVersion: "1", 1103 Annotations: map[string]string{api.MirrorPodAnnotationKey: "someVal"}, 1104 }, 1105 Spec: api.PodSpec{ 1106 RestartPolicy: api.RestartPolicyAlways, 1107 DNSPolicy: api.DNSDefault, 1108 Containers: []api.Container{ 1109 { 1110 Name: "container", 1111 Image: "image", 1112 ImagePullPolicy: "IfNotPresent", 1113 TerminationMessagePolicy: "File", 1114 }, 1115 }, 1116 EphemeralContainers: []api.EphemeralContainer{ 1117 { 1118 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1119 Name: "debugger", 1120 Image: "image", 1121 ImagePullPolicy: "IfNotPresent", 1122 TerminationMessagePolicy: "File", 1123 }, 1124 }, 1125 }, 1126 NodeName: "example.com", 1127 }, 1128 }, 1129 }, 1130 { 1131 name: "remove ephemeral container from regular pod and expect failure", 1132 newPod: &api.Pod{ 1133 ObjectMeta: metav1.ObjectMeta{ 1134 Name: "test-pod", 1135 Namespace: "test-ns", 1136 ResourceVersion: "1", 1137 }, 1138 Spec: api.PodSpec{ 1139 RestartPolicy: api.RestartPolicyAlways, 1140 DNSPolicy: api.DNSDefault, 1141 Containers: []api.Container{ 1142 { 1143 Name: "container", 1144 Image: "image", 1145 ImagePullPolicy: "IfNotPresent", 1146 TerminationMessagePolicy: "File", 1147 }, 1148 }, 1149 }, 1150 }, 1151 oldPod: &api.Pod{ 1152 ObjectMeta: metav1.ObjectMeta{ 1153 Name: "test-pod", 1154 Namespace: "test-ns", 1155 ResourceVersion: "1", 1156 }, 1157 Spec: api.PodSpec{ 1158 RestartPolicy: api.RestartPolicyAlways, 1159 DNSPolicy: api.DNSDefault, 1160 Containers: []api.Container{ 1161 { 1162 Name: "container", 1163 Image: "image", 1164 ImagePullPolicy: "IfNotPresent", 1165 TerminationMessagePolicy: "File", 1166 }, 1167 }, 1168 EphemeralContainers: []api.EphemeralContainer{ 1169 { 1170 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1171 Name: "debugger", 1172 Image: "image", 1173 ImagePullPolicy: "IfNotPresent", 1174 TerminationMessagePolicy: "File", 1175 }, 1176 }, 1177 }, 1178 }, 1179 }, 1180 }, 1181 { 1182 name: "change ephemeral container from regular pod and expect failure", 1183 newPod: &api.Pod{ 1184 ObjectMeta: metav1.ObjectMeta{ 1185 Name: "test-pod", 1186 Namespace: "test-ns", 1187 ResourceVersion: "1", 1188 }, 1189 Spec: api.PodSpec{ 1190 RestartPolicy: api.RestartPolicyAlways, 1191 DNSPolicy: api.DNSDefault, 1192 Containers: []api.Container{ 1193 { 1194 Name: "container", 1195 Image: "image", 1196 ImagePullPolicy: "IfNotPresent", 1197 TerminationMessagePolicy: "File", 1198 }, 1199 }, 1200 EphemeralContainers: []api.EphemeralContainer{ 1201 { 1202 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1203 Name: "debugger", 1204 Image: "image2", 1205 ImagePullPolicy: "IfNotPresent", 1206 TerminationMessagePolicy: "File", 1207 }, 1208 }, 1209 }, 1210 }, 1211 }, 1212 oldPod: &api.Pod{ 1213 ObjectMeta: metav1.ObjectMeta{ 1214 Name: "test-pod", 1215 Namespace: "test-ns", 1216 ResourceVersion: "1", 1217 }, 1218 Spec: api.PodSpec{ 1219 RestartPolicy: api.RestartPolicyAlways, 1220 DNSPolicy: api.DNSDefault, 1221 Containers: []api.Container{ 1222 { 1223 Name: "container", 1224 Image: "image", 1225 ImagePullPolicy: "IfNotPresent", 1226 TerminationMessagePolicy: "File", 1227 }, 1228 }, 1229 EphemeralContainers: []api.EphemeralContainer{ 1230 { 1231 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1232 Name: "debugger", 1233 Image: "image", 1234 ImagePullPolicy: "IfNotPresent", 1235 TerminationMessagePolicy: "File", 1236 }, 1237 }, 1238 }, 1239 }, 1240 }, 1241 }, 1242 } 1243 1244 // expect one error 1245 for _, tc := range test { 1246 t.Run(tc.name, func(t *testing.T) { 1247 errs := EphemeralContainersStrategy.ValidateUpdate(genericapirequest.NewContext(), tc.newPod, tc.oldPod) 1248 if len(errs) == 0 { 1249 t.Errorf("unexpected success:ephemeral containers are not supported for static pods") 1250 } else if len(errs) != 1 { 1251 t.Errorf("unexpected errors:expected one error about ephemeral containers are not supported for static pods:got:%v:", errs) 1252 } 1253 }) 1254 } 1255 } 1256 1257 func TestPodStrategyValidateUpdate(t *testing.T) { 1258 test := []struct { 1259 name string 1260 newPod *api.Pod 1261 oldPod *api.Pod 1262 }{ 1263 { 1264 name: "an existing pod with indivisible hugepages values to a new pod with indivisible hugepages values", 1265 newPod: newPodWithHugePageValue(api.ResourceHugePagesPrefix+"2Mi", resource.MustParse("2.1Mi")), 1266 oldPod: newPodWithHugePageValue(api.ResourceHugePagesPrefix+"2Mi", resource.MustParse("2.1Mi")), 1267 }, 1268 } 1269 1270 for _, tc := range test { 1271 t.Run(tc.name, func(t *testing.T) { 1272 if errs := Strategy.ValidateUpdate(genericapirequest.NewContext(), tc.newPod, tc.oldPod); len(errs) != 0 { 1273 t.Errorf("unexpected error:%v", errs) 1274 } 1275 }) 1276 } 1277 } 1278 1279 func TestDropNonEphemeralContainerUpdates(t *testing.T) { 1280 tests := []struct { 1281 name string 1282 oldPod, newPod, wantPod *api.Pod 1283 }{ 1284 { 1285 name: "simple ephemeral container append", 1286 oldPod: &api.Pod{ 1287 ObjectMeta: metav1.ObjectMeta{ 1288 Name: "test-pod", 1289 Namespace: "test-ns", 1290 ResourceVersion: "1", 1291 }, 1292 Spec: api.PodSpec{ 1293 Containers: []api.Container{ 1294 { 1295 Name: "container", 1296 Image: "image", 1297 }, 1298 }, 1299 }, 1300 }, 1301 newPod: &api.Pod{ 1302 ObjectMeta: metav1.ObjectMeta{ 1303 Name: "test-pod", 1304 Namespace: "test-ns", 1305 ResourceVersion: "1", 1306 }, 1307 Spec: api.PodSpec{ 1308 Containers: []api.Container{ 1309 { 1310 Name: "container", 1311 Image: "image", 1312 }, 1313 }, 1314 EphemeralContainers: []api.EphemeralContainer{ 1315 { 1316 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1317 Name: "container", 1318 Image: "image", 1319 }, 1320 }, 1321 }, 1322 }, 1323 }, 1324 wantPod: &api.Pod{ 1325 ObjectMeta: metav1.ObjectMeta{ 1326 Name: "test-pod", 1327 Namespace: "test-ns", 1328 ResourceVersion: "1", 1329 }, 1330 Spec: api.PodSpec{ 1331 Containers: []api.Container{ 1332 { 1333 Name: "container", 1334 Image: "image", 1335 }, 1336 }, 1337 EphemeralContainers: []api.EphemeralContainer{ 1338 { 1339 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1340 Name: "container", 1341 Image: "image", 1342 }, 1343 }, 1344 }, 1345 }, 1346 }, 1347 }, 1348 { 1349 name: "whoops wrong pod", 1350 oldPod: &api.Pod{ 1351 ObjectMeta: metav1.ObjectMeta{ 1352 Name: "test-pod", 1353 Namespace: "test-ns", 1354 ResourceVersion: "1", 1355 UID: "blue", 1356 }, 1357 }, 1358 newPod: &api.Pod{ 1359 ObjectMeta: metav1.ObjectMeta{ 1360 Name: "new-pod", 1361 Namespace: "new-ns", 1362 ResourceVersion: "1", 1363 UID: "green", 1364 }, 1365 }, 1366 wantPod: &api.Pod{ 1367 ObjectMeta: metav1.ObjectMeta{ 1368 Name: "new-pod", 1369 Namespace: "new-ns", 1370 ResourceVersion: "1", 1371 UID: "green", 1372 }, 1373 }, 1374 }, 1375 { 1376 name: "resource conflict during update", 1377 oldPod: &api.Pod{ 1378 ObjectMeta: metav1.ObjectMeta{ 1379 Name: "test-pod", 1380 Namespace: "test-ns", 1381 ResourceVersion: "2", 1382 UID: "blue", 1383 }, 1384 }, 1385 newPod: &api.Pod{ 1386 ObjectMeta: metav1.ObjectMeta{ 1387 Name: "test-pod", 1388 Namespace: "test-ns", 1389 ResourceVersion: "1", 1390 UID: "blue", 1391 }, 1392 }, 1393 wantPod: &api.Pod{ 1394 ObjectMeta: metav1.ObjectMeta{ 1395 Name: "test-pod", 1396 Namespace: "test-ns", 1397 ResourceVersion: "1", 1398 UID: "blue", 1399 }, 1400 }, 1401 }, 1402 { 1403 name: "drop non-ephemeral container changes", 1404 oldPod: &api.Pod{ 1405 ObjectMeta: metav1.ObjectMeta{ 1406 Name: "test-pod", 1407 Namespace: "test-ns", 1408 ResourceVersion: "1", 1409 Annotations: map[string]string{"foo": "bar"}, 1410 }, 1411 Spec: api.PodSpec{ 1412 Containers: []api.Container{ 1413 { 1414 Name: "container", 1415 Image: "image", 1416 }, 1417 }, 1418 }, 1419 }, 1420 newPod: &api.Pod{ 1421 ObjectMeta: metav1.ObjectMeta{ 1422 Name: "test-pod", 1423 Namespace: "test-ns", 1424 ResourceVersion: "1", 1425 Annotations: map[string]string{"foo": "bar", "whiz": "pop"}, 1426 Finalizers: []string{"milo"}, 1427 }, 1428 Spec: api.PodSpec{ 1429 Containers: []api.Container{ 1430 { 1431 Name: "container", 1432 Image: "newimage", 1433 }, 1434 }, 1435 EphemeralContainers: []api.EphemeralContainer{ 1436 { 1437 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1438 Name: "container1", 1439 Image: "image", 1440 }, 1441 }, 1442 { 1443 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1444 Name: "container2", 1445 Image: "image", 1446 }, 1447 }, 1448 }, 1449 }, 1450 Status: api.PodStatus{ 1451 Message: "hi.", 1452 }, 1453 }, 1454 wantPod: &api.Pod{ 1455 ObjectMeta: metav1.ObjectMeta{ 1456 Name: "test-pod", 1457 Namespace: "test-ns", 1458 ResourceVersion: "1", 1459 Annotations: map[string]string{"foo": "bar"}, 1460 }, 1461 Spec: api.PodSpec{ 1462 Containers: []api.Container{ 1463 { 1464 Name: "container", 1465 Image: "image", 1466 }, 1467 }, 1468 EphemeralContainers: []api.EphemeralContainer{ 1469 { 1470 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1471 Name: "container1", 1472 Image: "image", 1473 }, 1474 }, 1475 { 1476 EphemeralContainerCommon: api.EphemeralContainerCommon{ 1477 Name: "container2", 1478 Image: "image", 1479 }, 1480 }, 1481 }, 1482 }, 1483 }, 1484 }, 1485 } 1486 1487 for _, tc := range tests { 1488 t.Run(tc.name, func(t *testing.T) { 1489 gotPod := dropNonEphemeralContainerUpdates(tc.newPod, tc.oldPod) 1490 if diff := cmp.Diff(tc.wantPod, gotPod); diff != "" { 1491 t.Errorf("unexpected diff when dropping fields (-want, +got):\n%s", diff) 1492 } 1493 }) 1494 } 1495 } 1496 1497 func TestNodeInclusionPolicyEnablementInCreating(t *testing.T) { 1498 var ( 1499 honor = api.NodeInclusionPolicyHonor 1500 ignore = api.NodeInclusionPolicyIgnore 1501 emptyConstraints = []api.TopologySpreadConstraint{ 1502 { 1503 WhenUnsatisfiable: api.DoNotSchedule, 1504 TopologyKey: "kubernetes.io/hostname", 1505 MaxSkew: 1, 1506 }, 1507 } 1508 defaultConstraints = []api.TopologySpreadConstraint{ 1509 { 1510 NodeAffinityPolicy: &honor, 1511 NodeTaintsPolicy: &ignore, 1512 WhenUnsatisfiable: api.DoNotSchedule, 1513 TopologyKey: "kubernetes.io/hostname", 1514 MaxSkew: 1, 1515 }, 1516 } 1517 ) 1518 1519 tests := []struct { 1520 name string 1521 topologySpreadConstraints []api.TopologySpreadConstraint 1522 wantTopologySpreadConstraints []api.TopologySpreadConstraint 1523 enableNodeInclusionPolicy bool 1524 }{ 1525 { 1526 name: "nodeInclusionPolicy enabled with topology unset", 1527 topologySpreadConstraints: emptyConstraints, 1528 wantTopologySpreadConstraints: emptyConstraints, 1529 enableNodeInclusionPolicy: true, 1530 }, 1531 { 1532 name: "nodeInclusionPolicy enabled with topology configured", 1533 topologySpreadConstraints: defaultConstraints, 1534 wantTopologySpreadConstraints: defaultConstraints, 1535 enableNodeInclusionPolicy: true, 1536 }, 1537 { 1538 name: "nodeInclusionPolicy disabled with topology configured", 1539 topologySpreadConstraints: defaultConstraints, 1540 wantTopologySpreadConstraints: emptyConstraints, 1541 }, 1542 } 1543 1544 for _, tc := range tests { 1545 t.Run(tc.name, func(t *testing.T) { 1546 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeInclusionPolicyInPodTopologySpread, tc.enableNodeInclusionPolicy)() 1547 1548 pod := &api.Pod{ 1549 ObjectMeta: metav1.ObjectMeta{ 1550 Namespace: "default", 1551 Name: "foo", 1552 }, 1553 Spec: api.PodSpec{ 1554 RestartPolicy: api.RestartPolicyAlways, 1555 DNSPolicy: api.DNSDefault, 1556 Containers: []api.Container{ 1557 { 1558 Name: "container", 1559 Image: "image", 1560 ImagePullPolicy: "IfNotPresent", 1561 TerminationMessagePolicy: "File", 1562 }, 1563 }, 1564 }, 1565 } 1566 wantPod := pod.DeepCopy() 1567 pod.Spec.TopologySpreadConstraints = append(pod.Spec.TopologySpreadConstraints, tc.topologySpreadConstraints...) 1568 1569 errs := Strategy.Validate(genericapirequest.NewContext(), pod) 1570 if len(errs) != 0 { 1571 t.Errorf("Unexpected error: %v", errs.ToAggregate()) 1572 } 1573 1574 Strategy.PrepareForCreate(genericapirequest.NewContext(), pod) 1575 wantPod.Spec.TopologySpreadConstraints = append(wantPod.Spec.TopologySpreadConstraints, tc.wantTopologySpreadConstraints...) 1576 if diff := cmp.Diff(wantPod, pod, cmpopts.IgnoreFields(pod.Status, "Phase", "QOSClass")); diff != "" { 1577 t.Errorf("%s unexpected result (-want, +got): %s", tc.name, diff) 1578 } 1579 }) 1580 } 1581 } 1582 1583 func TestNodeInclusionPolicyEnablementInUpdating(t *testing.T) { 1584 var ( 1585 honor = api.NodeInclusionPolicyHonor 1586 ignore = api.NodeInclusionPolicyIgnore 1587 ) 1588 1589 // Enable the Feature Gate during the first rule creation 1590 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeInclusionPolicyInPodTopologySpread, true)() 1591 ctx := genericapirequest.NewDefaultContext() 1592 1593 pod := &api.Pod{ 1594 ObjectMeta: metav1.ObjectMeta{ 1595 Namespace: "default", 1596 Name: "foo", 1597 ResourceVersion: "1", 1598 }, 1599 Spec: api.PodSpec{ 1600 RestartPolicy: api.RestartPolicyAlways, 1601 DNSPolicy: api.DNSDefault, 1602 Containers: []api.Container{ 1603 { 1604 Name: "container", 1605 Image: "image", 1606 ImagePullPolicy: "IfNotPresent", 1607 TerminationMessagePolicy: "File", 1608 }, 1609 }, 1610 TopologySpreadConstraints: []api.TopologySpreadConstraint{ 1611 { 1612 NodeAffinityPolicy: &ignore, 1613 NodeTaintsPolicy: &honor, 1614 WhenUnsatisfiable: api.DoNotSchedule, 1615 TopologyKey: "kubernetes.io/hostname", 1616 MaxSkew: 1, 1617 }, 1618 }, 1619 }, 1620 } 1621 1622 errs := Strategy.Validate(ctx, pod) 1623 if len(errs) != 0 { 1624 t.Errorf("Unexpected error: %v", errs.ToAggregate()) 1625 } 1626 1627 createdPod := pod.DeepCopy() 1628 Strategy.PrepareForCreate(ctx, createdPod) 1629 1630 if len(createdPod.Spec.TopologySpreadConstraints) != 1 || 1631 *createdPod.Spec.TopologySpreadConstraints[0].NodeAffinityPolicy != ignore || 1632 *createdPod.Spec.TopologySpreadConstraints[0].NodeTaintsPolicy != honor { 1633 t.Error("NodeInclusionPolicy created with unexpected result") 1634 } 1635 1636 // Disable the Feature Gate and expect these fields still exist after updating. 1637 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeInclusionPolicyInPodTopologySpread, false)() 1638 1639 updatedPod := createdPod.DeepCopy() 1640 updatedPod.Labels = map[string]string{"foo": "bar"} 1641 updatedPod.ResourceVersion = "2" 1642 1643 errs = Strategy.ValidateUpdate(ctx, updatedPod, createdPod) 1644 if len(errs) != 0 { 1645 t.Errorf("Unexpected error: %v", errs.ToAggregate()) 1646 } 1647 1648 Strategy.PrepareForUpdate(ctx, updatedPod, createdPod) 1649 1650 if len(updatedPod.Spec.TopologySpreadConstraints) != 1 || 1651 *updatedPod.Spec.TopologySpreadConstraints[0].NodeAffinityPolicy != ignore || 1652 *updatedPod.Spec.TopologySpreadConstraints[0].NodeTaintsPolicy != honor { 1653 t.Error("NodeInclusionPolicy updated with unexpected result") 1654 } 1655 1656 // Enable the Feature Gate again to check whether configured fields still exist after updating. 1657 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeInclusionPolicyInPodTopologySpread, true)() 1658 1659 updatedPod2 := updatedPod.DeepCopy() 1660 updatedPod2.Labels = map[string]string{"foo": "fuz"} 1661 updatedPod2.ResourceVersion = "3" 1662 1663 errs = Strategy.ValidateUpdate(ctx, updatedPod2, updatedPod) 1664 if len(errs) != 0 { 1665 t.Errorf("Unexpected error: %v", errs.ToAggregate()) 1666 } 1667 1668 Strategy.PrepareForUpdate(ctx, updatedPod2, updatedPod) 1669 if len(updatedPod2.Spec.TopologySpreadConstraints) != 1 || 1670 *updatedPod2.Spec.TopologySpreadConstraints[0].NodeAffinityPolicy != ignore || 1671 *updatedPod2.Spec.TopologySpreadConstraints[0].NodeTaintsPolicy != honor { 1672 t.Error("NodeInclusionPolicy updated with unexpected result") 1673 } 1674 } 1675 1676 func Test_mutatePodAffinity(t *testing.T) { 1677 tests := []struct { 1678 name string 1679 pod *api.Pod 1680 wantPod *api.Pod 1681 featureGateEnabled bool 1682 }{ 1683 { 1684 name: "matchLabelKeys are merged into labelSelector with In and mismatchLabelKeys are merged with NotIn", 1685 featureGateEnabled: true, 1686 pod: &api.Pod{ 1687 ObjectMeta: metav1.ObjectMeta{ 1688 Labels: map[string]string{ 1689 "country": "Japan", 1690 "city": "Kyoto", 1691 }, 1692 }, 1693 Spec: api.PodSpec{ 1694 Affinity: &api.Affinity{ 1695 PodAffinity: &api.PodAffinity{ 1696 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1697 { 1698 LabelSelector: &metav1.LabelSelector{ 1699 MatchLabels: map[string]string{ 1700 "region": "Asia", 1701 }, 1702 }, 1703 MatchLabelKeys: []string{"country"}, 1704 MismatchLabelKeys: []string{"city"}, 1705 }, 1706 }, 1707 PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ 1708 { 1709 PodAffinityTerm: api.PodAffinityTerm{ 1710 LabelSelector: &metav1.LabelSelector{ 1711 MatchLabels: map[string]string{ 1712 "region": "Asia", 1713 }, 1714 }, 1715 MatchLabelKeys: []string{"country"}, 1716 MismatchLabelKeys: []string{"city"}, 1717 }, 1718 }, 1719 }, 1720 }, 1721 PodAntiAffinity: &api.PodAntiAffinity{ 1722 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1723 { 1724 LabelSelector: &metav1.LabelSelector{ 1725 MatchLabels: map[string]string{ 1726 "region": "Asia", 1727 }, 1728 }, 1729 MatchLabelKeys: []string{"country"}, 1730 MismatchLabelKeys: []string{"city"}, 1731 }, 1732 }, 1733 PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ 1734 { 1735 PodAffinityTerm: api.PodAffinityTerm{ 1736 LabelSelector: &metav1.LabelSelector{ 1737 MatchLabels: map[string]string{ 1738 "region": "Asia", 1739 }, 1740 }, 1741 MatchLabelKeys: []string{"country"}, 1742 MismatchLabelKeys: []string{"city"}, 1743 }, 1744 }, 1745 }, 1746 }, 1747 }, 1748 }, 1749 }, 1750 wantPod: &api.Pod{ 1751 ObjectMeta: metav1.ObjectMeta{ 1752 Labels: map[string]string{ 1753 "country": "Japan", 1754 "city": "Kyoto", 1755 }, 1756 }, 1757 Spec: api.PodSpec{ 1758 Affinity: &api.Affinity{ 1759 PodAffinity: &api.PodAffinity{ 1760 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1761 { 1762 LabelSelector: &metav1.LabelSelector{ 1763 MatchLabels: map[string]string{ 1764 "region": "Asia", 1765 }, 1766 MatchExpressions: []metav1.LabelSelectorRequirement{ 1767 { 1768 Key: "country", 1769 Operator: metav1.LabelSelectorOpIn, 1770 Values: []string{"Japan"}, 1771 }, 1772 { 1773 Key: "city", 1774 Operator: metav1.LabelSelectorOpNotIn, 1775 Values: []string{"Kyoto"}, 1776 }, 1777 }, 1778 }, 1779 MatchLabelKeys: []string{"country"}, 1780 MismatchLabelKeys: []string{"city"}, 1781 }, 1782 }, 1783 PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ 1784 { 1785 PodAffinityTerm: api.PodAffinityTerm{ 1786 LabelSelector: &metav1.LabelSelector{ 1787 MatchLabels: map[string]string{ 1788 "region": "Asia", 1789 }, 1790 MatchExpressions: []metav1.LabelSelectorRequirement{ 1791 { 1792 Key: "country", 1793 Operator: metav1.LabelSelectorOpIn, 1794 Values: []string{"Japan"}, 1795 }, 1796 { 1797 Key: "city", 1798 Operator: metav1.LabelSelectorOpNotIn, 1799 Values: []string{"Kyoto"}, 1800 }, 1801 }, 1802 }, 1803 MatchLabelKeys: []string{"country"}, 1804 MismatchLabelKeys: []string{"city"}, 1805 }, 1806 }, 1807 }, 1808 }, 1809 PodAntiAffinity: &api.PodAntiAffinity{ 1810 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1811 { 1812 LabelSelector: &metav1.LabelSelector{ 1813 MatchLabels: map[string]string{ 1814 "region": "Asia", 1815 }, 1816 MatchExpressions: []metav1.LabelSelectorRequirement{ 1817 { 1818 Key: "country", 1819 Operator: metav1.LabelSelectorOpIn, 1820 Values: []string{"Japan"}, 1821 }, 1822 { 1823 Key: "city", 1824 Operator: metav1.LabelSelectorOpNotIn, 1825 Values: []string{"Kyoto"}, 1826 }, 1827 }, 1828 }, 1829 MatchLabelKeys: []string{"country"}, 1830 MismatchLabelKeys: []string{"city"}, 1831 }, 1832 }, 1833 PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ 1834 { 1835 PodAffinityTerm: api.PodAffinityTerm{ 1836 LabelSelector: &metav1.LabelSelector{ 1837 MatchLabels: map[string]string{ 1838 "region": "Asia", 1839 }, 1840 MatchExpressions: []metav1.LabelSelectorRequirement{ 1841 { 1842 Key: "country", 1843 Operator: metav1.LabelSelectorOpIn, 1844 Values: []string{"Japan"}, 1845 }, 1846 { 1847 Key: "city", 1848 Operator: metav1.LabelSelectorOpNotIn, 1849 Values: []string{"Kyoto"}, 1850 }, 1851 }, 1852 }, 1853 MatchLabelKeys: []string{"country"}, 1854 MismatchLabelKeys: []string{"city"}, 1855 }, 1856 }, 1857 }, 1858 }, 1859 }, 1860 }, 1861 }, 1862 }, 1863 { 1864 name: "keys, which are not found in Pod labels, are ignored", 1865 featureGateEnabled: true, 1866 pod: &api.Pod{ 1867 ObjectMeta: metav1.ObjectMeta{ 1868 Labels: map[string]string{ 1869 "country": "Japan", 1870 "city": "Kyoto", 1871 }, 1872 }, 1873 Spec: api.PodSpec{ 1874 Affinity: &api.Affinity{ 1875 PodAffinity: &api.PodAffinity{ 1876 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1877 { 1878 LabelSelector: &metav1.LabelSelector{ 1879 MatchLabels: map[string]string{ 1880 "region": "Asia", 1881 }, 1882 }, 1883 MatchLabelKeys: []string{"city", "not-found"}, 1884 }, 1885 }, 1886 }, 1887 }, 1888 }, 1889 }, 1890 wantPod: &api.Pod{ 1891 ObjectMeta: metav1.ObjectMeta{ 1892 Labels: map[string]string{ 1893 "country": "Japan", 1894 "city": "Kyoto", 1895 }, 1896 }, 1897 Spec: api.PodSpec{ 1898 Affinity: &api.Affinity{ 1899 PodAffinity: &api.PodAffinity{ 1900 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1901 { 1902 LabelSelector: &metav1.LabelSelector{ 1903 MatchLabels: map[string]string{ 1904 "region": "Asia", 1905 }, 1906 MatchExpressions: []metav1.LabelSelectorRequirement{ 1907 // "city" should be added correctly even if matchLabelKey has "not-found" key. 1908 { 1909 Key: "city", 1910 Operator: metav1.LabelSelectorOpIn, 1911 Values: []string{"Kyoto"}, 1912 }, 1913 }, 1914 }, 1915 MatchLabelKeys: []string{"city", "not-found"}, 1916 }, 1917 }, 1918 }, 1919 }, 1920 }, 1921 }, 1922 }, 1923 { 1924 name: "matchLabelKeys is ignored if the labelSelector is nil", 1925 featureGateEnabled: true, 1926 pod: &api.Pod{ 1927 ObjectMeta: metav1.ObjectMeta{ 1928 Labels: map[string]string{ 1929 "country": "Japan", 1930 "city": "Kyoto", 1931 }, 1932 }, 1933 Spec: api.PodSpec{ 1934 Affinity: &api.Affinity{ 1935 PodAffinity: &api.PodAffinity{ 1936 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1937 { 1938 MatchLabelKeys: []string{"country"}, 1939 MismatchLabelKeys: []string{"city"}, 1940 }, 1941 }, 1942 }, 1943 }, 1944 }, 1945 }, 1946 wantPod: &api.Pod{ 1947 ObjectMeta: metav1.ObjectMeta{ 1948 Labels: map[string]string{ 1949 "country": "Japan", 1950 "city": "Kyoto", 1951 }, 1952 }, 1953 Spec: api.PodSpec{ 1954 Affinity: &api.Affinity{ 1955 PodAffinity: &api.PodAffinity{ 1956 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1957 { 1958 MatchLabelKeys: []string{"country"}, 1959 MismatchLabelKeys: []string{"city"}, 1960 }, 1961 }, 1962 }, 1963 }, 1964 }, 1965 }, 1966 }, 1967 { 1968 name: "the feature gate is disabled and matchLabelKeys is ignored", 1969 pod: &api.Pod{ 1970 ObjectMeta: metav1.ObjectMeta{ 1971 Labels: map[string]string{ 1972 "country": "Japan", 1973 "city": "Kyoto", 1974 }, 1975 }, 1976 Spec: api.PodSpec{ 1977 Affinity: &api.Affinity{ 1978 PodAffinity: &api.PodAffinity{ 1979 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 1980 { 1981 LabelSelector: &metav1.LabelSelector{ 1982 MatchLabels: map[string]string{ 1983 "region": "Asia", 1984 }, 1985 }, 1986 MatchLabelKeys: []string{"country"}, 1987 MismatchLabelKeys: []string{"city"}, 1988 }, 1989 }, 1990 }, 1991 }, 1992 }, 1993 }, 1994 wantPod: &api.Pod{ 1995 ObjectMeta: metav1.ObjectMeta{ 1996 Labels: map[string]string{ 1997 "country": "Japan", 1998 "city": "Kyoto", 1999 }, 2000 }, 2001 Spec: api.PodSpec{ 2002 Affinity: &api.Affinity{ 2003 PodAffinity: &api.PodAffinity{ 2004 RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ 2005 { 2006 LabelSelector: &metav1.LabelSelector{ 2007 MatchLabels: map[string]string{ 2008 "region": "Asia", 2009 }, 2010 }, 2011 MatchLabelKeys: []string{"country"}, 2012 MismatchLabelKeys: []string{"city"}, 2013 }, 2014 }, 2015 }, 2016 }, 2017 }, 2018 }, 2019 }, 2020 } 2021 2022 for _, tc := range tests { 2023 t.Run(tc.name, func(t *testing.T) { 2024 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodAffinity, tc.featureGateEnabled)() 2025 2026 pod := tc.pod 2027 mutatePodAffinity(pod) 2028 if diff := cmp.Diff(tc.wantPod.Spec.Affinity, pod.Spec.Affinity); diff != "" { 2029 t.Errorf("unexpected affinity (-want, +got): %s\n", diff) 2030 } 2031 }) 2032 } 2033 }