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  }