Skip to content

Commit

Permalink
chore(merge): merge docker changes from #136
Browse files Browse the repository at this point in the history
  • Loading branch information
karlromets committed Jan 5, 2025
1 parent a3d10fd commit b28a2ba
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 9 deletions.
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
}
15 changes: 15 additions & 0 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 @@ -44,6 +45,20 @@ func main() {
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) {
roomCode := httpRequest.PathValue("roomCode")
api.FetchLogo(httpWriter, roomCode)
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
9 changes: 9 additions & 0 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 @@ -43,19 +45,26 @@ module.exports = defineConfig({

/* Configure projects for major browsers */
projects: [
{
name: 'docker',
testMatch: /global\.setup\.ts/,
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['docker'],
},

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

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

/* Test against mobile viewports. */
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/admin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ test('can hide game board from player', async ({ browser }) => {

const adminPage = new AdminPage(host.page);
await adminPage.gameSelector.selectOption({ index: 1 });
await adminPage.startRoundOneButton.click();
expect(buzzerPage1.playerBlindFoldedText).not.toBeVisible();
await adminPage.player0Team1HideGameButton.click();
expect(buzzerPage1.playerBlindFoldedText).toBeVisible();
await adminPage.startRoundOneButton.click({ timeout: 2000 });
await expect(buzzerPage1.playerBlindFoldedText).not.toBeVisible({ timeout: 2000 });
await adminPage.player0Team1HideGameButton.click({ timeout: 2000 });
await expect(buzzerPage1.playerBlindFoldedText).toBeVisible({ timeout: 2000 });
});
62 changes: 62 additions & 0 deletions e2e/tests/global.setup.ts
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');
});

0 comments on commit b28a2ba

Please sign in to comment.