kubeprobes/kubeprobes.go
Piotr Icikowski 9dd25ff024
All checks were successful
ci/woodpecker/pr/test Pipeline was successful
feat(probes)!: rewrite probes logic with named probes
- `ProbeFunction` is now an interface for easier use
- `ProbeFunction`s can be auto-updated with specified update interval
- `StatefulProbes` changed into `ManualProbes` and implement `ProbeFunction` interface
- `ManualProbes` allows for marking probe as unhealthy with custom cause
- handlers now return JSON response with failed probes
- handler's response can be set to verbose via `Kubeprobes` option or via `?v` request param

BREAKING CHANGE: type definitions were replaced with more robust implementation.
2024-03-03 00:00:57 +01:00

148 lines
3.7 KiB
Go

package kubeprobes
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
)
// Kubeprobes represents liveness & readiness probes handler.
type Kubeprobes interface {
http.Handler
// LivenessHandler returns [http.Handler] for liveness probes.
LivenessHandler() http.Handler
// ReadinessHandler returns [http.Handler] for readiness probes.
ReadinessHandler() http.Handler
}
type kubeprobes struct {
livenessProbes []ProbeFunction
readinessProbes []ProbeFunction
verbose bool
pathLive string
pathReady string
}
// New returns a new instance of a Kubernetes probes with given options.
func New(options ...Option) (Kubeprobes, error) {
kp := &kubeprobes{
livenessProbes: []ProbeFunction{},
readinessProbes: []ProbeFunction{},
pathLive: defaultLivenessPath,
pathReady: defaultReadinessPath,
}
for _, option := range options {
option.apply(kp)
}
if err := kp.validate(); err != nil {
return nil, err
}
return kp, nil
}
func (kp *kubeprobes) validate() error {
var err error
if kp.pathLive == "" {
err = errors.Join(err, fmt.Errorf("liveness probe path must not be empty"))
}
if kp.pathReady == "" {
err = errors.Join(err, fmt.Errorf("readiness probe path must not be empty"))
}
if !strings.HasPrefix(kp.pathLive, "/") {
err = errors.Join(err, fmt.Errorf("liveness probe path must start with slash (current: %q)", kp.pathLive))
}
if !strings.HasPrefix(kp.pathReady, "/") {
err = errors.Join(err, fmt.Errorf("readiness probe path must start with slash (current: %q)", kp.pathReady))
}
if kp.pathLive == kp.pathReady {
err = errors.Join(err, fmt.Errorf("liveness and readiness probes have the same values (both %q)", kp.pathLive))
}
if len(kp.livenessProbes) == 0 {
err = errors.Join(err, fmt.Errorf("no liveness probes defined"))
}
if len(kp.readinessProbes) == 0 {
err = errors.Join(err, fmt.Errorf("no readiness probes defined"))
}
return err
}
type probesResponse struct {
Passed []statusEntry `json:"passed,omitempty"`
Failed []statusEntry `json:"failed,omitempty"`
}
func (kp *kubeprobes) handleLiveness(w http.ResponseWriter, r *http.Request) {
sq := newStatusQuery(kp.livenessProbes)
output := probesResponse{}
sq.wait()
output.Failed = sq.failed
if r.URL.Query().Has(verboseOutputFlag) || kp.verbose {
output.Passed = sq.passed
}
w.Header().Add(headerContentType, contentTypeJSON)
if sq.ok {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(output)
}
func (kp *kubeprobes) handleReadiness(w http.ResponseWriter, r *http.Request) {
sq := newStatusQuery(append(kp.livenessProbes, kp.readinessProbes...))
output := probesResponse{}
sq.wait()
output.Failed = sq.failed
if r.URL.Query().Has(verboseOutputFlag) || kp.verbose {
output.Passed = sq.passed
}
w.Header().Add(headerContentType, contentTypeJSON)
if sq.ok {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(output)
}
// LivenessHandler implements Kubeprobes.
func (kp *kubeprobes) LivenessHandler() http.Handler {
return http.HandlerFunc(kp.handleLiveness)
}
// ReadinessHandler implements Kubeprobes.
func (kp *kubeprobes) ReadinessHandler() http.Handler {
return http.HandlerFunc(kp.handleReadiness)
}
// ServeHTTP implements Kubeprobes.
func (kp *kubeprobes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case kp.pathLive:
kp.handleLiveness(w, r)
case kp.pathReady:
kp.handleReadiness(w, r)
default:
w.WriteHeader(http.StatusNotFound)
}
}