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