istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/cmd/pilot-agent/status/server_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package status 16 17 import ( 18 "context" 19 "crypto/tls" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "net" 25 "net/http" 26 "net/http/httptest" 27 "reflect" 28 "strconv" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/prometheus/client_golang/prometheus" 34 "github.com/prometheus/common/expfmt" 35 "github.com/prometheus/prometheus/model/labels" 36 "github.com/prometheus/prometheus/model/textparse" 37 "go.uber.org/atomic" 38 "google.golang.org/grpc" 39 "google.golang.org/grpc/health" 40 grpcHealth "google.golang.org/grpc/health/grpc_health_v1" 41 "k8s.io/apimachinery/pkg/util/intstr" 42 43 "istio.io/istio/pilot/cmd/pilot-agent/status/ready" 44 "istio.io/istio/pilot/cmd/pilot-agent/status/testserver" 45 "istio.io/istio/pkg/kube/apimirror" 46 "istio.io/istio/pkg/lazy" 47 "istio.io/istio/pkg/log" 48 "istio.io/istio/pkg/test" 49 "istio.io/istio/pkg/test/env" 50 "istio.io/istio/pkg/test/util/assert" 51 "istio.io/istio/pkg/test/util/retry" 52 ) 53 54 type handler struct { 55 // LastALPN stores the most recent ALPN requested. This is needed to determine info about a request, 56 // since the appProber strips all headers/responses. 57 lastAlpn *atomic.String 58 } 59 60 const ( 61 testHeader = "Some-Header" 62 testHeaderValue = "some-value" 63 testHostValue = "test.com:9999" 64 ) 65 66 var liveServerStats = "cluster_manager.cds.update_success: 1\nlistener_manager.lds.update_success: 1\nserver.state: 0\nlistener_manager.workers_started: 1" 67 68 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 69 h.lastAlpn.Store(r.Proto) 70 segments := strings.Split(r.URL.Path[1:], "/") 71 switch segments[0] { 72 case "header": 73 if r.Host != testHostValue { 74 log.Errorf("Missing expected host value %s, got %v", testHostValue, r.Host) 75 w.WriteHeader(http.StatusBadRequest) 76 } 77 if r.Header.Get(testHeader) != testHeaderValue { 78 log.Errorf("Missing expected Some-Header, got %v", r.Header) 79 w.WriteHeader(http.StatusBadRequest) 80 } 81 case "redirect": 82 http.Redirect(w, r, "/", http.StatusMovedPermanently) 83 case "redirect-loop": 84 http.Redirect(w, r, "/redirect-loop", http.StatusMovedPermanently) 85 case "remote-redirect": 86 http.Redirect(w, r, "http://example.com/foo", http.StatusMovedPermanently) 87 case "", "hello/sunnyvale": 88 w.Write([]byte("welcome, it works")) 89 case "status": 90 code, _ := strconv.Atoi(segments[1]) 91 w.Header().Set("Location", "/") 92 w.WriteHeader(code) 93 default: 94 return 95 } 96 } 97 98 func TestNewServer(t *testing.T) { 99 testCases := []struct { 100 probe string 101 err string 102 }{ 103 // Json can't be parsed. 104 { 105 probe: "invalid-prober-json-encoding", 106 err: "failed to decode", 107 }, 108 // map key is not well formed. 109 { 110 probe: `{"abc": {"path": "/app-foo/health"}}`, 111 err: "invalid path", 112 }, 113 // invalid probe type 114 { 115 probe: `{"/app-health/hello-world/readyz": {"exec": {"command": [ "true" ]}}}`, 116 err: "invalid prober type", 117 }, 118 // tcp probes are valid as well 119 { 120 probe: `{"/app-health/hello-world/readyz": {"tcpSocket": {"port": 8888}}}`, 121 }, 122 // probes must be one of tcp, http or gRPC 123 { 124 probe: `{"/app-health/hello-world/readyz": {"tcpSocket": {"port": 8888}, "httpGet": {"path": "/", "port": 7777}}}`, 125 err: "must be one of type httpGet, tcpSocket or gRPC", 126 }, 127 // probes must be one of tcp, http or gRPC 128 { 129 probe: `{"/app-health/hello-world/readyz": {"grpc": {"port": 8888}, "httpGet": {"path": "/", "port": 7777}}}`, 130 err: "must be one of type httpGet, tcpSocket or gRPC", 131 }, 132 // Port is not Int typed (tcpSocket). 133 { 134 probe: `{"/app-health/hello-world/readyz": {"tcpSocket": {"port": "tcp"}}}`, 135 err: "must be int type", 136 }, 137 // Port is not Int typed (httpGet). 138 { 139 probe: `{"/app-health/hello-world/readyz": {"httpGet": {"path": "/hello/sunnyvale", "port": "container-port-dontknow"}}}`, 140 err: "must be int type", 141 }, 142 // A valid input. 143 { 144 probe: `{"/app-health/hello-world/readyz": {"httpGet": {"path": "/hello/sunnyvale", "port": 8080}},` + 145 `"/app-health/business/livez": {"httpGet": {"path": "/buisiness/live", "port": 9090}}}`, 146 }, 147 // long request timeout 148 { 149 probe: `{"/app-health/hello-world/readyz": {"httpGet": {"path": "/hello/sunnyvale", "port": 8080},` + 150 `"initialDelaySeconds": 120,"timeoutSeconds": 10,"periodSeconds": 20}}`, 151 }, 152 // A valid input with empty probing path, which happens when HTTPGetAction.Path is not specified. 153 { 154 probe: `{"/app-health/hello-world/readyz": {"httpGet": {"path": "/hello/sunnyvale", "port": 8080}}, 155 "/app-health/business/livez": {"httpGet": {"port": 9090}}}`, 156 }, 157 // A valid input without any prober info. 158 { 159 probe: `{}`, 160 }, 161 // A valid input with probing path not starting with /, which happens when HTTPGetAction.Path does not start with a /. 162 { 163 probe: `{"/app-health/hello-world/readyz": {"httpGet": {"path": "hello/sunnyvale", "port": 8080}}, 164 "/app-health/business/livez": {"httpGet": {"port": 9090}}}`, 165 }, 166 // A valid gRPC probe. 167 { 168 probe: `{"/app-health/hello-world/readyz": {"gRPC": {"port": 8080}}}`, 169 }, 170 // A valid gRPC probe with null service. 171 { 172 probe: `{"/app-health/hello-world/readyz": {"gRPC": {"port": 8080, "service": null}}}`, 173 }, 174 // A valid gRPC probe with service. 175 { 176 probe: `{"/app-health/hello-world/readyz": {"gRPC": {"port": 8080, "service": "foo"}}}`, 177 }, 178 // A valid gRPC probe with service and timeout. 179 { 180 probe: `{"/app-health/hello-world/readyz": {"gRPC": {"port": 8080, "service": "foo"}, "timeoutSeconds": 10}}`, 181 }, 182 } 183 for _, tc := range testCases { 184 _, err := NewServer(Options{ 185 KubeAppProbers: tc.probe, 186 PrometheusRegistry: TestingRegistry(t), 187 }) 188 189 if err == nil { 190 if tc.err != "" { 191 t.Errorf("test case failed [%v], expect error %v", tc.probe, tc.err) 192 } 193 continue 194 } 195 if tc.err == "" { 196 t.Errorf("test case failed [%v], expect no error, got %v", tc.probe, err) 197 } 198 // error case, error string should match. 199 if !strings.Contains(err.Error(), tc.err) { 200 t.Errorf("test case failed [%v], expect error %v, got %v", tc.probe, tc.err, err) 201 } 202 } 203 } 204 205 func NewTestServer(t test.Failer, o Options) *Server { 206 if o.PrometheusRegistry == nil { 207 o.PrometheusRegistry = TestingRegistry(t) 208 } 209 server, err := NewServer(o) 210 if err != nil { 211 t.Fatalf("failed to create status server %v", err) 212 } 213 ctx, cancel := context.WithCancel(context.Background()) 214 t.Cleanup(cancel) 215 go server.Run(ctx) 216 217 if err := retry.UntilSuccess(func() error { 218 server.mutex.RLock() 219 statusPort := server.statusPort 220 server.mutex.RUnlock() 221 if statusPort == 0 { 222 return fmt.Errorf("no port allocated") 223 } 224 return nil 225 }, retry.Delay(time.Microsecond)); err != nil { 226 t.Fatalf("failed to getport: %v", err) 227 } 228 229 return server 230 } 231 232 func TestPprof(t *testing.T) { 233 pprofPath := "/debug/pprof/cmdline" 234 // Starts the pilot agent status server. 235 server := NewTestServer(t, Options{EnableProfiling: true}) 236 client := http.Client{} 237 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%v/%s", server.statusPort, pprofPath), nil) 238 if err != nil { 239 t.Fatalf("[%v] failed to create request", pprofPath) 240 } 241 resp, err := client.Do(req) 242 if err != nil { 243 t.Fatal("request failed: ", err) 244 } 245 defer resp.Body.Close() 246 if resp.StatusCode != http.StatusOK { 247 t.Errorf("[%v] unexpected status code, want = %v, got = %v", pprofPath, http.StatusOK, resp.StatusCode) 248 } 249 } 250 251 func TestStats(t *testing.T) { 252 cases := []struct { 253 name string 254 envoy string 255 app string 256 output string 257 expectParseError bool 258 }{ 259 { 260 name: "envoy metric only", 261 envoy: `# TYPE my_metric counter 262 my_metric{} 0 263 # TYPE my_other_metric counter 264 my_other_metric{} 0 265 `, 266 output: `# TYPE my_metric counter 267 my_metric{} 0 268 # TYPE my_other_metric counter 269 my_other_metric{} 0 270 `, 271 }, 272 { 273 name: "app metric only", 274 app: `# TYPE my_metric counter 275 my_metric{} 0 276 # TYPE my_other_metric counter 277 my_other_metric{} 0 278 `, 279 output: `# TYPE my_metric counter 280 my_metric{} 0 281 # TYPE my_other_metric counter 282 my_other_metric{} 0 283 `, 284 }, 285 { 286 name: "multiple metric", 287 envoy: `# TYPE my_metric counter 288 my_metric{} 0 289 `, 290 app: `# TYPE my_other_metric counter 291 my_other_metric{} 0 292 `, 293 output: `# TYPE my_metric counter 294 my_metric{} 0 295 # TYPE my_other_metric counter 296 my_other_metric{} 0 297 `, 298 }, 299 { 300 name: "agent metric", 301 envoy: ``, 302 app: ``, 303 // Agent metric is dynamic, so we just check a substring of it not the actual metric 304 output: ` 305 # TYPE istio_agent_scrapes_total counter 306 istio_agent_scrapes_total`, 307 }, 308 // When the application and envoy share a metric, Prometheus will fail. This negative check validates this 309 // assumption. 310 { 311 name: "conflict metric", 312 envoy: `# TYPE my_metric counter 313 my_metric{} 0 314 # TYPE my_other_metric counter 315 my_other_metric{} 0 316 `, 317 app: `# TYPE my_metric counter 318 my_metric{} 0 319 `, 320 output: `# TYPE my_metric counter 321 my_metric{} 0 322 # TYPE my_other_metric counter 323 my_other_metric{} 0 324 # TYPE my_metric counter 325 my_metric{} 0 326 `, 327 expectParseError: true, 328 }, 329 { 330 name: "conflict metric labeled", 331 envoy: `# TYPE my_metric counter 332 my_metric{app="foo"} 0 333 `, 334 app: `# TYPE my_metric counter 335 my_metric{app="bar"} 0 336 `, 337 output: `# TYPE my_metric counter 338 my_metric{app="foo"} 0 339 # TYPE my_metric counter 340 my_metric{app="bar"} 0 341 `, 342 expectParseError: true, 343 }, 344 } 345 for _, tt := range cases { 346 t.Run(tt.name, func(t *testing.T) { 347 rec := httptest.NewRecorder() 348 envoy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 349 if _, err := w.Write([]byte(tt.envoy)); err != nil { 350 t.Fatalf("write failed: %v", err) 351 } 352 })) 353 defer envoy.Close() 354 app := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 355 if _, err := w.Write([]byte(tt.app)); err != nil { 356 t.Fatalf("write failed: %v", err) 357 } 358 })) 359 defer app.Close() 360 envoyPort, err := strconv.Atoi(strings.Split(envoy.URL, ":")[2]) 361 if err != nil { 362 t.Fatal(err) 363 } 364 server := &Server{ 365 prometheus: &PrometheusScrapeConfiguration{ 366 Port: strings.Split(app.URL, ":")[2], 367 }, 368 envoyStatsPort: envoyPort, 369 http: &http.Client{}, 370 registry: TestingRegistry(t), 371 } 372 req := &http.Request{} 373 server.handleStats(rec, req) 374 if rec.Code != 200 { 375 t.Fatalf("handleStats() => %v; want 200", rec.Code) 376 } 377 if !strings.Contains(rec.Body.String(), tt.output) { 378 t.Fatalf("handleStats() => %v; want %v", rec.Body.String(), tt.output) 379 } 380 381 parser := expfmt.TextParser{} 382 mfMap, err := parser.TextToMetricFamilies(strings.NewReader(rec.Body.String())) 383 if err != nil && !tt.expectParseError { 384 t.Fatalf("failed to parse metrics: %v", err) 385 } else if err == nil && tt.expectParseError { 386 t.Fatalf("expected a prse error, got %+v", mfMap) 387 } 388 }) 389 } 390 } 391 392 func TestNegotiateMetricsFormat(t *testing.T) { 393 cases := []struct { 394 name string 395 contentType string 396 expected expfmt.Format 397 }{ 398 { 399 name: "openmetrics minimal accept header", 400 contentType: `application/openmetrics-text; version=0.0.1`, 401 expected: FmtOpenMetrics_0_0_1, 402 }, 403 { 404 name: "openmetrics minimal v1 accept header", 405 contentType: `application/openmetrics-text; version=1.0.0`, 406 expected: FmtOpenMetrics_1_0_0, 407 }, 408 { 409 name: "openmetrics accept header", 410 contentType: `application/openmetrics-text; version=0.0.1; charset=utf-8`, 411 expected: FmtOpenMetrics_0_0_1, 412 }, 413 { 414 name: "openmetrics v1 accept header", 415 contentType: `application/openmetrics-text; version=1.0.0; charset=utf-8`, 416 expected: FmtOpenMetrics_1_0_0, 417 }, 418 { 419 name: "plaintext accept header", 420 contentType: "text/plain; version=0.0.4; charset=utf-8", 421 expected: expfmt.NewFormat(expfmt.TypeTextPlain), 422 }, 423 { 424 name: "empty accept header", 425 contentType: "", 426 expected: expfmt.NewFormat(expfmt.TypeTextPlain), 427 }, 428 } 429 for _, tt := range cases { 430 t.Run(tt.name, func(t *testing.T) { 431 assert.Equal(t, negotiateMetricsFormat(tt.contentType), tt.expected) 432 }) 433 } 434 } 435 436 func TestStatsContentType(t *testing.T) { 437 appOpenMetrics := `# TYPE jvm info 438 # HELP jvm VM version info 439 jvm_info{runtime="OpenJDK Runtime Environment",vendor="AdoptOpenJDK",version="16.0.1+9"} 1.0 440 # TYPE jmx_config_reload_success counter 441 # HELP jmx_config_reload_success Number of times configuration have successfully been reloaded. 442 jmx_config_reload_success_total 0.0 443 jmx_config_reload_success_created 1.623984612719E9 444 # EOF 445 ` 446 appText004 := `# HELP jvm_info VM version info 447 # TYPE jvm_info gauge 448 jvm_info{runtime="OpenJDK Runtime Environment",vendor="AdoptOpenJDK",version="16.0.1+9",} 1.0 449 # HELP jmx_config_reload_failure_created Number of times configuration have failed to be reloaded. 450 # TYPE jmx_config_reload_failure_created gauge 451 jmx_config_reload_failure_created 1.624025983489E9 452 ` 453 envoy := `# TYPE my_metric counter 454 my_metric{} 0 455 # TYPE my_other_metric counter 456 my_other_metric{} 0 457 ` 458 cases := []struct { 459 name string 460 acceptHeader string 461 expectParseError bool 462 }{ 463 { 464 name: "openmetrics accept header", 465 acceptHeader: `application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1`, 466 }, 467 { 468 name: "openmetrics v1 accept header", 469 acceptHeader: `application/openmetrics-text; version=1.0.0,text/plain;version=0.0.4;q=0.5,*/*;q=0.1`, 470 }, 471 { 472 name: "plaintext accept header", 473 acceptHeader: string(FmtText), 474 }, 475 { 476 name: "empty accept header", 477 acceptHeader: "", 478 }, 479 } 480 for _, tt := range cases { 481 t.Run(tt.name, func(t *testing.T) { 482 rec := httptest.NewRecorder() 483 envoy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 484 if _, err := w.Write([]byte(envoy)); err != nil { 485 t.Fatalf("write failed: %v", err) 486 } 487 })) 488 defer envoy.Close() 489 app := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 490 format := expfmt.NegotiateIncludingOpenMetrics(r.Header) 491 var negotiatedMetrics string 492 if strings.Contains(string(format), "text/plain") { 493 negotiatedMetrics = appText004 494 } else { 495 negotiatedMetrics = appOpenMetrics 496 } 497 w.Header().Set("Content-Type", string(format)) 498 if _, err := w.Write([]byte(negotiatedMetrics)); err != nil { 499 t.Fatalf("write failed: %v", err) 500 } 501 })) 502 defer app.Close() 503 envoyPort, err := strconv.Atoi(strings.Split(envoy.URL, ":")[2]) 504 if err != nil { 505 t.Fatal(err) 506 } 507 server := &Server{ 508 prometheus: &PrometheusScrapeConfiguration{ 509 Port: strings.Split(app.URL, ":")[2], 510 }, 511 registry: TestingRegistry(t), 512 envoyStatsPort: envoyPort, 513 http: &http.Client{}, 514 } 515 req := &http.Request{} 516 req.Header = make(http.Header) 517 req.Header.Add("Accept", tt.acceptHeader) 518 server.handleStats(rec, req) 519 if rec.Code != 200 { 520 t.Fatalf("handleStats() => %v; want 200", rec.Code) 521 } 522 523 if negotiateMetricsFormat(rec.Header().Get("Content-Type")) == FmtText { 524 textParser := expfmt.TextParser{} 525 _, err := textParser.TextToMetricFamilies(strings.NewReader(rec.Body.String())) 526 if err != nil { 527 t.Fatalf("failed to parse text metrics: %v", err) 528 } 529 } else { 530 omParser := textparse.NewOpenMetricsParser(rec.Body.Bytes(), labels.NewSymbolTable()) 531 for { 532 _, err := omParser.Next() 533 if err == io.EOF { 534 break 535 } 536 if err != nil { 537 t.Fatalf("failed to parse openmetrics: %v", err) 538 } 539 } 540 } 541 }) 542 } 543 } 544 545 func TestStatsError(t *testing.T) { 546 fail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 547 w.WriteHeader(http.StatusInternalServerError) 548 })) 549 defer fail.Close() 550 pass := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 551 w.WriteHeader(http.StatusOK) 552 })) 553 defer pass.Close() 554 failPort, err := strconv.Atoi(strings.Split(fail.URL, ":")[2]) 555 if err != nil { 556 t.Fatal(err) 557 } 558 passPort, err := strconv.Atoi(strings.Split(pass.URL, ":")[2]) 559 if err != nil { 560 t.Fatal(err) 561 } 562 cases := []struct { 563 name string 564 envoy int 565 app int 566 }{ 567 {"both pass", passPort, passPort}, 568 {"envoy pass", passPort, failPort}, 569 {"app pass", failPort, passPort}, 570 {"both fail", failPort, failPort}, 571 } 572 for _, tt := range cases { 573 t.Run(tt.name, func(t *testing.T) { 574 server := &Server{ 575 prometheus: &PrometheusScrapeConfiguration{ 576 Port: strconv.Itoa(tt.app), 577 }, 578 registry: TestingRegistry(t), 579 envoyStatsPort: tt.envoy, 580 http: &http.Client{}, 581 } 582 req := &http.Request{} 583 rec := httptest.NewRecorder() 584 server.handleStats(rec, req) 585 if rec.Code != 200 { 586 t.Fatalf("handleStats() => %v; want 200", rec.Code) 587 } 588 }) 589 } 590 } 591 592 // initServerWithSize size is kB 593 func initServerWithSize(t *testing.B, size int) *Server { 594 appText := `# TYPE jvm info 595 # HELP jvm VM version info 596 jvm_info{runtime="OpenJDK Runtime Environment",vendor="AdoptOpenJDK",version="16.0.1+9"} 1.0 597 # TYPE jmx_config_reload_success counter 598 # HELP jmx_config_reload_success Number of times configuration have successfully been reloaded. 599 jmx_config_reload_success_total 0.0 600 jmx_config_reload_success_created 1.623984612719E9 601 ` 602 appOpenMetrics := appText + "# EOF" 603 604 envoy := strings.Builder{} 605 envoy.Grow(size << 10 * 100) 606 envoy.WriteString(`# TYPE my_metric counter 607 my_metric{} 0 608 # TYPE my_other_metric counter 609 my_other_metric{} 0 610 `) 611 for i := 0; envoy.Len()+len(appText) < size<<10; i++ { 612 envoy.WriteString("#TYPE my_other_metric_" + strconv.Itoa(i) + " counter\nmy_other_metric_" + strconv.Itoa(i) + " 0\n") 613 } 614 eb := []byte(envoy.String()) 615 616 envoyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 617 if _, err := w.Write(eb); err != nil { 618 t.Fatalf("write failed: %v", err) 619 } 620 })) 621 t.Cleanup(envoyServer.Close) 622 app := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 623 format := expfmt.NegotiateIncludingOpenMetrics(r.Header) 624 var negotiatedMetrics string 625 if format == FmtText { 626 negotiatedMetrics = appText 627 } else { 628 negotiatedMetrics = appOpenMetrics 629 } 630 w.Header().Set("Content-Type", string(format)) 631 if _, err := w.Write([]byte(negotiatedMetrics)); err != nil { 632 t.Fatalf("write failed: %v", err) 633 } 634 })) 635 t.Cleanup(app.Close) 636 envoyPort, err := strconv.Atoi(strings.Split(envoyServer.URL, ":")[2]) 637 if err != nil { 638 t.Fatal(err) 639 } 640 registry, err := initializeMonitoring() 641 if err != nil { 642 t.Fatal(err) 643 } 644 server := &Server{ 645 registry: registry, 646 prometheus: &PrometheusScrapeConfiguration{ 647 Port: strings.Split(app.URL, ":")[2], 648 }, 649 envoyStatsPort: envoyPort, 650 http: &http.Client{}, 651 } 652 t.ResetTimer() 653 return server 654 } 655 656 func BenchmarkStats(t *testing.B) { 657 tests := map[int]string{ 658 1: "1kb", 659 1 << 10: "1mb", 660 10 << 10: "10mb", 661 } 662 for size, v := range tests { 663 server := initServerWithSize(t, size) 664 t.Run("stats-fmttext-"+v, func(t *testing.B) { 665 for i := 0; i < t.N; i++ { 666 req := &http.Request{} 667 req.Header = make(http.Header) 668 req.Header.Add("Accept", string(FmtText)) 669 rec := httptest.NewRecorder() 670 server.handleStats(rec, req) 671 } 672 }) 673 t.Run("stats-fmtopenmetrics-"+v, func(t *testing.B) { 674 for i := 0; i < t.N; i++ { 675 req := &http.Request{} 676 req.Header = make(http.Header) 677 req.Header.Add("Accept", string(FmtOpenMetrics_1_0_0)) 678 rec := httptest.NewRecorder() 679 server.handleStats(rec, req) 680 } 681 }) 682 } 683 } 684 685 func TestAppProbe(t *testing.T) { 686 // Starts the application first. 687 listener, err := net.Listen("tcp", ":0") 688 if err != nil { 689 t.Errorf("failed to allocate unused port %v", err) 690 } 691 go http.Serve(listener, &handler{lastAlpn: atomic.NewString("")}) 692 appPort := listener.Addr().(*net.TCPAddr).Port 693 694 simpleHTTPConfig := KubeAppProbers{ 695 "/app-health/hello-world/readyz": &Prober{ 696 HTTPGet: &apimirror.HTTPGetAction{ 697 Path: "/hello/sunnyvale", 698 Port: intstr.IntOrString{IntVal: int32(appPort)}, 699 }, 700 }, 701 "/app-health/hello-world/livez": &Prober{ 702 HTTPGet: &apimirror.HTTPGetAction{ 703 Port: intstr.IntOrString{IntVal: int32(appPort)}, 704 }, 705 }, 706 } 707 simpleTCPConfig := KubeAppProbers{ 708 "/app-health/hello-world/readyz": &Prober{ 709 TCPSocket: &apimirror.TCPSocketAction{ 710 Port: intstr.IntOrString{IntVal: int32(appPort)}, 711 }, 712 }, 713 "/app-health/hello-world/livez": &Prober{ 714 TCPSocket: &apimirror.TCPSocketAction{ 715 Port: intstr.IntOrString{IntVal: int32(appPort)}, 716 }, 717 }, 718 } 719 720 type test struct { 721 name string 722 probePath string 723 config KubeAppProbers 724 podIP string 725 ipv6 bool 726 statusCode int 727 } 728 testCases := []test{ 729 { 730 name: "http-bad-path", 731 probePath: "bad-path-should-be-404", 732 config: simpleHTTPConfig, 733 statusCode: http.StatusNotFound, 734 }, 735 { 736 name: "http-readyz", 737 probePath: "app-health/hello-world/readyz", 738 config: simpleHTTPConfig, 739 statusCode: http.StatusOK, 740 }, 741 { 742 name: "http-livez", 743 probePath: "app-health/hello-world/livez", 744 config: simpleHTTPConfig, 745 statusCode: http.StatusOK, 746 }, 747 { 748 name: "http-livez-localhost", 749 probePath: "app-health/hello-world/livez", 750 config: simpleHTTPConfig, 751 statusCode: http.StatusOK, 752 podIP: "localhost", 753 }, 754 { 755 name: "http-readyz-header", 756 probePath: "app-health/header/readyz", 757 config: KubeAppProbers{ 758 "/app-health/header/readyz": &Prober{ 759 HTTPGet: &apimirror.HTTPGetAction{ 760 Port: intstr.IntOrString{IntVal: int32(appPort)}, 761 Path: "/header", 762 HTTPHeaders: []apimirror.HTTPHeader{ 763 {Name: testHeader, Value: testHeaderValue}, 764 {Name: "Host", Value: testHostValue}, 765 }, 766 }, 767 }, 768 }, 769 statusCode: http.StatusOK, 770 }, 771 { 772 name: "http-readyz-path", 773 probePath: "app-health/hello-world/readyz", 774 config: KubeAppProbers{ 775 "/app-health/hello-world/readyz": &Prober{ 776 HTTPGet: &apimirror.HTTPGetAction{ 777 Path: "hello/texas", 778 Port: intstr.IntOrString{IntVal: int32(appPort)}, 779 }, 780 }, 781 }, 782 statusCode: http.StatusOK, 783 }, 784 { 785 name: "http-livez-path", 786 probePath: "app-health/hello-world/livez", 787 config: KubeAppProbers{ 788 "/app-health/hello-world/livez": &Prober{ 789 HTTPGet: &apimirror.HTTPGetAction{ 790 Path: "hello/texas", 791 Port: intstr.IntOrString{IntVal: int32(appPort)}, 792 }, 793 }, 794 }, 795 statusCode: http.StatusOK, 796 }, 797 { 798 name: "tcp-readyz", 799 probePath: "app-health/hello-world/readyz", 800 config: simpleTCPConfig, 801 statusCode: http.StatusOK, 802 }, 803 { 804 name: "tcp-livez", 805 probePath: "app-health/hello-world/livez", 806 config: simpleTCPConfig, 807 statusCode: http.StatusOK, 808 }, 809 { 810 name: "tcp-livez-ipv4", 811 probePath: "app-health/hello-world/livez", 812 config: simpleTCPConfig, 813 statusCode: http.StatusOK, 814 podIP: "127.0.0.1", 815 }, 816 { 817 name: "tcp-livez-ipv6", 818 probePath: "app-health/hello-world/livez", 819 config: simpleTCPConfig, 820 statusCode: http.StatusOK, 821 podIP: "::1", 822 ipv6: true, 823 }, 824 { 825 name: "tcp-livez-wrapped-ipv6", 826 probePath: "app-health/hello-world/livez", 827 config: simpleTCPConfig, 828 statusCode: http.StatusInternalServerError, 829 podIP: "[::1]", 830 ipv6: true, 831 }, 832 { 833 name: "tcp-livez-localhost", 834 probePath: "app-health/hello-world/livez", 835 config: simpleTCPConfig, 836 statusCode: http.StatusOK, 837 podIP: "localhost", 838 }, 839 { 840 name: "redirect", 841 probePath: "app-health/redirect/livez", 842 config: KubeAppProbers{ 843 "/app-health/redirect/livez": &Prober{ 844 HTTPGet: &apimirror.HTTPGetAction{ 845 Path: "redirect", 846 Port: intstr.IntOrString{IntVal: int32(appPort)}, 847 }, 848 }, 849 }, 850 statusCode: http.StatusOK, 851 }, 852 { 853 name: "redirect loop", 854 probePath: "app-health/redirect-loop/livez", 855 config: KubeAppProbers{ 856 "/app-health/redirect-loop/livez": &Prober{ 857 HTTPGet: &apimirror.HTTPGetAction{ 858 Path: "redirect-loop", 859 Port: intstr.IntOrString{IntVal: int32(appPort)}, 860 }, 861 }, 862 }, 863 statusCode: http.StatusInternalServerError, 864 }, 865 { 866 name: "remote redirect", 867 probePath: "app-health/remote-redirect/livez", 868 config: KubeAppProbers{ 869 "/app-health/remote-redirect/livez": &Prober{ 870 HTTPGet: &apimirror.HTTPGetAction{ 871 Path: "remote-redirect", 872 Port: intstr.IntOrString{IntVal: int32(appPort)}, 873 }, 874 }, 875 }, 876 statusCode: http.StatusOK, 877 }, 878 } 879 testFn := func(t *testing.T, tc test) { 880 appProber, err := json.Marshal(tc.config) 881 if err != nil { 882 t.Fatalf("invalid app probers") 883 } 884 config := Options{ 885 KubeAppProbers: string(appProber), 886 PodIP: tc.podIP, 887 IPv6: tc.ipv6, 888 } 889 server := NewTestServer(t, config) 890 // Starts the pilot agent status server. 891 if tc.ipv6 { 892 server.upstreamLocalAddress = &net.TCPAddr{IP: net.ParseIP("::1")} // required because ::6 is NOT a loopback address (IPv6 only has ::1) 893 } 894 895 client := http.Client{} 896 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%v/%s", server.statusPort, tc.probePath), nil) 897 if err != nil { 898 t.Fatalf("[%v] failed to create request", tc.probePath) 899 } 900 if c := tc.config["/"+tc.probePath]; c != nil { 901 if hc := c.HTTPGet; hc != nil { 902 for _, h := range hc.HTTPHeaders { 903 req.Header[h.Name] = append(req.Header[h.Name], h.Value) 904 } 905 } 906 } 907 // This is simulating the kubelet behavior of setting the Host to Header["Host"]. 908 // https://github.com/kubernetes/kubernetes/blob/d3b7391dc2f1040083ee2a8bfcb02edf7b0ded4b/pkg/probe/http/request.go#L84C1-L84C1 909 req.Host = req.Header.Get("Host") 910 resp, err := client.Do(req) 911 if err != nil { 912 t.Fatal("request failed: ", err) 913 } 914 defer resp.Body.Close() 915 if resp.StatusCode != tc.statusCode { 916 t.Errorf("[%v] unexpected status code, want = %v, got = %v", tc.probePath, tc.statusCode, resp.StatusCode) 917 } 918 } 919 for _, tc := range testCases { 920 t.Run(tc.name, func(t *testing.T) { testFn(t, tc) }) 921 } 922 // Next we check ever 923 t.Run("status codes", func(t *testing.T) { 924 for code := http.StatusOK; code <= http.StatusNetworkAuthenticationRequired; code++ { 925 if http.StatusText(code) == "" { // Not a valid HTTP code 926 continue 927 } 928 expect := code 929 if isRedirect(code) { 930 expect = 200 931 } 932 t.Run(fmt.Sprint(code), func(t *testing.T) { 933 testFn(t, test{ 934 probePath: "app-health/code/livez", 935 config: KubeAppProbers{ 936 "/app-health/code/livez": &Prober{ 937 TimeoutSeconds: 1, 938 HTTPGet: &apimirror.HTTPGetAction{ 939 Path: fmt.Sprintf("status/%d", code), 940 Port: intstr.IntOrString{IntVal: int32(appPort)}, 941 }, 942 }, 943 }, 944 statusCode: expect, 945 }) 946 }) 947 } 948 }) 949 } 950 951 func TestHttpsAppProbe(t *testing.T) { 952 setupServer := func(t *testing.T, alpn []string) (uint16, func() string) { 953 // Starts the application first. 954 listener, err := net.Listen("tcp", ":0") 955 if err != nil { 956 t.Errorf("failed to allocate unused port %v", err) 957 } 958 t.Cleanup(func() { listener.Close() }) 959 keyFile := env.IstioSrc + "/pilot/cmd/pilot-agent/status/test-cert/cert.key" 960 crtFile := env.IstioSrc + "/pilot/cmd/pilot-agent/status/test-cert/cert.crt" 961 cert, err := tls.LoadX509KeyPair(crtFile, keyFile) 962 if err != nil { 963 t.Fatalf("could not load TLS keys: %v", err) 964 } 965 serverTLSConfig := &tls.Config{ 966 Certificates: []tls.Certificate{cert}, 967 NextProtos: alpn, 968 MinVersion: tls.VersionTLS12, 969 } 970 tlsListener := tls.NewListener(listener, serverTLSConfig) 971 h := &handler{lastAlpn: atomic.NewString("")} 972 srv := http.Server{Handler: h} 973 go srv.Serve(tlsListener) 974 appPort := listener.Addr().(*net.TCPAddr).Port 975 976 // Starts the pilot agent status server. 977 server := NewTestServer(t, Options{ 978 KubeAppProbers: fmt.Sprintf(`{"/app-health/hello-world/readyz": {"httpGet": {"path": "/hello/sunnyvale", "port": %v, "scheme": "HTTPS"}}, 979 "/app-health/hello-world/livez": {"httpGet": {"port": %v, "scheme": "HTTPS"}}}`, appPort, appPort), 980 }) 981 return server.statusPort, h.lastAlpn.Load 982 } 983 testCases := []struct { 984 name string 985 probePath string 986 expectedProtocol string 987 statusCode int 988 alpns []string 989 }{ 990 { 991 name: "bad-path-should-be-disallowed", 992 probePath: "bad-path-should-be-disallowed", 993 statusCode: http.StatusNotFound, 994 }, 995 { 996 name: "readyz", 997 probePath: "app-health/hello-world/readyz", 998 statusCode: http.StatusOK, 999 expectedProtocol: "HTTP/1.1", 1000 alpns: nil, 1001 }, 1002 { 1003 name: "livez", 1004 probePath: "app-health/hello-world/livez", 1005 statusCode: http.StatusOK, 1006 expectedProtocol: "HTTP/1.1", 1007 }, 1008 { 1009 name: "h1 only", 1010 probePath: "app-health/hello-world/readyz", 1011 statusCode: http.StatusOK, 1012 expectedProtocol: "HTTP/1.1", 1013 alpns: []string{"http/1.1"}, 1014 }, 1015 { 1016 name: "h2 only", 1017 probePath: "app-health/hello-world/readyz", 1018 statusCode: http.StatusOK, 1019 expectedProtocol: "HTTP/2.0", 1020 alpns: []string{"h2"}, 1021 }, 1022 { 1023 name: "prefer h2", 1024 probePath: "app-health/hello-world/readyz", 1025 statusCode: http.StatusOK, 1026 expectedProtocol: "HTTP/2.0", 1027 alpns: []string{"h2", "http/1.1"}, 1028 }, 1029 { 1030 name: "prefer h1", 1031 probePath: "app-health/hello-world/readyz", 1032 statusCode: http.StatusOK, 1033 expectedProtocol: "HTTP/2.0", 1034 alpns: []string{"h2", "http/1.1"}, 1035 }, 1036 { 1037 name: "unknown alpn", 1038 probePath: "app-health/hello-world/readyz", 1039 statusCode: http.StatusInternalServerError, 1040 alpns: []string{"foo"}, 1041 }, 1042 } 1043 for _, tc := range testCases { 1044 t.Run(tc.name, func(t *testing.T) { 1045 statusPort, getAlpn := setupServer(t, tc.alpns) 1046 client := http.Client{} 1047 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/%s", statusPort, tc.probePath), nil) 1048 if err != nil { 1049 t.Fatalf("failed to create request") 1050 } 1051 resp, err := client.Do(req) 1052 if err != nil { 1053 t.Fatal("request failed") 1054 } 1055 defer resp.Body.Close() 1056 if resp.StatusCode != tc.statusCode { 1057 t.Errorf("unexpected status code, want = %v, got = %v", tc.statusCode, resp.StatusCode) 1058 } 1059 if got := getAlpn(); got != tc.expectedProtocol { 1060 t.Errorf("unexpected protocol, want = %v, got = %v", tc.expectedProtocol, got) 1061 } 1062 }) 1063 } 1064 } 1065 1066 func TestGRPCAppProbe(t *testing.T) { 1067 appServer := grpc.NewServer() 1068 healthServer := health.NewServer() 1069 healthServer.SetServingStatus("serving-svc", grpcHealth.HealthCheckResponse_SERVING) 1070 healthServer.SetServingStatus("unknown-svc", grpcHealth.HealthCheckResponse_UNKNOWN) 1071 healthServer.SetServingStatus("not-serving-svc", grpcHealth.HealthCheckResponse_NOT_SERVING) 1072 grpcHealth.RegisterHealthServer(appServer, healthServer) 1073 1074 listener, err := net.Listen("tcp", ":0") 1075 if err != nil { 1076 t.Errorf("failed to allocate unused port %v", err) 1077 } 1078 go appServer.Serve(listener) 1079 defer appServer.GracefulStop() 1080 1081 appPort := listener.Addr().(*net.TCPAddr).Port 1082 // Starts the pilot agent status server. 1083 server := NewTestServer(t, Options{ 1084 KubeAppProbers: fmt.Sprintf(` 1085 { 1086 "/app-health/foo/livez": { 1087 "grpc": { 1088 "port": %v, 1089 "service": null 1090 }, 1091 "timeoutSeconds": 1 1092 }, 1093 "/app-health/foo/readyz": { 1094 "grpc": { 1095 "port": %v, 1096 "service": "not-serving-svc" 1097 }, 1098 "timeoutSeconds": 1 1099 }, 1100 "/app-health/bar/livez": { 1101 "grpc": { 1102 "port": %v, 1103 "service": "serving-svc" 1104 }, 1105 "timeoutSeconds": 10 1106 }, 1107 "/app-health/bar/readyz": { 1108 "grpc": { 1109 "port": %v, 1110 "service": "unknown-svc" 1111 }, 1112 "timeoutSeconds": 10 1113 } 1114 }`, appPort, appPort, appPort, appPort), 1115 }) 1116 statusPort := server.statusPort 1117 t.Logf("status server starts at port %v, app starts at port %v", statusPort, appPort) 1118 1119 testCases := []struct { 1120 name string 1121 probePath string 1122 statusCode int 1123 }{ 1124 { 1125 name: "bad-path-should-be-disallowed", 1126 probePath: fmt.Sprintf(":%v/bad-path-should-be-disallowed", statusPort), 1127 statusCode: http.StatusNotFound, 1128 }, 1129 { 1130 name: "foo-livez", 1131 probePath: fmt.Sprintf(":%v/app-health/foo/livez", statusPort), 1132 statusCode: http.StatusOK, 1133 }, 1134 { 1135 name: "foo-readyz", 1136 probePath: fmt.Sprintf(":%v/app-health/foo/readyz", statusPort), 1137 statusCode: http.StatusInternalServerError, 1138 }, 1139 { 1140 name: "bar-livez", 1141 probePath: fmt.Sprintf(":%v/app-health/bar/livez", statusPort), 1142 statusCode: http.StatusOK, 1143 }, 1144 { 1145 name: "bar-readyz", 1146 probePath: fmt.Sprintf(":%v/app-health/bar/readyz", statusPort), 1147 statusCode: http.StatusInternalServerError, 1148 }, 1149 } 1150 for _, tc := range testCases { 1151 t.Run(tc.name, func(t *testing.T) { 1152 client := http.Client{} 1153 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost%s", tc.probePath), nil) 1154 if err != nil { 1155 t.Errorf("[%v] failed to create request", tc.probePath) 1156 } 1157 resp, err := client.Do(req) 1158 if err != nil { 1159 t.Fatal("request failed") 1160 } 1161 defer resp.Body.Close() 1162 if resp.StatusCode != tc.statusCode { 1163 t.Errorf("[%v] unexpected status code, want = %v, got = %v", tc.probePath, tc.statusCode, resp.StatusCode) 1164 } 1165 }) 1166 } 1167 } 1168 1169 func TestGRPCAppProbeWithIPV6(t *testing.T) { 1170 appServer := grpc.NewServer() 1171 healthServer := health.NewServer() 1172 healthServer.SetServingStatus("serving-svc", grpcHealth.HealthCheckResponse_SERVING) 1173 healthServer.SetServingStatus("unknown-svc", grpcHealth.HealthCheckResponse_UNKNOWN) 1174 healthServer.SetServingStatus("not-serving-svc", grpcHealth.HealthCheckResponse_NOT_SERVING) 1175 grpcHealth.RegisterHealthServer(appServer, healthServer) 1176 1177 listener, err := net.Listen("tcp", ":0") 1178 if err != nil { 1179 t.Errorf("failed to allocate unused port %v", err) 1180 } 1181 go appServer.Serve(listener) 1182 defer appServer.GracefulStop() 1183 1184 appPort := listener.Addr().(*net.TCPAddr).Port 1185 // Starts the pilot agent status server. 1186 server := NewTestServer(t, Options{ 1187 IPv6: true, 1188 PodIP: "::1", 1189 KubeAppProbers: fmt.Sprintf(` 1190 { 1191 "/app-health/foo/livez": { 1192 "grpc": { 1193 "port": %v, 1194 "service": null 1195 }, 1196 "timeoutSeconds": 1 1197 }, 1198 "/app-health/foo/readyz": { 1199 "grpc": { 1200 "port": %v, 1201 "service": "not-serving-svc" 1202 }, 1203 "timeoutSeconds": 1 1204 }, 1205 "/app-health/bar/livez": { 1206 "grpc": { 1207 "port": %v, 1208 "service": "serving-svc" 1209 }, 1210 "timeoutSeconds": 10 1211 }, 1212 "/app-health/bar/readyz": { 1213 "grpc": { 1214 "port": %v, 1215 "service": "unknown-svc" 1216 }, 1217 "timeoutSeconds": 10 1218 } 1219 }`, appPort, appPort, appPort, appPort), 1220 }) 1221 1222 server.upstreamLocalAddress = &net.TCPAddr{IP: net.ParseIP("::1")} // required because ::6 is NOT a loopback address (IPv6 only has ::1) 1223 1224 testCases := []struct { 1225 name string 1226 probePath string 1227 statusCode int 1228 }{ 1229 { 1230 name: "bad-path-should-be-disallowed", 1231 probePath: fmt.Sprintf(":%v/bad-path-should-be-disallowed", server.statusPort), 1232 statusCode: http.StatusNotFound, 1233 }, 1234 { 1235 name: "foo-livez", 1236 probePath: fmt.Sprintf(":%v/app-health/foo/livez", server.statusPort), 1237 statusCode: http.StatusOK, 1238 }, 1239 { 1240 name: "foo-readyz", 1241 probePath: fmt.Sprintf(":%v/app-health/foo/readyz", server.statusPort), 1242 statusCode: http.StatusInternalServerError, 1243 }, 1244 { 1245 name: "bar-livez", 1246 probePath: fmt.Sprintf(":%v/app-health/bar/livez", server.statusPort), 1247 statusCode: http.StatusOK, 1248 }, 1249 { 1250 name: "bar-readyz", 1251 probePath: fmt.Sprintf(":%v/app-health/bar/readyz", server.statusPort), 1252 statusCode: http.StatusInternalServerError, 1253 }, 1254 } 1255 for _, tc := range testCases { 1256 t.Run(tc.name, func(t *testing.T) { 1257 client := http.Client{} 1258 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost%s", tc.probePath), nil) 1259 if err != nil { 1260 t.Errorf("[%v] failed to create request", tc.probePath) 1261 } 1262 resp, err := client.Do(req) 1263 if err != nil { 1264 t.Fatal("request failed") 1265 } 1266 defer resp.Body.Close() 1267 if resp.StatusCode != tc.statusCode { 1268 t.Errorf("[%v] unexpected status code, want = %v, got = %v", tc.probePath, tc.statusCode, resp.StatusCode) 1269 } 1270 }) 1271 } 1272 } 1273 1274 func TestProbeHeader(t *testing.T) { 1275 headerChecker := func(t *testing.T, header http.Header) net.Listener { 1276 listener, err := net.Listen("tcp", ":0") 1277 if err != nil { 1278 t.Fatalf("failed to allocate unused port %v", err) 1279 } 1280 go http.Serve(listener, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 1281 r.Header.Del("User-Agent") 1282 r.Header.Del("Accept-Encoding") 1283 if !reflect.DeepEqual(r.Header, header) { 1284 t.Errorf("unexpected header, want = %v, got = %v", header, r.Header) 1285 http.Error(rw, "", http.StatusBadRequest) 1286 return 1287 } 1288 http.Error(rw, "", http.StatusOK) 1289 })) 1290 return listener 1291 } 1292 1293 testCases := []struct { 1294 name string 1295 originHeaders http.Header 1296 proxyHeaders []apimirror.HTTPHeader 1297 want http.Header 1298 }{ 1299 { 1300 name: "Only Origin", 1301 originHeaders: http.Header{ 1302 testHeader: []string{testHeaderValue}, 1303 }, 1304 proxyHeaders: []apimirror.HTTPHeader{}, 1305 want: http.Header{ 1306 testHeader: []string{testHeaderValue}, 1307 "Connection": []string{"close"}, 1308 }, 1309 }, 1310 { 1311 name: "Only Origin, has multiple values", 1312 originHeaders: http.Header{ 1313 testHeader: []string{testHeaderValue, testHeaderValue}, 1314 }, 1315 proxyHeaders: []apimirror.HTTPHeader{}, 1316 want: http.Header{ 1317 testHeader: []string{testHeaderValue, testHeaderValue}, 1318 "Connection": []string{"close"}, 1319 }, 1320 }, 1321 { 1322 name: "Only Proxy", 1323 originHeaders: http.Header{}, 1324 proxyHeaders: []apimirror.HTTPHeader{ 1325 { 1326 Name: testHeader, 1327 Value: testHeaderValue, 1328 }, 1329 }, 1330 want: http.Header{ 1331 "Connection": []string{"close"}, 1332 }, 1333 }, 1334 { 1335 name: "Only Proxy, has multiple values", 1336 originHeaders: http.Header{}, 1337 proxyHeaders: []apimirror.HTTPHeader{ 1338 { 1339 Name: testHeader, 1340 Value: testHeaderValue, 1341 }, 1342 { 1343 Name: testHeader, 1344 Value: testHeaderValue, 1345 }, 1346 }, 1347 want: http.Header{ 1348 "Connection": []string{"close"}, 1349 }, 1350 }, 1351 { 1352 name: "Proxy overwrites Origin", 1353 originHeaders: http.Header{ 1354 testHeader: []string{testHeaderValue}, 1355 }, 1356 proxyHeaders: []apimirror.HTTPHeader{ 1357 { 1358 Name: testHeader, 1359 Value: testHeaderValue + "over", 1360 }, 1361 }, 1362 want: http.Header{ 1363 testHeader: []string{testHeaderValue}, 1364 "Connection": []string{"close"}, 1365 }, 1366 }, 1367 } 1368 for _, tc := range testCases { 1369 t.Run(tc.name, func(t *testing.T) { 1370 svc := headerChecker(t, tc.want) 1371 defer svc.Close() 1372 probePath := "/app-health/hello-world/livez" 1373 appAddress := svc.Addr().(*net.TCPAddr) 1374 appProber, err := json.Marshal(KubeAppProbers{ 1375 probePath: &Prober{ 1376 HTTPGet: &apimirror.HTTPGetAction{ 1377 Port: intstr.IntOrString{IntVal: int32(appAddress.Port)}, 1378 Host: appAddress.IP.String(), 1379 Path: "/header", 1380 HTTPHeaders: tc.proxyHeaders, 1381 }, 1382 }, 1383 }) 1384 if err != nil { 1385 t.Fatalf("invalid app probers") 1386 } 1387 config := Options{ 1388 KubeAppProbers: string(appProber), 1389 } 1390 // Starts the pilot agent status server. 1391 server := NewTestServer(t, config) 1392 client := http.Client{} 1393 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%v%s", server.statusPort, probePath), nil) 1394 if err != nil { 1395 t.Fatal("failed to create request: ", err) 1396 } 1397 req.Header = tc.originHeaders 1398 resp, err := client.Do(req) 1399 if err != nil { 1400 t.Fatal("request failed: ", err) 1401 } 1402 defer resp.Body.Close() 1403 if resp.StatusCode != http.StatusOK { 1404 t.Errorf("unexpected status code, want = %v, got = %v", http.StatusOK, resp.StatusCode) 1405 } 1406 }) 1407 } 1408 } 1409 1410 func TestHandleQuit(t *testing.T) { 1411 tests := []struct { 1412 name string 1413 method string 1414 remoteAddr string 1415 expected int 1416 }{ 1417 { 1418 name: "should send a sigterm for valid requests", 1419 method: "POST", 1420 remoteAddr: "127.0.0.1", 1421 expected: http.StatusOK, 1422 }, 1423 { 1424 name: "should send a sigterm for valid ipv6 requests", 1425 method: "POST", 1426 remoteAddr: "[::1]", 1427 expected: http.StatusOK, 1428 }, 1429 { 1430 name: "should require POST method", 1431 method: "GET", 1432 remoteAddr: "127.0.0.1", 1433 expected: http.StatusMethodNotAllowed, 1434 }, 1435 { 1436 name: "should require localhost", 1437 method: "POST", 1438 expected: http.StatusForbidden, 1439 }, 1440 } 1441 1442 for _, tt := range tests { 1443 t.Run(tt.name, func(t *testing.T) { 1444 shutdown := make(chan struct{}) 1445 s := NewTestServer(t, Options{ 1446 Shutdown: func() { 1447 close(shutdown) 1448 }, 1449 }) 1450 req, err := http.NewRequest(tt.method, "/quitquitquit", nil) 1451 if err != nil { 1452 t.Fatal(err) 1453 } 1454 1455 if tt.remoteAddr != "" { 1456 req.RemoteAddr = tt.remoteAddr + ":" + fmt.Sprint(s.statusPort) 1457 } 1458 1459 resp := httptest.NewRecorder() 1460 s.handleQuit(resp, req) 1461 if resp.Code != tt.expected { 1462 t.Fatalf("Expected response code %v got %v", tt.expected, resp.Code) 1463 } 1464 1465 if tt.expected == http.StatusOK { 1466 select { 1467 case <-shutdown: 1468 case <-time.After(time.Second): 1469 t.Fatalf("Failed to receive expected shutdown") 1470 } 1471 } else { 1472 select { 1473 case <-shutdown: 1474 t.Fatalf("unexpected shutdown") 1475 default: 1476 } 1477 } 1478 }) 1479 } 1480 } 1481 1482 func TestAdditionalProbes(t *testing.T) { 1483 rp := readyProbe{} 1484 urp := unreadyProbe{} 1485 testCases := []struct { 1486 name string 1487 probes []ready.Prober 1488 err error 1489 }{ 1490 { 1491 name: "success probe", 1492 probes: []ready.Prober{rp}, 1493 err: nil, 1494 }, 1495 { 1496 name: "not ready probe", 1497 probes: []ready.Prober{urp}, 1498 err: errors.New("not ready"), 1499 }, 1500 { 1501 name: "both probes", 1502 probes: []ready.Prober{rp, urp}, 1503 err: errors.New("not ready"), 1504 }, 1505 } 1506 testServer := testserver.CreateAndStartServer(liveServerStats) 1507 defer testServer.Close() 1508 for _, tc := range testCases { 1509 server, err := NewServer(Options{ 1510 Probes: tc.probes, 1511 AdminPort: uint16(testServer.Listener.Addr().(*net.TCPAddr).Port), 1512 }) 1513 if err != nil { 1514 t.Errorf("failed to construct server") 1515 } 1516 err = server.isReady() 1517 if tc.err == nil { 1518 if err != nil { 1519 t.Errorf("Unexpected result, expected: %v got: %v", tc.err, err) 1520 } 1521 } else { 1522 if err.Error() != tc.err.Error() { 1523 t.Errorf("Unexpected result, expected: %v got: %v", tc.err, err) 1524 } 1525 } 1526 1527 } 1528 } 1529 1530 type readyProbe struct{} 1531 1532 func (s readyProbe) Check() error { 1533 return nil 1534 } 1535 1536 type unreadyProbe struct{} 1537 1538 func (u unreadyProbe) Check() error { 1539 return errors.New("not ready") 1540 } 1541 1542 var reg = lazy.New(initializeMonitoring) 1543 1544 func TestingRegistry(t test.Failer) prometheus.Gatherer { 1545 r, err := reg.Get() 1546 if err != nil { 1547 t.Fatal(err) 1548 } 1549 return r 1550 }