Merge pull request 'Major enhancements' (#6) from enhancements into devel
All checks were successful
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/push/test Pipeline was successful

Reviewed-on: #6
This commit is contained in:
Piotr Icikowski 2024-03-01 23:46:03 +01:00
commit 25962571c2
9 changed files with 250 additions and 74 deletions

View File

@ -10,19 +10,23 @@ go get -u pkg.icikowski.pl/kubeprobes
## Usage ## Usage
The package provides `kubeprobes.New` function which returns a probes handler compliant with `http.Handler` interface. The package provides `kubeprobes.New` function which returns a probes handler of type `kubeprobes.Kubeprobes`, which is compliant with `http.Handler` interface.
The handler serves two endpoints, which are used to implement liveness and readiness probes by returning either `200` (healthy) or `503` (unhealthy) status: The handler serves two endpoints, which are used to implement liveness and readiness probes by returning either `200` (healthy) or `503` (unhealthy) status:
- `/live` - endpoint for liveness probe; - `/live` - endpoint for liveness probe;
- `/ready` - endpoint for readiness probe. - `/ready` - endpoint for readiness probe.
Accessing any other endpoint will return `404` status. In order to provide maximum performance, no body is ever returned. Default paths can be overriden with options described below. Accessing any other endpoint will return `404` status. In order to provide maximum performance, no body is ever returned.
The `kubeprobes.New` function accepts following options-applying functions as arguments: The `kubeprobes.New` function accepts following options as arguments:
- `kubeprobes.WithLivenessProbes(/* ... */)` - adds particular [probes](#probes) to the list of liveness probes; - `kubeprobes.WithLivenessProbes(...)` - adds particular [probe functions](#probe-functions) to the list of liveness probes;
- `kubeprobes.WithReadinessProbes(/* ... */)` - adds particular [probes](#probes) to the list of readiness probes. - `kubeprobes.WithLivenessStatefulProbes(...)` - adds particular [`StatefulProbe`s](#stateful-probes) to the list of liveness probes;
- `kubeprobes.WithLivenessPath("/some/liveness/path")` - sets liveness probe path to given path (default is `/live`);
- `kubeprobes.WithReadinessProbes(...)` - adds particular [probe functions](#probe-functions) to the list of readiness probes;
- `kubeprobes.WithReadinessStatefulProbes(...)` - adds particular [`StatefulProbe`s](#stateful-probes) to the list of readiness probes;
- `kubeprobes.WithReadinessPath("/some/readiness/path")` - sets readiness probe path to given path (default is `/ready`).
## Probes ## Probes
@ -47,7 +51,7 @@ someOtherProbe := func() error {
} }
// Use functions in probes handler // Use functions in probes handler
kp := kubeprobes.New( kp, _ := kubeprobes.New(
kubeprobes.WithLivenessProbes(someOtherProbe), kubeprobes.WithLivenessProbes(someOtherProbe),
kubeprobes.WithReadinessProbes(someProbe), kubeprobes.WithReadinessProbes(someProbe),
) )
@ -63,24 +67,52 @@ someProbe := kubeprobes.NewStatefulProbe()
someOtherProbe := kubeprobes.NewStatefulProbe() someOtherProbe := kubeprobes.NewStatefulProbe()
// Use it in probes handler // Use it in probes handler
kp := kubeprobes.New( kp, _ := kubeprobes.New(
kubeprobes.WithLivenessProbes(someProbe.GetProbeFunction()), kubeprobes.WithLivenessStatefulProbes(someProbe),
kubeprobes.WithReadinessProbes(someOtherProbe.GetProbeFunction()), kubeprobes.WithReadinessStatefulProbes(someOtherProbe),
) )
``` ```
## Direct handler access
It is possible to fetch `http.Handler`s for liveness & readiness probes from `kubeprobes.Kubeprobes` instance as follows:
```go
kp, _ := kubeprobes.New(
// ...
)
livenessHandler := kp.GetLivenessHandler()
readinessHandler := kp.GetReadinessHandler()
```
Those handler can be used for manually mounting them on other servers/routers/muxes (eg. `go-chi/chi`, `gorilla/mux`, `http`'s `ServeMux` etc.).
## Example usage ## Example usage
```go ```go
// Create probe functions
appProbe := func() error {
// Some logic for checking app status
return nil
}
// Create stateful probes // Create stateful probes
live := kubeprobes.NewStatefulProbe() live := kubeprobes.NewStatefulProbe()
ready := kubeprobes.NewStatefulProbe() ready := kubeprobes.NewStatefulProbe()
// Prepare handler // Prepare handler
kp := kubeprobes.New( kp, err := kubeprobes.New(
kubeprobes.WithLivenessProbes(live.GetProbeFunction()), kubeprobes.WithLivenessStatefulProbes(live),
kubeprobes.WithReadinessProbes(ready.GetProbeFunction()), kubeprobes.WithReadinessStatefulProbes(ready),
kubeprobes.WithReadinessProbes(appProbe),
kubeprobes.WithLivenessPath("/livez"),
kubeprobes.WithReadinessPath("/readyz"),
) )
if err != nil {
// Kubeprobes object is validated for invalid or conflicting paths! ;)
panic(err)
}
// Start the probes server // Start the probes server
probes := &http.Server{ probes := &http.Server{

7
costants.go Normal file
View File

@ -0,0 +1,7 @@
package kubeprobes
const (
defaultLivenessPath string = "/live"
defaultReadinessPath string = "/ready"
verboseOutputFlag string = "v"
)

67
options.go Normal file
View File

@ -0,0 +1,67 @@
package kubeprobes
type option struct {
fn func(*kubeprobes)
}
func (o *option) apply(kp *kubeprobes) {
o.fn(kp)
}
// WithLivenessProbes adds given probe functions to the set of liveness probes.
func WithLivenessProbes(probes ...ProbeFunction) Option {
return &option{
fn: func(kp *kubeprobes) {
kp.livenessProbes = append(kp.livenessProbes, probes...)
},
}
}
// WithLivenessStatefulProbes adds given [StatefulProbe]s to the set of liveness probes.
func WithLivenessStatefulProbes(probes ...*StatefulProbe) Option {
return &option{
fn: func(kp *kubeprobes) {
for _, p := range probes {
kp.livenessProbes = append(kp.livenessProbes, p.GetProbeFunction())
}
},
}
}
// WithLivenessPath sets custom path for liveness probe (default is "/live").
func WithLivenessPath(path string) Option {
return &option{
fn: func(kp *kubeprobes) {
kp.pathLive = path
},
}
}
// WithReadinessProbes adds given probe functions to the set of readiness probes.
func WithReadinessProbes(probes ...ProbeFunction) Option {
return &option{
fn: func(kp *kubeprobes) {
kp.readinessProbes = append(kp.readinessProbes, probes...)
},
}
}
// WithReadinessProbes adds given [StatefulProbe]s to the set of readiness probes.
func WithReadinessStatefulProbes(probes ...*StatefulProbe) Option {
return &option{
fn: func(kp *kubeprobes) {
for _, p := range probes {
kp.readinessProbes = append(kp.readinessProbes, p.GetProbeFunction())
}
},
}
}
// WithReadinessPath sets custom path for readiness probe (default is "/ready").
func WithReadinessPath(path string) Option {
return &option{
fn: func(kp *kubeprobes) {
kp.pathReady = path
},
}
}

124
probes.go
View File

@ -1,6 +1,8 @@
package kubeprobes package kubeprobes
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
) )
@ -8,57 +10,107 @@ import (
type kubeprobes struct { type kubeprobes struct {
livenessProbes []ProbeFunction livenessProbes []ProbeFunction
readinessProbes []ProbeFunction readinessProbes []ProbeFunction
pathLive string
pathReady string
} }
// ServeHTTP implements http.Handler interface // New returns a new instance of a Kubernetes probes with given options.
func (kp *kubeprobes) ServeHTTP(w http.ResponseWriter, r *http.Request) { func New(options ...Option) (Kubeprobes, error) {
subs := strings.Split(strings.TrimSuffix(r.URL.Path, "/"), "/") kp := &kubeprobes{
switch subs[len(subs)-1] { livenessProbes: []ProbeFunction{},
case "live": readinessProbes: []ProbeFunction{},
pathLive: "/live",
pathReady: "/ready",
}
for _, option := range options {
option.apply(kp)
}
if err := kp.validate(); err != nil {
return nil, err
}
return kp, nil
}
func (kp *kubeprobes) validate() error {
errs := []error{}
if kp.pathLive == "" {
errs = append(
errs,
fmt.Errorf("liveness probe path must not be empty"),
)
}
if kp.pathReady == "" {
errs = append(
errs,
fmt.Errorf("readiness probe path must not be empty"),
)
}
if !strings.HasPrefix(kp.pathLive, "/") {
errs = append(
errs,
fmt.Errorf("liveness probe path must start with slash (current: %q)", kp.pathLive),
)
}
if !strings.HasPrefix(kp.pathReady, "/") {
errs = append(
errs,
fmt.Errorf("readiness probe path must start with slash (current: %q)", kp.pathReady),
)
}
if kp.pathLive == kp.pathReady {
errs = append(
errs,
fmt.Errorf("liveness and readiness probes have the same values (both %q)", kp.pathLive),
)
}
return errors.Join(errs...)
}
func (kp *kubeprobes) handleLiveness(w http.ResponseWriter, _ *http.Request) {
sq := newStatusQuery(kp.livenessProbes) sq := newStatusQuery(kp.livenessProbes)
if sq.isAllGreen() { if sq.isAllGreen() {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} else { } else {
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
} }
case "ready": }
func (kp *kubeprobes) handleReadiness(w http.ResponseWriter, _ *http.Request) {
sq := newStatusQuery(append(kp.livenessProbes, kp.readinessProbes...)) sq := newStatusQuery(append(kp.livenessProbes, kp.readinessProbes...))
if sq.isAllGreen() { if sq.isAllGreen() {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} else { } else {
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
} }
}
// GetLivenessHandler implements Kubeprobes.
func (kp *kubeprobes) GetLivenessHandler() http.Handler {
return http.HandlerFunc(kp.handleLiveness)
}
// GetReadinessHandler implements Kubeprobes.
func (kp *kubeprobes) GetReadinessHandler() 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: default:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
} }
} }
type option func(*kubeprobes)
// New returns a new instance of a Kubernetes probes
func New(options ...option) *kubeprobes {
kp := &kubeprobes{
livenessProbes: []ProbeFunction{},
readinessProbes: []ProbeFunction{},
}
for _, option := range options {
option(kp)
}
return kp
}
// WithLivenessProbes adds given liveness probes to the set of probes
func WithLivenessProbes(probes ...ProbeFunction) option {
return func(kp *kubeprobes) {
kp.livenessProbes = append(kp.livenessProbes, probes...)
}
}
// WithReadinessProbes adds given readiness probes to the set of probes
func WithReadinessProbes(probes ...ProbeFunction) option {
return func(kp *kubeprobes) {
kp.readinessProbes = append(kp.readinessProbes, probes...)
}
}

View File

@ -50,9 +50,9 @@ func TestKubeprobes(t *testing.T) {
}, },
} }
kp := New( kp, _ := New(
WithLivenessProbes(live.GetProbeFunction()), WithLivenessStatefulProbes(live),
WithReadinessProbes(ready.GetProbeFunction()), WithReadinessStatefulProbes(ready),
) )
srv := httptest.NewServer(kp) srv := httptest.NewServer(kp)
@ -65,8 +65,8 @@ func TestKubeprobes(t *testing.T) {
test.livenessProbeTransformation(t, live) test.livenessProbeTransformation(t, live)
test.readinessProbeTransformation(t, ready) test.readinessProbeTransformation(t, ready)
liveStatus := getStatusFromEndpoint(t, client, srv.URL+"/live") liveStatus := getStatusFromEndpoint(t, client, srv.URL+defaultLivenessPath)
readyStatus := getStatusFromEndpoint(t, client, srv.URL+"/ready") readyStatus := getStatusFromEndpoint(t, client, srv.URL+defaultReadinessPath)
otherStatus := getStatusFromEndpoint(t, client, srv.URL+"/something") otherStatus := getStatusFromEndpoint(t, client, srv.URL+"/something")
if liveStatus != test.expectedLiveStatus { if liveStatus != test.expectedLiveStatus {

View File

@ -2,11 +2,6 @@ package kubeprobes
import "sync" import "sync"
// ProbeFunction is a function that determines whether
// the given metric may be marked as correctly functioning.
// It not, the error should be returned.
type ProbeFunction func() error
type statusQuery struct { type statusQuery struct {
allGreen bool allGreen bool
mux sync.Mutex mux sync.Mutex

View File

@ -5,13 +5,13 @@ import (
"sync" "sync"
) )
var errProbeDown = errors.New("DOWN") var errProbeDown = errors.New("probe is down")
// StatefulProbe represents the simple probe that can be either // StatefulProbe represents the simple probe that can be either
// marked as "up" (healthy) or "down" (unhealthy). // marked as "up" (healthy) or "down" (unhealthy).
type StatefulProbe struct { type StatefulProbe struct {
status bool
mux sync.Mutex mux sync.Mutex
status bool
} }
// NewStatefulProbe returns a new instance of a stateful probe // NewStatefulProbe returns a new instance of a stateful probe
@ -19,19 +19,19 @@ type StatefulProbe struct {
// The probe is initially marked as "down". // The probe is initially marked as "down".
func NewStatefulProbe() *StatefulProbe { func NewStatefulProbe() *StatefulProbe {
return &StatefulProbe{ return &StatefulProbe{
status: false,
mux: sync.Mutex{}, mux: sync.Mutex{},
status: false,
} }
} }
// MarkAsUp marks the probe as healthy // MarkAsUp marks the probe as healthy.
func (sp *StatefulProbe) MarkAsUp() { func (sp *StatefulProbe) MarkAsUp() {
sp.mux.Lock() sp.mux.Lock()
defer sp.mux.Unlock() defer sp.mux.Unlock()
sp.status = true sp.status = true
} }
// MarkAsDown marks the probe as unhealthy // MarkAsDown marks the probe as unhealthy.
func (sp *StatefulProbe) MarkAsDown() { func (sp *StatefulProbe) MarkAsDown() {
sp.mux.Lock() sp.mux.Lock()
defer sp.mux.Unlock() defer sp.mux.Unlock()

23
types.go Normal file
View File

@ -0,0 +1,23 @@
package kubeprobes
import "net/http"
// Kubeprobes represents liveness & readiness probes handler.
type Kubeprobes interface {
http.Handler
// GetLivenessHandler returns [http.Handler] for liveness probes.
GetLivenessHandler() http.Handler
// GetReadinessHandler returns [http.Handler] for readiness probes.
GetReadinessHandler() http.Handler
}
// Option represents a [Kubeprobes] constructor option.
type Option interface {
apply(kp *kubeprobes)
}
// ProbeFunction is a function that determines whether
// the given metric may be marked as correctly functioning.
// It not, the error should be returned.
type ProbeFunction func() error