1 package healthz
2
3 import (
4 "fmt"
5 "net/http"
6 "path"
7 "sort"
8 "strings"
9
10 "github.com/go-logr/logr"
11 "k8s.io/apimachinery/pkg/util/sets"
12 )
13
14
15
16
17
18
19 type Handler struct {
20 Checks map[string]Checker
21 Log logr.Logger
22 }
23
24
25 type checkStatus struct {
26 name string
27 healthy bool
28 excluded bool
29 }
30
31 func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
32 failed := false
33 excluded := getExcludedChecks(req)
34
35 parts := make([]checkStatus, 0, len(h.Checks))
36
37
38 for checkName, check := range h.Checks {
39
40 if excluded.Has(checkName) {
41 excluded.Delete(checkName)
42 parts = append(parts, checkStatus{name: checkName, healthy: true, excluded: true})
43 continue
44 }
45 if err := check(req); err != nil {
46 h.Log.Error(err, "healthz check failed", "checker", checkName)
47 parts = append(parts, checkStatus{name: checkName, healthy: false})
48 failed = true
49 } else {
50 parts = append(parts, checkStatus{name: checkName, healthy: true})
51 }
52 }
53
54
55 if len(h.Checks) == 0 {
56 parts = append(parts, checkStatus{name: "ping", healthy: true})
57 }
58
59 for _, c := range excluded.UnsortedList() {
60 h.Log.Error(nil, "cannot exclude health check, no matches for it", "checker", c)
61 }
62
63
64 sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
65
66
67
68 _, forceVerbose := req.URL.Query()["verbose"]
69 h.writeStatusesAsText(resp, parts, excluded, failed, forceVerbose)
70 }
71
72
73
74
75
76
77 func (h *Handler) writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.Set[string], failed, forceVerbose bool) {
78 resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
79 resp.Header().Set("X-Content-Type-Options", "nosniff")
80
81
82 if failed {
83 resp.WriteHeader(http.StatusInternalServerError)
84 } else {
85 resp.WriteHeader(http.StatusOK)
86 }
87
88
89 if !failed && !forceVerbose {
90 fmt.Fprint(resp, "ok")
91 return
92 }
93
94
95
96 for _, checkOut := range parts {
97 switch {
98 case checkOut.excluded:
99 fmt.Fprintf(resp, "[+]%s excluded: ok\n", checkOut.name)
100 case checkOut.healthy:
101 fmt.Fprintf(resp, "[+]%s ok\n", checkOut.name)
102 default:
103
104
105 fmt.Fprintf(resp, "[-]%s failed: reason withheld\n", checkOut.name)
106 }
107 }
108
109 if unknownExcludes.Len() > 0 {
110 fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.UnsortedList()...))
111 }
112
113 if failed {
114
115 h.Log.Error(nil, "healthz check failed", "statuses", parts)
116 fmt.Fprintf(resp, "healthz check failed\n")
117 } else {
118 fmt.Fprint(resp, "healthz check passed\n")
119 }
120 }
121
122 func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
123
124
125 reqPath := req.URL.Path
126 if reqPath == "" || reqPath[0] != '/' {
127 reqPath = "/" + reqPath
128 }
129
130
131 reqPath = path.Clean(reqPath)
132
133
134 if reqPath == "/" {
135 h.serveAggregated(resp, req)
136 return
137 }
138
139
140 if len(h.Checks) == 0 && reqPath[1:] == "ping" {
141 CheckHandler{Checker: Ping}.ServeHTTP(resp, req)
142 return
143 }
144
145
146 checkName := reqPath[1:]
147 checker, known := h.Checks[checkName]
148 if !known {
149 http.NotFoundHandler().ServeHTTP(resp, req)
150 return
151 }
152
153 CheckHandler{Checker: checker}.ServeHTTP(resp, req)
154 }
155
156
157
158 type CheckHandler struct {
159 Checker
160 }
161
162 func (h CheckHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
163 if err := h.Checker(req); err != nil {
164 http.Error(resp, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
165 } else {
166 fmt.Fprint(resp, "ok")
167 }
168 }
169
170
171 type Checker func(req *http.Request) error
172
173
174 var Ping Checker = func(_ *http.Request) error { return nil }
175
176
177 func getExcludedChecks(r *http.Request) sets.Set[string] {
178 checks, found := r.URL.Query()["exclude"]
179 if found {
180 return sets.New[string](checks...)
181 }
182 return sets.New[string]()
183 }
184
185
186
187 func formatQuoted(names ...string) string {
188 quoted := make([]string, 0, len(names))
189 for _, name := range names {
190 quoted = append(quoted, fmt.Sprintf("%q", name))
191 }
192 return strings.Join(quoted, ",")
193 }
194
View as plain text