Skip to content

Commit 1692d51

Browse files
added health probes
1 parent 4e3e16b commit 1692d51

File tree

7 files changed

+676
-25
lines changed

7 files changed

+676
-25
lines changed

pkg/healthz/doc.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// This package is almost exact copy of apiserver's healthz package:
18+
// https://github.com/kubernetes/apiserver/tree/master/pkg/server/healthz
19+
//
20+
// Except that LogHealthz checker removed
21+
// and some style fixes is made to satisfy linters
22+
package healthz
23+
24+
import (
25+
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
26+
)
27+
28+
var log = logf.RuntimeLog.WithName("healthz")

pkg/healthz/healthz.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package healthz
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"net/http"
23+
"strings"
24+
25+
"k8s.io/apimachinery/pkg/util/sets"
26+
)
27+
28+
// HealthzChecker is a named healthz checker.
29+
type Checker interface {
30+
Name() string
31+
Check(req *http.Request) error
32+
}
33+
34+
// PingHealthz returns true automatically when checked
35+
var PingHealthz Checker = ping{}
36+
37+
// ping implements the simplest possible healthz checker.
38+
type ping struct{}
39+
40+
func (ping) Name() string {
41+
return "ping"
42+
}
43+
44+
// PingHealthz is a health check that returns true.
45+
func (ping) Check(_ *http.Request) error {
46+
return nil
47+
}
48+
49+
// NamedCheck returns a healthz checker for the given name and function.
50+
func NamedCheck(name string, check func(r *http.Request) error) Checker {
51+
return &healthzCheck{name, check}
52+
}
53+
54+
// InstallPathHandler registers handlers for health checking on
55+
// a specific path to mux. *All handlers* for the path must be
56+
// specified in exactly one call to InstallPathHandler. Calling
57+
// InstallPathHandler more than once for the same path and mux will
58+
// result in a panic.
59+
func InstallPathHandler(mux mux, path string, checks ...Checker) {
60+
if len(checks) == 0 {
61+
log.V(1).Info("No default health checks specified. Installing the ping handler.")
62+
checks = []Checker{PingHealthz}
63+
}
64+
65+
mux.Handle(path, handleRootHealthz(checks...))
66+
for _, check := range checks {
67+
log.V(1).Info("installing healthz checker", "checker", check.Name())
68+
mux.Handle(fmt.Sprintf("%s/%s", path, check.Name()), adaptCheckToHandler(check.Check))
69+
}
70+
}
71+
72+
// mux is an interface describing the methods InstallHandler requires.
73+
type mux interface {
74+
Handle(pattern string, handler http.Handler)
75+
}
76+
77+
// healthzCheck implements HealthzChecker on an arbitrary name and check function.
78+
type healthzCheck struct {
79+
name string
80+
check func(r *http.Request) error
81+
}
82+
83+
var _ Checker = &healthzCheck{}
84+
85+
func (c *healthzCheck) Name() string {
86+
return c.name
87+
}
88+
89+
func (c *healthzCheck) Check(r *http.Request) error {
90+
return c.check(r)
91+
}
92+
93+
// getExcludedChecks extracts the health check names to be excluded from the query param
94+
func getExcludedChecks(r *http.Request) sets.String {
95+
checks, found := r.URL.Query()["exclude"]
96+
if found {
97+
return sets.NewString(checks...)
98+
}
99+
return sets.NewString()
100+
}
101+
102+
// handleRootHealthz returns an http.HandlerFunc that serves the provided checks.
103+
func handleRootHealthz(checks ...Checker) http.HandlerFunc {
104+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
105+
failed := false
106+
excluded := getExcludedChecks(r)
107+
var verboseOut bytes.Buffer
108+
for _, check := range checks {
109+
// no-op the check if we've specified we want to exclude the check
110+
if excluded.Has(check.Name()) {
111+
excluded.Delete(check.Name())
112+
fmt.Fprintf(&verboseOut, "[+]%s excluded: ok\n", check.Name())
113+
continue
114+
}
115+
if err := check.Check(r); err != nil {
116+
// don't include the error since this endpoint is public. If someone wants more detail
117+
// they should have explicit permission to the detailed checks.
118+
log.V(1).Info("healthz check failed", "checker", check.Name(), "error", err)
119+
fmt.Fprintf(&verboseOut, "[-]%s failed: reason withheld\n", check.Name())
120+
failed = true
121+
} else {
122+
fmt.Fprintf(&verboseOut, "[+]%s ok\n", check.Name())
123+
}
124+
}
125+
if excluded.Len() > 0 {
126+
fmt.Fprintf(&verboseOut, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(excluded.List()...))
127+
for _, c := range excluded.List() {
128+
log.Info("cannot exclude health check, no matches for it", "checker", c)
129+
}
130+
}
131+
// always be verbose on failure
132+
if failed {
133+
log.V(1).Info("healthz check failed", "message", verboseOut.String())
134+
http.Error(w, fmt.Sprintf("%shealthz check failed", verboseOut.String()), http.StatusInternalServerError)
135+
return
136+
}
137+
138+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
139+
w.Header().Set("X-Content-Type-Options", "nosniff")
140+
if _, found := r.URL.Query()["verbose"]; !found {
141+
fmt.Fprint(w, "ok")
142+
return
143+
}
144+
145+
_, err := verboseOut.WriteTo(w)
146+
if err != nil {
147+
log.V(1).Info("healthz check failed", "message", verboseOut.String())
148+
http.Error(w, fmt.Sprintf("%shealthz check failed", verboseOut.String()), http.StatusInternalServerError)
149+
return
150+
}
151+
fmt.Fprint(w, "healthz check passed\n")
152+
})
153+
}
154+
155+
// adaptCheckToHandler returns an http.HandlerFunc that serves the provided checks.
156+
func adaptCheckToHandler(c func(r *http.Request) error) http.HandlerFunc {
157+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158+
err := c(r)
159+
if err != nil {
160+
http.Error(w, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
161+
} else {
162+
fmt.Fprint(w, "ok")
163+
}
164+
})
165+
}
166+
167+
// formatQuoted returns a formatted string of the health check names,
168+
// preserving the order passed in.
169+
func formatQuoted(names ...string) string {
170+
quoted := make([]string, 0, len(names))
171+
for _, name := range names {
172+
quoted = append(quoted, fmt.Sprintf("%q", name))
173+
}
174+
return strings.Join(quoted, ",")
175+
}

