Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker healthcheck #136

Merged
merged 6 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ backend/public/
backend/Cold-Friendly-Feud
backend/famf.db
test-results/
backend/.cache/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ WORKDIR /src
RUN npm install

FROM base AS dev
RUN apk add curl
COPY --from=builder /src/node_modules/ /src/node_modules/
COPY . /src/
WORKDIR /src
Expand Down
7 changes: 5 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ WORKDIR /src

FROM base as dev
ENV CGO_ENABLED 1
RUN apk add gcc musl-dev sqlite
ENV GOCACHE=/src/.cache/go-build
ENV GOMODCACHE=/src/.cache/go-mod
RUN apk add gcc musl-dev sqlite curl
RUN go install github.com/air-verse/air@latest
COPY --from=games . /src/games/
COPY . .
RUN go mod download
RUN mkdir -p .cache/go-build .cache/go-mod && \
go mod download
CMD ["air", "--build.cmd", "go build .", "--build.bin", "/src/Cold-Friendly-Feud"]

FROM base AS builder
Expand Down
94 changes: 94 additions & 0 deletions backend/api/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package api

import (
"fmt"
"time"

"github.com/gorilla/websocket"
)

type HealthStatus struct {
Status string `json:"status"`
Details HealthDetails `json:"details"`
}

type HealthDetails struct {
WebSocket ComponentStatus `json:"websocket"`
Database ComponentStatus `json:"database"`
}

type ComponentStatus struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp,omitempty"`
Error string `json:"error,omitempty"`
}

func HealthTest(port string) (HealthStatus, error) {
status := HealthStatus{
Status: "up",
Details: HealthDetails{
WebSocket: checkWebSocket(port),
Database: checkDatabase(),
},
}

if status.Details.WebSocket.Status == "down" || status.Details.Database.Status == "down" {
status.Status = "down"
return status, fmt.Errorf("one or more components are down")
}

return status, nil
}

func checkWebSocket(port string) ComponentStatus {
status := ComponentStatus{
Status: "up",
Timestamp: time.Now().UTC(),
}

url := fmt.Sprintf("ws://localhost%s/api/ws", port)

// Set a shorter timeout for health checks
dialer := websocket.Dialer{
HandshakeTimeout: 5 * time.Second,
}

// Attempt connection
conn, _, err := dialer.Dial(url, nil)
if err != nil {
status.Status = "down"
status.Error = fmt.Sprintf("websocket connection failed: %v", err)
return status
}
defer conn.Close()

// Ping and wait for pong to verify connection is working
err = conn.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
status.Status = "down"
status.Error = fmt.Sprintf("failed to send ping: %v", err)
}

return status
}

func checkDatabase() ComponentStatus {
status := ComponentStatus{
Status: "up",
Timestamp: time.Now().UTC(),
}

if store == nil {
status.Status = "down"
status.Error = "store not initialized"
return status
}

rooms := store.currentRooms()
if rooms == nil {
status.Status = "down"
status.Error = "failed to query rooms"
}

return status
}
16 changes: 13 additions & 3 deletions backend/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"flag"
"log"
"net/http"
Expand Down Expand Up @@ -39,9 +40,18 @@ func main() {
api.ServeWs(httpWriter, httpRequest)
})

http.HandleFunc("/api/healthcheckz", func(httpWriter http.ResponseWriter, httpRequest *http.Request) {
httpWriter.Write([]byte("ok"))
httpWriter.WriteHeader(200)
http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

status, err := api.HealthTest(cfg.addr)

if err != nil {
w.WriteHeader(503)
} else {
w.WriteHeader(200)
}

json.NewEncoder(w).Encode(status)
})

