diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index 227b552d675..c67e439a8c9 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -39,11 +39,11 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" nodeControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller" replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/empty_dir" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/standalone" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -84,8 +84,8 @@ func (fakeKubeletClient) GetPodStatus(host, podNamespace, podID string) (api.Pod return c.GetPodStatus("localhost", podNamespace, podID) } -func (fakeKubeletClient) HealthCheck(host string) (health.Status, error) { - return health.Healthy, nil +func (fakeKubeletClient) HealthCheck(host string) (probe.Status, error) { + return probe.Success, nil } type delegateHandler struct { diff --git a/pkg/apiserver/validator.go b/pkg/apiserver/validator.go index bd8bb1e21e6..e1c47803cb9 100644 --- a/pkg/apiserver/validator.go +++ b/pkg/apiserver/validator.go @@ -24,7 +24,7 @@ import ( "net/http" "strconv" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" ) // TODO: this basic interface is duplicated in N places. consolidate? @@ -45,29 +45,30 @@ type validator struct { client httpGet } -func (s *Server) check(client httpGet) (health.Status, string, error) { +// TODO: can this use pkg/probe/http +func (s *Server) check(client httpGet) (probe.Status, string, error) { resp, err := client.Get("http://" + net.JoinHostPort(s.Addr, strconv.Itoa(s.Port)) + s.Path) if err != nil { - return health.Unknown, "", err + return probe.Unknown, "", err } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - return health.Unknown, string(data), err + return probe.Unknown, string(data), err } if resp.StatusCode != http.StatusOK { - return health.Unhealthy, string(data), + return probe.Failure, string(data), fmt.Errorf("unhealthy http status code: %d (%s)", resp.StatusCode, resp.Status) } - return health.Healthy, string(data), nil + return probe.Success, string(data), nil } type ServerStatus struct { - Component string `json:"component,omitempty"` - Health string `json:"health,omitempty"` - HealthCode health.Status `json:"healthCode,omitempty"` - Msg string `json:"msg,omitempty"` - Err string `json:"err,omitempty"` + Component string `json:"component,omitempty"` + Health string `json:"health,omitempty"` + HealthCode probe.Status `json:"healthCode,omitempty"` + Msg string `json:"msg,omitempty"` + Err string `json:"err,omitempty"` } func (v *validator) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/apiserver/validator_test.go b/pkg/apiserver/validator_test.go index ee4d5f03b86..0a9477135a2 100644 --- a/pkg/apiserver/validator_test.go +++ b/pkg/apiserver/validator_test.go @@ -25,7 +25,7 @@ import ( "net/http/httptest" "testing" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -54,13 +54,13 @@ func TestValidate(t *testing.T) { tests := []struct { err error data string - expectedStatus health.Status + expectedStatus probe.Status code int expectErr bool }{ - {fmt.Errorf("test error"), "", health.Unknown, 500 /*ignored*/, true}, - {nil, "foo", health.Healthy, 200, false}, - {nil, "foo", health.Unhealthy, 500, true}, + {fmt.Errorf("test error"), "", probe.Unknown, 500 /*ignored*/, true}, + {nil, "foo", probe.Success, 200, false}, + {nil, "foo", probe.Failure, 500, true}, } s := Server{Addr: "foo.com", Port: 8080, Path: "/healthz"} diff --git a/pkg/client/kubelet.go b/pkg/client/kubelet.go index 8c1c5b7186b..7bbc396d232 100644 --- a/pkg/client/kubelet.go +++ b/pkg/client/kubelet.go @@ -26,7 +26,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + httprobe "github.com/GoogleCloudPlatform/kubernetes/pkg/probe/http" ) // ErrPodInfoNotAvailable may be returned when the requested pod info is not available. @@ -40,7 +41,7 @@ type KubeletClient interface { // KubeletHealthchecker is an interface for healthchecking kubelets type KubeletHealthChecker interface { - HealthCheck(host string) (health.Status, error) + HealthCheck(host string) (probe.Status, error) } // PodInfoGetter is an interface for things that can get information about a pod's containers. @@ -146,8 +147,8 @@ func (c *HTTPKubeletClient) GetPodStatus(host, podNamespace, podID string) (api. return status, nil } -func (c *HTTPKubeletClient) HealthCheck(host string) (health.Status, error) { - return health.DoHTTPCheck(fmt.Sprintf("%s/healthz", c.url(host)), c.Client) +func (c *HTTPKubeletClient) HealthCheck(host string) (probe.Status, error) { + return httprobe.DoHTTPProbe(fmt.Sprintf("%s/healthz", c.url(host)), c.Client) } // FakeKubeletClient is a fake implementation of KubeletClient which returns an error @@ -160,6 +161,6 @@ func (c FakeKubeletClient) GetPodStatus(host, podNamespace string, podID string) return api.PodStatusResult{}, errors.New("Not Implemented") } -func (c FakeKubeletClient) HealthCheck(host string) (health.Status, error) { - return health.Unknown, errors.New("Not Implemented") +func (c FakeKubeletClient) HealthCheck(host string) (probe.Status, error) { + return probe.Unknown, errors.New("Not Implemented") } diff --git a/pkg/client/kubelet_test.go b/pkg/client/kubelet_test.go index a36b2986ec2..a4ceda47f61 100644 --- a/pkg/client/kubelet_test.go +++ b/pkg/client/kubelet_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -134,8 +134,8 @@ func TestNewKubeletClient(t *testing.T) { host := "127.0.0.1" healthStatus, err := client.HealthCheck(host) - if healthStatus != health.Unhealthy { - t.Errorf("Expected %v and got %v.", health.Unhealthy, healthStatus) + if healthStatus != probe.Failure { + t.Errorf("Expected %v and got %v.", probe.Failure, healthStatus) } if err != nil { t.Error("Expected a nil error") diff --git a/pkg/health/exec.go b/pkg/health/exec.go deleted file mode 100644 index 9f0ad4434c1..00000000000 --- a/pkg/health/exec.go +++ /dev/null @@ -1,59 +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" - "strings" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/types" - "github.com/golang/glog" -) - -const defaultHealthyOutput = "ok" - -type CommandRunner interface { - RunInContainer(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) -} - -type ExecHealthChecker struct { - runner CommandRunner -} - -func NewExecHealthChecker(runner CommandRunner) HealthChecker { - return &ExecHealthChecker{runner} -} - -func (e *ExecHealthChecker) HealthCheck(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (Status, error) { - if container.LivenessProbe.Exec == nil { - return Unknown, fmt.Errorf("missing exec parameters") - } - data, err := e.runner.RunInContainer(podFullName, podUID, container.Name, container.LivenessProbe.Exec.Command) - glog.V(1).Infof("container %s health check response: %s", podFullName, string(data)) - if err != nil { - return Unknown, err - } - if strings.ToLower(string(data)) != defaultHealthyOutput { - return Unhealthy, nil - } - return Healthy, nil -} - -func (e *ExecHealthChecker) CanCheck(probe *api.LivenessProbe) bool { - return probe.Exec != nil -} diff --git a/pkg/health/exec_test.go b/pkg/health/exec_test.go deleted file mode 100644 index a9aa2199c89..00000000000 --- a/pkg/health/exec_test.go +++ /dev/null @@ -1,85 +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" - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/types" -) - -type FakeExec struct { - cmd []string - out []byte - err error -} - -func (f *FakeExec) RunInContainer(podFullName string, uid types.UID, container string, cmd []string) ([]byte, error) { - f.cmd = cmd - return f.out, f.err -} - -type healthCheckTest struct { - expectedStatus Status - probe *api.LivenessProbe - expectError bool - output []byte - err error -} - -func TestExec(t *testing.T) { - fake := FakeExec{} - checker := ExecHealthChecker{&fake} - tests := []healthCheckTest{ - // Missing parameters - {Unknown, &api.LivenessProbe{}, true, nil, nil}, - // Ok - {Healthy, &api.LivenessProbe{ - Exec: &api.ExecAction{Command: []string{"ls", "-l"}}, - }, false, []byte("OK"), nil}, - // Run returns error - {Unknown, &api.LivenessProbe{ - Exec: &api.ExecAction{ - Command: []string{"ls", "-l"}, - }, - }, true, []byte("OK, NOT"), fmt.Errorf("test error")}, - // Unhealthy - {Unhealthy, &api.LivenessProbe{ - Exec: &api.ExecAction{Command: []string{"ls", "-l"}}, - }, false, []byte("Fail"), nil}, - } - for _, test := range tests { - fake.out = test.output - fake.err = test.err - status, err := checker.HealthCheck("test", "", api.PodStatus{}, api.Container{LivenessProbe: test.probe}) - if status != test.expectedStatus { - t.Errorf("expected %v, got %v", test.expectedStatus, status) - } - if err != nil && test.expectError == false { - t.Errorf("unexpected error: %v", err) - } - if err == nil && test.expectError == true { - t.Errorf("unexpected non-error") - } - if test.probe.Exec != nil && !reflect.DeepEqual(fake.cmd, test.probe.Exec.Command) { - t.Errorf("expected: %v, got %v", test.probe.Exec.Command, fake.cmd) - } - } -} diff --git a/pkg/health/health.go b/pkg/health/health.go deleted file mode 100644 index 64233e166ee..00000000000 --- a/pkg/health/health.go +++ /dev/null @@ -1,115 +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 ( - "sync" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/types" - "github.com/golang/glog" -) - -// Status represents the result of a single health-check operation. -type Status int - -// Status values must be one of these constants. -const ( - Healthy Status = iota - Unhealthy - Unknown -) - -// HealthChecker defines an abstract interface for checking container health. -type HealthChecker interface { - HealthCheck(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (Status, error) - CanCheck(probe *api.LivenessProbe) bool -} - -// protects allCheckers -var checkerLock = sync.Mutex{} -var allCheckers = []HealthChecker{} - -// AddHealthChecker adds a health checker to the list of known HealthChecker objects. -// Any subsequent call to NewHealthChecker will know about this HealthChecker. -func AddHealthChecker(checker HealthChecker) { - checkerLock.Lock() - defer checkerLock.Unlock() - allCheckers = append(allCheckers, checker) -} - -// NewHealthChecker creates a new HealthChecker which supports multiple types of liveness probes. -func NewHealthChecker() HealthChecker { - checkerLock.Lock() - defer checkerLock.Unlock() - return &muxHealthChecker{ - checkers: append([]HealthChecker{}, allCheckers...), - } -} - -// muxHealthChecker bundles multiple implementations of HealthChecker of different types. -type muxHealthChecker struct { - // Given a LivenessProbe, cycle through each known checker and see if it supports - // the specific kind of probe (by returning non-nil). - checkers []HealthChecker -} - -func (m *muxHealthChecker) findCheckerFor(probe *api.LivenessProbe) HealthChecker { - for i := range m.checkers { - if m.checkers[i].CanCheck(probe) { - return m.checkers[i] - } - } - return nil -} - -// HealthCheck delegates the health-checking of the container to one of the bundled implementations. -// If there is no health checker that can check container it returns Unknown, nil. -func (m *muxHealthChecker) HealthCheck(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (Status, error) { - checker := m.findCheckerFor(container.LivenessProbe) - if checker == nil { - glog.Warningf("Failed to find health checker for %s %+v", container.Name, container.LivenessProbe) - return Unknown, nil - } - return checker.HealthCheck(podFullName, podUID, status, container) -} - -func (m *muxHealthChecker) CanCheck(probe *api.LivenessProbe) bool { - return m.findCheckerFor(probe) != nil -} - -// findPortByName is 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 -} - -func (s Status) String() string { - switch s { - case Healthy: - return "healthy" - case Unhealthy: - return "unhealthy" - default: - return "unknown" - } -} diff --git a/pkg/health/health_test.go b/pkg/health/health_test.go deleted file mode 100644 index c3b29aad482..00000000000 --- a/pkg/health/health_test.go +++ /dev/null @@ -1,142 +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 ( - "net" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" -) - -const statusServerEarlyShutdown = -1 - -func TestHealthChecker(t *testing.T) { - AddHealthChecker(&HTTPHealthChecker{client: &http.Client{}}) - var healthCheckerTests = []struct { - status int - health Status - }{ - {http.StatusOK, Healthy}, - {statusServerEarlyShutdown, Unhealthy}, - {http.StatusBadRequest, Unhealthy}, - {http.StatusBadGateway, Unhealthy}, - {http.StatusInternalServerError, Unhealthy}, - } - for _, healthCheckerTest := range healthCheckerTests { - tt := healthCheckerTest - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.status) - })) - defer ts.Close() - u, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if tt.status == statusServerEarlyShutdown { - ts.Close() - } - container := api.Container{ - LivenessProbe: &api.LivenessProbe{ - HTTPGet: &api.HTTPGetAction{ - Port: util.NewIntOrStringFromString(port), - Path: "/foo/bar", - Host: host, - }, - }, - } - hc := NewHealthChecker() - health, err := hc.HealthCheck("test", "", api.PodStatus{}, container) - if err != nil && tt.health != Unhealthy { - t.Errorf("Unexpected error: %v", err) - } - if health != tt.health { - t.Errorf("Expected %v, got %v", tt.health, health) - } - } -} - -func TestFindPortByName(t *testing.T) { - container := api.Container{ - Ports: []api.Port{ - { - Name: "foo", - HostPort: 8080, - }, - { - Name: "bar", - HostPort: 9000, - }, - }, - } - want := 8080 - got := findPortByName(container, "foo") - if got != want { - t.Errorf("Expected %v, got %v", want, got) - } -} - -func TestMuxHealthChecker(t *testing.T) { - muxHealthCheckerTests := []struct { - health Status - }{ - // TODO: This test should run through a few different checker types. - {Healthy}, - } - mc := &muxHealthChecker{ - checkers: []HealthChecker{ - &HTTPHealthChecker{client: &http.Client{}}, - }, - } - for _, muxHealthCheckerTest := range muxHealthCheckerTests { - tt := muxHealthCheckerTest - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer ts.Close() - u, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - container := api.Container{ - LivenessProbe: &api.LivenessProbe{ - HTTPGet: &api.HTTPGetAction{}, - }, - } - container.LivenessProbe.HTTPGet.Port = util.NewIntOrStringFromString(port) - container.LivenessProbe.HTTPGet.Host = host - health, err := mc.HealthCheck("test", "", api.PodStatus{}, container) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if health != tt.health { - t.Errorf("Expected %v, got %v", tt.health, health) - } - } -} diff --git a/pkg/health/http.go b/pkg/health/http.go deleted file mode 100644 index f5b44793887..00000000000 --- a/pkg/health/http.go +++ /dev/null @@ -1,119 +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" - "net/url" - "strconv" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/types" - "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) -} - -// HTTPHealthChecker is an implementation of HealthChecker which checks container health by sending HTTP Get requests. -type HTTPHealthChecker struct { - client HTTPGetInterface -} - -func NewHTTPHealthChecker(client *http.Client) HealthChecker { - return &HTTPHealthChecker{client: &http.Client{}} -} - -// getURLParts parses the components of the target URL. For testability. -func getURLParts(status api.PodStatus, 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 = status.PodIP - } - - return host, port, params.Path, nil -} - -// formatURL formats a URL from args. For testability. -func formatURL(host string, port int, path string) string { - u := url.URL{ - Scheme: "http", - Host: net.JoinHostPort(host, strconv.Itoa(port)), - Path: path, - } - return u.String() -} - -// 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 unsuccessful or HTTP communication fails, it returns Unhealthy. -// 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 { - glog.V(1).Infof("HTTP probe error: %v", err) - return Unhealthy, nil - } - 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(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (Status, error) { - host, port, path, err := getURLParts(status, container) - if err != nil { - return Unknown, err - } - return DoHTTPCheck(formatURL(host, port, path), h.client) -} - -func (h *HTTPHealthChecker) CanCheck(probe *api.LivenessProbe) bool { - return probe.HTTPGet != nil -} diff --git a/pkg/health/tcp.go b/pkg/health/tcp.go deleted file mode 100644 index 039e82d140c..00000000000 --- a/pkg/health/tcp.go +++ /dev/null @@ -1,88 +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" - "strconv" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/types" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/golang/glog" -) - -type TCPHealthChecker struct{} - -// getTCPAddrParts parses the components of a TCP connection address. For testability. -func getTCPAddrParts(status api.PodStatus, 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(status.PodIP) == 0 { - return "", -1, fmt.Errorf("no host specified.") - } - - return status.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(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (Status, error) { - host, port, err := getTCPAddrParts(status, container) - if err != nil { - return Unknown, err - } - return DoTCPCheck(net.JoinHostPort(host, strconv.Itoa(port))) -} - -func (t *TCPHealthChecker) CanCheck(probe *api.LivenessProbe) bool { - return probe.TCPSocket != nil -} diff --git a/pkg/health/tcp_test.go b/pkg/health/tcp_test.go deleted file mode 100644 index b693a9731f6..00000000000 --- a/pkg/health/tcp_test.go +++ /dev/null @@ -1,115 +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 ( - "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.TCPSocketAction - ok bool - host string - port int - }{ - {&api.TCPSocketAction{Port: util.NewIntOrStringFromInt(-1)}, false, "", -1}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("")}, false, "", -1}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("-1")}, false, "", -1}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("not-found")}, false, "", -1}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("found")}, true, "1.2.3.4", 93}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromInt(76)}, true, "1.2.3.4", 76}, - {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("118")}, true, "1.2.3.4", 118}, - } - - for _, test := range testCases { - state := api.PodStatus{PodIP: "1.2.3.4"} - container := api.Container{ - Ports: []api.Port{{Name: "found", HostPort: 93}}, - LivenessProbe: &api.LivenessProbe{ - TCPSocket: test.probe, - }, - } - 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.TCPSocketAction - expectedStatus Status - expectError bool - }{ - // The probe will be filled in below. This is primarily testing that a connection is made. - {&api.TCPSocketAction{}, Healthy, false}, - {&api.TCPSocketAction{}, Unhealthy, false}, - {nil, Unknown, true}, - } - - checker := &TCPHealthChecker{} - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - 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, - }, - } - params := container.LivenessProbe.TCPSocket - if params != nil && test.expectedStatus == Healthy { - params.Port = util.NewIntOrStringFromString(port) - } - status, err := checker.HealthCheck("test", "", api.PodStatus{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/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 327e520b8f8..eee389685c6 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -36,11 +36,11 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/envvars" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -164,8 +164,6 @@ type Kubelet struct { // Optional, no events will be sent without it etcdClient tools.EtcdClient - // Optional, defaults to simple implementaiton - healthChecker health.HealthChecker // Optional, defaults to simple Docker implementation dockerPuller dockertools.DockerPuller // Optional, defaults to /logs/ from /var/log @@ -427,9 +425,6 @@ func (kl *Kubelet) Run(updates <-chan PodUpdate) { if kl.dockerPuller == nil { kl.dockerPuller = dockertools.NewDockerPuller(kl.dockerClient, kl.pullQPS, kl.pullBurst) } - if kl.healthChecker == nil { - kl.healthChecker = health.NewHealthChecker() - } kl.syncLoop(updates, kl) } @@ -1038,13 +1033,13 @@ func (kl *Kubelet) syncPod(pod *api.BoundPod, dockerContainers dockertools.Docke // look for changes in the container. if hash == 0 || hash == expectedHash { // TODO: This should probably be separated out into a separate goroutine. - healthy, err := kl.healthy(podFullName, uid, podStatus, container, dockerContainer) + healthy, err := kl.probeLiveness(podFullName, uid, podStatus, container, dockerContainer) if err != nil { glog.V(1).Infof("health check errored: %v", err) containersToKeep[containerID] = empty{} continue } - if healthy == health.Healthy { + if healthy == probe.Success { containersToKeep[containerID] = empty{} continue } @@ -1404,18 +1399,15 @@ func (kl *Kubelet) GetPodStatus(podFullName string, uid types.UID) (api.PodStatu return podStatus, err } -func (kl *Kubelet) healthy(podFullName string, podUID types.UID, status api.PodStatus, container api.Container, dockerContainer *docker.APIContainers) (health.Status, error) { +func (kl *Kubelet) probeLiveness(podFullName string, podUID types.UID, status api.PodStatus, container api.Container, dockerContainer *docker.APIContainers) (probe.Status, error) { // Give the container 60 seconds to start up. if container.LivenessProbe == nil { - return health.Healthy, nil + return probe.Success, nil } if time.Now().Unix()-dockerContainer.Created < container.LivenessProbe.InitialDelaySeconds { - return health.Healthy, nil + return probe.Success, nil } - if kl.healthChecker == nil { - return health.Healthy, nil - } - return kl.healthChecker.HealthCheck(podFullName, podUID, status, container) + return kl.probeContainer(container.LivenessProbe, podFullName, podUID, status, container) } // Returns logs of current machine. diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 29f3d987739..fabe65a67b3 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -31,7 +31,6 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/host_path" @@ -845,19 +844,8 @@ func TestSyncPodDeletesDuplicate(t *testing.T) { } } -type FalseHealthChecker struct{} - -func (f *FalseHealthChecker) HealthCheck(podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (health.Status, error) { - return health.Unhealthy, nil -} - -func (f *FalseHealthChecker) CanCheck(probe *api.LivenessProbe) bool { - return true -} - func TestSyncPodBadHash(t *testing.T) { kubelet, fakeDocker := newTestKubelet(t) - kubelet.healthChecker = &FalseHealthChecker{} dockerContainers := dockertools.DockerContainers{ "1234": &docker.APIContainers{ // the k8s prefix is required for the kubelet to manage the container @@ -904,7 +892,6 @@ func TestSyncPodBadHash(t *testing.T) { func TestSyncPodUnhealthy(t *testing.T) { kubelet, fakeDocker := newTestKubelet(t) - kubelet.healthChecker = &FalseHealthChecker{} dockerContainers := dockertools.DockerContainers{ "1234": &docker.APIContainers{ // the k8s prefix is required for the kubelet to manage the container diff --git a/pkg/kubelet/probe.go b/pkg/kubelet/probe.go new file mode 100644 index 00000000000..6cd41186a26 --- /dev/null +++ b/pkg/kubelet/probe.go @@ -0,0 +1,125 @@ +/* +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 kubelet + +import ( + "fmt" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + execprobe "github.com/GoogleCloudPlatform/kubernetes/pkg/probe/exec" + httprobe "github.com/GoogleCloudPlatform/kubernetes/pkg/probe/http" + tcprobe "github.com/GoogleCloudPlatform/kubernetes/pkg/probe/tcp" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec" + + "github.com/golang/glog" +) + +var ( + execprober = execprobe.New() + httprober = httprobe.New() + tcprober = tcprobe.New() +) + +func (kl *Kubelet) probeContainer(p *api.LivenessProbe, podFullName string, podUID types.UID, status api.PodStatus, container api.Container) (probe.Status, error) { + if p.Exec != nil { + return execprober.Probe(kl.newExecInContainer(podFullName, podUID, container)) + } + if p.HTTPGet != nil { + port, err := extractPort(p.HTTPGet.Port, container) + if err != nil { + return probe.Unknown, err + } + return httprober.Probe(extractGetParams(p.HTTPGet, status, port)) + } + if p.TCPSocket != nil { + port, err := extractPort(p.TCPSocket.Port, container) + if err != nil { + return probe.Unknown, err + } + return tcprober.Probe(status.PodIP, port) + } + glog.Warningf("Failed to find probe builder for %s %+v", container.Name, container.LivenessProbe) + return probe.Unknown, nil +} + +func extractGetParams(action *api.HTTPGetAction, status api.PodStatus, port int) (string, int, string) { + host := action.Host + if host == "" { + host = status.PodIP + } + return host, port, action.Path +} + +func extractPort(param util.IntOrString, container api.Container) (int, error) { + port := -1 + var err error + switch param.Kind { + case util.IntstrInt: + port := param.IntVal + if port > 0 && port < 65536 { + return port, nil + } + return port, fmt.Errorf("invalid port number: %v", port) + case util.IntstrString: + port = findPortByName(container, param.StrVal) + if port == -1 { + // Last ditch effort - maybe it was an int stored as string? + if port, err = strconv.Atoi(param.StrVal); err != nil { + return port, err + } + } + if port > 0 && port < 65536 { + return port, nil + } + return port, fmt.Errorf("invalid port number: %v", port) + default: + return port, fmt.Errorf("IntOrString had no kind: %+v", param) + } +} + +// findPortByName is 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 +} + +type execInContainer struct { + run func() ([]byte, error) +} + +func (kl *Kubelet) newExecInContainer(podFullName string, podUID types.UID, container api.Container) exec.Cmd { + return execInContainer{func() ([]byte, error) { + return kl.RunInContainer(podFullName, podUID, container.Name, container.LivenessProbe.Exec.Command) + }} +} + +func (eic execInContainer) CombinedOutput() ([]byte, error) { + return eic.run() +} + +func (eic execInContainer) SetDir(dir string) { + //unimplemented +} diff --git a/pkg/health/http_test.go b/pkg/kubelet/probe_test.go similarity index 59% rename from pkg/health/http_test.go rename to pkg/kubelet/probe_test.go index f83649cb9c5..9855e30a0ef 100644 --- a/pkg/health/http_test.go +++ b/pkg/kubelet/probe_test.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Google Inc. All rights reserved. +Copyright 2015 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. @@ -14,19 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -package health +package kubelet import ( - "net" - "net/http" - "net/http/httptest" - "net/url" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) +func TestFindPortByName(t *testing.T) { + container := api.Container{ + Ports: []api.Port{ + { + Name: "foo", + HostPort: 8080, + }, + { + Name: "bar", + HostPort: 9000, + }, + }, + } + want := 8080 + got := findPortByName(container, "foo") + if got != want { + t.Errorf("Expected %v, got %v", want, got) + } +} + func TestGetURLParts(t *testing.T) { testCases := []struct { probe *api.HTTPGetAction @@ -53,13 +69,14 @@ func TestGetURLParts(t *testing.T) { HTTPGet: test.probe, }, } - 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) - } + p, err := extractPort(test.probe.Port, container) if test.ok && err != nil { t.Errorf("Unexpected error: %v", err) } + host, port, path := extractGetParams(test.probe, state, p) + if !test.ok && err == nil { + t.Errorf("Expected error for %+v, got %s:%d/%s", test, host, port, path) + } if test.ok { if host != test.host || port != test.port || path != test.path { t.Errorf("Expected %s:%d/%s, got %s:%d/%s", @@ -69,69 +86,41 @@ func TestGetURLParts(t *testing.T) { } } -func TestFormatURL(t *testing.T) { +func TestGetTCPAddrParts(t *testing.T) { testCases := []struct { - host string - port int - path string - result string + probe *api.TCPSocketAction + ok bool + host string + port int }{ - {"localhost", 93, "", "http://localhost:93"}, - {"localhost", 93, "/path", "http://localhost:93/path"}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromInt(-1)}, false, "", -1}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("")}, false, "", -1}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("-1")}, false, "", -1}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("not-found")}, false, "", -1}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("found")}, true, "1.2.3.4", 93}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromInt(76)}, true, "1.2.3.4", 76}, + {&api.TCPSocketAction{Port: util.NewIntOrStringFromString("118")}, true, "1.2.3.4", 118}, } - 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.HTTPGetAction - status int - health Status - }{ - // The probe will be filled in below. This is primarily testing that an HTTP GET happens. - {&api.HTTPGetAction{}, http.StatusOK, Healthy}, - {&api.HTTPGetAction{}, -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) - } + host := "1.2.3.4" container := api.Container{ + Ports: []api.Port{{Name: "found", HostPort: 93}}, LivenessProbe: &api.LivenessProbe{ - HTTPGet: test.probe, + TCPSocket: test.probe, }, } - params := container.LivenessProbe.HTTPGet - if params != nil { - params.Port = util.NewIntOrStringFromString(port) - params.Host = host + port, err := extractPort(test.probe.Port, container) + if !test.ok && err == nil { + t.Errorf("Expected error for %+v, got %s:%d", test, host, port) } - health, err := hc.HealthCheck("test", "", api.PodStatus{PodIP: host}, container) - if test.health == Unknown && err == nil { - t.Errorf("Expected error") - } - if test.health != Unknown && err != nil { + if test.ok && err != nil { t.Errorf("Unexpected error: %v", err) } - if health != test.health { - t.Errorf("Expected %v, got %v", test.health, health) + if test.ok { + if host != test.host || port != test.port { + t.Errorf("Expected %s:%d, got %s:%d", test.host, test.port, host, port) + } } } } diff --git a/pkg/kubelet/util.go b/pkg/kubelet/util.go index 079acc04658..07bb42bbb8a 100644 --- a/pkg/kubelet/util.go +++ b/pkg/kubelet/util.go @@ -17,14 +17,12 @@ limitations under the License. package kubelet import ( - "net/http" "strconv" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/coreos/go-etcd/etcd" @@ -46,15 +44,6 @@ func MonitorCAdvisor(k *Kubelet, cp uint) { k.SetCadvisorClient(cadvisorClient) } -// TODO: move this into the kubelet itself -func InitHealthChecking(k *Kubelet) { - // TODO: These should probably become more plugin-ish: register a factory func - // in each checker's init(), iterate those here. - health.AddHealthChecker(health.NewExecHealthChecker(k)) - health.AddHealthChecker(health.NewHTTPHealthChecker(&http.Client{})) - health.AddHealthChecker(&health.TCPHealthChecker{}) -} - // TODO: move this into a pkg/tools/etcd_tools func EtcdClientOrDie(etcdServerList util.StringList, etcdConfigFile string) tools.EtcdClient { if len(etcdServerList) > 0 { diff --git a/pkg/health/doc.go b/pkg/probe/doc.go similarity index 76% rename from pkg/health/doc.go rename to pkg/probe/doc.go index 6c5580a1301..dded05f852d 100644 --- a/pkg/health/doc.go +++ b/pkg/probe/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Google Inc. All rights reserved. +Copyright 2015 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. @@ -14,5 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package health contains utilities for health checking, as well as health status information. -package health +// Package probe contains utilities for health probing, as well as health status information. +package probe diff --git a/pkg/probe/exec/exec.go b/pkg/probe/exec/exec.go new file mode 100644 index 00000000000..ac5a0f2ccc6 --- /dev/null +++ b/pkg/probe/exec/exec.go @@ -0,0 +1,46 @@ +/* +Copyright 2015 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 exec + +import ( + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + uexec "github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec" + + "github.com/golang/glog" +) + +const defaultHealthyOutput = "ok" + +func New() ExecProber { + return ExecProber{} +} + +type ExecProber struct{} + +func (pr ExecProber) Probe(e uexec.Cmd) (probe.Status, error) { + data, err := e.CombinedOutput() + glog.V(4).Infof("health check response: %s", string(data)) + if err != nil { + return probe.Unknown, err + } + if strings.ToLower(string(data)) != defaultHealthyOutput { + return probe.Failure, nil + } + return probe.Success, nil +} diff --git a/pkg/probe/exec/exec_test.go b/pkg/probe/exec/exec_test.go new file mode 100644 index 00000000000..a2f21365abd --- /dev/null +++ b/pkg/probe/exec/exec_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2015 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 exec + +import ( + "fmt" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" +) + +type FakeCmd struct { + out []byte + err error +} + +func (f *FakeCmd) CombinedOutput() ([]byte, error) { + return f.out, f.err +} + +func (f *FakeCmd) SetDir(dir string) {} + +type healthCheckTest struct { + expectedStatus probe.Status + expectError bool + output []byte + err error +} + +func TestExec(t *testing.T) { + prober := New() + fake := FakeCmd{} + tests := []healthCheckTest{ + // Ok + {probe.Success, false, []byte("OK"), nil}, + // Run returns error + {probe.Unknown, true, []byte("OK, NOT"), fmt.Errorf("test error")}, + // Unhealthy + {probe.Failure, false, []byte("Fail"), nil}, + } + for _, test := range tests { + fake.out = test.output + fake.err = test.err + status, err := prober.Probe(&fake) + if status != test.expectedStatus { + t.Errorf("expected %v, got %v", test.expectedStatus, status) + } + if err != nil && test.expectError == false { + t.Errorf("unexpected error: %v", err) + } + if err == nil && test.expectError == true { + t.Errorf("unexpected non-error") + } + } +} diff --git a/pkg/probe/http/http.go b/pkg/probe/http/http.go new file mode 100644 index 00000000000..fee63737cfc --- /dev/null +++ b/pkg/probe/http/http.go @@ -0,0 +1,73 @@ +/* +Copyright 2015 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 http + +import ( + "net" + "net/http" + "net/url" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + + "github.com/golang/glog" +) + +func New() HTTPProber { + return HTTPProber{&http.Client{}} +} + +type HTTPProber struct { + client HTTPGetInterface +} + +// Probe returns a ProbeRunner capable of running an http check. +func (pr *HTTPProber) Probe(host string, port int, path string) (probe.Status, error) { + return DoHTTPProbe(formatURL(host, port, path), pr.client) +} + +type HTTPGetInterface interface { + Get(u string) (*http.Response, error) +} + +// DoHTTPProbe 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 or HTTP communication fails, it returns Unhealthy. +// This is exported because some other packages may want to do direct HTTP probes. +func DoHTTPProbe(url string, client HTTPGetInterface) (probe.Status, error) { + res, err := client.Get(url) + if err != nil { + glog.V(1).Infof("HTTP probe error: %v", err) + return probe.Failure, nil + } + defer res.Body.Close() + if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest { + return probe.Success, nil + } + glog.V(1).Infof("Health check failed for %s, Response: %v", url, *res) + return probe.Failure, nil +} + +// formatURL formats a URL from args. For testability. +func formatURL(host string, port int, path string) string { + u := url.URL{ + Scheme: "http", + Host: net.JoinHostPort(host, strconv.Itoa(port)), + Path: path, + } + return u.String() +} diff --git a/pkg/probe/http/http_test.go b/pkg/probe/http/http_test.go new file mode 100644 index 00000000000..fd9009f6615 --- /dev/null +++ b/pkg/probe/http/http_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2015 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 http + +import ( + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" +) + +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 TestHTTPProbeChecker(t *testing.T) { + prober := New() + testCases := []struct { + status int + health probe.Status + }{ + // The probe will be filled in below. This is primarily testing that an HTTP GET happens. + {http.StatusOK, probe.Success}, + {-1, probe.Failure}, + } + 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) + } + p, err := strconv.Atoi(port) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + health, err := prober.Probe(host, p, "") + if test.health == probe.Unknown && err == nil { + t.Errorf("Expected error") + } + if test.health != probe.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/probe/probe.go b/pkg/probe/probe.go new file mode 100644 index 00000000000..b9d1a363a38 --- /dev/null +++ b/pkg/probe/probe.go @@ -0,0 +1,37 @@ +/* +Copyright 2015 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 probe + +type Status int + +// Status values must be one of these constants. +const ( + Success Status = iota + Failure + Unknown +) + +func (s Status) String() string { + switch s { + case Success: + return "success" + case Failure: + return "failure" + default: + return "unknown" + } +} diff --git a/pkg/probe/tcp/tcp.go b/pkg/probe/tcp/tcp.go new file mode 100644 index 00000000000..206aa74a13e --- /dev/null +++ b/pkg/probe/tcp/tcp.go @@ -0,0 +1,52 @@ +/* +Copyright 2015 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 tcp + +import ( + "net" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + + "github.com/golang/glog" +) + +func New() TCPProber { + return TCPProber{} +} + +type TCPProber struct{} + +func (pr TCPProber) Probe(host string, port int) (probe.Status, error) { + return DoTCPProbe(net.JoinHostPort(host, strconv.Itoa(port))) +} + +// DoTCPProbe 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 probes. +func DoTCPProbe(addr string) (probe.Status, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return probe.Failure, nil + } + err = conn.Close() + if err != nil { + glog.Errorf("unexpected error closing health check socket: %v (%#v)", err, err) + } + return probe.Success, nil +} diff --git a/pkg/probe/tcp/tcp_test.go b/pkg/probe/tcp/tcp_test.go new file mode 100644 index 00000000000..bd8f61fdac0 --- /dev/null +++ b/pkg/probe/tcp/tcp_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2015 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 tcp + +import ( + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" +) + +func TestTcpHealthChecker(t *testing.T) { + prober := New() + tests := []struct { + expectedStatus probe.Status + usePort bool + expectError bool + }{ + // The probe will be filled in below. This is primarily testing that a connection is made. + {probe.Success, true, false}, + {probe.Failure, false, false}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + 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 { + p, err := strconv.Atoi(port) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !test.usePort { + p = -1 + } + status, err := prober.Probe(host, p) + 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/minion/healthy_registry.go b/pkg/registry/minion/healthy_registry.go index a6d32607e08..2ce68129e11 100644 --- a/pkg/registry/minion/healthy_registry.go +++ b/pkg/registry/minion/healthy_registry.go @@ -22,8 +22,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -115,7 +115,7 @@ func (r *HealthyRegistry) doCheck(key string) util.T { case err != nil: glog.V(2).Infof("HealthyRegistry: node %q health check error: %v", key, err) nodeStatus = api.ConditionUnknown - case status == health.Unhealthy: + case status == probe.Failure: nodeStatus = api.ConditionNone default: nodeStatus = api.ConditionFull diff --git a/pkg/registry/minion/healthy_registry_test.go b/pkg/registry/minion/healthy_registry_test.go index ba4ea3bb531..4bbfa728334 100644 --- a/pkg/registry/minion/healthy_registry_test.go +++ b/pkg/registry/minion/healthy_registry_test.go @@ -22,15 +22,15 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/health" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) type alwaysYes struct{} -func (alwaysYes) HealthCheck(host string) (health.Status, error) { - return health.Healthy, nil +func (alwaysYes) HealthCheck(host string) (probe.Status, error) { + return probe.Success, nil } func TestBasicDelegation(t *testing.T) { @@ -75,11 +75,11 @@ type notMinion struct { minion string } -func (n *notMinion) HealthCheck(host string) (health.Status, error) { +func (n *notMinion) HealthCheck(host string) (probe.Status, error) { if host != n.minion { - return health.Healthy, nil + return probe.Success, nil } else { - return health.Unhealthy, nil + return probe.Failure, nil } } diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index 6cac5e3e940..4aa110090e5 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -305,7 +305,6 @@ func createAndInitKubelet(kc *KubeletConfig, pc *config.PodConfig) (*kubelet.Kub go k.GarbageCollectLoop() go kubelet.MonitorCAdvisor(k, kc.CAdvisorPort) - kubelet.InitHealthChecking(k) return k, nil }