diff --git a/Dockerfile b/Dockerfile index 9f472df..e6d63b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,8 @@ COPY web/robots.txt ./web/robots.txt RUN go build -trimpath -mod=readonly -buildvcs=false -ldflags="-s -w" \ -o /out/secret-api ./cmd/server +RUN go build -trimpath -mod=readonly -buildvcs=false -ldflags="-s -w" \ + -o /out/healthcheck ./cmd/healthcheck # runtime FROM gcr.io/distroless/base:nonroot@sha256:746b9dbe3065a124395d4a7698241dbd6f3febbf01b73e48f942aabd7b8e5eac @@ -36,6 +38,7 @@ FROM gcr.io/distroless/base:nonroot@sha256:746b9dbe3065a124395d4a7698241dbd6f3fe WORKDIR /app COPY --from=builder --chown=nonroot:nonroot /out/secret-api /app/secret-api +COPY --from=builder --chown=nonroot:nonroot /out/healthcheck /app/healthcheck COPY --from=builder --chown=nonroot:nonroot /src/web/static /app/web/static COPY --from=builder --chown=nonroot:nonroot /src/web/robots.txt /app/web/robots.txt diff --git a/cmd/healthcheck/main.go b/cmd/healthcheck/main.go new file mode 100644 index 0000000..f216c81 --- /dev/null +++ b/cmd/healthcheck/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "time" +) + +func check(port string) error { + url := fmt.Sprintf("http://localhost:%s/health", port) + + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf( + "unexpected status code: %d", resp.StatusCode, + ) + } + return nil +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + if err := check(port); err != nil { + os.Exit(1) + } +} diff --git a/cmd/healthcheck/main_test.go b/cmd/healthcheck/main_test.go new file mode 100644 index 0000000..9b2d1d0 --- /dev/null +++ b/cmd/healthcheck/main_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "net" + "net/http" + "testing" +) + +func testServer( + t *testing.T, status int, +) string { + t.Helper() + + mux := http.NewServeMux() + mux.HandleFunc("/health", func( + w http.ResponseWriter, r *http.Request, + ) { + w.WriteHeader(status) + }) + + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + } + + srv := &http.Server{Handler: mux} + go func() { _ = srv.Serve(ln) }() + t.Cleanup(func() { srv.Close() }) + + _, port, _ := net.SplitHostPort(ln.Addr().String()) + return port +} + +func TestCheck(t *testing.T) { + t.Run("returns nil when server is healthy", func(t *testing.T) { + port := testServer(t, http.StatusOK) + if err := check(port); err != nil { + t.Fatalf("expected healthy, got error: %v", err) + } + }) + + t.Run("returns error on unhealthy status", func(t *testing.T) { + port := testServer(t, http.StatusServiceUnavailable) + if err := check(port); err == nil { + t.Fatal("expected error for unhealthy status") + } + }) + + t.Run("returns error when no server is running", func(t *testing.T) { + if err := check("0"); err == nil { + t.Fatal("expected error when no server running") + } + }) +} diff --git a/docker-compose.yml b/docker-compose.yml index 79b7b6a..fd4e9c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: ports: - "8080:8080" healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:8080/health/"] + test: ["CMD", "/app/healthcheck"] interval: 30s timeout: 5s retries: 3 diff --git a/web/frontend/App.css b/web/frontend/App.css index f77b7bf..53a0cd8 100644 --- a/web/frontend/App.css +++ b/web/frontend/App.css @@ -15,7 +15,7 @@ color-scheme: light; } -[data-theme="dark"] { +[data-theme='dark'] { --primary-color: #e8e8e8; --secondary-color: #111; --background-color: #111; diff --git a/web/frontend/components/Layout/index.tsx b/web/frontend/components/Layout/index.tsx index 36d0159..a89aae7 100644 --- a/web/frontend/components/Layout/index.tsx +++ b/web/frontend/components/Layout/index.tsx @@ -10,11 +10,21 @@ export function Layout({ children, onToggleTheme }: LayoutProps) { return (
- create - about + + create + + + about + {`secretapi \u00A9 ${new Date().getFullYear()}`}