diff --git a/kubeprobes.go b/kubeprobes.go index 61804ff..0fb5990 100644 --- a/kubeprobes.go +++ b/kubeprobes.go @@ -1,147 +1,147 @@ -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 []Probe - readinessProbes []Probe - - 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: []Probe{}, - readinessProbes: []Probe{}, - 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) - } -} +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 []Probe + readinessProbes []Probe + + 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: []Probe{}, + readinessProbes: []Probe{}, + 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) + } +} diff --git a/kubeprobes_test.go b/kubeprobes_test.go index 9e7318b..33e7408 100644 --- a/kubeprobes_test.go +++ b/kubeprobes_test.go @@ -1,182 +1,182 @@ -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 TestValidation(t *testing.T) { - var ( - live, _ = NewManualProbe("live") - ready, _ = NewManualProbe("ready") - ) - - tests := map[string]struct { - opts []Option - expectedError bool - }{ - "no modifications and no error": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - }, - }, - "modifications and no error": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithLivenessPath("/livez"), - WithReadinessPath("/readyz"), - }, - }, - "missing liveness probes": { - opts: []Option{ - WithReadinessProbes(ready), - }, - expectedError: true, - }, - "missing readiness probes": { - opts: []Option{ - WithLivenessProbes(live), - }, - expectedError: true, - }, - "liveness probe path empty": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithLivenessPath(""), - }, - expectedError: true, - }, - "readiness probe path empty": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithReadinessPath(""), - }, - expectedError: true, - }, - "liveness probe path does not start with slash": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithLivenessPath("livez"), - }, - expectedError: true, - }, - "readiness probe path does not start with slash": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithReadinessPath("readyz"), - }, - expectedError: true, - }, - "liveness and readiness probe paths are equal": { - opts: []Option{ - WithLivenessProbes(live), - WithReadinessProbes(ready), - WithLivenessPath("/check"), - WithReadinessPath("/check"), - }, - expectedError: true, - }, - } - - for name, tc := range tests { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - _, err := New(tc.opts...) - switch { - case err == nil && tc.expectedError: - t.Error("expected error, but no error was returned") - case err != nil && !tc.expectedError: - t.Errorf("expected no error but got %v", err) - } - }) - } -} - -func TestHandler(t *testing.T) { - var ( - live, _ = NewManualProbe("live") - ready, _ = NewManualProbe("ready") - ) - - tests := map[string]struct { - livenessProbeTransformation func(*testing.T, ManualProbe) - readinessProbeTransformation func(*testing.T, ManualProbe) - 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, err := New( - WithLivenessProbes(live), - WithReadinessProbes(ready), - ) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - 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+defaultLivenessPath) - readyStatus := getStatusFromEndpoint(t, client, srv.URL+defaultReadinessPath) - 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) - } - }) - } -} +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 TestValidation(t *testing.T) { + var ( + live, _ = NewManualProbe("live") + ready, _ = NewManualProbe("ready") + ) + + tests := map[string]struct { + opts []Option + expectedError bool + }{ + "no modifications and no error": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + }, + }, + "modifications and no error": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithLivenessPath("/livez"), + WithReadinessPath("/readyz"), + }, + }, + "missing liveness probes": { + opts: []Option{ + WithReadinessProbes(ready), + }, + expectedError: true, + }, + "missing readiness probes": { + opts: []Option{ + WithLivenessProbes(live), + }, + expectedError: true, + }, + "liveness probe path empty": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithLivenessPath(""), + }, + expectedError: true, + }, + "readiness probe path empty": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithReadinessPath(""), + }, + expectedError: true, + }, + "liveness probe path does not start with slash": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithLivenessPath("livez"), + }, + expectedError: true, + }, + "readiness probe path does not start with slash": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithReadinessPath("readyz"), + }, + expectedError: true, + }, + "liveness and readiness probe paths are equal": { + opts: []Option{ + WithLivenessProbes(live), + WithReadinessProbes(ready), + WithLivenessPath("/check"), + WithReadinessPath("/check"), + }, + expectedError: true, + }, + } + + for name, tc := range tests { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + _, err := New(tc.opts...) + switch { + case err == nil && tc.expectedError: + t.Error("expected error, but no error was returned") + case err != nil && !tc.expectedError: + t.Errorf("expected no error but got %v", err) + } + }) + } +} + +func TestHandler(t *testing.T) { + var ( + live, _ = NewManualProbe("live") + ready, _ = NewManualProbe("ready") + ) + + tests := map[string]struct { + livenessProbeTransformation func(*testing.T, ManualProbe) + readinessProbeTransformation func(*testing.T, ManualProbe) + 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, err := New( + WithLivenessProbes(live), + WithReadinessProbes(ready), + ) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + 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+defaultLivenessPath) + readyStatus := getStatusFromEndpoint(t, client, srv.URL+defaultReadinessPath) + 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) + } + }) + } +}