diff --git a/pkg/api/types.go b/pkg/api/types.go index ee6ad93debf..5b98f1b2738 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -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. diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index b719776bdd2..b24d35b2b73 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -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. diff --git a/pkg/health/health.go b/pkg/health/health.go index 2610482d2ac..9ebd2bc34d5 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -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 } diff --git a/pkg/health/health_check.go b/pkg/health/health_check.go deleted file mode 100644 index abb06fa7be2..00000000000 --- a/pkg/health/health_check.go +++ /dev/null @@ -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 -} diff --git a/pkg/health/health_check_test.go b/pkg/health/health_test.go similarity index 53% rename from pkg/health/health_check_test.go rename to pkg/health/health_test.go index bc9841680cd..2b17dcd709e 100644 --- a/pkg/health/health_check_test.go +++ b/pkg/health/health_test.go @@ -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 { diff --git a/pkg/health/http.go b/pkg/health/http.go new file mode 100644 index 00000000000..0f212d21212 --- /dev/null +++ b/pkg/health/http.go @@ -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) +} diff --git a/pkg/health/http_test.go b/pkg/health/http_test.go new file mode 100644 index 00000000000..a4932e943d8 --- /dev/null +++ b/pkg/health/http_test.go @@ -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) + } + } +} diff --git a/pkg/health/tcp.go b/pkg/health/tcp.go new file mode 100644 index 00000000000..7818c511bf0 --- /dev/null +++ b/pkg/health/tcp.go @@ -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))) +} diff --git a/pkg/health/tcp_test.go b/pkg/health/tcp_test.go new file mode 100644 index 00000000000..3a70f889bc8 --- /dev/null +++ b/pkg/health/tcp_test.go @@ -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.") + } + } +} diff --git a/pkg/registry/healthy_minion_registry.go b/pkg/registry/healthy_minion_registry.go index 39c99331a49..658052f13e8 100644 --- a/pkg/registry/healthy_minion_registry.go +++ b/pkg/registry/healthy_minion_registry.go @@ -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 } diff --git a/pkg/util/util.go b/pkg/util/util.go index 1058a50ed46..27ab57eb920 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -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) { diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 8e4171b8edc..5bd72518245 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -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"` }