mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
Merge pull request #847 from thockin/health
Use IntOrString for health checks
This commit is contained in:
commit
c372b74bc8
@ -136,18 +136,18 @@ type EnvVar struct {
|
||||
|
||||
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
||||
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"`
|
||||
// Name or number of the port to access on the container
|
||||
Port string `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Host name to connect to. Optional, default: "localhost"
|
||||
// Required: Name or number of the port to access on the container.
|
||||
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Optional: Host name to connect to, defaults to the pod IP.
|
||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// TCPSocketProbe describes a liveness probe based on opening a socket
|
||||
type TCPSocketProbe struct {
|
||||
// Port is the port to connect to. Required.
|
||||
Port int `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Required: Port to connect to.
|
||||
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// LivenessProbe describes a liveness probe to be examined to the container.
|
||||
|
@ -139,18 +139,18 @@ type EnvVar struct {
|
||||
|
||||
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
||||
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"`
|
||||
// Name or number of the port to access on the container
|
||||
Port string `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Host name to connect to. Optional, default: "localhost"
|
||||
// Required: Name or number of the port to access on the container.
|
||||
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Optional: Host name to connect to, defaults to the pod IP.
|
||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// TCPSocketProbe describes a liveness probe based on opening a socket
|
||||
type TCPSocketProbe struct {
|
||||
// Port is the port to connect to. Required.
|
||||
Port int `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
// Required: Port to connect to.
|
||||
Port util.IntOrString `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// LivenessProbe describes a liveness probe to be examined to the container.
|
||||
|
@ -19,36 +19,61 @@ package health
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Status represents the result of a single health-check operation.
|
||||
type Status int
|
||||
|
||||
// Status takes only one of values of these constants.
|
||||
// Status values must be one of these constants.
|
||||
const (
|
||||
Healthy Status = iota
|
||||
Unhealthy
|
||||
Unknown
|
||||
)
|
||||
|
||||
// HTTPGetInterface is an abstract interface for testability. It abstracts the interface of http.Client.Get.
|
||||
type HTTPGetInterface interface {
|
||||
Get(url string) (*http.Response, error)
|
||||
// HealthChecker defines an abstract interface for checking container health.
|
||||
type HealthChecker interface {
|
||||
HealthCheck(currentState api.PodState, container api.Container) (Status, error)
|
||||
}
|
||||
|
||||
// Check 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 unsuccessful, it returns Unhealthy.
|
||||
// It returns Unknown and err if the HTTP communication itself fails.
|
||||
func Check(url string, client HTTPGetInterface) (Status, error) {
|
||||
res, err := client.Get(url)
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
// NewHealthChecker creates a new HealthChecker which supports multiple types of liveness probes.
|
||||
func NewHealthChecker() HealthChecker {
|
||||
return &muxHealthChecker{
|
||||
checkers: map[string]HealthChecker{
|
||||
"http": &HTTPHealthChecker{
|
||||
client: &http.Client{},
|
||||
},
|
||||
"tcp": &TCPHealthChecker{},
|
||||
},
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest {
|
||||
return Healthy, nil
|
||||
}
|
||||
glog.V(1).Infof("Health check failed for %s, Response: %v", url, *res)
|
||||
return Unhealthy, nil
|
||||
}
|
||||
|
||||
// muxHealthChecker bundles multiple implementations of HealthChecker of different types.
|
||||
type muxHealthChecker struct {
|
||||
checkers map[string]HealthChecker
|
||||
}
|
||||
|
||||
// HealthCheck delegates the health-checking of the container to one of the bundled implementations.
|
||||
// It chooses an implementation according to container.LivenessProbe.Type.
|
||||
// If there is no matching health checker it returns Unknown, nil.
|
||||
func (m *muxHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
||||
checker, ok := m.checkers[container.LivenessProbe.Type]
|
||||
if !ok || checker == nil {
|
||||
glog.Warningf("Failed to find health checker for %s %s", container.Name, container.LivenessProbe.Type)
|
||||
return Unknown, nil
|
||||
}
|
||||
return checker.HealthCheck(currentState, container)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if port.Name == portName {
|
||||
return port.HostPort
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
@ -1,121 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// HealthChecker defines an abstract interface for checking container health.
|
||||
type HealthChecker interface {
|
||||
HealthCheck(currentState api.PodState, container api.Container) (Status, error)
|
||||
}
|
||||
|
||||
// NewHealthChecker creates a new HealthChecker which supports multiple types of liveness probes.
|
||||
func NewHealthChecker() HealthChecker {
|
||||
return &MuxHealthChecker{
|
||||
checkers: map[string]HealthChecker{
|
||||
"http": &HTTPHealthChecker{
|
||||
client: &http.Client{},
|
||||
},
|
||||
"tcp": &TCPHealthChecker{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MuxHealthChecker bundles multiple implementations of HealthChecker of different types.
|
||||
type MuxHealthChecker struct {
|
||||
checkers map[string]HealthChecker
|
||||
}
|
||||
|
||||
// HealthCheck delegates the health-checking of the container to one of the bundled implementations.
|
||||
// It chooses an implementation according to container.LivenessProbe.Type.
|
||||
// If there is no matching health checker it returns Unknown, nil.
|
||||
func (m *MuxHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
||||
checker, ok := m.checkers[container.LivenessProbe.Type]
|
||||
if !ok || checker == nil {
|
||||
glog.Warningf("Failed to find health checker for %s %s", container.Name, container.LivenessProbe.Type)
|
||||
return Unknown, nil
|
||||
}
|
||||
return checker.HealthCheck(currentState, container)
|
||||
}
|
||||
|
||||
// HTTPHealthChecker is an implementation of HealthChecker which checks container health by sending HTTP Get requests.
|
||||
type HTTPHealthChecker struct {
|
||||
client HTTPGetInterface
|
||||
}
|
||||
|
||||
func (h *HTTPHealthChecker) findPort(container api.Container, portName string) int64 {
|
||||
for _, port := range container.Ports {
|
||||
if port.Name == portName {
|
||||
// TODO This means you can only health check exposed ports
|
||||
return int64(port.HostPort)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// 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) {
|
||||
params := container.LivenessProbe.HTTPGet
|
||||
if params == nil {
|
||||
return Unknown, fmt.Errorf("Error, no HTTP parameters specified: %v", container)
|
||||
}
|
||||
port := h.findPort(container, params.Port)
|
||||
if port == -1 {
|
||||
var err error
|
||||
port, err = strconv.ParseInt(params.Port, 10, 0)
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
}
|
||||
}
|
||||
var host string
|
||||
if len(params.Host) > 0 {
|
||||
host = params.Host
|
||||
} else {
|
||||
host = currentState.PodIP
|
||||
}
|
||||
url := fmt.Sprintf("http://%s:%d%s", host, port, params.Path)
|
||||
return Check(url, h.client)
|
||||
}
|
||||
|
||||
type TCPHealthChecker struct{}
|
||||
|
||||
func (t *TCPHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
||||
params := container.LivenessProbe.TCPSocket
|
||||
if params == nil {
|
||||
return Unknown, fmt.Errorf("error, no TCP parameters specified: %v", container)
|
||||
}
|
||||
if len(currentState.PodIP) == 0 {
|
||||
return Unknown, fmt.Errorf("no host specified.")
|
||||
}
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort(currentState.PodIP, strconv.Itoa(params.Port)))
|
||||
if err != nil {
|
||||
return Unhealthy, nil
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error closing health check socket: %v (%#v)", err, err)
|
||||
}
|
||||
return Healthy, nil
|
||||
}
|
@ -21,10 +21,10 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
const statusServerEarlyShutdown = -1
|
||||
@ -59,7 +59,7 @@ func TestHealthChecker(t *testing.T) {
|
||||
container := api.Container{
|
||||
LivenessProbe: &api.LivenessProbe{
|
||||
HTTPGet: &api.HTTPGetProbe{
|
||||
Port: port,
|
||||
Port: util.MakeIntOrStringFromString(port),
|
||||
Path: "/foo/bar",
|
||||
Host: host,
|
||||
},
|
||||
@ -77,7 +77,7 @@ func TestHealthChecker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPort(t *testing.T) {
|
||||
func TestFindPortByName(t *testing.T) {
|
||||
container := api.Container{
|
||||
Ports: []api.Port{
|
||||
{
|
||||
@ -90,110 +90,13 @@ func TestFindPort(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
checker := HTTPHealthChecker{}
|
||||
want := int64(8080)
|
||||
got := checker.findPort(container, "foo")
|
||||
want := 8080
|
||||
got := findPortByName(container, "foo")
|
||||
if got != want {
|
||||
t.Errorf("Expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHealthChecker(t *testing.T) {
|
||||
httpHealthCheckerTests := []struct {
|
||||
probe *api.HTTPGetProbe
|
||||
status int
|
||||
health Status
|
||||
}{
|
||||
{&api.HTTPGetProbe{Host: "httptest"}, http.StatusOK, Healthy},
|
||||
{&api.HTTPGetProbe{}, http.StatusOK, Healthy},
|
||||
{nil, -1, Unknown},
|
||||
{&api.HTTPGetProbe{Port: "-1"}, -1, Unknown},
|
||||
}
|
||||
hc := &HTTPHealthChecker{
|
||||
client: &http.Client{},
|
||||
}
|
||||
for _, httpHealthCheckerTest := range httpHealthCheckerTests {
|
||||
tt := httpHealthCheckerTest
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.status)
|
||||
}))
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
container := api.Container{
|
||||
LivenessProbe: &api.LivenessProbe{
|
||||
HTTPGet: tt.probe,
|
||||
Type: "http",
|
||||
},
|
||||
}
|
||||
params := container.LivenessProbe.HTTPGet
|
||||
if params != nil {
|
||||
if params.Port == "" {
|
||||
params.Port = port
|
||||
}
|
||||
if params.Host == "httptest" {
|
||||
params.Host = host
|
||||
}
|
||||
}
|
||||
health, err := hc.HealthCheck(api.PodState{PodIP: host}, container)
|
||||
if tt.health == Unknown && err == nil {
|
||||
t.Errorf("Expected error")
|
||||
}
|
||||
if tt.health != Unknown && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if health != tt.health {
|
||||
t.Errorf("Expected %v, got %v", tt.health, health)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTcpHealthChecker(t *testing.T) {
|
||||
type tcpHealthTest struct {
|
||||
probe *api.LivenessProbe
|
||||
expectedStatus Status
|
||||
expectError bool
|
||||
}
|
||||
|
||||
checker := &TCPHealthChecker{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
|
||||
tests := []tcpHealthTest{
|
||||
{&api.LivenessProbe{TCPSocket: &api.TCPSocketProbe{Port: portNum}}, Healthy, false},
|
||||
{&api.LivenessProbe{TCPSocket: &api.TCPSocketProbe{Port: 100000}}, Unhealthy, false},
|
||||
{&api.LivenessProbe{}, Unknown, true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
probe := test.probe
|
||||
container := api.Container{
|
||||
LivenessProbe: probe,
|
||||
}
|
||||
status, err := checker.HealthCheck(api.PodState{PodIP: host}, container)
|
||||
if status != test.expectedStatus {
|
||||
t.Errorf("expected: %v, got: %v", test.expectedStatus, status)
|
||||
}
|
||||
if err != nil && !test.expectError {
|
||||
t.Errorf("unexpected error: %#v", err)
|
||||
}
|
||||
if err == nil && test.expectError {
|
||||
t.Errorf("unexpected non-error.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMuxHealthChecker(t *testing.T) {
|
||||
muxHealthCheckerTests := []struct {
|
||||
health Status
|
||||
@ -202,7 +105,7 @@ func TestMuxHealthChecker(t *testing.T) {
|
||||
{Healthy, "http"},
|
||||
{Unknown, "ftp"},
|
||||
}
|
||||
mc := &MuxHealthChecker{
|
||||
mc := &muxHealthChecker{
|
||||
checkers: make(map[string]HealthChecker),
|
||||
}
|
||||
hc := &HTTPHealthChecker{
|
||||
@ -228,7 +131,7 @@ func TestMuxHealthChecker(t *testing.T) {
|
||||
},
|
||||
}
|
||||
container.LivenessProbe.Type = tt.probeType
|
||||
container.LivenessProbe.HTTPGet.Port = port
|
||||
container.LivenessProbe.HTTPGet.Port = util.MakeIntOrStringFromString(port)
|
||||
container.LivenessProbe.HTTPGet.Host = host
|
||||
health, err := mc.HealthCheck(api.PodState{}, container)
|
||||
if err != nil {
|
103
pkg/health/http.go
Normal file
103
pkg/health/http.go
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// HTTPGetInterface is an abstract interface for testability. It abstracts the interface of http.Client.Get.
|
||||
// This is exported because some other packages may want to do direct HTTP checks.
|
||||
type HTTPGetInterface interface {
|
||||
Get(url string) (*http.Response, error)
|
||||
}
|
||||
|
||||
// DoHTTPCheck checks if a GET request to the url succeeds.
|
||||
// HTTPHealthChecker is an implementation of HealthChecker which checks container health by sending HTTP Get requests.
|
||||
type HTTPHealthChecker struct {
|
||||
client HTTPGetInterface
|
||||
}
|
||||
|
||||
// Get the components of the target URL. For testability.
|
||||
func getURLParts(currentState api.PodState, container api.Container) (string, int, string, error) {
|
||||
params := container.LivenessProbe.HTTPGet
|
||||
if params == nil {
|
||||
return "", -1, "", fmt.Errorf("no HTTP parameters specified: %v", container)
|
||||
}
|
||||
port := -1
|
||||
switch params.Port.Kind {
|
||||
case util.IntstrInt:
|
||||
port = params.Port.IntVal
|
||||
case util.IntstrString:
|
||||
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
|
||||
if len(params.Host) > 0 {
|
||||
host = params.Host
|
||||
} else {
|
||||
host = currentState.PodIP
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// It returns Unknown and err if the HTTP communication itself fails.
|
||||
// This is exported because some other packages may want to do direct HTTP checks.
|
||||
func DoHTTPCheck(url string, client HTTPGetInterface) (Status, error) {
|
||||
res, err := client.Get(url)
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest {
|
||||
return Healthy, nil
|
||||
}
|
||||
glog.V(1).Infof("Health check failed for %s, Response: %v", url, *res)
|
||||
return Unhealthy, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
139
pkg/health/http_test.go
Normal file
139
pkg/health/http_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 health
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
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) {
|
||||
testCases := []struct {
|
||||
probe *api.HTTPGetProbe
|
||||
status int
|
||||
health Status
|
||||
}{
|
||||
// The probe will be filled in below. This is primarily testing that an HTTP GET happens.
|
||||
{&api.HTTPGetProbe{}, http.StatusOK, Healthy},
|
||||
{&api.HTTPGetProbe{}, -1, Unhealthy},
|
||||
{nil, -1, Unknown},
|
||||
}
|
||||
hc := &HTTPHealthChecker{
|
||||
client: &http.Client{},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.status)
|
||||
}))
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
container := api.Container{
|
||||
LivenessProbe: &api.LivenessProbe{
|
||||
HTTPGet: test.probe,
|
||||
Type: "http",
|
||||
},
|
||||
}
|
||||
params := container.LivenessProbe.HTTPGet
|
||||
if params != nil {
|
||||
params.Port = util.MakeIntOrStringFromString(port)
|
||||
params.Host = host
|
||||
}
|
||||
health, err := hc.HealthCheck(api.PodState{PodIP: host}, container)
|
||||
if test.health == Unknown && err == nil {
|
||||
t.Errorf("Expected error")
|
||||
}
|
||||
if test.health != Unknown && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if health != test.health {
|
||||
t.Errorf("Expected %v, got %v", test.health, health)
|
||||
}
|
||||
}
|
||||
}
|
83
pkg/health/tcp.go
Normal file
83
pkg/health/tcp.go
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type TCPHealthChecker struct{}
|
||||
|
||||
// Get the components of a TCP connection address. For testability.
|
||||
func getTCPAddrParts(currentState api.PodState, container api.Container) (string, int, error) {
|
||||
params := container.LivenessProbe.TCPSocket
|
||||
if params == nil {
|
||||
return "", -1, fmt.Errorf("error, no TCP parameters specified: %v", container)
|
||||
}
|
||||
port := -1
|
||||
switch params.Port.Kind {
|
||||
case util.IntstrInt:
|
||||
port = params.Port.IntVal
|
||||
case util.IntstrString:
|
||||
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)
|
||||
}
|
||||
if len(currentState.PodIP) == 0 {
|
||||
return "", -1, fmt.Errorf("no host specified.")
|
||||
}
|
||||
|
||||
return currentState.PodIP, port, nil
|
||||
}
|
||||
|
||||
// DoTCPCheck checks that a TCP socket to the address can be opened.
|
||||
// If the socket can be opened, it returns Healthy.
|
||||
// If the socket fails to open, it returns Unhealthy.
|
||||
// This is exported because some other packages may want to do direct TCP checks.
|
||||
func DoTCPCheck(addr string) (Status, error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return Unhealthy, nil
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error closing health check socket: %v (%#v)", err, err)
|
||||
}
|
||||
return Healthy, nil
|
||||
}
|
||||
|
||||
func (t *TCPHealthChecker) HealthCheck(currentState api.PodState, container api.Container) (Status, error) {
|
||||
host, port, err := getTCPAddrParts(currentState, container)
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
}
|
||||
return DoTCPCheck(net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
}
|
116
pkg/health/tcp_test.go
Normal file
116
pkg/health/tcp_test.go
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 health
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func TestGetTCPAddrParts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
probe *api.TCPSocketProbe
|
||||
ok bool
|
||||
host string
|
||||
port int
|
||||
}{
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromInt(-1)}, false, "", -1},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromString("")}, false, "", -1},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromString("-1")}, false, "", -1},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromString("not-found")}, false, "", -1},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromString("found")}, true, "1.2.3.4", 93},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromInt(76)}, true, "1.2.3.4", 76},
|
||||
{&api.TCPSocketProbe{Port: util.MakeIntOrStringFromString("118")}, true, "1.2.3.4", 118},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
state := api.PodState{PodIP: "1.2.3.4"}
|
||||
container := api.Container{
|
||||
Ports: []api.Port{{Name: "found", HostPort: 93}},
|
||||
LivenessProbe: &api.LivenessProbe{
|
||||
TCPSocket: test.probe,
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
host, port, err := getTCPAddrParts(state, container)
|
||||
if !test.ok && err == nil {
|
||||
t.Errorf("Expected error for %+v, got %s:%d", test, host, port)
|
||||
}
|
||||
if test.ok && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if test.ok {
|
||||
if host != test.host || port != test.port {
|
||||
t.Errorf("Expected %s:%d, got %s:%d", test.host, test.port, host, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTcpHealthChecker(t *testing.T) {
|
||||
tests := []struct {
|
||||
probe *api.TCPSocketProbe
|
||||
expectedStatus Status
|
||||
expectError bool
|
||||
}{
|
||||
// The probe will be filled in below. This is primarily testing that a connection is made.
|
||||
{&api.TCPSocketProbe{}, Healthy, false},
|
||||
{&api.TCPSocketProbe{}, Unhealthy, false},
|
||||
{nil, Unknown, true},
|
||||
}
|
||||
|
||||
checker := &TCPHealthChecker{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
for _, test := range tests {
|
||||
container := api.Container{
|
||||
LivenessProbe: &api.LivenessProbe{
|
||||
TCPSocket: test.probe,
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
params := container.LivenessProbe.TCPSocket
|
||||
if params != nil && test.expectedStatus == Healthy {
|
||||
params.Port = util.MakeIntOrStringFromString(port)
|
||||
}
|
||||
status, err := checker.HealthCheck(api.PodState{PodIP: host}, container)
|
||||
if status != test.expectedStatus {
|
||||
t.Errorf("expected: %v, got: %v", test.expectedStatus, status)
|
||||
}
|
||||
if err != nil && !test.expectError {
|
||||
t.Errorf("unexpected error: %#v", err)
|
||||
}
|
||||
if err == nil && test.expectError {
|
||||
t.Errorf("unexpected non-error.")
|
||||
}
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ func (h *HealthyMinionRegistry) List() (currentMinions []string, err error) {
|
||||
return result, err
|
||||
}
|
||||
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 {
|
||||
glog.Errorf("%s failed health check with error: %s", minion, err)
|
||||
continue
|
||||
@ -77,7 +77,7 @@ func (h *HealthyMinionRegistry) Contains(minion string) (bool, error) {
|
||||
if !contains {
|
||||
return false, nil
|
||||
}
|
||||
status, err := health.Check(h.makeMinionURL(minion), h.client)
|
||||
status, err := health.DoHTTPCheck(h.makeMinionURL(minion), h.client)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -83,6 +83,16 @@ const (
|
||||
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.
|
||||
func (intstr *IntOrString) SetYAML(tag string, value interface{}) bool {
|
||||
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 {
|
||||
IOrS IntOrString `json:"val" yaml:"val"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user