From 16f683a360306458fff34e357b0b72ac4ab04fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Sun, 4 Jan 2026 15:14:55 +0100 Subject: [PATCH] Add Docker, Kubernetes configurations, and CI workflows for deployment. Integrate Gin server for API, WebSocket support, and static file hosting. Refactor WebSocket gateway to use Gin router. --- .dockerignore | 35 ++++ .github/workflows/cleanup.yaml | 68 ++++++++ .github/workflows/deploy.yaml | 128 ++++++++++++++ .gitignore | 8 - Dockerfile | 45 +++++ README.md | 296 +++++++++++++++++++++++++++++++++ cmd/server/gin_server.go | 88 ++++++++++ cmd/server/main.go | 10 +- k8s/app.yaml | 72 ++++++++ k8s/hpa.yaml | 24 +++ k8s/ingress.yaml | 25 +++ k8s/pvc.yaml | 11 ++ k8s/redis.yaml | 75 +++++++++ 13 files changed, 872 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/cleanup.yaml create mode 100644 .github/workflows/deploy.yaml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cmd/server/gin_server.go create mode 100644 k8s/app.yaml create mode 100644 k8s/hpa.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/pvc.yaml create mode 100644 k8s/redis.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c1944c0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib +server +client + +# Test files +*.test +*.out + +# Development +.git/ +.github/ +.vscode/ +.idea/ + +# Documentation +*.md +!README.md + +# Temporary +tmp/ +temp/ +*.tmp +*.log + +# OS +.DS_Store +Thumbs.db + +# Dependencies (werden neu gebaut) +vendor/ diff --git a/.github/workflows/cleanup.yaml b/.github/workflows/cleanup.yaml new file mode 100644 index 0000000..cbb3122 --- /dev/null +++ b/.github/workflows/cleanup.yaml @@ -0,0 +1,68 @@ +name: Cleanup Environment +on: [delete] + +jobs: + cleanup: + runs-on: ubuntu-latest + # Nur ausführen, wenn ein Branch gelöscht wurde (keine Tags) + if: github.event.ref_type == 'branch' + + steps: + # 1. Variablen berechnen (MIT FIX FÜR REFS/HEADS & MAIN-CHECK) + - name: Prepare Variables + run: | + # Repo Name klein (z.B. "it232abschied") + REPO_LOWER=$(echo "${{ gitea.repository }}" | cut -d'/' -f2 | tr '[:upper:]' '[:lower:]') + + # Branch Name aus Event (z.B. "refs/heads/feature-x") + RAW_REF="${{ github.event.ref }}" + # "refs/heads/" entfernen + BRANCH_CLEAN=${RAW_REF#refs/heads/} + # Kleinschreiben & Sonderzeichen + BRANCH_LOWER=$(echo "$BRANCH_CLEAN" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g') + + # Logik synchron zum Deploy: + # Main/Master -> Namespace ist nur der Repo-Name + # Anderes -> Namespace ist Repo-Branch + if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then + TARGET_NS="${REPO_LOWER}" + IS_MAIN="true" + else + TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}" + IS_MAIN="false" + fi + + echo "DEBUG: Clean Branch: $BRANCH_LOWER" + echo "DEBUG: Target NS: $TARGET_NS" + + echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV + echo "IS_MAIN=$IS_MAIN" >> $GITHUB_ENV + + # 2. Sicherheits-Check: Niemals Main/Master löschen! + # Wir prüfen jetzt die Variable IS_MAIN, statt den Namen hart zu codieren + - name: Protect Main + if: env.IS_MAIN == 'true' + run: | + echo "❌ ABBRUCH: Der Produktions-Namespace ${{ env.TARGET_NS }} darf nicht gelöscht werden!" + exit 1 + + # 3. Kubectl einrichten + - name: Setup Kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + mkdir -p $HOME/.kube + echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config + chmod 600 $HOME/.kube/config + + # Der Trick für interne Kommunikation + sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config + + # 4. Namespace löschen + - name: Delete Namespace + run: | + echo "🗑️ Lösche Namespace: ${{ env.TARGET_NS }}" + # Wir löschen den Namespace ohne zu warten (async), das geht schneller + kubectl delete namespace ${{ env.TARGET_NS }} --ignore-not-found --wait=false \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..60f2f85 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,128 @@ +name: Dynamic Branch Deploy +on: [push] + +env: + REGISTRY: git.zb-server.de + # WICHTIG: Deine echte Haupt-Domain + BASE_DOMAIN: escape-from-school.de + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + # 1. Code auschecken + - name: Checkout Code + uses: actions/checkout@v3 + + # 2. Variablen vorbereiten (MIT HAUPT-DOMAIN LOGIK) + - name: Prepare Environment Variables + id: prep + run: | + # 1. Repo und Branch Namen säubern + # Voller Pfad für Docker Image (z.B. "user/escape-teacher") + FULL_IMAGE_PATH=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]') + + # Nur der Projektname für K8s (z.B. "escape-teacher") + REPO_LOWER=$(echo "$FULL_IMAGE_PATH" | cut -d'/' -f2) + + # Branch Name säubern (Sonderzeichen zu Bindestrichen) + BRANCH_LOWER=$(echo "${{ gitea.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g') + + # 2. Logik: Ist es der Haupt-Branch? + if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then + # PRODUKTION: + # URL ist direkt die Domain (ohne Subdomain) + APP_URL="${{ env.BASE_DOMAIN }}" + # Namespace ist nur der Projektname (ohne Branch-Suffix) + TARGET_NS="${REPO_LOWER}" + echo "Mode: PRODUCTION (Root Domain)" + else + # ENTWICKLUNG: + # URL ist repo-branch.domain.de + APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}" + # Namespace ist repo-branch + TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}" + echo "Mode: DEVELOPMENT (Subdomain)" + fi + + # Image Tag (Commit Hash) + IMAGE_TAG="${{ gitea.sha }}" + + # Debug Ausgabe + echo "DEBUG: Branch: $BRANCH_LOWER" + echo "DEBUG: Namespace: $TARGET_NS" + echo "DEBUG: URL: $APP_URL" + + # In Gitea Actions Environment schreiben + echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV + echo "REPO_NAME=$REPO_LOWER" >> $GITHUB_ENV + echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV + echo "APP_URL=$APP_URL" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + + # 3. Kaniko Build + - name: Build and Push with Kaniko + uses: aevea/action-kaniko@v0.12.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.PACKAGE_TOKEN }} + image: ${{ env.FULL_IMAGE_PATH }} + tag: ${{ env.IMAGE_TAG }} + cache: true + extra_args: --skip-tls-verify-pull --insecure + + # 4. Setup Kubectl (Interner Trick) + - name: Setup Kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + mkdir -p $HOME/.kube + echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config + chmod 600 $HOME/.kube/config + + # Internal DNS Trick (für Kommunikation innerhalb des Clusters) + sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config + + # 5. Deploy to Kubernetes + - name: Deploy to Kubernetes + run: | + # Namespace erstellen (falls nicht existiert) + kubectl create namespace ${{ env.TARGET_NS }} --dry-run=client -o yaml | kubectl apply -f - + + # Vollen Image Pfad bauen + FULL_IMAGE_URL="${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.IMAGE_TAG }}" + + # 1. Ingress anpassen (Hier wird die URL eingesetzt!) + sed -i "s|\${APP_URL}|${{ env.APP_URL }}|g" k8s/ingress.yaml + + # 2. App Deployment anpassen (Image) + sed -i "s|\${IMAGE_NAME}|$FULL_IMAGE_URL|g" k8s/app.yaml + + # Anwenden + echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}" + kubectl apply -f k8s/pvc.yaml -n ${{ env.TARGET_NS }} + kubectl apply -f k8s/redis.yaml -n ${{ env.TARGET_NS }} + kubectl apply -f k8s/app.yaml -n ${{ env.TARGET_NS }} + kubectl apply -f k8s/ingress.yaml -n ${{ env.TARGET_NS }} + + # HPA (Autoscaling) nur für Main/Master Branch aktivieren + # Wir vergleichen den Namespace mit dem Repo-Namen + # Wenn Namespace == RepoName, dann sind wir im Main Branch + if [ "${{ env.TARGET_NS }}" == "${{ env.REPO_NAME }}" ]; then + echo "Main Branch detected: Applying HPA (Autoscaling)..." + kubectl apply -f k8s/hpa.yaml -n ${{ env.TARGET_NS }} + else + echo "Feature Branch: Skipping HPA." + # Optional: HPA löschen, falls es versehentlich da ist + kubectl delete hpa escape-game-hpa -n ${{ env.TARGET_NS }} --ignore-not-found + fi + + # Force Update (damit das neue Image sicher geladen wird) + kubectl rollout restart deployment/escape-game -n ${{ env.TARGET_NS }} + + # 6. Summary + - name: Summary + run: echo "🚀 Deployed successfully to https://${{ env.APP_URL }}" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 882e71b..d015e92 100644 --- a/.gitignore +++ b/.gitignore @@ -51,14 +51,6 @@ Thumbs.db .env config.local.json -# Generated assets (if you want to regenerate them) -# Uncomment if assets should be generated, not committed -# /cmd/client/web/assets/*.png -# /cmd/client/web/assets/assets.json - -# Keep chunks but ignore generated ones if needed -# /cmd/client/web/assets/chunks/*.json - # Debug files debug __debug_bin diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6ae9d31 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# Stage 1: Builder +FROM golang:1.25.5-alpine AS builder + +# Build-Dependencies +RUN apk add --no-cache git + +WORKDIR /app + +# Dependencies cachen +COPY go.mod go.sum ./ +RUN go mod download + +# Source Code kopieren +COPY . . + +# Server binary bauen +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o server ./cmd/server + +# WASM Client bauen +RUN GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client + +# Stage 2: Production Image +FROM alpine:latest + +RUN apk --no-cache add ca-certificates curl tzdata && \ + addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +WORKDIR /app + +# Binary und Web-Dateien vom Builder kopieren +COPY --from=builder /app/server . +COPY --from=builder /app/cmd/client/web ./cmd/client/web + +# User wechseln +USER appuser + +# Port für HTTP/WebSocket +EXPOSE 8080 + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +CMD ["./server"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a87adb --- /dev/null +++ b/README.md @@ -0,0 +1,296 @@ +# 🏃 Escape From Teacher + +Ein Endless-Runner-Spiel entwickelt als Schulprojekt. Renne vor dem Lehrer davon, sammle Münzen, nutze Power-Ups und klettere an Wänden! + +![License](https://img.shields.io/badge/license-Proprietary-red) +![Go Version](https://img.shields.io/badge/go-1.21+-blue.svg) +![Platform](https://img.shields.io/badge/platform-Web%20%7C%20Desktop-green) + +## 📋 Inhaltsverzeichnis + +- [Features](#-features) +- [Technologie-Stack](#-technologie-stack) +- [Installation](#-installation) +- [Spielmodi](#-spielmodi) +- [Entwicklung](#-entwicklung) +- [Architektur](#-architektur) +- [Tools](#-tools) +- [Lizenz](#-lizenz) + +## ✨ Features + +### Gameplay +- 🏃 **Endless Runner**: Unendliches Spiel mit prozedural generierten Levels +- 🧗 **Wall Climbing**: Klettere an Wänden hoch für neue Strategien +- 💰 **Coins & Power-Ups**: Sammle Münzen und nutze Power-Ups wie Double Jump und Godmode +- 🎯 **Hindernisse**: Weiche bewegenden Plattformen und Objekten aus +- ⚡ **Dynamische Schwierigkeit**: Das Spiel wird mit der Zeit schneller + +### Modi +- 👤 **Solo-Modus**: Spiele alleine und sammle Highscores +- 👥 **Coop-Modus**: Spiele mit Freunden im Team + - Team-Namen für Leaderboard + - Multiplizierter Score basierend auf lebenden Spielern + - Host-Controls für Lobby-Management + +### Features +- 🏆 **Leaderboard**: Redis-basiertes globales Leaderboard mit Proof-Codes +- 🎮 **Cross-Platform**: Web (WASM) und Desktop (Native) +- 🎨 **Level Editor**: Erstelle eigene Levels mit dem integrierten Level Builder +- 🔊 **Audio**: Musik und Sound-Effekte mit Lautstärkeregelung +- 📱 **Mobile Support**: Touch-Controls für mobile Geräte + +## 🛠 Technologie-Stack + +### Backend +- **Go 1.25+**: Server und Game-Logik +- **NATS**: Message Broker für Echtzeit-Kommunikation (Legacy) +- **WebSocket**: Direkte WebSocket-Verbindungen für niedrige Latenz +- **Redis**: Persistentes Leaderboard mit Proof-Code-Validierung + +### Frontend +- **Go + Ebiten**: Desktop-Client (Native) +- **Go + WASM**: Web-Client kompiliert zu WebAssembly +- **HTML/CSS/JavaScript**: Web-UI und Menüs + +### Architektur +- **Client-Side Prediction**: Flüssiges Gameplay trotz Netzwerk-Latenz +- **Server-Authoritative**: Server validiert alle Spielaktionen +- **Deterministische Physik**: Gleiche Physik auf Client und Server + +## 📦 Installation + +### Voraussetzungen + +```bash +# Go 1.25 oder höher +go version + +# Redis für Leaderboard +redis-server --version +``` + +### Repository klonen + +```bash +git clone https://git.zb-server.de/ZB-Server/EscapeFromTeacher.git +cd EscapeFromTeacher +``` + +### Assets generieren + +Die Assets müssen aus den Raw-Dateien generiert werden: + +```bash +# Asset-Builder starten (GUI) +go run ./cmd/builder + +# Im GUI: Assets laden, anpassen und speichern +# Dies erstellt cmd/client/web/assets/assets.json und die PNG-Dateien +``` + +### Server starten + +```bash +# Redis starten (Terminal 1) +redis-server + +# Server starten (Terminal 2) +go run ./cmd/server +``` + +Server läuft auf `http://localhost:8080` + +### Clients starten + +#### Web-Client (WASM) + +```bash +# WASM kompilieren +GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client + +# Web-Server starten (z.B. mit Python) +cd cmd/client/web +python3 -m http.server 8000 + +# Browser öffnen: http://localhost:8000 +``` + +#### Desktop-Client (Native) + +```bash +go run ./cmd/client +``` + +## 🎮 Spielmodi + +### Solo-Modus +- Starte direkt aus dem Hauptmenü +- Automatischer Room-Erstellen +- Score basiert auf zurückgelegter Distanz +- Eigener Highscore im Leaderboard + +### Coop-Modus +1. **Raum erstellen** (Host): + - "RAUM ERSTELLEN" klicken + - Team-Namen festlegen (optional) + - Raum-Code mit Freunden teilen + +2. **Raum beitreten**: + - Raum-Code eingeben + - "RAUM BEITRETEN" klicken + +3. **Gameplay**: + - Host startet das Spiel wenn alle bereit sind + - Team-Score = Distanz × Anzahl lebender Spieler + - Beispiel: 100 Tiles mit 3 Spielern = 300 Punkte pro Spieler + +### Steuerung + +**Desktop:** +- `Leertaste` / `W` / `↑`: Springen +- `A` / `←`: Links bewegen +- `D` / `→`: Rechts bewegen +- `S` / `↓`: Schneller fallen + +**Mobile:** +- Touch-Buttons auf dem Bildschirm + +**Wall Climbing:** +- Laufe gegen eine Wand +- Halte Richtungstaste gedrückt zum Hochklettern +- Lasse los zum Herunterrutschen + +## 🔧 Entwicklung + +### Projekt-Struktur + +``` +EscapeFromTeacher/ +├── cmd/ +│ ├── server/ # Game Server +│ ├── client/ # Desktop & WASM Client +│ │ └── web/ # Web Assets & HTML +│ ├── builder/ # Asset Builder Tool +│ └── levelbuilder/ # Level Editor +├── pkg/ +│ ├── game/ # Gemeinsame Game-Logik +│ ├── server/ # Server-spezifische Logik +│ └── config/ # Konfiguration +└── assets_raw/ # Rohe Asset-Dateien +``` + +### Neue Features entwickeln + +1. **Server-Logik** in `pkg/server/` +2. **Game-Logik** (gemeinsam) in `pkg/game/` +3. **Client-Rendering** in `cmd/client/` +4. **Web-UI** in `cmd/client/web/` + +### Build-Befehle + +```bash +# Server +go build -o server ./cmd/server + +# Desktop-Client +go build -o client ./cmd/client + +# WASM-Client +GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client + +# Level Builder +go build -o levelbuilder ./cmd/levelbuilder +``` + +## 🧰 Tools + +### Level Builder + +Erstelle eigene Levels mit dem visuellen Editor: + +```bash +go run ./cmd/levelbuilder +``` + +**Features:** +- Drag & Drop von Assets +- Bewegende Plattformen konfigurieren +- Chunk-basiertes Level-Design +- JSON-Export in `cmd/client/web/assets/chunks/` + +### Asset Builder + +Konvertiere Raw-Assets zu optimierten Spiel-Assets: + +```bash +go run ./cmd/builder +``` + +**Features:** +- PNG-Kompression und -Skalierung +- Hitbox-Editor +- Asset-Metadaten (Typ, Offsets, etc.) +- Generiert `assets.json` + +## 📐 Architektur + +### Client-Server Kommunikation + +``` +Client Server + | | + |------ WebSocket ------->| + | (Input Events) | + | | + |<----- Broadcast --------| + | (Game State) | + | | + |-- Score Submission ---->| + | | + |<--- Proof Code ---------| +``` + +### Game Loop + +**Server (60 FPS):** +1. Verarbeite Inputs von allen Clients +2. Update Physik (Gravity, Kollision, etc.) +3. Update Map (Chunk spawning/despawning) +4. Broadcast Game State an alle Clients + +**Client:** +1. Sende lokale Inputs an Server +2. Empfange Game State vom Server +3. Render Game State +4. (Optional) Client-Side Prediction für lokalen Spieler + +### Score-System + +**Solo:** +- Score = Zurückgelegte Distanz (in Tiles) +- Coins: +200 Punkte + +**Coop:** +- Distanz-Score akkumuliert pro Tick +- Pro Tick: `Score += Anzahl lebender Spieler` +- Beispiel: 3 Spieler → +3 Punkte/Tick (180 Punkte/Sekunde) +- Coins werden geteilt: Alle Spieler bekommen +200 + +## 📄 Lizenz + +Dies ist ein **Schulprojekt**. Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt. Alle Rechte liegen bei den Urhebern. + +**Projektleitung & Code:** Sebastian Unterschütz +**Musik & Sound Design:** Max E. + +## 🙏 Credits + +- **Ebiten**: Go 2D Game Engine +- **NATS**: Message Broker +- **Redis**: In-Memory Database +- **Go WASM**: WebAssembly Support + +--- + +**Entwickelt als Abschlussprojekt für IT232 (2025/2026)** diff --git a/cmd/server/gin_server.go b/cmd/server/gin_server.go new file mode 100644 index 0000000..a1eb54b --- /dev/null +++ b/cmd/server/gin_server.go @@ -0,0 +1,88 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/nats-io/nats.go" +) + +// SetupGinServer erstellt und konfiguriert den Gin-Server +func SetupGinServer(ec *nats.EncodedConn, port string) *gin.Engine { + // Production mode für bessere Performance + gin.SetMode(gin.ReleaseMode) + + r := gin.Default() + + // Logging Middleware (bereits in gin.Default()) + // Custom Logger für bessere Übersicht + r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return "🌐 " + param.TimeStamp.Format("2006-01-02 15:04:05") + + " | " + param.Method + + " | " + string(rune(param.StatusCode)) + + " | " + param.Latency.String() + + " | " + param.ClientIP + + " | " + param.Path + "\n" + })) + + // Recovery Middleware + r.Use(gin.Recovery()) + + // Health Check Endpoint + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + "service": "EscapeFromTeacher", + "version": "1.0.0", + }) + }) + + // Metrics Endpoint (optional für Kubernetes) + r.GET("/metrics", func(c *gin.Context) { + mu.RLock() + roomCount := len(rooms) + playerCount := len(playerSessions) + mu.RUnlock() + + c.JSON(200, gin.H{ + "rooms": roomCount, + "players": playerCount, + }) + }) + + // WebSocket Endpoint + r.GET("/ws", func(c *gin.Context) { + handleWebSocketGin(c.Writer, c.Request, ec) + }) + + // Static Files - Serve Web Client + r.Static("/assets", "./cmd/client/web/assets") + r.StaticFile("/", "./cmd/client/web/index.html") + r.StaticFile("/index.html", "./cmd/client/web/index.html") + r.StaticFile("/game.js", "./cmd/client/web/game.js") + r.StaticFile("/style.css", "./cmd/client/web/style.css") + r.StaticFile("/wasm_exec.js", "./cmd/client/web/wasm_exec.js") + r.StaticFile("/main.wasm", "./cmd/client/web/main.wasm") + r.StaticFile("/background.jpg", "./cmd/client/web/background.jpg") + + // 404 Handler + r.NoRoute(func(c *gin.Context) { + c.JSON(404, gin.H{ + "error": "Route not found", + }) + }) + + log.Printf("🚀 Gin-Server konfiguriert auf Port %s", port) + log.Printf("📁 Statische Dateien: ./cmd/client/web/") + log.Printf("🌐 WebSocket Endpoint: /ws") + log.Printf("❤️ Health Check: /health") + + return r +} + +// handleWebSocketGin verwaltet WebSocket-Verbindungen über Gin +func handleWebSocketGin(w http.ResponseWriter, r *http.Request, ec *nats.EncodedConn) { + // Verwende die handleWebSocket Funktion aus websocket_gateway.go + handleWebSocket(w, r, ec) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 927d763..d66ee4b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -172,11 +172,11 @@ func main() { log.Println("✅ Server bereit. Warte auf Spieler...") - // 9. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients) - go StartWebSocketGateway("8080", ec) - - // Block forever - select {} + // 9. GIN-SERVER STARTEN (statische Dateien + WebSocket) + router := SetupGinServer(ec, "8080") + if err := router.Run(":8080"); err != nil { + log.Fatal("❌ Gin-Server Fehler:", err) + } } func loadServerAssets(w *game.World) { diff --git a/k8s/app.yaml b/k8s/app.yaml new file mode 100644 index 0000000..b5e4695 --- /dev/null +++ b/k8s/app.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: escape-game + labels: + app: escape-game +spec: + replicas: 2 + selector: + matchLabels: + app: escape-game + template: + metadata: + labels: + app: escape-game + spec: + containers: + - name: server + image: ${IMAGE_NAME} + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + resources: + requests: + memory: "128Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "1000m" + volumeMounts: + - name: assets + mountPath: /root/cmd/client/web/assets + volumes: + - name: assets + persistentVolumeClaim: + claimName: game-assets-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: escape-game + labels: + app: escape-game +spec: + type: ClusterIP + selector: + app: escape-game + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 3600 diff --git a/k8s/hpa.yaml b/k8s/hpa.yaml new file mode 100644 index 0000000..0f1a583 --- /dev/null +++ b/k8s/hpa.yaml @@ -0,0 +1,24 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: escape-game-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: escape-game + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..49294ec --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: web, websecure + traefik.ingress.kubernetes.io/router.middlewares: gitea-redirect-https@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - ${APP_URL} + secretName: game-tls-secret + rules: + - host: ${APP_URL} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: escape-game + port: + number: 80 \ No newline at end of file diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..a21793d --- /dev/null +++ b/k8s/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: game-assets-pvc +spec: + accessModes: + - ReadWriteMany + storageClassName: longhorn + resources: + requests: + storage: 2Gi \ No newline at end of file diff --git a/k8s/redis.yaml b/k8s/redis.yaml new file mode 100644 index 0000000..e82ebcf --- /dev/null +++ b/k8s/redis.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-data +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + ports: + - port: 6379 + selector: + app: redis +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + labels: + app: redis +spec: + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + initContainers: + - name: fix-permissions + image: busybox + command: ["sh", "-c", "chown -R 999:999 /data"] + volumeMounts: + - name: data + mountPath: /data + containers: + - name: redis + image: redis:alpine + ports: + - containerPort: 6379 + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "64Mi" + cpu: "50m" # 0.05 CPU Cores + limits: + memory: "256Mi" + cpu: "1000m" # 0.5 CPU Cores + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: redis-data \ No newline at end of file