-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(merge): merge docker changes from #136
- Loading branch information
1 parent
a3d10fd
commit b28a2ba
Showing
9 changed files
with
217 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ backend/public/ | |
backend/Cold-Friendly-Feud | ||
backend/famf.db | ||
test-results/ | ||
backend/.cache/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { test as setup } from '@playwright/test'; | ||
import { exec } from 'child_process'; | ||
import { promisify } from 'util'; | ||
|
||
const execAsync = promisify(exec); | ||
|
||
setup('docker', async () => { | ||
const maxRetries = 60; | ||
const waitSeconds = 1; | ||
|
||
for (let i = 0; i < maxRetries; i++) { | ||
try { | ||
// Get status of all running containers | ||
const { stdout } = await execAsync('docker ps --format "{{.Names}} {{.Status}}"'); | ||
|
||
// Regex patterns to extract container name and health status | ||
// "famf-backend-1 Up 4 minutes (healthy)" | ||
const nameRegex = /^[^\s]+/; // "famf-backend-1" | ||
const statusRegex = /(healthy|unhealthy|starting)/i; // "healthy" | ||
|
||
// Boolean filter to exclude empty strings | ||
const containers = stdout.split('\n').filter(Boolean).map((container => { | ||
const nameMatch = container.match(nameRegex); | ||
const statusMatch = container.match(statusRegex); | ||
|
||
if (!nameMatch?.[0] || !statusMatch?.[1]) { | ||
throw new Error('Failed to parse container info'); | ||
} | ||
|
||
const name = nameMatch[0]; | ||
const status = statusMatch[1]; | ||
|
||
return { | ||
name, status | ||
} | ||
})) | ||
|
||
|
||
// Check if all required containers are healthy | ||
// These container names must be substrings of the actual container names | ||
// e.g. 'frontend' will match 'famf-frontend-1' | ||
const requiredContainers = ['backend', 'frontend', 'proxy']; | ||
const containerStatuses = requiredContainers.map(name => ({ | ||
name, | ||
isHealthy: containers.find(c => c.name.includes(name))?.status === 'healthy' ?? false | ||
})); | ||
|
||
const allHealthy = containerStatuses.every(c => c.isHealthy); | ||
if (allHealthy) { | ||
return; | ||
} | ||
|
||
console.log(`Waiting for containers to be healthy ${i + 1}/${maxRetries}`); | ||
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); | ||
} catch (err) { | ||
console.log(`Error checking container health ${i + 1}/${maxRetries}:`, err); | ||
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); | ||
} | ||
} | ||
|
||
throw new Error('containers failed to become healthy'); | ||
}); |