mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Use IntOrString for HTTP health check ports
Clean up code to be more testable. Add test cases for named and numeric ports in HTTP health checks. Improve tests.
This commit is contained in:
parent
e35dfedd79
commit
7201227cb1
@ -136,11 +136,11 @@ type EnvVar struct {
|
|||||||
|
|
||||||
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
||||||
type HTTPGetProbe struct {
|
type HTTPGetProbe struct {
|
||||||
// Path to access on the http server
|
// Optional: Path to access on the HTTP server.
|
||||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||||
// Name or number of the port to access on the container
|
// Required: Name or number of the port to access on the container.
|
||||||
Port string `yaml:"port,omitempty" json:"port,omitempty"`
|
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||||
// Host name to connect to. Optional, default: "localhost"
|
// Optional: Host name to connect to, defaults to the pod IP.
|
||||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +139,11 @@ type EnvVar struct {
|
|||||||
|
|
||||||
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
||||||
type HTTPGetProbe struct {
|
type HTTPGetProbe struct {
|
||||||
// Path to access on the http server
|
// Optional: Path to access on the HTTP server.
|
||||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||||
// Name or number of the port to access on the container
|
// Required: Name or number of the port to access on the container.
|
||||||
Port string `yaml:"port,omitempty" json:"port,omitempty"`
|
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||||
// Host name to connect to. Optional, default: "localhost"
|
// Optional: Host name to connect to, defaults to the pod IP.
|
||||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ type HTTPGetInterface interface {
|
|||||||
Get(url string) (*http.Response, error)
|
Get(url string) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check checks if a GET request to the url succeeds.
|
// DoHTTPCheck checks if a GET request to the url succeeds.
|
||||||
// If the HTTP response code is successful (i.e. 400 > code >= 200), it returns Healthy.
|
// If the HTTP response code is successful (i.e. 400 > code >= 200), it returns Healthy.
|
||||||
// If the HTTP response code is unsuccessful, it returns Unhealthy.
|
// If the HTTP response code is unsuccessful, it returns Unhealthy.
|
||||||
// It returns Unknown and err if the HTTP communication itself fails.
|
// It returns Unknown and err if the HTTP communication itself fails.
|
||||||
func Check(url string, client HTTPGetInterface) (Status, error) {
|
func DoHTTPCheck(url string, client HTTPGetInterface) (Status, error) {
|
||||||
res, err := client.Get(url)
|
res, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Unknown, err
|
return Unknown, err
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,38 +66,62 @@ type HTTPHealthChecker struct {
|
|||||||
client HTTPGetInterface
|
client HTTPGetInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPHealthChecker) findPort(container api.Container, portName string) int64 {
|
// A helper function to look up a port in a container by name.
|
||||||
|
// Returns the HostPort if found, -1 if not found.
|
||||||
|
func findPortByName(container api.Container, portName string) int {
|
||||||
for _, port := range container.Ports {
|
for _, port := range container.Ports {
|
||||||
if port.Name == portName {
|
if port.Name == portName {
|
||||||
// TODO This means you can only health check exposed ports
|
return port.HostPort
|
||||||
return int64(port.HostPort)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheck checks if the container is healthy by trying sending HTTP Get requests to the container.
|
// Get the components of the target URL. For testability.
|
||||||
func (h *HTTPHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
func getURLParts(currentState api.PodState, container api.Container) (string, int, string, error) {
|
||||||
params := container.LivenessProbe.HTTPGet
|
params := container.LivenessProbe.HTTPGet
|
||||||
if params == nil {
|
if params == nil {
|
||||||
return Unknown, fmt.Errorf("Error, no HTTP parameters specified: %v", container)
|
return "", -1, "", fmt.Errorf("no HTTP parameters specified: %v", container)
|
||||||
}
|
}
|
||||||
port := h.findPort(container, params.Port)
|
port := -1
|
||||||
if port == -1 {
|
switch params.Port.Kind {
|
||||||
var err error
|
case util.IntstrInt:
|
||||||
port, err = strconv.ParseInt(params.Port, 10, 0)
|
port = params.Port.IntVal
|
||||||
if err != nil {
|
case util.IntstrString:
|
||||||
return Unknown, err
|
port = findPortByName(container, params.Port.StrVal)
|
||||||
|
if port == -1 {
|
||||||
|
// Last ditch effort - maybe it was an int stored as string?
|
||||||
|
var err error
|
||||||
|
if port, err = strconv.Atoi(params.Port.StrVal); err != nil {
|
||||||
|
return "", -1, "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if port == -1 {
|
||||||
|
return "", -1, "", fmt.Errorf("unknown port: %v", params.Port)
|
||||||
|
}
|
||||||
var host string
|
var host string
|
||||||
if len(params.Host) > 0 {
|
if len(params.Host) > 0 {
|
||||||
host = params.Host
|
host = params.Host
|
||||||
} else {
|
} else {
|
||||||
host = currentState.PodIP
|
host = currentState.PodIP
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("http://%s:%d%s", host, port, params.Path)
|
|
||||||
return Check(url, h.client)
|
return host, port, params.Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a URL from args. For testability.
|
||||||
|
func formatURL(host string, port int, path string) string {
|
||||||
|
return fmt.Sprintf("http://%s:%d%s", host, port, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck checks if the container is healthy by trying sending HTTP Get requests to the container.
|
||||||
|
func (h *HTTPHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
||||||
|
host, port, path, err := getURLParts(currentState, container)
|
||||||
|
if err != nil {
|
||||||
|
return Unknown, err
|
||||||
|
}
|
||||||
|
return DoHTTPCheck(formatURL(host, port, path), h.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPHealthChecker struct{}
|
type TCPHealthChecker struct{}
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const statusServerEarlyShutdown = -1
|
const statusServerEarlyShutdown = -1
|
||||||
@ -59,7 +60,7 @@ func TestHealthChecker(t *testing.T) {
|
|||||||
container := api.Container{
|
container := api.Container{
|
||||||
LivenessProbe: &api.LivenessProbe{
|
LivenessProbe: &api.LivenessProbe{
|
||||||
HTTPGet: &api.HTTPGetProbe{
|
HTTPGet: &api.HTTPGetProbe{
|
||||||
Port: port,
|
Port: util.MakeIntOrStringFromString(port),
|
||||||
Path: "/foo/bar",
|
Path: "/foo/bar",
|
||||||
Host: host,
|
Host: host,
|
||||||
},
|
},
|
||||||
@ -90,32 +91,91 @@ func TestFindPort(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
checker := HTTPHealthChecker{}
|
want := 8080
|
||||||
want := int64(8080)
|
got := findPortByName(container, "foo")
|
||||||
got := checker.findPort(container, "foo")
|
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("Expected %v, got %v", want, got)
|
t.Errorf("Expected %v, got %v", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetURLParts(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
probe *api.HTTPGetProbe
|
||||||
|
ok bool
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromInt(-1), Path: ""}, false, "", -1, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromString(""), Path: ""}, false, "", -1, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromString("-1"), Path: ""}, false, "", -1, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromString("not-found"), Path: ""}, false, "", -1, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromString("found"), Path: ""}, true, "127.0.0.1", 93, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromInt(76), Path: ""}, true, "127.0.0.1", 76, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "", Port: util.MakeIntOrStringFromString("118"), Path: ""}, true, "127.0.0.1", 118, ""},
|
||||||
|
{&api.HTTPGetProbe{Host: "hostname", Port: util.MakeIntOrStringFromInt(76), Path: "path"}, true, "hostname", 76, "path"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
state := api.PodState{PodIP: "127.0.0.1"}
|
||||||
|
container := api.Container{
|
||||||
|
Ports: []api.Port{{Name: "found", HostPort: 93}},
|
||||||
|
LivenessProbe: &api.LivenessProbe{
|
||||||
|
HTTPGet: test.probe,
|
||||||
|
Type: "http",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
host, port, path, err := getURLParts(state, container)
|
||||||
|
if !test.ok && err == nil {
|
||||||
|
t.Errorf("Expected error for %+v, got %s:%d/%s", test, host, port, path)
|
||||||
|
}
|
||||||
|
if test.ok && err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if test.ok {
|
||||||
|
if host != test.host || port != test.port || path != test.path {
|
||||||
|
t.Errorf("Expected %s:%d/%s, got %s:%d/%s",
|
||||||
|
test.host, test.port, test.path, host, port, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatURL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
path string
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{"localhost", 93, "", "http://localhost:93"},
|
||||||
|
{"localhost", 93, "/path", "http://localhost:93/path"},
|
||||||
|
}
|
||||||
|
for _, test := range testCases {
|
||||||
|
url := formatURL(test.host, test.port, test.path)
|
||||||
|
if url != test.result {
|
||||||
|
t.Errorf("Expected %s, got %s", test.result, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHTTPHealthChecker(t *testing.T) {
|
func TestHTTPHealthChecker(t *testing.T) {
|
||||||
httpHealthCheckerTests := []struct {
|
testCases := []struct {
|
||||||
probe *api.HTTPGetProbe
|
probe *api.HTTPGetProbe
|
||||||
status int
|
status int
|
||||||
health Status
|
health Status
|
||||||
}{
|
}{
|
||||||
{&api.HTTPGetProbe{Host: "httptest"}, http.StatusOK, Healthy},
|
// The probe will be filled in below. This is primarily testing that an HTTP GET happens.
|
||||||
{&api.HTTPGetProbe{}, http.StatusOK, Healthy},
|
{&api.HTTPGetProbe{}, http.StatusOK, Healthy},
|
||||||
|
{&api.HTTPGetProbe{}, -1, Unhealthy},
|
||||||
{nil, -1, Unknown},
|
{nil, -1, Unknown},
|
||||||
{&api.HTTPGetProbe{Port: "-1"}, -1, Unknown},
|
|
||||||
}
|
}
|
||||||
hc := &HTTPHealthChecker{
|
hc := &HTTPHealthChecker{
|
||||||
client: &http.Client{},
|
client: &http.Client{},
|
||||||
}
|
}
|
||||||
for _, httpHealthCheckerTest := range httpHealthCheckerTests {
|
for _, test := range testCases {
|
||||||
tt := httpHealthCheckerTest
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(tt.status)
|
w.WriteHeader(test.status)
|
||||||
}))
|
}))
|
||||||
u, err := url.Parse(ts.URL)
|
u, err := url.Parse(ts.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -127,28 +187,24 @@ func TestHTTPHealthChecker(t *testing.T) {
|
|||||||
}
|
}
|
||||||
container := api.Container{
|
container := api.Container{
|
||||||
LivenessProbe: &api.LivenessProbe{
|
LivenessProbe: &api.LivenessProbe{
|
||||||
HTTPGet: tt.probe,
|
HTTPGet: test.probe,
|
||||||
Type: "http",
|
Type: "http",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params := container.LivenessProbe.HTTPGet
|
params := container.LivenessProbe.HTTPGet
|
||||||
if params != nil {
|
if params != nil {
|
||||||
if params.Port == "" {
|
params.Port = util.MakeIntOrStringFromString(port)
|
||||||
params.Port = port
|
params.Host = host
|
||||||
}
|
|
||||||
if params.Host == "httptest" {
|
|
||||||
params.Host = host
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
health, err := hc.HealthCheck(api.PodState{PodIP: host}, container)
|
health, err := hc.HealthCheck(api.PodState{PodIP: host}, container)
|
||||||
if tt.health == Unknown && err == nil {
|
if test.health == Unknown && err == nil {
|
||||||
t.Errorf("Expected error")
|
t.Errorf("Expected error")
|
||||||
}
|
}
|
||||||
if tt.health != Unknown && err != nil {
|
if test.health != Unknown && err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if health != tt.health {
|
if health != test.health {
|
||||||
t.Errorf("Expected %v, got %v", tt.health, health)
|
t.Errorf("Expected %v, got %v", test.health, health)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +284,7 @@ func TestMuxHealthChecker(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
container.LivenessProbe.Type = tt.probeType
|
container.LivenessProbe.Type = tt.probeType
|
||||||
container.LivenessProbe.HTTPGet.Port = port
|
container.LivenessProbe.HTTPGet.Port = util.MakeIntOrStringFromString(port)
|
||||||
container.LivenessProbe.HTTPGet.Host = host
|
container.LivenessProbe.HTTPGet.Host = host
|
||||||
health, err := mc.HealthCheck(api.PodState{}, container)
|
health, err := mc.HealthCheck(api.PodState{}, container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -49,7 +49,7 @@ func (h *HealthyMinionRegistry) List() (currentMinions []string, err error) {
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
for _, minion := range list {
|
for _, minion := range list {
|
||||||
status, err := health.Check(h.makeMinionURL(minion), h.client)
|
status, err := health.DoHTTPCheck(h.makeMinionURL(minion), h.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("%s failed health check with error: %s", minion, err)
|
glog.Errorf("%s failed health check with error: %s", minion, err)
|
||||||
continue
|
continue
|
||||||
@ -77,7 +77,7 @@ func (h *HealthyMinionRegistry) Contains(minion string) (bool, error) {
|
|||||||
if !contains {
|
if !contains {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
status, err := health.Check(h.makeMinionURL(minion), h.client)
|
status, err := health.DoHTTPCheck(h.makeMinionURL(minion), h.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,16 @@ const (
|
|||||||
IntstrString // The IntOrString holds a string.
|
IntstrString // The IntOrString holds a string.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MakeIntOrStringFromInt creates an IntOrString object with an int value.
|
||||||
|
func MakeIntOrStringFromInt(val int) IntOrString {
|
||||||
|
return IntOrString{Kind: IntstrInt, IntVal: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeIntOrStringFromInt creates an IntOrString object with a string value.
|
||||||
|
func MakeIntOrStringFromString(val string) IntOrString {
|
||||||
|
return IntOrString{Kind: IntstrString, StrVal: val}
|
||||||
|
}
|
||||||
|
|
||||||
// SetYAML implements the yaml.Setter interface.
|
// SetYAML implements the yaml.Setter interface.
|
||||||
func (intstr *IntOrString) SetYAML(tag string, value interface{}) bool {
|
func (intstr *IntOrString) SetYAML(tag string, value interface{}) bool {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
|
@ -71,6 +71,20 @@ func TestHandleCrash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMakeIntOrStringFromInt(t *testing.T) {
|
||||||
|
i := MakeIntOrStringFromInt(93)
|
||||||
|
if i.Kind != IntstrInt || i.IntVal != 93 {
|
||||||
|
t.Errorf("Expected IntVal=93, got %+v", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeIntOrStringFromString(t *testing.T) {
|
||||||
|
i := MakeIntOrStringFromString("76")
|
||||||
|
if i.Kind != IntstrString || i.StrVal != "76" {
|
||||||
|
t.Errorf("Expected StrVal=\"76\", got %+v", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type IntOrStringHolder struct {
|
type IntOrStringHolder struct {
|
||||||
IOrS IntOrString `json:"val" yaml:"val"`
|
IOrS IntOrString `json:"val" yaml:"val"`
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user