pkg/healthz/healthz_suite_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package healthz_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo"
23+
. "github.com/onsi/gomega"
24+
"sigs.k8s.io/controller-runtime/pkg/envtest"
25+
logf "sigs.k8s.io/controller-runtime/pkg/log"
26+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
27+
)
28+
29+
func TestHealthz(t *testing.T) {
30+
RegisterFailHandler(Fail)
31+
RunSpecsWithDefaultAndCustomReporters(t, "Healthz Suite", []Reporter{envtest.NewlineReporter{}})
32+
}
33+
34+
var _ = BeforeSuite(func() {
35+
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
36+
})

pkg/healthz/healthz_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package healthz_test
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"net/http"
23+
"net/http/httptest"
24+
25+
. "github.com/onsi/ginkgo"
26+
. "github.com/onsi/gomega"
27+
"sigs.k8s.io/controller-runtime/pkg/healthz"
28+
)
29+
30+
const (
31+
contentType = "text/plain; charset=utf-8"
32+
)
33+
34+
var _ = Describe("Healthz", func() {
35+
Describe("Install", func() {
36+
It("should install handler", func(done Done) {
37+
mux := http.NewServeMux()
38+
healthz.InstallPathHandler(mux, "/healthz/test")
39+
req, err := http.NewRequest("GET", "http://example.com/healthz/test", nil)
40+
Expect(req).ToNot(BeNil())
41+
Expect(err).ToNot(HaveOccurred())
42+
43+
w := httptest.NewRecorder()
44+
mux.ServeHTTP(w, req)
45+
Expect(w.Code).To(Equal(http.StatusOK))
46+
Expect(w.Header().Get("Content-Type")).To(Equal(contentType))
47+
Expect(w.Body.String()).To(Equal("ok"))
48+
49+
close(done)
50+
})
51+
})
52+
53+
Describe("Checks", func() {
54+
var testMultipleChecks = func(path string) {
55+
tests := []struct {
56+
path string
57+
expectedResponse string
58+
expectedStatus int
59+
addBadCheck bool
60+
}{
61+
{"?verbose", "[+]ping ok\nhealthz check passed\n", http.StatusOK,
62+
false},
63+
{"?exclude=dontexist", "ok", http.StatusOK, false},
64+
{"?exclude=bad", "ok", http.StatusOK, true},
65+
{"?verbose=true&exclude=bad", "[+]ping ok\n[+]bad excluded: ok\nhealthz check passed\n",
66+
http.StatusOK, true},
67+
{"?verbose=true&exclude=dontexist",
68+
"[+]ping ok\nwarn: some health checks cannot be excluded: no matches for \"dontexist\"\nhealthz check passed\n",
69+
http.StatusOK, false},
70+
{"/ping", "ok", http.StatusOK, false},
71+
{"", "ok", http.StatusOK, false},
72+
{"?verbose", "[+]ping ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
73+
http.StatusInternalServerError, true},
74+
{"/ping", "ok", http.StatusOK, true},
75+
{"/bad", "internal server error: this will fail\n",
76+
http.StatusInternalServerError, true},
77+
{"", "[+]ping ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
78+
http.StatusInternalServerError, true},
79+
}
80+
81+
for _, test := range tests {
82+
mux := http.NewServeMux()
83+
checks := []healthz.Checker{healthz.PingHealthz}
84+
if test.addBadCheck {
85+
checks = append(checks, healthz.NamedCheck("bad", func(_ *http.Request) error {
86+
return errors.New("this will fail")
87+
}))
88+
}
89+
if path == "" {
90+
path = "/healthz"
91+
}
92+
93+
healthz.InstallPathHandler(mux, path, checks...)
94+
req, err := http.NewRequest("GET", fmt.Sprintf("http://example.com%s%v", path, test.path), nil)
95+
Expect(req).ToNot(BeNil())
96+
Expect(err).ToNot(HaveOccurred())
97+
98+
w := httptest.NewRecorder()
99+
mux.ServeHTTP(w, req)
100+
Expect(w.Code).To(Equal(test.expectedStatus))
101+
Expect(w.Header().Get("Content-Type")).To(Equal(contentType))
102+
Expect(w.Body.String()).To(Equal(test.expectedResponse))
103+
}
104+
}
105+
106+
It("should do multiple checks", func(done Done) {
107+
testMultipleChecks("")
108+
close(done)
109+
})
110+
111+
It("should do multiple path checks", func(done Done) {
112+
testMultipleChecks("/ready")
113+
close(done)
114+
})
115+
})
116+
})

0 commit comments

Comments
 (0)