From d33e9f19ea353cd320d584ad093c311f66eb1b28 Mon Sep 17 00:00:00 2001 From: Piotr Icikowski Date: Fri, 1 Mar 2024 23:28:31 +0100 Subject: [PATCH 1/3] refactor(kubeprobes): refactor code - refactored types, interfaces, options etc. - added new options dedicated for `StatefulProbe`s --- costants.go | 7 ++ options.go | 67 +++++++++++++++ probes.go | 136 ++++++++++++++++++++----------- probes_test.go | 10 +-- commons.go => query.go | 5 -- commons_test.go => query_test.go | 0 stateful_probe.go | 10 +-- types.go | 18 ++++ 8 files changed, 191 insertions(+), 62 deletions(-) create mode 100644 costants.go create mode 100644 options.go rename commons.go => query.go (77%) rename commons_test.go => query_test.go (100%) create mode 100644 types.go diff --git a/costants.go b/costants.go new file mode 100644 index 0000000..9f784a6 --- /dev/null +++ b/costants.go @@ -0,0 +1,7 @@ +package kubeprobes + +const ( + defaultLivenessPath string = "/live" + defaultReadinessPath string = "/ready" + verboseOutputFlag string = "v" +) diff --git a/options.go b/options.go new file mode 100644 index 0000000..e277038 --- /dev/null +++ b/options.go @@ -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 + }, + } +} diff --git a/probes.go b/probes.go index 5948ba6..974f380 100644 --- a/probes.go +++ b/probes.go @@ -1,6 +1,8 @@ package kubeprobes import ( + "errors" + "fmt" "net/http" "strings" ) @@ -8,57 +10,97 @@ import ( type kubeprobes struct { livenessProbes []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 New(options ...Option) (Kubeprobes, error) { + kp := &kubeprobes{ + livenessProbes: []ProbeFunction{}, + 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) + if sq.isAllGreen() { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } +} + +func (kp *kubeprobes) handleReadiness(w http.ResponseWriter, _ *http.Request) { + sq := newStatusQuery(append(kp.livenessProbes, kp.readinessProbes...)) + if sq.isAllGreen() { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } +} + +// ServeHTTP implements Kubeprobes. func (kp *kubeprobes) ServeHTTP(w http.ResponseWriter, r *http.Request) { - subs := strings.Split(strings.TrimSuffix(r.URL.Path, "/"), "/") - switch subs[len(subs)-1] { - case "live": - sq := newStatusQuery(kp.livenessProbes) - if sq.isAllGreen() { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusServiceUnavailable) - } - case "ready": - sq := newStatusQuery(append(kp.livenessProbes, kp.readinessProbes...)) - if sq.isAllGreen() { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusServiceUnavailable) - } + switch r.URL.Path { + case kp.pathLive: + kp.handleLiveness(w, r) + case kp.pathReady: + kp.handleReadiness(w, r) default: 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...) - } -} diff --git a/probes_test.go b/probes_test.go index 9139480..059c288 100644 --- a/probes_test.go +++ b/probes_test.go @@ -50,9 +50,9 @@ func TestKubeprobes(t *testing.T) { }, } - kp := New( - WithLivenessProbes(live.GetProbeFunction()), - WithReadinessProbes(ready.GetProbeFunction()), + kp, _ := New( + WithLivenessStatefulProbes(live), + WithReadinessStatefulProbes(ready), ) srv := httptest.NewServer(kp) @@ -65,8 +65,8 @@ func TestKubeprobes(t *testing.T) { test.livenessProbeTransformation(t, live) test.readinessProbeTransformation(t, ready) - liveStatus := getStatusFromEndpoint(t, client, srv.URL+"/live") - readyStatus := getStatusFromEndpoint(t, client, srv.URL+"/ready") + liveStatus := getStatusFromEndpoint(t, client, srv.URL+defaultLivenessPath) + readyStatus := getStatusFromEndpoint(t, client, srv.URL+defaultReadinessPath) otherStatus := getStatusFromEndpoint(t, client, srv.URL+"/something") if liveStatus != test.expectedLiveStatus { diff --git a/commons.go b/query.go similarity index 77% rename from commons.go rename to query.go index ef0b136..d4050e1 100644 --- a/commons.go +++ b/query.go @@ -2,11 +2,6 @@ package kubeprobes 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 { allGreen bool mux sync.Mutex diff --git a/commons_test.go b/query_test.go similarity index 100% rename from commons_test.go rename to query_test.go diff --git a/stateful_probe.go b/stateful_probe.go index fba59b7..e78475c 100644 --- a/stateful_probe.go +++ b/stateful_probe.go @@ -5,13 +5,13 @@ import ( "sync" ) -var errProbeDown = errors.New("DOWN") +var errProbeDown = errors.New("probe is down") // StatefulProbe represents the simple probe that can be either // marked as "up" (healthy) or "down" (unhealthy). type StatefulProbe struct { - status bool mux sync.Mutex + status bool } // NewStatefulProbe returns a new instance of a stateful probe @@ -19,19 +19,19 @@ type StatefulProbe struct { // The probe is initially marked as "down". func NewStatefulProbe() *StatefulProbe { return &StatefulProbe{ - status: false, mux: sync.Mutex{}, + status: false, } } -// MarkAsUp marks the probe as healthy +// MarkAsUp marks the probe as healthy. func (sp *StatefulProbe) MarkAsUp() { sp.mux.Lock() defer sp.mux.Unlock() sp.status = true } -// MarkAsDown marks the probe as unhealthy +// MarkAsDown marks the probe as unhealthy. func (sp *StatefulProbe) MarkAsDown() { sp.mux.Lock() defer sp.mux.Unlock() diff --git a/types.go b/types.go new file mode 100644 index 0000000..bd0b7ea --- /dev/null +++ b/types.go @@ -0,0 +1,18 @@ +package kubeprobes + +import "net/http" + +// Kubeprobes represents liveness & readiness probes handler. +type Kubeprobes interface { + 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 -- 2.43.0 From bb108ad9baf7a14c7e39709a58987706a713d4ce Mon Sep 17 00:00:00 2001 From: Piotr Icikowski Date: Fri, 1 Mar 2024 23:38:04 +0100 Subject: [PATCH 2/3] feat(handlers): add individual handlers fetching --- probes.go | 10 ++++++++++ types.go | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/probes.go b/probes.go index 974f380..e21dd89 100644 --- a/probes.go +++ b/probes.go @@ -93,6 +93,16 @@ func (kp *kubeprobes) handleReadiness(w http.ResponseWriter, _ *http.Request) { } } +// 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 { diff --git a/types.go b/types.go index bd0b7ea..c976a1a 100644 --- a/types.go +++ b/types.go @@ -5,6 +5,11 @@ 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. -- 2.43.0 From d6beb81938a99fcc6003a64b30c66d8b86a91ba1 Mon Sep 17 00:00:00 2001 From: Piotr Icikowski Date: Fri, 1 Mar 2024 23:43:53 +0100 Subject: [PATCH 3/3] docs(kubeprobes): update README --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4650d02..750af72 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,23 @@ go get -u pkg.icikowski.pl/kubeprobes ## 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: - `/live` - endpoint for liveness 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.WithReadinessProbes(/* ... */)` - adds particular [probes](#probes) to the list of readiness probes. +- `kubeprobes.WithLivenessProbes(...)` - adds particular [probe functions](#probe-functions) to the list of liveness 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 @@ -47,7 +51,7 @@ someOtherProbe := func() error { } // Use functions in probes handler -kp := kubeprobes.New( +kp, _ := kubeprobes.New( kubeprobes.WithLivenessProbes(someOtherProbe), kubeprobes.WithReadinessProbes(someProbe), ) @@ -63,24 +67,52 @@ someProbe := kubeprobes.NewStatefulProbe() someOtherProbe := kubeprobes.NewStatefulProbe() // Use it in probes handler -kp := kubeprobes.New( - kubeprobes.WithLivenessProbes(someProbe.GetProbeFunction()), - kubeprobes.WithReadinessProbes(someOtherProbe.GetProbeFunction()), +kp, _ := kubeprobes.New( + kubeprobes.WithLivenessStatefulProbes(someProbe), + 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 ```go +// Create probe functions +appProbe := func() error { + // Some logic for checking app status + return nil +} + // Create stateful probes live := kubeprobes.NewStatefulProbe() ready := kubeprobes.NewStatefulProbe() // Prepare handler -kp := kubeprobes.New( - kubeprobes.WithLivenessProbes(live.GetProbeFunction()), - kubeprobes.WithReadinessProbes(ready.GetProbeFunction()), +kp, err := kubeprobes.New( + kubeprobes.WithLivenessStatefulProbes(live), + 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 probes := &http.Server{ -- 2.43.0