k8s.io/kubernetes@v1.29.3/pkg/kubelet/lifecycle/handlers_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 lifecycle 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "net" 24 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 v1 "k8s.io/api/core/v1" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/apimachinery/pkg/util/intstr" 35 utilfeature "k8s.io/apiserver/pkg/util/feature" 36 "k8s.io/client-go/tools/record" 37 featuregatetesting "k8s.io/component-base/featuregate/testing" 38 "k8s.io/component-base/metrics/legacyregistry" 39 "k8s.io/component-base/metrics/testutil" 40 "k8s.io/kubernetes/pkg/features" 41 kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" 42 "k8s.io/kubernetes/pkg/kubelet/metrics" 43 "k8s.io/kubernetes/pkg/kubelet/util/format" 44 ) 45 46 func TestResolvePort(t *testing.T) { 47 for _, testCase := range []struct { 48 container *v1.Container 49 stringPort string 50 expected int 51 }{ 52 { 53 stringPort: "foo", 54 container: &v1.Container{ 55 Ports: []v1.ContainerPort{{Name: "foo", ContainerPort: int32(80)}}, 56 }, 57 expected: 80, 58 }, 59 { 60 container: &v1.Container{}, 61 stringPort: "80", 62 expected: 80, 63 }, 64 { 65 container: &v1.Container{ 66 Ports: []v1.ContainerPort{ 67 {Name: "bar", ContainerPort: int32(80)}, 68 }, 69 }, 70 stringPort: "foo", 71 expected: -1, 72 }, 73 } { 74 port, err := resolvePort(intstr.FromString(testCase.stringPort), testCase.container) 75 if testCase.expected != -1 && err != nil { 76 t.Fatalf("unexpected error while resolving port: %s", err) 77 } 78 if testCase.expected == -1 && err == nil { 79 t.Errorf("expected error when a port fails to resolve") 80 } 81 if testCase.expected != port { 82 t.Errorf("failed to resolve port, expected %d, got %d", testCase.expected, port) 83 } 84 } 85 } 86 87 type fakeContainerCommandRunner struct { 88 Cmd []string 89 ID kubecontainer.ContainerID 90 Err error 91 Msg string 92 } 93 94 func (f *fakeContainerCommandRunner) RunInContainer(_ context.Context, id kubecontainer.ContainerID, cmd []string, timeout time.Duration) ([]byte, error) { 95 f.Cmd = cmd 96 f.ID = id 97 return []byte(f.Msg), f.Err 98 } 99 100 func stubPodStatusProvider(podIP string) podStatusProvider { 101 return podStatusProviderFunc(func(uid types.UID, name, namespace string) (*kubecontainer.PodStatus, error) { 102 return &kubecontainer.PodStatus{ 103 ID: uid, 104 Name: name, 105 Namespace: namespace, 106 IPs: []string{podIP}, 107 }, nil 108 }) 109 } 110 111 type podStatusProviderFunc func(uid types.UID, name, namespace string) (*kubecontainer.PodStatus, error) 112 113 func (f podStatusProviderFunc) GetPodStatus(_ context.Context, uid types.UID, name, namespace string) (*kubecontainer.PodStatus, error) { 114 return f(uid, name, namespace) 115 } 116 117 func TestRunHandlerExec(t *testing.T) { 118 ctx := context.Background() 119 fakeCommandRunner := fakeContainerCommandRunner{} 120 handlerRunner := NewHandlerRunner(&fakeHTTP{}, &fakeCommandRunner, nil, nil) 121 122 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 123 containerName := "containerFoo" 124 125 container := v1.Container{ 126 Name: containerName, 127 Lifecycle: &v1.Lifecycle{ 128 PostStart: &v1.LifecycleHandler{ 129 Exec: &v1.ExecAction{ 130 Command: []string{"ls", "-a"}, 131 }, 132 }, 133 }, 134 } 135 136 pod := v1.Pod{} 137 pod.ObjectMeta.Name = "podFoo" 138 pod.ObjectMeta.Namespace = "nsFoo" 139 pod.Spec.Containers = []v1.Container{container} 140 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 141 if err != nil { 142 t.Errorf("unexpected error: %v", err) 143 } 144 if fakeCommandRunner.ID != containerID || 145 !reflect.DeepEqual(container.Lifecycle.PostStart.Exec.Command, fakeCommandRunner.Cmd) { 146 t.Errorf("unexpected commands: %v", fakeCommandRunner) 147 } 148 } 149 150 type fakeHTTP struct { 151 url string 152 headers http.Header 153 err error 154 resp *http.Response 155 } 156 157 func (f *fakeHTTP) Do(req *http.Request) (*http.Response, error) { 158 f.url = req.URL.String() 159 f.headers = req.Header.Clone() 160 return f.resp, f.err 161 } 162 163 func TestRunHandlerHttp(t *testing.T) { 164 ctx := context.Background() 165 fakeHTTPGetter := fakeHTTP{} 166 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 167 handlerRunner := NewHandlerRunner(&fakeHTTPGetter, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 168 169 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 170 containerName := "containerFoo" 171 172 container := v1.Container{ 173 Name: containerName, 174 Lifecycle: &v1.Lifecycle{ 175 PostStart: &v1.LifecycleHandler{ 176 HTTPGet: &v1.HTTPGetAction{ 177 Host: "foo", 178 Port: intstr.FromInt32(8080), 179 Path: "bar", 180 }, 181 }, 182 }, 183 } 184 pod := v1.Pod{} 185 pod.ObjectMeta.Name = "podFoo" 186 pod.ObjectMeta.Namespace = "nsFoo" 187 pod.ObjectMeta.UID = "foo-bar-quux" 188 pod.Spec.Containers = []v1.Container{container} 189 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 190 191 if err != nil { 192 t.Errorf("unexpected error: %v", err) 193 } 194 if fakeHTTPGetter.url != "http://foo:8080/bar" { 195 t.Errorf("unexpected url: %s", fakeHTTPGetter.url) 196 } 197 } 198 199 func TestRunHandlerHttpWithHeaders(t *testing.T) { 200 ctx := context.Background() 201 fakeHTTPDoer := fakeHTTP{} 202 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 203 204 handlerRunner := NewHandlerRunner(&fakeHTTPDoer, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 205 206 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 207 containerName := "containerFoo" 208 209 container := v1.Container{ 210 Name: containerName, 211 Lifecycle: &v1.Lifecycle{ 212 PostStart: &v1.LifecycleHandler{ 213 HTTPGet: &v1.HTTPGetAction{ 214 Host: "foo", 215 Port: intstr.FromInt32(8080), 216 Path: "/bar", 217 HTTPHeaders: []v1.HTTPHeader{ 218 {Name: "Foo", Value: "bar"}, 219 }, 220 }, 221 }, 222 }, 223 } 224 pod := v1.Pod{} 225 pod.ObjectMeta.Name = "podFoo" 226 pod.ObjectMeta.Namespace = "nsFoo" 227 pod.Spec.Containers = []v1.Container{container} 228 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 229 230 if err != nil { 231 t.Errorf("unexpected error: %v", err) 232 } 233 if fakeHTTPDoer.url != "http://foo:8080/bar" { 234 t.Errorf("unexpected url: %s", fakeHTTPDoer.url) 235 } 236 if fakeHTTPDoer.headers["Foo"][0] != "bar" { 237 t.Errorf("missing http header: %s", fakeHTTPDoer.headers) 238 } 239 } 240 241 func TestRunHandlerHttps(t *testing.T) { 242 ctx := context.Background() 243 fakeHTTPDoer := fakeHTTP{} 244 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 245 handlerRunner := NewHandlerRunner(&fakeHTTPDoer, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 246 247 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 248 containerName := "containerFoo" 249 250 container := v1.Container{ 251 Name: containerName, 252 Lifecycle: &v1.Lifecycle{ 253 PostStart: &v1.LifecycleHandler{ 254 HTTPGet: &v1.HTTPGetAction{ 255 Scheme: v1.URISchemeHTTPS, 256 Host: "foo", 257 Path: "bar", 258 }, 259 }, 260 }, 261 } 262 pod := v1.Pod{} 263 pod.ObjectMeta.Name = "podFoo" 264 pod.ObjectMeta.Namespace = "nsFoo" 265 pod.Spec.Containers = []v1.Container{container} 266 267 t.Run("consistent", func(t *testing.T) { 268 container.Lifecycle.PostStart.HTTPGet.Port = intstr.FromString("70") 269 pod.Spec.Containers = []v1.Container{container} 270 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 271 272 if err != nil { 273 t.Errorf("unexpected error: %v", err) 274 } 275 if fakeHTTPDoer.url != "https://foo:70/bar" { 276 t.Errorf("unexpected url: %s", fakeHTTPDoer.url) 277 } 278 }) 279 280 t.Run("inconsistent", func(t *testing.T) { 281 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentHTTPGetHandlers, false)() 282 container.Lifecycle.PostStart.HTTPGet.Port = intstr.FromString("70") 283 pod.Spec.Containers = []v1.Container{container} 284 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 285 286 if err != nil { 287 t.Errorf("unexpected error: %v", err) 288 } 289 if fakeHTTPDoer.url != "http://foo:70/bar" { 290 t.Errorf("unexpected url: %q", fakeHTTPDoer.url) 291 } 292 }) 293 } 294 295 func TestRunHandlerHTTPPort(t *testing.T) { 296 tests := []struct { 297 Name string 298 FeatureGateEnabled bool 299 Port intstr.IntOrString 300 ExpectError bool 301 Expected string 302 }{ 303 { 304 Name: "consistent/with port", 305 FeatureGateEnabled: true, 306 Port: intstr.FromString("70"), 307 Expected: "https://foo:70/bar", 308 }, { 309 Name: "consistent/without port", 310 FeatureGateEnabled: true, 311 Port: intstr.FromString(""), 312 ExpectError: true, 313 }, { 314 Name: "inconsistent/with port", 315 FeatureGateEnabled: false, 316 Port: intstr.FromString("70"), 317 Expected: "http://foo:70/bar", 318 }, { 319 Name: "inconsistent/without port", 320 Port: intstr.FromString(""), 321 FeatureGateEnabled: false, 322 Expected: "http://foo:80/bar", 323 }, 324 } 325 326 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 327 328 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 329 containerName := "containerFoo" 330 331 container := v1.Container{ 332 Name: containerName, 333 Lifecycle: &v1.Lifecycle{ 334 PostStart: &v1.LifecycleHandler{ 335 HTTPGet: &v1.HTTPGetAction{ 336 Scheme: v1.URISchemeHTTPS, 337 Host: "foo", 338 Port: intstr.FromString("unexpected"), 339 Path: "bar", 340 }, 341 }, 342 }, 343 } 344 pod := v1.Pod{} 345 pod.ObjectMeta.Name = "podFoo" 346 pod.ObjectMeta.Namespace = "nsFoo" 347 pod.Spec.Containers = []v1.Container{container} 348 349 for _, tt := range tests { 350 t.Run(tt.Name, func(t *testing.T) { 351 ctx := context.Background() 352 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentHTTPGetHandlers, tt.FeatureGateEnabled)() 353 fakeHTTPDoer := fakeHTTP{} 354 handlerRunner := NewHandlerRunner(&fakeHTTPDoer, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 355 356 container.Lifecycle.PostStart.HTTPGet.Port = tt.Port 357 pod.Spec.Containers = []v1.Container{container} 358 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 359 360 if hasError := (err != nil); hasError != tt.ExpectError { 361 t.Errorf("unexpected error: %v", err) 362 } 363 364 if fakeHTTPDoer.url != tt.Expected { 365 t.Errorf("unexpected url: %s", fakeHTTPDoer.url) 366 } 367 }) 368 } 369 } 370 371 func TestRunHTTPHandler(t *testing.T) { 372 type expected struct { 373 OldURL string 374 OldHeader http.Header 375 NewURL string 376 NewHeader http.Header 377 } 378 379 tests := []struct { 380 Name string 381 PodIP string 382 HTTPGet *v1.HTTPGetAction 383 Expected expected 384 }{ 385 { 386 Name: "missing pod IP", 387 PodIP: "", 388 HTTPGet: &v1.HTTPGetAction{ 389 Path: "foo", 390 Port: intstr.FromString("42"), 391 Host: "example.test", 392 Scheme: "http", 393 HTTPHeaders: []v1.HTTPHeader{}, 394 }, 395 Expected: expected{ 396 OldURL: "http://example.test:42/foo", 397 OldHeader: http.Header{}, 398 NewURL: "http://example.test:42/foo", 399 NewHeader: http.Header{ 400 "Accept": {"*/*"}, 401 "User-Agent": {"kube-lifecycle/."}, 402 }, 403 }, 404 }, { 405 Name: "missing host", 406 PodIP: "233.252.0.1", 407 HTTPGet: &v1.HTTPGetAction{ 408 Path: "foo", 409 Port: intstr.FromString("42"), 410 Scheme: "http", 411 HTTPHeaders: []v1.HTTPHeader{}, 412 }, 413 Expected: expected{ 414 OldURL: "http://233.252.0.1:42/foo", 415 OldHeader: http.Header{}, 416 NewURL: "http://233.252.0.1:42/foo", 417 NewHeader: http.Header{ 418 "Accept": {"*/*"}, 419 "User-Agent": {"kube-lifecycle/."}, 420 }, 421 }, 422 }, { 423 Name: "path with leading slash", 424 PodIP: "233.252.0.1", 425 HTTPGet: &v1.HTTPGetAction{ 426 Path: "/foo", 427 Port: intstr.FromString("42"), 428 Scheme: "http", 429 HTTPHeaders: []v1.HTTPHeader{}, 430 }, 431 Expected: expected{ 432 OldURL: "http://233.252.0.1:42//foo", 433 OldHeader: http.Header{}, 434 NewURL: "http://233.252.0.1:42/foo", 435 NewHeader: http.Header{ 436 "Accept": {"*/*"}, 437 "User-Agent": {"kube-lifecycle/."}, 438 }, 439 }, 440 }, { 441 Name: "path without leading slash", 442 PodIP: "233.252.0.1", 443 HTTPGet: &v1.HTTPGetAction{ 444 Path: "foo", 445 Port: intstr.FromString("42"), 446 Scheme: "http", 447 HTTPHeaders: []v1.HTTPHeader{}, 448 }, 449 Expected: expected{ 450 OldURL: "http://233.252.0.1:42/foo", 451 OldHeader: http.Header{}, 452 NewURL: "http://233.252.0.1:42/foo", 453 NewHeader: http.Header{ 454 "Accept": {"*/*"}, 455 "User-Agent": {"kube-lifecycle/."}, 456 }, 457 }, 458 }, { 459 Name: "port resolution", 460 PodIP: "233.252.0.1", 461 HTTPGet: &v1.HTTPGetAction{ 462 Path: "foo", 463 Port: intstr.FromString("quux"), 464 Scheme: "http", 465 HTTPHeaders: []v1.HTTPHeader{}, 466 }, 467 Expected: expected{ 468 OldURL: "http://233.252.0.1:8080/foo", 469 OldHeader: http.Header{}, 470 NewURL: "http://233.252.0.1:8080/foo", 471 NewHeader: http.Header{ 472 "Accept": {"*/*"}, 473 "User-Agent": {"kube-lifecycle/."}, 474 }, 475 }, 476 }, { 477 Name: "https", 478 PodIP: "233.252.0.1", 479 HTTPGet: &v1.HTTPGetAction{ 480 Path: "foo", 481 Port: intstr.FromString("4430"), 482 Scheme: "https", 483 HTTPHeaders: []v1.HTTPHeader{}, 484 }, 485 Expected: expected{ 486 OldURL: "http://233.252.0.1:4430/foo", 487 OldHeader: http.Header{}, 488 NewURL: "https://233.252.0.1:4430/foo", 489 NewHeader: http.Header{ 490 "Accept": {"*/*"}, 491 "User-Agent": {"kube-lifecycle/."}, 492 }, 493 }, 494 }, { 495 Name: "unknown scheme", 496 PodIP: "233.252.0.1", 497 HTTPGet: &v1.HTTPGetAction{ 498 Path: "foo", 499 Port: intstr.FromString("80"), 500 Scheme: "baz", 501 HTTPHeaders: []v1.HTTPHeader{}, 502 }, 503 Expected: expected{ 504 OldURL: "http://233.252.0.1:80/foo", 505 OldHeader: http.Header{}, 506 NewURL: "baz://233.252.0.1:80/foo", 507 NewHeader: http.Header{ 508 "Accept": {"*/*"}, 509 "User-Agent": {"kube-lifecycle/."}, 510 }, 511 }, 512 }, { 513 Name: "query param", 514 PodIP: "233.252.0.1", 515 HTTPGet: &v1.HTTPGetAction{ 516 Path: "foo?k=v", 517 Port: intstr.FromString("80"), 518 Scheme: "http", 519 HTTPHeaders: []v1.HTTPHeader{}, 520 }, 521 Expected: expected{ 522 OldURL: "http://233.252.0.1:80/foo?k=v", 523 OldHeader: http.Header{}, 524 NewURL: "http://233.252.0.1:80/foo?k=v", 525 NewHeader: http.Header{ 526 "Accept": {"*/*"}, 527 "User-Agent": {"kube-lifecycle/."}, 528 }, 529 }, 530 }, { 531 Name: "fragment", 532 PodIP: "233.252.0.1", 533 HTTPGet: &v1.HTTPGetAction{ 534 Path: "foo#frag", 535 Port: intstr.FromString("80"), 536 Scheme: "http", 537 HTTPHeaders: []v1.HTTPHeader{}, 538 }, 539 Expected: expected{ 540 OldURL: "http://233.252.0.1:80/foo#frag", 541 OldHeader: http.Header{}, 542 NewURL: "http://233.252.0.1:80/foo#frag", 543 NewHeader: http.Header{ 544 "Accept": {"*/*"}, 545 "User-Agent": {"kube-lifecycle/."}, 546 }, 547 }, 548 }, { 549 Name: "headers", 550 PodIP: "233.252.0.1", 551 HTTPGet: &v1.HTTPGetAction{ 552 Path: "foo", 553 Port: intstr.FromString("80"), 554 Scheme: "http", 555 HTTPHeaders: []v1.HTTPHeader{ 556 { 557 Name: "Foo", 558 Value: "bar", 559 }, 560 }, 561 }, 562 Expected: expected{ 563 OldURL: "http://233.252.0.1:80/foo", 564 OldHeader: http.Header{}, 565 NewURL: "http://233.252.0.1:80/foo", 566 NewHeader: http.Header{ 567 "Accept": {"*/*"}, 568 "Foo": {"bar"}, 569 "User-Agent": {"kube-lifecycle/."}, 570 }, 571 }, 572 }, { 573 Name: "host header", 574 PodIP: "233.252.0.1", 575 HTTPGet: &v1.HTTPGetAction{ 576 Host: "example.test", 577 Path: "foo", 578 Port: intstr.FromString("80"), 579 Scheme: "http", 580 HTTPHeaders: []v1.HTTPHeader{ 581 { 582 Name: "Host", 583 Value: "from.header", 584 }, 585 }, 586 }, 587 Expected: expected{ 588 OldURL: "http://example.test:80/foo", 589 OldHeader: http.Header{}, 590 NewURL: "http://example.test:80/foo", 591 NewHeader: http.Header{ 592 "Accept": {"*/*"}, 593 "User-Agent": {"kube-lifecycle/."}, 594 "Host": {"from.header"}, 595 }, 596 }, 597 }, 598 } 599 600 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 601 containerName := "containerFoo" 602 603 container := v1.Container{ 604 Name: containerName, 605 Lifecycle: &v1.Lifecycle{ 606 PostStart: &v1.LifecycleHandler{}, 607 }, 608 Ports: []v1.ContainerPort{ 609 { 610 Name: "quux", 611 ContainerPort: 8080, 612 }, 613 }, 614 } 615 616 pod := v1.Pod{} 617 pod.ObjectMeta.Name = "podFoo" 618 pod.ObjectMeta.Namespace = "nsFoo" 619 pod.Spec.Containers = []v1.Container{container} 620 621 for _, tt := range tests { 622 t.Run(tt.Name, func(t *testing.T) { 623 ctx := context.Background() 624 fakePodStatusProvider := stubPodStatusProvider(tt.PodIP) 625 626 container.Lifecycle.PostStart.HTTPGet = tt.HTTPGet 627 pod.Spec.Containers = []v1.Container{container} 628 629 verify := func(t *testing.T, expectedHeader http.Header, expectedURL string) { 630 fakeHTTPDoer := fakeHTTP{} 631 handlerRunner := NewHandlerRunner(&fakeHTTPDoer, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 632 633 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 634 if err != nil { 635 t.Fatal(err) 636 } 637 638 if diff := cmp.Diff(expectedHeader, fakeHTTPDoer.headers); diff != "" { 639 t.Errorf("unexpected header (-want, +got)\n:%s", diff) 640 } 641 if fakeHTTPDoer.url != expectedURL { 642 t.Errorf("url = %v; want %v", fakeHTTPDoer.url, tt.Expected.NewURL) 643 } 644 } 645 646 t.Run("consistent", func(t *testing.T) { 647 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentHTTPGetHandlers, true)() 648 verify(t, tt.Expected.NewHeader, tt.Expected.NewURL) 649 }) 650 651 t.Run("inconsistent", func(t *testing.T) { 652 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentHTTPGetHandlers, false)() 653 verify(t, tt.Expected.OldHeader, tt.Expected.OldURL) 654 }) 655 }) 656 } 657 } 658 659 func TestRunHandlerNil(t *testing.T) { 660 ctx := context.Background() 661 handlerRunner := NewHandlerRunner(&fakeHTTP{}, &fakeContainerCommandRunner{}, nil, nil) 662 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 663 podName := "podFoo" 664 podNamespace := "nsFoo" 665 containerName := "containerFoo" 666 667 container := v1.Container{ 668 Name: containerName, 669 Lifecycle: &v1.Lifecycle{ 670 PostStart: &v1.LifecycleHandler{}, 671 }, 672 } 673 pod := v1.Pod{} 674 pod.ObjectMeta.Name = podName 675 pod.ObjectMeta.Namespace = podNamespace 676 pod.Spec.Containers = []v1.Container{container} 677 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 678 if err == nil { 679 t.Errorf("expect error, but got nil") 680 } 681 } 682 683 func TestRunHandlerExecFailure(t *testing.T) { 684 ctx := context.Background() 685 expectedErr := fmt.Errorf("invalid command") 686 fakeCommandRunner := fakeContainerCommandRunner{Err: expectedErr, Msg: expectedErr.Error()} 687 handlerRunner := NewHandlerRunner(&fakeHTTP{}, &fakeCommandRunner, nil, nil) 688 689 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 690 containerName := "containerFoo" 691 command := []string{"ls", "--a"} 692 693 container := v1.Container{ 694 Name: containerName, 695 Lifecycle: &v1.Lifecycle{ 696 PostStart: &v1.LifecycleHandler{ 697 Exec: &v1.ExecAction{ 698 Command: command, 699 }, 700 }, 701 }, 702 } 703 704 pod := v1.Pod{} 705 pod.ObjectMeta.Name = "podFoo" 706 pod.ObjectMeta.Namespace = "nsFoo" 707 pod.Spec.Containers = []v1.Container{container} 708 expectedErrMsg := fmt.Sprintf("Exec lifecycle hook (%s) for Container %q in Pod %q failed - error: %v, message: %q", command, containerName, format.Pod(&pod), expectedErr, expectedErr.Error()) 709 msg, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 710 if err == nil { 711 t.Errorf("expected error: %v", expectedErr) 712 } 713 if msg != expectedErrMsg { 714 t.Errorf("unexpected error message: %q; expected %q", msg, expectedErrMsg) 715 } 716 } 717 718 func TestRunHandlerHttpFailure(t *testing.T) { 719 ctx := context.Background() 720 expectedErr := fmt.Errorf("fake http error") 721 expectedResp := http.Response{ 722 Body: io.NopCloser(strings.NewReader(expectedErr.Error())), 723 } 724 fakeHTTPGetter := fakeHTTP{err: expectedErr, resp: &expectedResp} 725 726 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 727 728 handlerRunner := NewHandlerRunner(&fakeHTTPGetter, &fakeContainerCommandRunner{}, fakePodStatusProvider, nil) 729 730 containerName := "containerFoo" 731 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 732 container := v1.Container{ 733 Name: containerName, 734 Lifecycle: &v1.Lifecycle{ 735 PostStart: &v1.LifecycleHandler{ 736 HTTPGet: &v1.HTTPGetAction{ 737 Host: "foo", 738 Port: intstr.FromInt32(8080), 739 Path: "bar", 740 }, 741 }, 742 }, 743 } 744 pod := v1.Pod{} 745 pod.ObjectMeta.Name = "podFoo" 746 pod.ObjectMeta.Namespace = "nsFoo" 747 pod.Spec.Containers = []v1.Container{container} 748 expectedErrMsg := fmt.Sprintf("HTTP lifecycle hook (%s) for Container %q in Pod %q failed - error: %v", "bar", containerName, format.Pod(&pod), expectedErr) 749 msg, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 750 if err == nil { 751 t.Errorf("expected error: %v", expectedErr) 752 } 753 if msg != expectedErrMsg { 754 t.Errorf("unexpected error message: %q; expected %q", msg, expectedErrMsg) 755 } 756 if fakeHTTPGetter.url != "http://foo:8080/bar" { 757 t.Errorf("unexpected url: %s", fakeHTTPGetter.url) 758 } 759 } 760 761 func TestRunHandlerHttpsFailureFallback(t *testing.T) { 762 ctx := context.Background() 763 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentHTTPGetHandlers, true)() 764 765 // Since prometheus' gatherer is global, other tests may have updated metrics already, so 766 // we need to reset them prior running this test. 767 // This also implies that we can't run this test in parallel with other tests. 768 metrics.Register() 769 legacyregistry.Reset() 770 771 var actualHeaders http.Header 772 srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { 773 actualHeaders = r.Header.Clone() 774 })) 775 defer srv.Close() 776 _, port, err := net.SplitHostPort(srv.Listener.Addr().String()) 777 if err != nil { 778 t.Fatal(err) 779 } 780 781 recorder := &record.FakeRecorder{Events: make(chan string, 10)} 782 783 fakePodStatusProvider := stubPodStatusProvider("127.0.0.1") 784 785 handlerRunner := NewHandlerRunner(srv.Client(), &fakeContainerCommandRunner{}, fakePodStatusProvider, recorder).(*handlerRunner) 786 787 containerName := "containerFoo" 788 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 789 container := v1.Container{ 790 Name: containerName, 791 Lifecycle: &v1.Lifecycle{ 792 PostStart: &v1.LifecycleHandler{ 793 HTTPGet: &v1.HTTPGetAction{ 794 // set the scheme to https to ensure it falls back to HTTP. 795 Scheme: "https", 796 Host: "127.0.0.1", 797 Port: intstr.FromString(port), 798 Path: "bar", 799 HTTPHeaders: []v1.HTTPHeader{ 800 { 801 Name: "Authorization", 802 Value: "secret", 803 }, 804 }, 805 }, 806 }, 807 }, 808 } 809 pod := v1.Pod{} 810 pod.ObjectMeta.Name = "podFoo" 811 pod.ObjectMeta.Namespace = "nsFoo" 812 pod.Spec.Containers = []v1.Container{container} 813 msg, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PostStart) 814 815 if err != nil { 816 t.Errorf("unexpected error: %v", err) 817 } 818 if msg != "" { 819 t.Errorf("unexpected error message: %q", msg) 820 } 821 if actualHeaders.Get("Authorization") != "" { 822 t.Error("unexpected Authorization header") 823 } 824 825 expectedMetrics := ` 826 # HELP kubelet_lifecycle_handler_http_fallbacks_total [ALPHA] The number of times lifecycle handlers successfully fell back to http from https. 827 # TYPE kubelet_lifecycle_handler_http_fallbacks_total counter 828 kubelet_lifecycle_handler_http_fallbacks_total 1 829 ` 830 831 if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedMetrics), "kubelet_lifecycle_handler_http_fallbacks_total"); err != nil { 832 t.Fatal(err) 833 } 834 835 select { 836 case event := <-recorder.Events: 837 if !strings.Contains(event, "LifecycleHTTPFallback") { 838 t.Fatalf("expected LifecycleHTTPFallback event, got %q", event) 839 } 840 default: 841 t.Fatal("no event recorded") 842 } 843 } 844 845 func TestIsHTTPResponseError(t *testing.T) { 846 s := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) 847 defer s.Close() 848 req, err := http.NewRequest("GET", s.URL, nil) 849 if err != nil { 850 t.Fatal(err) 851 } 852 req.URL.Scheme = "https" 853 _, err = http.DefaultClient.Do(req) 854 if !isHTTPResponseError(err) { 855 t.Errorf("unexpected http response error: %v", err) 856 } 857 } 858 859 func TestRunSleepHandler(t *testing.T) { 860 handlerRunner := NewHandlerRunner(&fakeHTTP{}, &fakeContainerCommandRunner{}, nil, nil) 861 containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"} 862 containerName := "containerFoo" 863 container := v1.Container{ 864 Name: containerName, 865 Lifecycle: &v1.Lifecycle{ 866 PreStop: &v1.LifecycleHandler{}, 867 }, 868 } 869 pod := v1.Pod{} 870 pod.ObjectMeta.Name = "podFoo" 871 pod.ObjectMeta.Namespace = "nsFoo" 872 pod.Spec.Containers = []v1.Container{container} 873 874 tests := []struct { 875 name string 876 sleepSeconds int64 877 terminationGracePeriodSeconds int64 878 expectErr bool 879 expectedErr string 880 }{ 881 { 882 name: "valid seconds", 883 sleepSeconds: 5, 884 terminationGracePeriodSeconds: 30, 885 }, 886 { 887 name: "longer than TerminationGracePeriodSeconds", 888 sleepSeconds: 3, 889 terminationGracePeriodSeconds: 2, 890 expectErr: true, 891 expectedErr: "container terminated before sleep hook finished", 892 }, 893 } 894 895 for _, tt := range tests { 896 t.Run(tt.name, func(t *testing.T) { 897 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLifecycleSleepAction, true)() 898 899 pod.Spec.Containers[0].Lifecycle.PreStop.Sleep = &v1.SleepAction{Seconds: tt.sleepSeconds} 900 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(tt.terminationGracePeriodSeconds)*time.Second) 901 defer cancel() 902 903 _, err := handlerRunner.Run(ctx, containerID, &pod, &container, container.Lifecycle.PreStop) 904 905 if !tt.expectErr && err != nil { 906 t.Errorf("unexpected success") 907 } 908 if tt.expectErr && err.Error() != tt.expectedErr { 909 t.Errorf("%s: expected error want %s, got %s", tt.name, tt.expectedErr, err.Error()) 910 } 911 }) 912 } 913 }