1
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
30
31
32
33
34 type Handler struct {
35 Checks map[string]Checker
36 }
37
38
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
52 for checkName, check := range h.Checks {
53
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
69 if len(h.Checks) == 0 {
70 parts = append(parts, checkStatus{name: "ping", healthy: true})
71 }
72
73 for _, c := range excluded.UnsortedList() {
74 log.V(1).Info("cannot exclude health check, no matches for it", "checker", c)
75 }
76
77
78 sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
79
80
81
82 _, forceVerbose := req.URL.Query()["verbose"]
83 writeStatusesAsText(resp, parts, excluded, failed, forceVerbose)
84 }
85
86
87
88
89
90
91 func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.Set[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
96 if failed {
97 resp.WriteHeader(http.StatusInternalServerError)
98 } else {
99 resp.WriteHeader(http.StatusOK)
100 }
101
102
103 if !failed && !forceVerbose {
104 fmt.Fprint(resp, "ok")
105 return
106 }
107
108
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
118
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.UnsortedList()...))
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
137
138 reqPath := req.URL.Path
139 if reqPath == "" || reqPath[0] != '/' {
140 reqPath = "/" + reqPath
141 }
142
143
144 reqPath = path.Clean(reqPath)
145
146
147 if reqPath == "/" {
148 h.serveAggregated(resp, req)
149 return
150 }
151
152
153 if len(h.Checks) == 0 && reqPath[1:] == "ping" {
154 CheckHandler{Checker: Ping}.ServeHTTP(resp, req)
155 return
156 }
157
158
159 checkName := reqPath[1:]
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
170
171 type CheckHandler struct {
172 Checker
173 }
174
175 func (h CheckHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
176 if err := h.Checker(req); err != nil {
177 http.Error(resp, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
178 } else {
179 fmt.Fprint(resp, "ok")
180 }
181 }
182
183
184 type Checker func(req *http.Request) error
185
186
187 var Ping Checker = func(_ *http.Request) error { return nil }
188
189
190 func getExcludedChecks(r *http.Request) sets.Set[string] {
191 checks, found := r.URL.Query()["exclude"]
192 if found {
193 return sets.New[string](checks...)
194 }
195 return sets.New[string]()
196 }
197
198
199
200 func formatQuoted(names ...string) string {
201 quoted := make([]string, 0, len(names))
202 for _, name := range names {
203 quoted = append(quoted, fmt.Sprintf("%q", name))
204 }
205 return strings.Join(quoted, ",")
206 }
207
View as plain text