feat(pkg): add initial source code
This commit is contained in:
parent
b86b58ec88
commit
a5627a4f41
44
commons.go
Normal file
44
commons.go
Normal file
@ -0,0 +1,44 @@
|
||||
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
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (sq *statusQuery) isAllGreen() bool {
|
||||
sq.wg.Wait()
|
||||
sq.mux.Lock()
|
||||
defer sq.mux.Unlock()
|
||||
return sq.allGreen
|
||||
}
|
||||
|
||||
func newStatusQuery(probes []ProbeFunction) *statusQuery {
|
||||
sq := &statusQuery{
|
||||
allGreen: true,
|
||||
mux: sync.Mutex{},
|
||||
wg: sync.WaitGroup{},
|
||||
}
|
||||
|
||||
sq.wg.Add(len(probes))
|
||||
for _, probe := range probes {
|
||||
probe := probe
|
||||
go func() {
|
||||
defer sq.wg.Done()
|
||||
if err := probe(); err != nil {
|
||||
sq.mux.Lock()
|
||||
sq.allGreen = false
|
||||
sq.mux.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return sq
|
||||
}
|
45
commons_test.go
Normal file
45
commons_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package kubeprobes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStatusQueryIsAllGreen(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
probes []ProbeFunction
|
||||
expectedStatus bool
|
||||
}{
|
||||
"all green": {
|
||||
probes: []ProbeFunction{
|
||||
func() error { return nil },
|
||||
func() error { time.Sleep(2 * time.Second); return nil },
|
||||
},
|
||||
expectedStatus: true,
|
||||
},
|
||||
"some failed": {
|
||||
probes: []ProbeFunction{
|
||||
func() error { return nil },
|
||||
func() error { time.Sleep(2 * time.Second); return errors.New("failed") },
|
||||
},
|
||||
expectedStatus: false,
|
||||
},
|
||||
"all failed": {
|
||||
probes: []ProbeFunction{
|
||||
func() error { return errors.New("failed") },
|
||||
func() error { time.Sleep(2 * time.Second); return errors.New("failed") },
|
||||
},
|
||||
expectedStatus: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
sq := newStatusQuery(test.probes)
|
||||
if sq.isAllGreen() != test.expectedStatus {
|
||||
t.Errorf("expected status %v, got %v", test.expectedStatus, sq.isAllGreen())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
62
probes.go
Normal file
62
probes.go
Normal file
@ -0,0 +1,62 @@
|
||||
package kubeprobes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type kubeprobes struct {
|
||||
livenessProbes []ProbeFunction
|
||||
readinessProbes []ProbeFunction
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler interface
|
||||
func (kp *kubeprobes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
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)
|
||||
}
|
||||
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...)
|
||||
}
|
||||
}
|
83
probes_test.go
Normal file
83
probes_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package kubeprobes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getStatusFromEndpoint(t *testing.T, client *http.Client, endpoint string) int {
|
||||
t.Helper()
|
||||
resp, err := client.Get(endpoint)
|
||||
if err != nil {
|
||||
t.Errorf("error getting status from endpoint: %s", err)
|
||||
}
|
||||
return resp.StatusCode
|
||||
}
|
||||
|
||||
func TestKubeprobes(t *testing.T) {
|
||||
live, ready := NewStatefulProbe(), NewStatefulProbe()
|
||||
|
||||
tests := map[string]struct {
|
||||
livenessProbeTransformation func(*testing.T, *StatefulProbe)
|
||||
readinessProbeTransformation func(*testing.T, *StatefulProbe)
|
||||
expectedLiveStatus int
|
||||
expectedReadyStatus int
|
||||
}{
|
||||
"not live": {
|
||||
livenessProbeTransformation: markAsDown,
|
||||
readinessProbeTransformation: markAsDown,
|
||||
expectedLiveStatus: http.StatusServiceUnavailable,
|
||||
expectedReadyStatus: http.StatusServiceUnavailable,
|
||||
},
|
||||
"live but not ready": {
|
||||
livenessProbeTransformation: markAsUp,
|
||||
readinessProbeTransformation: markAsDown,
|
||||
expectedLiveStatus: http.StatusOK,
|
||||
expectedReadyStatus: http.StatusServiceUnavailable,
|
||||
},
|
||||
"live and ready": {
|
||||
livenessProbeTransformation: markAsUp,
|
||||
readinessProbeTransformation: markAsUp,
|
||||
expectedLiveStatus: http.StatusOK,
|
||||
expectedReadyStatus: http.StatusOK,
|
||||
},
|
||||
"ready but not live - should never happen": {
|
||||
livenessProbeTransformation: markAsDown,
|
||||
readinessProbeTransformation: markAsUp,
|
||||
expectedLiveStatus: http.StatusServiceUnavailable,
|
||||
expectedReadyStatus: http.StatusServiceUnavailable,
|
||||
},
|
||||
}
|
||||
|
||||
kp := New(
|
||||
WithLivenessProbes(live.GetProbeFunction()),
|
||||
WithReadinessProbes(ready.GetProbeFunction()),
|
||||
)
|
||||
|
||||
srv := httptest.NewServer(kp)
|
||||
defer srv.Close()
|
||||
client := srv.Client()
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(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")
|
||||
otherStatus := getStatusFromEndpoint(t, client, srv.URL+"/something")
|
||||
|
||||
if liveStatus != test.expectedLiveStatus {
|
||||
t.Errorf("expected live status %d, got %d", test.expectedLiveStatus, liveStatus)
|
||||
}
|
||||
if readyStatus != test.expectedReadyStatus {
|
||||
t.Errorf("expected ready status %d, got %d", test.expectedReadyStatus, readyStatus)
|
||||
}
|
||||
if otherStatus != http.StatusNotFound {
|
||||
t.Errorf("expected 404 status, got %d", otherStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
52
stateful_probe.go
Normal file
52
stateful_probe.go
Normal file
@ -0,0 +1,52 @@
|
||||
package kubeprobes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var errProbeDown = errors.New("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
|
||||
}
|
||||
|
||||
// NewStatefulProbe returns a new instance of a stateful probe
|
||||
// which can be either marked as "up" (healthy) or "down" (unhealthy).
|
||||
// The probe is initially marked as "down".
|
||||
func NewStatefulProbe() *StatefulProbe {
|
||||
return &StatefulProbe{
|
||||
status: false,
|
||||
mux: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (sp *StatefulProbe) MarkAsDown() {
|
||||
sp.mux.Lock()
|
||||
defer sp.mux.Unlock()
|
||||
sp.status = false
|
||||
}
|
||||
|
||||
// GetProbeFunction returns a function that can be used to check
|
||||
// whether the probe is healthy or not.
|
||||
func (sp *StatefulProbe) GetProbeFunction() ProbeFunction {
|
||||
return func() error {
|
||||
sp.mux.Lock()
|
||||
defer sp.mux.Unlock()
|
||||
if sp.status {
|
||||
return nil
|
||||
}
|
||||
return errProbeDown
|
||||
}
|
||||
}
|
42
stateful_probe_test.go
Normal file
42
stateful_probe_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package kubeprobes
|
||||
|
||||
import "testing"
|
||||
|
||||
var (
|
||||
markAsDown func(*testing.T, *StatefulProbe) = func(t *testing.T, sp *StatefulProbe) {
|
||||
t.Helper()
|
||||
sp.MarkAsDown()
|
||||
}
|
||||
markAsUp func(*testing.T, *StatefulProbe) = func(t *testing.T, sp *StatefulProbe) {
|
||||
t.Helper()
|
||||
sp.MarkAsUp()
|
||||
}
|
||||
)
|
||||
|
||||
func TestStatefulProbe(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
probeTransformation func(*testing.T, *StatefulProbe)
|
||||
expectedError bool
|
||||
}{
|
||||
"mark as up": {
|
||||
probeTransformation: markAsUp,
|
||||
expectedError: false,
|
||||
},
|
||||
"mark as down": {
|
||||
probeTransformation: markAsDown,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
sp := NewStatefulProbe()
|
||||
test.probeTransformation(t, sp)
|
||||
probeFunc := sp.GetProbeFunction()
|
||||
if (probeFunc() != nil) != test.expectedError {
|
||||
t.Error("result not as expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user