Skip to content

Commit 16c93b0

Browse files
authored
Merge pull request #419 from GrigoriyMikhalkin/CR-297
✨ added health probes
2 parents d212411 + 52d6dbf commit 16c93b0

File tree

7 files changed

+796
-26
lines changed

7 files changed

+796
-26
lines changed

pkg/healthz/doc.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 contains helpers from supporting liveness and readiness endpoints.
18+
// (often referred to as healthz and readyz, respectively).
19+
//
20+
// This package draws heavily from the apiserver's healthz package
21+
// ( https://github.com/kubernetes/apiserver/tree/master/pkg/server/healthz )
22+
// but has some changes to bring it in line with controller-runtime's style.
23+
//
24+
// The main entrypoint is the Handler -- this serves both aggregated health status
25+
// and individual health check endpoints.
26+
package healthz
27+
28+
import (
29+
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
30+
)
31+
32+
var log = logf.RuntimeLog.WithName("healthz")

pkg/healthz/healthz.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
"fmt"
21+
"net/http"
22+
"path"
23+
"sort"
24+
"strings"
25+
26+
"k8s.io/apimachinery/pkg/util/sets"
27+
)
28+
29+
// Handler is an http.Handler that aggregates the results of the given
30+
// checkers to the root path, and supports calling individual checkers on
31+
// subpaths of the name of the checker.
32+
//
33+
// Adding checks on the fly is *not* threadsafe -- use a wrapper.
34+
type Handler struct {
35+
Checks map[string]Checker
36+
}
37+
38+
// checkStatus holds the output of a particular check
39+
type checkStatus struct {
40+
name string
41+
healthy bool
42+
excluded bool
43+
}
44+
45+
func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
46+
failed := false
47+
excluded := getExcludedChecks(req)
48+
49+
parts := make([]checkStatus, 0, len(h.Checks))
50+
51+
// calculate the results...
52+
for checkName, check := range h.Checks {
53+
// no-op the check if we've specified we want to exclude the check
54+
if excluded.Has(checkName) {
55+
excluded.Delete(checkName)
56+
parts = append(parts, checkStatus{name: checkName, healthy: true, excluded: true})
57+
continue
58+
}
59+
if err := check(req); err != nil {
60+
log.V(1).Info("healthz check failed", "checker", checkName, "error", err)
61+
parts = append(parts, checkStatus{name: checkName, healthy: false})
62+
failed = true
63+
} else {
64+
parts = append(parts, checkStatus{name: checkName, healthy: true})
65+
}
66+
}
67+
68+
// ...default a check if none is present...
69+
if len(h.Checks) == 0 {
70+
parts = append(parts, checkStatus{name: "ping", healthy: true})
71+
}
72+
73+
for _, c := range excluded.List() {
74+
log.V(1).Info("cannot exclude health check, no matches for it", "checker", c)
75+
}
76+
77+
// ...sort to be consistent...
78+
sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
79+
80+
// ...and write out the result
81+
// TODO(directxman12): this should also accept a request for JSON content (via a accept header)
82+
_, forceVerbose := req.URL.Query()["verbose"]
83+
writeStatusesAsText(resp, parts, excluded, failed, forceVerbose)
84+
}
85+
86+
// writeStatusAsText writes out the given check statuses in some semi-arbitrary
87+
// bespoke text format that we copied from Kubernetes. unknownExcludes lists
88+
// any checks that the user requested to have excluded, but weren't actually
89+
// known checks. writeStatusAsText is always verbose on failure, and can be
90+
// forced to be verbose on success using the given argument.
91+
func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.String, failed, forceVerbose bool) {
92+
resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
93+
resp.Header().Set("X-Content-Type-Options", "nosniff")
94+
95+
// always write status code first
96+
if failed {
97+
resp.WriteHeader(http.StatusInternalServerError)
98+
} else {
99+
resp.WriteHeader(http.StatusOK)
100+
}
101+
102+
// shortcut for easy non-verbose success
103+
if !failed && !forceVerbose {
104+
fmt.Fprint(resp, "ok")
105+
return
106+
}
107+
108+
// we're always verbose on failure, so from this point on we're guaranteed to be verbose
109+
110+
for _, checkOut := range parts {
111+
switch {
112+
case checkOut.excluded:
113+
fmt.Fprintf(resp, "[+]%s excluded: ok\n", checkOut.name)
114+
case checkOut.healthy:
115+
fmt.Fprintf(resp, "[+]%s ok\n", checkOut.name)
116+
default:
117+
// don't include the error since this endpoint is public. If someone wants more detail
118+
// they should have explicit permission to the detailed checks.
119+
fmt.Fprintf(resp, "[-]%s failed: reason withheld\n", checkOut.name)
120+
}
121+
}
122+
123+
if unknownExcludes.Len() > 0 {
124+
fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.List()...))
125+
}
126+
127+
if failed {
128+
log.Info("healthz check failed", "statuses", parts)
129+
fmt.Fprintf(resp, "healthz check failed\n")
130+
} else {
131+
fmt.Fprint(resp, "healthz check passed\n")
132+
}
133+
}
134+
135+
func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
136+
// clean up the request (duplicating the internal logic of http.ServeMux a bit)
137+
// clean up the path a bit
138+
reqPath := req.URL.Path
139+
if reqPath == "" || reqPath[0] != '/' {
140+
reqPath = "/" + reqPath
141+
}
142+
// path.Clean removes the trailing slash except for root for us
143+
// (which is fine, since we're only serving one layer of sub-paths)
144+
reqPath = path.Clean(reqPath)
145+
146+
// either serve the root endpoint...
147+
if reqPath == "/" {
148+
h.serveAggregated(resp, req)
149+
return
150+
}
151+
152+
// ...the default check (if nothing else is present)...
153+
if len(h.Checks) == 0 && reqPath[1:] == "ping" {
154+
CheckHandler{Checker: Ping}.ServeHTTP(resp, req)
155+
return
156+
}
157+
158+
// ...or an individual checker
159+
checkName := reqPath[1:] // ignore the leading slash
160+
checker, known := h.Checks[checkName]
161+
if !known {
162+
http.NotFoundHandler().ServeHTTP(resp, req)
163+
return
164+
}
165+
166+
CheckHandler{Checker: checker}.ServeHTTP(resp, req)
167+
}
168+
169+
// CheckHandler is an http.Handler that serves a health check endpoint at the root path,
170+
// based on its checker.
171+
type CheckHandler struct {
172+
Checker
173+
}
174+
175+
func (h CheckHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
176+
err := h.Checker(req)
177+
if err != nil {
178+
http.Error(resp, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
179+
} else {
180+
fmt.Fprint(resp, "ok")
181+
}
182+
}
183+
184+
// Checker knows how to perform a health check.
185+
type Checker func(req *http.Request) error
186+
187+
// Ping returns true automatically when checked
188+
var Ping Checker = func(_ *http.Request) error { return nil }
189+
190+
// getExcludedChecks extracts the health check names to be excluded from the query param
191+
func getExcludedChecks(r *http.Request) sets.String {
192+
checks, found := r.URL.Query()["exclude"]
193+
if found {
194+
return sets.NewString(checks...)
195+
}
196+
return sets.NewString()
197+
}
198+
199+
// formatQuoted returns a formatted string of the health check names,
200+
// preserving the order passed in.
201+
func formatQuoted(names ...string) string {
202+
quoted := make([]string, 0, len(names))
203+
for _, name := range names {
204+
quoted = append(quoted, fmt.Sprintf("%q", name))
205+
}
206+
return strings.Join(quoted, ",")
207+
}

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+
})

0 commit comments

Comments
 (0)