diff --git a/pkg/api/utils/healthCheck.go b/pkg/api/utils/healthCheck.go index 2e5eef60..f19f679d 100644 --- a/pkg/api/utils/healthCheck.go +++ b/pkg/api/utils/healthCheck.go @@ -7,6 +7,10 @@ import ( "net/http" ) +const defaultHealthEndpointPath = "/health" + +type ReadinessConditionFunc func() bool + type StatusBody struct { Status string `json:"status"` } @@ -16,7 +20,48 @@ func (s *StatusBody) ToJSON() ([]byte, error) { return json.Marshal(s) } -func healthHandler(w http.ResponseWriter, r *http.Request) { +type HealthHandlerOption func(h *healthHandler) + +// WithReadinessConditionFunc allows to specify a function that should determine if the endpoint should return an HTTP 200 (OK), or +// a 412 (Precondition failed) response +func WithReadinessConditionFunc(rc ReadinessConditionFunc) HealthHandlerOption { + return func(h *healthHandler) { + h.readinessConditionFunc = rc + } +} + +// WithPath allows to specify the path under which the endpoint should be reachable +func WithPath(path string) HealthHandlerOption { + return func(h *healthHandler) { + h.path = path + } +} + +type healthHandler struct { + readinessConditionFunc ReadinessConditionFunc + path string +} + +func newHealthHandler(opts ...HealthHandlerOption) *healthHandler { + h := &healthHandler{ + path: defaultHealthEndpointPath, + } + for _, o := range opts { + o(h) + } + return h +} + +func (h *healthHandler) healthCheck(w http.ResponseWriter, r *http.Request) { + ready := true + if h.readinessConditionFunc != nil { + ready = h.readinessConditionFunc() + } + + if !ready { + w.WriteHeader(http.StatusPreconditionFailed) + return + } status := StatusBody{Status: "OK"} body, err := status.ToJSON() @@ -32,9 +77,11 @@ func healthHandler(w http.ResponseWriter, r *http.Request) { } } -func RunHealthEndpoint(port string) { - - http.HandleFunc("/health", healthHandler) +// RunHealthEndpoint starts an http server on the specified port and provides a simple HTTP Get endpoint that can be used for health checks +// per default, the endpoint will be reachable under the path '/health' +func RunHealthEndpoint(port string, opts ...HealthHandlerOption) { + h := newHealthHandler(opts...) + http.HandleFunc(h.path, h.healthCheck) err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil) if err != nil { log.Println(err) diff --git a/pkg/api/utils/healthCheck_test.go b/pkg/api/utils/healthCheck_test.go new file mode 100644 index 00000000..14d8c2df --- /dev/null +++ b/pkg/api/utils/healthCheck_test.go @@ -0,0 +1,69 @@ +package api + +import ( + "github.com/stretchr/testify/require" + "net/http" + "testing" + "time" +) + +func TestRunHealthEndpoint(t *testing.T) { + go RunHealthEndpoint("8080") + + require.Eventually(t, func() bool { + get, err := http.Get("http://localhost:8080/health") + if err != nil { + return false + } + if get.StatusCode != http.StatusOK { + return false + } + return true + }, 2*time.Second, 50*time.Millisecond) +} + +func TestRunHealthEndpoint_WithReadinessCondition(t *testing.T) { + ready := false + go RunHealthEndpoint("8080", WithPath("/ready"), WithReadinessConditionFunc(func() bool { + return ready + })) + + require.Eventually(t, func() bool { + get, err := http.Get("http://localhost:8080/ready") + if err != nil { + return false + } + if get.StatusCode != http.StatusPreconditionFailed { + return false + } + return true + }, 2*time.Second, 50*time.Millisecond) + + ready = true + + require.Eventually(t, func() bool { + get, err := http.Get("http://localhost:8080/ready") + if err != nil { + return false + } + if get.StatusCode != http.StatusOK { + return false + } + return true + }, 2*time.Second, 50*time.Millisecond) +} + +func TestRunHealthEndpointCustomPath(t *testing.T) { + go RunHealthEndpoint("8080", WithPath("/readiness")) + + require.Eventually(t, func() bool { + get, err := http.Get("http://localhost:8080/readiness") + if err != nil { + return false + } + if get.StatusCode != http.StatusOK { + return false + } + return true + }, 2*time.Second, 50*time.Millisecond) +}