--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package health
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+ "sync"
+)
+
+// Func is a health-check function: it returns nil when healthy, an
+// error when not.
+type Func func() error
+
+// Routes is a map of URI path to health-check function.
+type Routes map[string]Func
+
+// Handler is an http.Handler that responds to authenticated
+// health-check requests with JSON responses like {"health":"OK"} or
+// {"health":"ERROR","error":"error text"}.
+//
+// Fields of a Handler should not be changed after the Handler is
+// first used.
+type Handler struct {
+ setupOnce sync.Once
+ mux *http.ServeMux
+
+ // Authentication token. If empty, all requests will return 404.
+ Token string
+
+ // Route prefix, typically "/_health/".
+ Prefix string
+
+ // Map of URI paths to health-check Func. The prefix is
+ // omitted: Routes["foo"] is the health check invoked by a
+ // request to "/_health/foo".
+ //
+ // If "ping" is not listed here, it will be added
+ // automatically and will always return a "healthy" response.
+ Routes Routes
+
+ // If non-nil, Log is called after handling each request. The
+ // error argument is nil if the request was succesfully
+ // authenticated and served, even if the health check itself
+ // failed.
+ Log func(*http.Request, error)
+}
+
+// ServeHTTP implements http.Handler.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ h.setupOnce.Do(h.setup)
+ h.mux.ServeHTTP(w, r)
+}
+
+func (h *Handler) setup() {
+ h.mux = http.NewServeMux()
+ prefix := h.Prefix
+ if !strings.HasSuffix(prefix, "/") {
+ prefix = prefix + "/"
+ }
+ for name, fn := range h.Routes {
+ h.mux.Handle(prefix+name, h.healthJSON(fn))
+ }
+ if _, ok := h.Routes["ping"]; !ok {
+ h.mux.Handle(prefix+"ping", h.healthJSON(func() error { return nil }))
+ }
+}
+
+var (
+ healthyBody = []byte(`{"health":"OK"}` + "\n")
+ errNotFound = errors.New(http.StatusText(http.StatusNotFound))
+ errUnauthorized = errors.New(http.StatusText(http.StatusUnauthorized))
+ errForbidden = errors.New(http.StatusText(http.StatusForbidden))
+)
+
+func (h *Handler) healthJSON(fn Func) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var err error
+ defer func() {
+ if h.Log != nil {
+ h.Log(r, err)
+ }
+ }()
+ if h.Token == "" {
+ http.Error(w, "disabled", http.StatusNotFound)
+ err = errNotFound
+ } else if ah := r.Header.Get("Authorization"); ah == "" {
+ http.Error(w, "authorization required", http.StatusUnauthorized)
+ err = errUnauthorized
+ } else if ah != "Bearer "+h.Token {
+ http.Error(w, "authorization error", http.StatusForbidden)
+ err = errForbidden
+ } else if err = fn(); err == nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(healthyBody)
+ } else {
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+ err = enc.Encode(map[string]string{
+ "health": "ERROR",
+ "error": err.Error(),
+ })
+ }
+ })
+}