mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-03 23:40:03 +00:00 
			
		
		
		
	PR#76518 introduced a change to limit the the read on various calls with utilio.ReadAtMost. By design, ReadAtMost will return an error if there is a truncation, but the calling code needs to know to handle it.
		
			
				
	
	
		
			439 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			439 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2015 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package http
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						|
	"k8s.io/kubernetes/pkg/probe"
 | 
						|
)
 | 
						|
 | 
						|
const FailureCode int = -1
 | 
						|
 | 
						|
func setEnv(key, value string) func() {
 | 
						|
	originalValue := os.Getenv(key)
 | 
						|
	os.Setenv(key, value)
 | 
						|
	if len(originalValue) > 0 {
 | 
						|
		return func() {
 | 
						|
			os.Setenv(key, originalValue)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return func() {}
 | 
						|
}
 | 
						|
 | 
						|
func unsetEnv(key string) func() {
 | 
						|
	originalValue := os.Getenv(key)
 | 
						|
	os.Unsetenv(key)
 | 
						|
	if len(originalValue) > 0 {
 | 
						|
		return func() {
 | 
						|
			os.Setenv(key, originalValue)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return func() {}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeProxy(t *testing.T) {
 | 
						|
	res := "welcome to http probe proxy"
 | 
						|
	localProxy := "http://127.0.0.1:9098/"
 | 
						|
 | 
						|
	defer setEnv("http_proxy", localProxy)()
 | 
						|
	defer setEnv("HTTP_PROXY", localProxy)()
 | 
						|
	defer unsetEnv("no_proxy")()
 | 
						|
	defer unsetEnv("NO_PROXY")()
 | 
						|
 | 
						|
	followNonLocalRedirects := true
 | 
						|
	prober := New(followNonLocalRedirects)
 | 
						|
 | 
						|
	go func() {
 | 
						|
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
						|
			fmt.Fprintf(w, res)
 | 
						|
		})
 | 
						|
		err := http.ListenAndServe(":9098", nil)
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Failed to start foo server: localhost:9098")
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// take some time to wait server boot
 | 
						|
	time.Sleep(2 * time.Second)
 | 
						|
	url, err := url.Parse("http://example.com")
 | 
						|
	if err != nil {
 | 
						|
		t.Errorf("proxy test unexpected error: %v", err)
 | 
						|
	}
 | 
						|
	_, response, _ := prober.Probe(url, http.Header{}, time.Second*3)
 | 
						|
 | 
						|
	if response == res {
 | 
						|
		t.Errorf("proxy test unexpected error: the probe is using proxy")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeChecker(t *testing.T) {
 | 
						|
	handleReq := func(s int, body string) func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		return func(w http.ResponseWriter, r *http.Request) {
 | 
						|
			w.WriteHeader(s)
 | 
						|
			w.Write([]byte(body))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Echo handler that returns the contents of request headers in the body
 | 
						|
	headerEchoHandler := func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		w.WriteHeader(200)
 | 
						|
		output := ""
 | 
						|
		for k, arr := range r.Header {
 | 
						|
			for _, v := range arr {
 | 
						|
				output += fmt.Sprintf("%s: %s\n", k, v)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		w.Write([]byte(output))
 | 
						|
	}
 | 
						|
 | 
						|
	redirectHandler := func(s int, bad bool) func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		return func(w http.ResponseWriter, r *http.Request) {
 | 
						|
			if r.URL.Path == "/" {
 | 
						|
				http.Redirect(w, r, "/new", s)
 | 
						|
			} else if bad && r.URL.Path == "/new" {
 | 
						|
				http.Error(w, "", http.StatusInternalServerError)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	followNonLocalRedirects := true
 | 
						|
	prober := New(followNonLocalRedirects)
 | 
						|
	testCases := []struct {
 | 
						|
		handler    func(w http.ResponseWriter, r *http.Request)
 | 
						|
		reqHeaders http.Header
 | 
						|
		health     probe.Result
 | 
						|
		accBody    string
 | 
						|
		notBody    string
 | 
						|
	}{
 | 
						|
		// The probe will be filled in below.  This is primarily testing that an HTTP GET happens.
 | 
						|
		{
 | 
						|
			handler: handleReq(http.StatusOK, "ok body"),
 | 
						|
			health:  probe.Success,
 | 
						|
			accBody: "ok body",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: headerEchoHandler,
 | 
						|
			reqHeaders: http.Header{
 | 
						|
				"X-Muffins-Or-Cupcakes": {"muffins"},
 | 
						|
			},
 | 
						|
			health:  probe.Success,
 | 
						|
			accBody: "X-Muffins-Or-Cupcakes: muffins",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: headerEchoHandler,
 | 
						|
			reqHeaders: http.Header{
 | 
						|
				"User-Agent": {"foo/1.0"},
 | 
						|
			},
 | 
						|
			health:  probe.Success,
 | 
						|
			accBody: "User-Agent: foo/1.0",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: headerEchoHandler,
 | 
						|
			reqHeaders: http.Header{
 | 
						|
				"User-Agent": {""},
 | 
						|
			},
 | 
						|
			health:  probe.Success,
 | 
						|
			notBody: "User-Agent",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler:    headerEchoHandler,
 | 
						|
			reqHeaders: http.Header{},
 | 
						|
			health:     probe.Success,
 | 
						|
			accBody:    "User-Agent: kube-probe/",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			// Echo handler that returns the contents of Host in the body
 | 
						|
			handler: func(w http.ResponseWriter, r *http.Request) {
 | 
						|
				w.WriteHeader(200)
 | 
						|
				w.Write([]byte(r.Host))
 | 
						|
			},
 | 
						|
			reqHeaders: http.Header{
 | 
						|
				"Host": {"muffins.cupcakes.org"},
 | 
						|
			},
 | 
						|
			health:  probe.Success,
 | 
						|
			accBody: "muffins.cupcakes.org",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: handleReq(FailureCode, "fail body"),
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: handleReq(http.StatusInternalServerError, "fail body"),
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: func(w http.ResponseWriter, r *http.Request) {
 | 
						|
				time.Sleep(3 * time.Second)
 | 
						|
			},
 | 
						|
			health: probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusMovedPermanently, false), // 301
 | 
						|
			health:  probe.Success,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusMovedPermanently, true), // 301
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusFound, false), // 302
 | 
						|
			health:  probe.Success,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusFound, true), // 302
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusTemporaryRedirect, false), // 307
 | 
						|
			health:  probe.Success,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusTemporaryRedirect, true), // 307
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusPermanentRedirect, false), // 308
 | 
						|
			health:  probe.Success,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			handler: redirectHandler(http.StatusPermanentRedirect, true), // 308
 | 
						|
			health:  probe.Failure,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for i, test := range testCases {
 | 
						|
		t.Run(fmt.Sprintf("case-%2d", i), func(t *testing.T) {
 | 
						|
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
				test.handler(w, r)
 | 
						|
			}))
 | 
						|
			defer server.Close()
 | 
						|
			u, err := url.Parse(server.URL)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("case %d: unexpected error: %v", i, err)
 | 
						|
			}
 | 
						|
			_, port, err := net.SplitHostPort(u.Host)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("case %d: unexpected error: %v", i, err)
 | 
						|
			}
 | 
						|
			_, err = strconv.Atoi(port)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("case %d: unexpected error: %v", i, err)
 | 
						|
			}
 | 
						|
			health, output, err := prober.Probe(u, test.reqHeaders, 1*time.Second)
 | 
						|
			if test.health == probe.Unknown && err == nil {
 | 
						|
				t.Errorf("case %d: expected error", i)
 | 
						|
			}
 | 
						|
			if test.health != probe.Unknown && err != nil {
 | 
						|
				t.Errorf("case %d: unexpected error: %v", i, err)
 | 
						|
			}
 | 
						|
			if health != test.health {
 | 
						|
				t.Errorf("case %d: expected %v, got %v", i, test.health, health)
 | 
						|
			}
 | 
						|
			if health != probe.Failure && test.health != probe.Failure {
 | 
						|
				if !strings.Contains(output, test.accBody) {
 | 
						|
					t.Errorf("Expected response body to contain %v, got %v", test.accBody, output)
 | 
						|
				}
 | 
						|
				if test.notBody != "" && strings.Contains(output, test.notBody) {
 | 
						|
					t.Errorf("Expected response not to contain %v, got %v", test.notBody, output)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeChecker_NonLocalRedirects(t *testing.T) {
 | 
						|
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		switch r.URL.Path {
 | 
						|
		case "/redirect":
 | 
						|
			loc, _ := url.QueryUnescape(r.URL.Query().Get("loc"))
 | 
						|
			http.Redirect(w, r, loc, http.StatusFound)
 | 
						|
		case "/loop":
 | 
						|
			http.Redirect(w, r, "/loop", http.StatusFound)
 | 
						|
		case "/success":
 | 
						|
			w.WriteHeader(http.StatusOK)
 | 
						|
		default:
 | 
						|
			http.Error(w, "", http.StatusInternalServerError)
 | 
						|
		}
 | 
						|
	})
 | 
						|
	server := httptest.NewServer(handler)
 | 
						|
	defer server.Close()
 | 
						|
 | 
						|
	newportServer := httptest.NewServer(handler)
 | 
						|
	defer newportServer.Close()
 | 
						|
 | 
						|
	testCases := map[string]struct {
 | 
						|
		redirect             string
 | 
						|
		expectLocalResult    probe.Result
 | 
						|
		expectNonLocalResult probe.Result
 | 
						|
	}{
 | 
						|
		"local success":   {"/success", probe.Success, probe.Success},
 | 
						|
		"local fail":      {"/fail", probe.Failure, probe.Failure},
 | 
						|
		"newport success": {newportServer.URL + "/success", probe.Success, probe.Success},
 | 
						|
		"newport fail":    {newportServer.URL + "/fail", probe.Failure, probe.Failure},
 | 
						|
		"bogus nonlocal":  {"http://0.0.0.0/fail", probe.Warning, probe.Failure},
 | 
						|
		"redirect loop":   {"/loop", probe.Failure, probe.Failure},
 | 
						|
	}
 | 
						|
	for desc, test := range testCases {
 | 
						|
		t.Run(desc+"-local", func(t *testing.T) {
 | 
						|
			followNonLocalRedirects := false
 | 
						|
			prober := New(followNonLocalRedirects)
 | 
						|
			target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
 | 
						|
			require.NoError(t, err)
 | 
						|
			result, _, _ := prober.Probe(target, nil, wait.ForeverTestTimeout)
 | 
						|
			assert.Equal(t, test.expectLocalResult, result)
 | 
						|
		})
 | 
						|
		t.Run(desc+"-nonlocal", func(t *testing.T) {
 | 
						|
			followNonLocalRedirects := true
 | 
						|
			prober := New(followNonLocalRedirects)
 | 
						|
			target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
 | 
						|
			require.NoError(t, err)
 | 
						|
			result, _, _ := prober.Probe(target, nil, wait.ForeverTestTimeout)
 | 
						|
			assert.Equal(t, test.expectNonLocalResult, result)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeChecker_HostHeaderPreservedAfterRedirect(t *testing.T) {
 | 
						|
	successHostHeader := "www.success.com"
 | 
						|
	failHostHeader := "www.fail.com"
 | 
						|
 | 
						|
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		switch r.URL.Path {
 | 
						|
		case "/redirect":
 | 
						|
			http.Redirect(w, r, "/success", http.StatusFound)
 | 
						|
		case "/success":
 | 
						|
			if r.Host == successHostHeader {
 | 
						|
				w.WriteHeader(http.StatusOK)
 | 
						|
			} else {
 | 
						|
				http.Error(w, "", http.StatusBadRequest)
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			http.Error(w, "", http.StatusInternalServerError)
 | 
						|
		}
 | 
						|
	})
 | 
						|
	server := httptest.NewServer(handler)
 | 
						|
	defer server.Close()
 | 
						|
 | 
						|
	testCases := map[string]struct {
 | 
						|
		hostHeader     string
 | 
						|
		expectedResult probe.Result
 | 
						|
	}{
 | 
						|
		"success": {successHostHeader, probe.Success},
 | 
						|
		"fail":    {failHostHeader, probe.Failure},
 | 
						|
	}
 | 
						|
	for desc, test := range testCases {
 | 
						|
		headers := http.Header{}
 | 
						|
		headers.Add("Host", test.hostHeader)
 | 
						|
		t.Run(desc+"local", func(t *testing.T) {
 | 
						|
			followNonLocalRedirects := false
 | 
						|
			prober := New(followNonLocalRedirects)
 | 
						|
			target, err := url.Parse(server.URL + "/redirect")
 | 
						|
			require.NoError(t, err)
 | 
						|
			result, _, _ := prober.Probe(target, headers, wait.ForeverTestTimeout)
 | 
						|
			assert.Equal(t, test.expectedResult, result)
 | 
						|
		})
 | 
						|
		t.Run(desc+"nonlocal", func(t *testing.T) {
 | 
						|
			followNonLocalRedirects := true
 | 
						|
			prober := New(followNonLocalRedirects)
 | 
						|
			target, err := url.Parse(server.URL + "/redirect")
 | 
						|
			require.NoError(t, err)
 | 
						|
			result, _, _ := prober.Probe(target, headers, wait.ForeverTestTimeout)
 | 
						|
			assert.Equal(t, test.expectedResult, result)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeChecker_PayloadTruncated(t *testing.T) {
 | 
						|
	successHostHeader := "www.success.com"
 | 
						|
	oversizePayload := bytes.Repeat([]byte("a"), maxRespBodyLength+1)
 | 
						|
	truncatedPayload := bytes.Repeat([]byte("a"), maxRespBodyLength)
 | 
						|
 | 
						|
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		switch r.URL.Path {
 | 
						|
		case "/success":
 | 
						|
			if r.Host == successHostHeader {
 | 
						|
				w.WriteHeader(http.StatusOK)
 | 
						|
				w.Write(oversizePayload)
 | 
						|
			} else {
 | 
						|
				http.Error(w, "", http.StatusBadRequest)
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			http.Error(w, "", http.StatusInternalServerError)
 | 
						|
		}
 | 
						|
	})
 | 
						|
	server := httptest.NewServer(handler)
 | 
						|
	defer server.Close()
 | 
						|
 | 
						|
	headers := http.Header{}
 | 
						|
	headers.Add("Host", successHostHeader)
 | 
						|
	t.Run("truncated payload", func(t *testing.T) {
 | 
						|
		prober := New(false)
 | 
						|
		target, err := url.Parse(server.URL + "/success")
 | 
						|
		require.NoError(t, err)
 | 
						|
		result, body, err := prober.Probe(target, headers, wait.ForeverTestTimeout)
 | 
						|
		assert.NoError(t, err)
 | 
						|
		assert.Equal(t, result, probe.Success)
 | 
						|
		assert.Equal(t, body, string(truncatedPayload))
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPProbeChecker_PayloadNormal(t *testing.T) {
 | 
						|
	successHostHeader := "www.success.com"
 | 
						|
	normalPayload := bytes.Repeat([]byte("a"), maxRespBodyLength-1)
 | 
						|
 | 
						|
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		switch r.URL.Path {
 | 
						|
		case "/success":
 | 
						|
			if r.Host == successHostHeader {
 | 
						|
				w.WriteHeader(http.StatusOK)
 | 
						|
				w.Write(normalPayload)
 | 
						|
			} else {
 | 
						|
				http.Error(w, "", http.StatusBadRequest)
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			http.Error(w, "", http.StatusInternalServerError)
 | 
						|
		}
 | 
						|
	})
 | 
						|
	server := httptest.NewServer(handler)
 | 
						|
	defer server.Close()
 | 
						|
 | 
						|
	headers := http.Header{}
 | 
						|
	headers.Add("Host", successHostHeader)
 | 
						|
	t.Run("normal payload", func(t *testing.T) {
 | 
						|
		prober := New(false)
 | 
						|
		target, err := url.Parse(server.URL + "/success")
 | 
						|
		require.NoError(t, err)
 | 
						|
		result, body, err := prober.Probe(target, headers, wait.ForeverTestTimeout)
 | 
						|
		assert.NoError(t, err)
 | 
						|
		assert.Equal(t, result, probe.Success)
 | 
						|
		assert.Equal(t, body, string(normalPayload))
 | 
						|
	})
 | 
						|
}
 |