...

Source file src/sigs.k8s.io/controller-runtime/pkg/healthz/healthz.go

Documentation: sigs.k8s.io/controller-runtime/pkg/healthz

     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  }
   207  

View as plain text