Skip to content

Commit 84ba26b

Browse files
committed
keepalive
1 parent 5a9bf9b commit 84ba26b

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM golang:1.21.12
2+
3+
WORKDIR /usr/src/app
4+
5+
# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
6+
COPY go.mod ./
7+
8+
COPY . .
9+
RUN go build -v -o /usr/bin/k8s-keepalive ./...
10+
11+
EXPOSE 5000
12+
13+
CMD ["/usr/bin/k8s-keepalive"]

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/mike-callahan/k8s-keepalive
2+
3+
go 1.21.12

k8s-keepalive.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"log"
7+
"net/http"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
)
13+
14+
func main() {
15+
log.Println("Starting keepalive service...")
16+
handler := http.HandlerFunc(HTTPProbe)
17+
server := &http.Server{
18+
Addr: ":5000",
19+
Handler: handler,
20+
}
21+
22+
go func() {
23+
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
24+
log.Fatalf("HTTP server error: %v", err)
25+
}
26+
log.Println("Stopped serving new connections.")
27+
}()
28+
29+
sigChan := make(chan os.Signal, 1)
30+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
31+
<-sigChan
32+
33+
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
34+
defer shutdownRelease()
35+
36+
if err := server.Shutdown(shutdownCtx); err != nil {
37+
log.Fatalf("HTTP shutdown error: %v", err)
38+
}
39+
log.Println("Graceful shutdown complete.")
40+
}

web.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
// Check if the status code passed by the user is valid
12+
func IsValidHTTPStatusCode(code int) bool {
13+
return code >= 100 && code <= 599
14+
}
15+
16+
// Respond to k8s health probes
17+
// Additional functionality of allowing user to pass arbitrary status codes for testing
18+
func HTTPProbe(w http.ResponseWriter, r *http.Request) {
19+
// Error if the request isn't Get or Post
20+
if r.Method != http.MethodGet && r.Method != http.MethodPost {
21+
w.WriteHeader(http.StatusBadRequest)
22+
fmt.Fprintln(w, "Only GET and POST requests are allowed")
23+
return
24+
}
25+
26+
// Send 200 for /
27+
path := strings.TrimPrefix(r.URL.Path, "/")
28+
if path == "" || path == "healthz" {
29+
// Default response
30+
w.WriteHeader(http.StatusOK)
31+
fmt.Fprintln(w, "Hello! Pass a status code in the URL path (e.g., /200 or /404)")
32+
return
33+
}
34+
35+
statusCode, _ := strconv.Atoi(path)
36+
_, err := strconv.Atoi(path)
37+
38+
// Error if string can't be converted to integer
39+
if err != nil {
40+
w.WriteHeader(http.StatusBadRequest)
41+
fmt.Fprintln(w, "That is not a valid status code or integer.")
42+
return
43+
}
44+
45+
// Error if integer is out of validity range
46+
err = nil
47+
if IsValidHTTPStatusCode(statusCode) == false {
48+
err = errors.New("That integer is not a valid status code")
49+
}
50+
51+
if err != nil {
52+
w.WriteHeader(http.StatusBadRequest)
53+
fmt.Fprintln(w, fmt.Errorf("Error processing data: %w", err))
54+
return
55+
}
56+
57+
// Write value back to header and body
58+
w.WriteHeader(statusCode)
59+
fmt.Fprintf(w, "%d", statusCode)
60+
}

web_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
)
8+
9+
func TestHTTPProbe(t *testing.T) {
10+
t.Run("Return default status", func(t *testing.T) {
11+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
12+
response := httptest.NewRecorder()
13+
14+
HTTPProbe(response, request)
15+
16+
got := response.Result().StatusCode
17+
want := 200
18+
19+
if got != want {
20+
t.Errorf("got %q, want %q", got, want)
21+
}
22+
23+
})
24+
t.Run("Return status 200", func(t *testing.T) {
25+
request, _ := http.NewRequest(http.MethodGet, "/200", nil)
26+
response := httptest.NewRecorder()
27+
28+
HTTPProbe(response, request)
29+
30+
got := response.Result().StatusCode
31+
want := 200
32+
33+
if got != want {
34+
t.Errorf("got %q, want %q", got, want)
35+
}
36+
37+
})
38+
39+
t.Run("Return status 404", func(t *testing.T) {
40+
request, _ := http.NewRequest(http.MethodGet, "/404", nil)
41+
response := httptest.NewRecorder()
42+
43+
HTTPProbe(response, request)
44+
45+
got := response.Result().StatusCode
46+
want := 404
47+
48+
if got != want {
49+
t.Errorf("got %q, want %q", got, want)
50+
}
51+
52+
})
53+
54+
t.Run("Return status non-status", func(t *testing.T) {
55+
request, _ := http.NewRequest(http.MethodGet, "/asdf", nil)
56+
response := httptest.NewRecorder()
57+
58+
HTTPProbe(response, request)
59+
60+
got := response.Result().StatusCode
61+
want := 400
62+
63+
if got != want {
64+
t.Errorf("got %d, want %d", got, want)
65+
}
66+
67+
})
68+
69+
t.Run("PATCH method", func(t *testing.T) {
70+
request, _ := http.NewRequest(http.MethodPatch, "/asdf", nil)
71+
response := httptest.NewRecorder()
72+
73+
HTTPProbe(response, request)
74+
75+
got := response.Result().StatusCode
76+
want := 400
77+
78+
if got != want {
79+
t.Errorf("got %d, want %d", got, want)
80+
}
81+
82+
})
83+
}

0 commit comments

Comments
 (0)