sigs.k8s.io/controller-runtime@v0.18.2/pkg/healthz/healthz.go (about) 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.UnsortedList() { 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.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 // 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.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 // 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 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 // Checker knows how to perform a health check. 184 type Checker func(req *http.Request) error 185 186 // Ping returns true automatically when checked. 187 var Ping Checker = func(_ *http.Request) error { return nil } 188 189 // getExcludedChecks extracts the health check names to be excluded from the query param. 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 // formatQuoted returns a formatted string of the health check names, 199 // preserving the order passed in. 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 }