Skip to content

Commit b28d04b

Browse files
added health probes
1 parent d7467fc commit b28d04b

File tree

7 files changed

+710
-25
lines changed

7 files changed

+710
-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: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
type Handler struct {
29+
checks []Checker
30+
}
31+
32+
// GetChecks returns all health checks.
33+
func (h *Handler) GetChecks() []Checker {
34+
return h.checks
35+
}
36+
37+
// AddCheck adds new health check to handler.
38+
func (h *Handler) AddCheck(check Checker) {
39+
h.checks = append(h.checks, check)
40+
}
41+
42+
// Build creates http.Handler that serves checks.
43+
func (h *Handler) Build() http.Handler {
44+
return handleRootHealthz(h.checks...)
45+
}
46+
47+
// HealthzChecker is a named healthz checker.
48+
type Checker interface {
49+
Name() string
50+
Check(req *http.Request) error
51+
}
52+
53+
// PingHealthz returns true automatically when checked
54+
var PingHealthz Checker = ping{}
55+
56+
// ping implements the simplest possible healthz checker.
57+
type ping struct{}
58+
59+
func (ping) Name() string {
60+
return "ping"
61+
}
62+
63+
// PingHealthz is a health check that returns true.
64+
func (ping) Check(_ *http.Request) error {
65+
return nil
66+
}
67+
68+
// NamedCheck returns a healthz checker for the given name and function.
69+
func NamedCheck(name string, check func(r *http.Request) error) Checker {
70+
return &healthzCheck{name, check}
71+
}
72+
73+
// InstallPathHandler registers handlers for health checking on
74+
// a specific path to mux. *All handlers* for the path must be
75+
// specified in exactly one call to InstallPathHandler. Calling
76+
// InstallPathHandler more than once for the same path and mux will
77+
// result in a panic.
78+
func InstallPathHandler(mux mux, path string, handler *Handler) {
79+
if len(handler.GetChecks()) == 0 {
80+
log.V(1).Info("No default health checks specified. Installing the ping handler.")
81+
handler.AddCheck(PingHealthz)
82+
}
83+
84+
mux.Handle(path, handler.Build())
85+
for _, check := range handler.GetChecks() {
86+
log.V(1).Info("installing healthz checker", "checker", check.Name())
87+
mux.Handle(fmt.Sprintf("%s/%s", path, check.Name()), adaptCheckToHandler(check.Check))
88+
}
89+
}
90+
91+
// mux is an interface describing the methods InstallHandler requires.
92+
type mux interface {
93+
Handle(pattern string, handler http.Handler)
94+
}
95+
96+
// healthzCheck implements HealthzChecker on an arbitrary name and check function.
97+
type healthzCheck struct {
98+
name string
99+
check func(r *http.Request) error
100+
}
101+
102+
var _ Checker = &healthzCheck{}
103+
104+
func (c *healthzCheck) Name() string {
105+
return c.name
106+
}
107+
108+
func (c *healthzCheck) Check(r *http.Request) error {
109+
return c.check(r)
110+
}
111+
112+
// getExcludedChecks extracts the health check names to be excluded from the query param
113+
func getExcludedChecks(r *http.Request) sets.String {
114+
checks, found := r.URL.Query()["exclude"]
115+
if found {
116+
return sets.NewString(checks...)
117+
}
118+
return sets.NewString()
119+
}
120+
121+
// handleRootHealthz returns an http.HandlerFunc that serves the provided checks.
122+
func handleRootHealthz(checks ...Checker) http.HandlerFunc {
123+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
124+
failed := false
125+
excluded := getExcludedChecks(r)
126+
var verboseOut bytes.Buffer
127+
for _, check := range checks {
128+
// no-op the check if we've specified we want to exclude the check
129+
if excluded.Has(check.Name()) {
130+
excluded.Delete(check.Name())
131+
fmt.Fprintf(&verboseOut, "[+]%s excluded: ok\n", check.Name())
132+
continue
133+
}
134+
if err := check.Check(r); err != nil {
135+
// don't include the error since this endpoint is public. If someone wants more detail
136+
// they should have explicit permission to the detailed checks.
137+
log.V(1).Info("healthz check failed", "checker", check.Name(), "error", err)
138+
fmt.Fprintf(&verboseOut, "[-]%s failed: reason withheld\n", check.Name())
139+
failed = true
140+
} else {
141+
fmt.Fprintf(&verboseOut, "[+]%s ok\n", check.Name())
142+
}
143+
}
144+
if excluded.Len() > 0 {
145+
fmt.Fprintf(&verboseOut, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(excluded.List()...))
146+
for _, c := range excluded.List() {
147+
log.Info("cannot exclude health check, no matches for it", "checker", c)
148+
}
149+
}
150+
// always be verbose on failure
151+
if failed {
152+
log.V(1).Info("healthz check failed", "message", verboseOut.String())
153+
http.Error(w, fmt.Sprintf("%shealthz check failed", verboseOut.String()), http.StatusInternalServerError)
154+
return
155+
}
156+
157+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
158+
w.Header().Set("X-Content-Type-Options", "nosniff")
159+
if _, found := r.URL.Query()["verbose"]; !found {
160+
fmt.Fprint(w, "ok")
161+
return
162+
}
163+
164+
_, err := verboseOut.WriteTo(w)
165+
if err != nil {
166+
log.V(1).Info("healthz check failed", "message", verboseOut.String())
167+
http.Error(w, fmt.Sprintf("%shealthz check failed", verboseOut.String()), http.StatusInternalServerError)
168+
return
169+
}
170+
fmt.Fprint(w, "healthz check passed\n")
171+
})
172+
}
173+
174+
// adaptCheckToHandler returns an http.HandlerFunc that serves the provided checks.
175+
func adaptCheckToHandler(c func(r *http.Request) error) http.HandlerFunc {
176+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
177+
err := c(r)
178+
if err != nil {
179+
http.Error(w, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
180+
} else {
181+
fmt.Fprint(w, "ok")
182+
}
183+
})
184+
}
185+
186+
// formatQuoted returns a formatted string of the health check names,
187+
// preserving the order passed in.
188+
func formatQuoted(names ...string) string {
189+
quoted := make([]string, 0, len(names))
190+
for _, name := range names {
191+
quoted = append(quoted, fmt.Sprintf("%q", name))
192+
}
193+
return strings.Join(quoted, ",")
194+
}

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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
handler := &healthz.Handler{}
39+
40+
healthz.InstallPathHandler(mux, "/healthz/test", handler)
41+
req, err := http.NewRequest("GET", "http://example.com/healthz/test", nil)
42+
Expect(req).ToNot(BeNil())
43+
Expect(err).ToNot(HaveOccurred())
44+
45+
w := httptest.NewRecorder()
46+
mux.ServeHTTP(w, req)
47+
Expect(w.Code).To(Equal(http.StatusOK))
48+
Expect(w.Header().Get("Content-Type")).To(Equal(contentType))
49+
Expect(w.Body.String()).To(Equal("ok"))
50+
51+
close(done)
52+
})
53+
})
54+
55+
Describe("Checks", func() {
56+
var testMultipleChecks = func(path string) {
57+
tests := []struct {
58+
path string
59+
expectedResponse string
60+
expectedStatus int
61+
addBadCheck bool
62+
}{
63+
{"?verbose", "[+]ping ok\nhealthz check passed\n", http.StatusOK,
64+
false},
65+
{"?exclude=dontexist", "ok", http.StatusOK, false},
66+
{"?exclude=bad", "ok", http.StatusOK, true},
67+
{"?verbose=true&exclude=bad", "[+]ping ok\n[+]bad excluded: ok\nhealthz check passed\n",
68+
http.StatusOK, true},
69+
{"?verbose=true&exclude=dontexist",
70+
"[+]ping ok\nwarn: some health checks cannot be excluded: no matches for \"dontexist\"\nhealthz check passed\n",
71+
http.StatusOK, false},
72+
{"/ping", "ok", http.StatusOK, false},
73+
{"", "ok", http.StatusOK, false},
74+
{"?verbose", "[+]ping ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
75+
http.StatusInternalServerError, true},
76+
{"/ping", "ok", http.StatusOK, true},
77+
{"/bad", "internal server error: this will fail\n",
78+
http.StatusInternalServerError, true},
79+
{"", "[+]ping ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
80+
http.StatusInternalServerError, true},
81+
}
82+
83+
for _, test := range tests {
84+
mux := http.NewServeMux()
85+
checks := []healthz.Checker{healthz.PingHealthz}
86+
if test.addBadCheck {
87+
checks = append(checks, healthz.NamedCheck("bad", func(_ *http.Request) error {
88+
return errors.New("this will fail")
89+
}))
90+
}
91+
handler := &healthz.Handler{}
92+
for _, check := range checks {
93+
handler.AddCheck(check)
94+
}
95+
96+
if path == "" {
97+
path = "/healthz"
98+
}
99+
100+
healthz.InstallPathHandler(mux, path, handler)
101+
req, err := http.NewRequest("GET", fmt.Sprintf("http://example.com%s%v", path, test.path), nil)
102+
Expect(req).ToNot(BeNil())
103+
Expect(err).ToNot(HaveOccurred())
104+
105+
w := httptest.NewRecorder()
106+
mux.ServeHTTP(w, req)
107+
Expect(w.Code).To(Equal(test.expectedStatus))
108+
Expect(w.Header().Get("Content-Type")).To(Equal(contentType))
109+
Expect(w.Body.String()).To(Equal(test.expectedResponse))
110+
}
111+
}
112+
113+
It("should do multiple checks", func(done Done) {
114+
testMultipleChecks("")
115+
close(done)
116+
})
117+
118+
It("should do multiple path checks", func(done Done) {
119+
testMultipleChecks("/ready")
120+
close(done)
121+
})
122+
})
123+
})

0 commit comments

Comments
 (0)