http.HandleFunc("/api/rooms/{roomCode}/logo", func(httpWriter http.ResponseWriter, httpRequest *http.Request) {
Expand Down
4 changes: 2 additions & 2 deletions chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ spec:
{{- end }}
livenessProbe:
httpGet:
path: /api/healthcheckz
path: /api/health
port: bhttp
readinessProbe:
httpGet:
path: /api/healthcheckz
path: /api/health
port: bhttp
{{- with .Values.backend.volumeMounts }}
volumeMounts:
Expand Down
29 changes: 26 additions & 3 deletions docker/docker-compose-dev-wsl.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
services:
frontend:
image: ${docker_registry}/famf-web:dev
network_mode: "host"
network_mode: 'host'
volumes:
- ../:/src
environment:
- HOST=0.0.0.0
healthcheck:
# Verifies that the frontend dev server is responding on port 3000
# Start period is longer due to npm install
test: ['CMD', 'curl', '-f', 'http://localhost:3000']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
backend:
image: ${docker_registry}/famf-server:dev
network_mode: "host"
network_mode: 'host'
environment:
- HOST=0.0.0.0
- GAME_STORE=${game_store}
volumes:
- ../backend:/src
- ../games/:/src/games/
# Verifies dedicated backend health endpoint returns 200
# Start period is longer due to go build
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/api/health']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
proxy:
image: nginx:1.27-alpine
network_mode: "host"
network_mode: 'host'
volumes:
- ./nginx/nginx.wsl.conf:/etc/nginx/nginx.conf
- ../dev/cert/:/etc/nginx/cert/
healthcheck:
# -k flag allows self-signed certificates in development
test: ['CMD', 'curl', '-f', '-k', 'https://localhost:443']
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
23 changes: 23 additions & 0 deletions docker/docker-compose-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ services:
- ../:/src
ports:
- 3000:3000
healthcheck:
# Verifies that the frontend dev server is responding on port 3000
# Start period is longer due to npm install
test: ['CMD', 'curl', '-f', 'http://localhost:3000']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
backend:
image: ${docker_registry}/famf-server:dev
ports:
Expand All @@ -15,10 +23,25 @@ services:
volumes:
- ../backend:/src
- ../games/:/src/games/
# Verifies dedicated backend health endpoint returns 200
# Start period is longer due to go build
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/api/health']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
proxy:
image: nginx:1.27-alpine
ports:
- 443:443
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ../dev/cert/:/etc/nginx/cert/
healthcheck:
# -k flag allows self-signed certificates in development
test: ['CMD', 'curl', '-f', '-k', 'https://localhost:443']
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
34 changes: 32 additions & 2 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
services:
frontend:
image: ${docker_registry}/famf-web:latest
image: ${docker_registry}/famf-web:dev
ports:
- 3000:3000
healthcheck:
# Verifies that the frontend dev server is responding on port 3000
# Start period is longer due to npm install
test: ['CMD', 'curl', '-f', 'http://localhost:3000']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
backend:
image: ${docker_registry}/famf-server:latest
image: ${docker_registry}/famf-server:dev
ports:
- 8080:8080
environment:
# One of memory, sqlite
GAME_STORE: ${game_store}
# Verifies dedicated backend health endpoint returns 200
# Start period is longer due to go build
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/api/health']
interval: 5s
timeout: 3s
retries: 3
start_period: 60s
proxy:
image: nginx:1.27-alpine
ports:
- 443:443
healthcheck:
# -k flag allows self-signed certificates in development
test: ['CMD', 'curl', '-f', '-k', 'https://localhost:443']
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
18 changes: 9 additions & 9 deletions e2e/playwright-dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ module.exports = defineConfig({
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
Expand Down
18 changes: 9 additions & 9 deletions e2e/playwright-prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ module.exports = defineConfig({
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
Expand Down
27 changes: 18 additions & 9 deletions e2e/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const { defineConfig, devices } = require('@playwright/test');
*/
module.exports = defineConfig({
testDir: './tests',
/* 60s timeout (initial build takes 60s) */
timeout: 60000,
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
Expand Down Expand Up @@ -44,19 +46,26 @@ module.exports = defineConfig({
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: 'docker',
testMatch: /global\.setup\.ts/,
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['docker'],
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// dependencies: ['docker'],
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// dependencies: ['docker'],
// },

/* Test against mobile viewports. */
// {
Expand Down
Loading