k8s.io/kubernetes@v1.29.3/pkg/probe/http/http_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 http 18 19 import ( 20 "bytes" 21 "fmt" 22 "net" 23 "net/http" 24 "net/http/httptest" 25 "net/url" 26 "os" 27 "sort" 28 "strconv" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/stretchr/testify/assert" 34 "github.com/stretchr/testify/require" 35 "k8s.io/apimachinery/pkg/util/wait" 36 "k8s.io/kubernetes/pkg/probe" 37 ) 38 39 const FailureCode int = -1 40 41 func unsetEnv(t testing.TB, key string) { 42 if originalValue, ok := os.LookupEnv(key); ok { 43 t.Cleanup(func() { os.Setenv(key, originalValue) }) 44 os.Unsetenv(key) 45 } 46 } 47 48 func TestHTTPProbeProxy(t *testing.T) { 49 res := "welcome to http probe proxy" 50 51 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 fmt.Fprint(w, res) 53 })) 54 defer server.Close() 55 56 localProxy := server.URL 57 58 t.Setenv("http_proxy", localProxy) 59 t.Setenv("HTTP_PROXY", localProxy) 60 unsetEnv(t, "no_proxy") 61 unsetEnv(t, "NO_PROXY") 62 63 followNonLocalRedirects := true 64 prober := New(followNonLocalRedirects) 65 66 // take some time to wait server boot 67 time.Sleep(2 * time.Second) 68 url, err := url.Parse("http://example.com") 69 if err != nil { 70 t.Errorf("proxy test unexpected error: %v", err) 71 } 72 73 req, err := NewProbeRequest(url, http.Header{}) 74 if err != nil { 75 t.Fatal(err) 76 } 77 78 _, response, _ := prober.Probe(req, time.Second*3) 79 80 if response == res { 81 t.Errorf("proxy test unexpected error: the probe is using proxy") 82 } 83 } 84 85 func TestHTTPProbeChecker(t *testing.T) { 86 handleReq := func(s int, body string) func(w http.ResponseWriter, r *http.Request) { 87 return func(w http.ResponseWriter, r *http.Request) { 88 w.WriteHeader(s) 89 w.Write([]byte(body)) 90 } 91 } 92 93 // Echo handler that returns the contents of request headers in the body 94 headerEchoHandler := func(w http.ResponseWriter, r *http.Request) { 95 w.WriteHeader(200) 96 output := "" 97 for k, arr := range r.Header { 98 for _, v := range arr { 99 output += fmt.Sprintf("%s: %s\n", k, v) 100 } 101 } 102 w.Write([]byte(output)) 103 } 104 105 // Handler that returns the number of request headers in the body 106 headerCounterHandler := func(w http.ResponseWriter, r *http.Request) { 107 w.WriteHeader(200) 108 w.Write([]byte(strconv.Itoa(len(r.Header)))) 109 } 110 111 // Handler that returns the keys of request headers in the body 112 headerKeysNamesHandler := func(w http.ResponseWriter, r *http.Request) { 113 w.WriteHeader(200) 114 keys := make([]string, 0, len(r.Header)) 115 for k := range r.Header { 116 keys = append(keys, k) 117 } 118 sort.Strings(keys) 119 120 w.Write([]byte(strings.Join(keys, "\n"))) 121 } 122 123 redirectHandler := func(s int, bad bool) func(w http.ResponseWriter, r *http.Request) { 124 return func(w http.ResponseWriter, r *http.Request) { 125 if r.URL.Path == "/" { 126 http.Redirect(w, r, "/new", s) 127 } else if bad && r.URL.Path == "/new" { 128 http.Error(w, "", http.StatusInternalServerError) 129 } 130 } 131 } 132 133 redirectHandlerWithBody := func(s int, body string) func(w http.ResponseWriter, r *http.Request) { 134 return func(w http.ResponseWriter, r *http.Request) { 135 if r.URL.Path == "/" { 136 http.Redirect(w, r, "/new", s) 137 } else if r.URL.Path == "/new" { 138 w.WriteHeader(s) 139 w.Write([]byte(body)) 140 } 141 } 142 } 143 144 followNonLocalRedirects := true 145 prober := New(followNonLocalRedirects) 146 testCases := []struct { 147 handler func(w http.ResponseWriter, r *http.Request) 148 reqHeaders http.Header 149 health probe.Result 150 accBody string 151 notBody string 152 }{ 153 // The probe will be filled in below. This is primarily testing that an HTTP GET happens. 154 { 155 handler: handleReq(http.StatusOK, "ok body"), 156 health: probe.Success, 157 accBody: "ok body", 158 }, 159 { 160 handler: headerCounterHandler, 161 reqHeaders: http.Header{}, 162 health: probe.Success, 163 accBody: "3", 164 }, 165 { 166 handler: headerKeysNamesHandler, 167 reqHeaders: http.Header{}, 168 health: probe.Success, 169 accBody: "Accept\nConnection\nUser-Agent", 170 }, 171 { 172 handler: headerEchoHandler, 173 reqHeaders: http.Header{ 174 "Accept-Encoding": {"gzip"}, 175 }, 176 health: probe.Success, 177 accBody: "Accept-Encoding: gzip", 178 }, 179 { 180 handler: headerEchoHandler, 181 reqHeaders: http.Header{ 182 "Accept-Encoding": {"foo"}, 183 }, 184 health: probe.Success, 185 accBody: "Accept-Encoding: foo", 186 }, 187 { 188 handler: headerEchoHandler, 189 reqHeaders: http.Header{ 190 "Accept-Encoding": {""}, 191 }, 192 health: probe.Success, 193 accBody: "Accept-Encoding: \n", 194 }, 195 { 196 handler: headerEchoHandler, 197 reqHeaders: http.Header{ 198 "X-Muffins-Or-Cupcakes": {"muffins"}, 199 }, 200 health: probe.Success, 201 accBody: "X-Muffins-Or-Cupcakes: muffins", 202 }, 203 { 204 handler: headerEchoHandler, 205 reqHeaders: http.Header{ 206 "User-Agent": {"foo/1.0"}, 207 }, 208 health: probe.Success, 209 accBody: "User-Agent: foo/1.0", 210 }, 211 { 212 handler: headerEchoHandler, 213 reqHeaders: http.Header{ 214 "User-Agent": {""}, 215 }, 216 health: probe.Success, 217 notBody: "User-Agent", 218 }, 219 { 220 handler: headerEchoHandler, 221 reqHeaders: http.Header{}, 222 health: probe.Success, 223 accBody: "User-Agent: kube-probe/", 224 }, 225 { 226 handler: headerEchoHandler, 227 reqHeaders: http.Header{ 228 "User-Agent": {"foo/1.0"}, 229 "Accept": {"text/html"}, 230 }, 231 health: probe.Success, 232 accBody: "Accept: text/html", 233 }, 234 { 235 handler: headerEchoHandler, 236 reqHeaders: http.Header{ 237 "User-Agent": {"foo/1.0"}, 238 "Accept": {"foo/*"}, 239 }, 240 health: probe.Success, 241 accBody: "User-Agent: foo/1.0", 242 }, 243 { 244 handler: headerEchoHandler, 245 reqHeaders: http.Header{ 246 "X-Muffins-Or-Cupcakes": {"muffins"}, 247 "Accept": {"foo/*"}, 248 }, 249 health: probe.Success, 250 accBody: "X-Muffins-Or-Cupcakes: muffins", 251 }, 252 { 253 handler: headerEchoHandler, 254 reqHeaders: http.Header{ 255 "Accept": {"foo/*"}, 256 }, 257 health: probe.Success, 258 accBody: "Accept: foo/*", 259 }, 260 { 261 handler: headerEchoHandler, 262 reqHeaders: http.Header{ 263 "Accept": {""}, 264 }, 265 health: probe.Success, 266 notBody: "Accept:", 267 }, 268 { 269 handler: headerEchoHandler, 270 reqHeaders: http.Header{ 271 "User-Agent": {"foo/1.0"}, 272 "Accept": {""}, 273 }, 274 health: probe.Success, 275 notBody: "Accept:", 276 }, 277 { 278 handler: headerEchoHandler, 279 reqHeaders: http.Header{}, 280 health: probe.Success, 281 accBody: "Accept: */*", 282 }, 283 { 284 // Echo handler that returns the contents of Host in the body 285 handler: func(w http.ResponseWriter, r *http.Request) { 286 w.WriteHeader(200) 287 w.Write([]byte(r.Host)) 288 }, 289 reqHeaders: http.Header{ 290 "Host": {"muffins.cupcakes.org"}, 291 }, 292 health: probe.Success, 293 accBody: "muffins.cupcakes.org", 294 }, 295 { 296 handler: handleReq(FailureCode, "fail body"), 297 health: probe.Failure, 298 }, 299 { 300 handler: handleReq(http.StatusInternalServerError, "fail body"), 301 health: probe.Failure, 302 }, 303 { 304 handler: func(w http.ResponseWriter, r *http.Request) { 305 time.Sleep(3 * time.Second) 306 }, 307 health: probe.Failure, 308 }, 309 { 310 handler: redirectHandler(http.StatusMovedPermanently, false), // 301 311 health: probe.Success, 312 }, 313 { 314 handler: redirectHandler(http.StatusMovedPermanently, true), // 301 315 health: probe.Failure, 316 }, 317 { 318 handler: redirectHandler(http.StatusFound, false), // 302 319 health: probe.Success, 320 }, 321 { 322 handler: redirectHandler(http.StatusFound, true), // 302 323 health: probe.Failure, 324 }, 325 { 326 handler: redirectHandler(http.StatusTemporaryRedirect, false), // 307 327 health: probe.Success, 328 }, 329 { 330 handler: redirectHandler(http.StatusTemporaryRedirect, true), // 307 331 health: probe.Failure, 332 }, 333 { 334 handler: redirectHandler(http.StatusPermanentRedirect, false), // 308 335 health: probe.Success, 336 }, 337 { 338 handler: redirectHandler(http.StatusPermanentRedirect, true), // 308 339 health: probe.Failure, 340 }, 341 { 342 handler: redirectHandlerWithBody(http.StatusPermanentRedirect, ""), // redirect with empty body 343 health: probe.Warning, 344 accBody: "Probe terminated redirects, Response body:", 345 }, 346 { 347 handler: redirectHandlerWithBody(http.StatusPermanentRedirect, "ok body"), // redirect with body 348 health: probe.Warning, 349 accBody: "Probe terminated redirects, Response body: ok body", 350 }, 351 } 352 for i, test := range testCases { 353 t.Run(fmt.Sprintf("case-%2d", i), func(t *testing.T) { 354 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 355 test.handler(w, r) 356 })) 357 defer server.Close() 358 u, err := url.Parse(server.URL) 359 if err != nil { 360 t.Errorf("case %d: unexpected error: %v", i, err) 361 } 362 _, port, err := net.SplitHostPort(u.Host) 363 if err != nil { 364 t.Errorf("case %d: unexpected error: %v", i, err) 365 } 366 _, err = strconv.Atoi(port) 367 if err != nil { 368 t.Errorf("case %d: unexpected error: %v", i, err) 369 } 370 req, err := NewProbeRequest(u, test.reqHeaders) 371 if err != nil { 372 t.Fatal(err) 373 } 374 health, output, err := prober.Probe(req, 1*time.Second) 375 if test.health == probe.Unknown && err == nil { 376 t.Errorf("case %d: expected error", i) 377 } 378 if test.health != probe.Unknown && err != nil { 379 t.Errorf("case %d: unexpected error: %v", i, err) 380 } 381 if health != test.health { 382 t.Errorf("case %d: expected %v, got %v", i, test.health, health) 383 } 384 if health != probe.Failure && test.health != probe.Failure { 385 if !strings.Contains(output, test.accBody) { 386 t.Errorf("Expected response body to contain %v, got %v", test.accBody, output) 387 } 388 if test.notBody != "" && strings.Contains(output, test.notBody) { 389 t.Errorf("Expected response not to contain %v, got %v", test.notBody, output) 390 } 391 } 392 }) 393 } 394 } 395 396 func TestHTTPProbeChecker_NonLocalRedirects(t *testing.T) { 397 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 398 switch r.URL.Path { 399 case "/redirect": 400 loc, _ := url.QueryUnescape(r.URL.Query().Get("loc")) 401 http.Redirect(w, r, loc, http.StatusFound) 402 case "/loop": 403 http.Redirect(w, r, "/loop", http.StatusFound) 404 case "/success": 405 w.WriteHeader(http.StatusOK) 406 default: 407 http.Error(w, "", http.StatusInternalServerError) 408 } 409 }) 410 server := httptest.NewServer(handler) 411 defer server.Close() 412 413 newportServer := httptest.NewServer(handler) 414 defer newportServer.Close() 415 416 testCases := map[string]struct { 417 redirect string 418 expectLocalResult probe.Result 419 expectNonLocalResult probe.Result 420 }{ 421 "local success": {"/success", probe.Success, probe.Success}, 422 "local fail": {"/fail", probe.Failure, probe.Failure}, 423 "newport success": {newportServer.URL + "/success", probe.Success, probe.Success}, 424 "newport fail": {newportServer.URL + "/fail", probe.Failure, probe.Failure}, 425 "bogus nonlocal": {"http://0.0.0.0/fail", probe.Warning, probe.Failure}, 426 "redirect loop": {"/loop", probe.Failure, probe.Failure}, 427 } 428 for desc, test := range testCases { 429 t.Run(desc+"-local", func(t *testing.T) { 430 followNonLocalRedirects := false 431 prober := New(followNonLocalRedirects) 432 target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect)) 433 require.NoError(t, err) 434 req, err := NewProbeRequest(target, nil) 435 require.NoError(t, err) 436 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout) 437 assert.Equal(t, test.expectLocalResult, result) 438 }) 439 t.Run(desc+"-nonlocal", func(t *testing.T) { 440 followNonLocalRedirects := true 441 prober := New(followNonLocalRedirects) 442 target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect)) 443 require.NoError(t, err) 444 req, err := NewProbeRequest(target, nil) 445 require.NoError(t, err) 446 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout) 447 assert.Equal(t, test.expectNonLocalResult, result) 448 }) 449 } 450 } 451 452 func TestHTTPProbeChecker_HostHeaderPreservedAfterRedirect(t *testing.T) { 453 successHostHeader := "www.success.com" 454 failHostHeader := "www.fail.com" 455 456 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 457 switch r.URL.Path { 458 case "/redirect": 459 http.Redirect(w, r, "/success", http.StatusFound) 460 case "/success": 461 if r.Host == successHostHeader { 462 w.WriteHeader(http.StatusOK) 463 } else { 464 http.Error(w, "", http.StatusBadRequest) 465 } 466 default: 467 http.Error(w, "", http.StatusInternalServerError) 468 } 469 }) 470 server := httptest.NewServer(handler) 471 defer server.Close() 472 473 testCases := map[string]struct { 474 hostHeader string 475 expectedResult probe.Result 476 }{ 477 "success": {successHostHeader, probe.Success}, 478 "fail": {failHostHeader, probe.Failure}, 479 } 480 for desc, test := range testCases { 481 headers := http.Header{} 482 headers.Add("Host", test.hostHeader) 483 t.Run(desc+"local", func(t *testing.T) { 484 followNonLocalRedirects := false 485 prober := New(followNonLocalRedirects) 486 target, err := url.Parse(server.URL + "/redirect") 487 require.NoError(t, err) 488 req, err := NewProbeRequest(target, headers) 489 require.NoError(t, err) 490 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout) 491 assert.Equal(t, test.expectedResult, result) 492 }) 493 t.Run(desc+"nonlocal", func(t *testing.T) { 494 followNonLocalRedirects := true 495 prober := New(followNonLocalRedirects) 496 target, err := url.Parse(server.URL + "/redirect") 497 require.NoError(t, err) 498 req, err := NewProbeRequest(target, headers) 499 require.NoError(t, err) 500 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout) 501 assert.Equal(t, test.expectedResult, result) 502 }) 503 } 504 } 505 506 func TestHTTPProbeChecker_PayloadTruncated(t *testing.T) { 507 successHostHeader := "www.success.com" 508 oversizePayload := bytes.Repeat([]byte("a"), maxRespBodyLength+1) 509 truncatedPayload := bytes.Repeat([]byte("a"), maxRespBodyLength) 510 511 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 512 switch r.URL.Path { 513 case "/success": 514 if r.Host == successHostHeader { 515 w.WriteHeader(http.StatusOK) 516 w.Write(oversizePayload) 517 } else { 518 http.Error(w, "", http.StatusBadRequest) 519 } 520 default: 521 http.Error(w, "", http.StatusInternalServerError) 522 } 523 }) 524 server := httptest.NewServer(handler) 525 defer server.Close() 526 527 headers := http.Header{} 528 headers.Add("Host", successHostHeader) 529 t.Run("truncated payload", func(t *testing.T) { 530 prober := New(false) 531 target, err := url.Parse(server.URL + "/success") 532 require.NoError(t, err) 533 req, err := NewProbeRequest(target, headers) 534 require.NoError(t, err) 535 result, body, err := prober.Probe(req, wait.ForeverTestTimeout) 536 assert.NoError(t, err) 537 assert.Equal(t, probe.Success, result) 538 assert.Equal(t, string(truncatedPayload), body) 539 }) 540 } 541 542 func TestHTTPProbeChecker_PayloadNormal(t *testing.T) { 543 successHostHeader := "www.success.com" 544 normalPayload := bytes.Repeat([]byte("a"), maxRespBodyLength-1) 545 546 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 547 switch r.URL.Path { 548 case "/success": 549 if r.Host == successHostHeader { 550 w.WriteHeader(http.StatusOK) 551 w.Write(normalPayload) 552 } else { 553 http.Error(w, "", http.StatusBadRequest) 554 } 555 default: 556 http.Error(w, "", http.StatusInternalServerError) 557 } 558 }) 559 server := httptest.NewServer(handler) 560 defer server.Close() 561 562 headers := http.Header{} 563 headers.Add("Host", successHostHeader) 564 t.Run("normal payload", func(t *testing.T) { 565 prober := New(false) 566 target, err := url.Parse(server.URL + "/success") 567 require.NoError(t, err) 568 req, err := NewProbeRequest(target, headers) 569 require.NoError(t, err) 570 result, body, err := prober.Probe(req, wait.ForeverTestTimeout) 571 assert.NoError(t, err) 572 assert.Equal(t, probe.Success, result) 573 assert.Equal(t, string(normalPayload), body) 574 }) 575 }