From 2b26cfbbc9b9a74eee54c07a99205e52c40014d5 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 5 Mar 2025 12:37:37 -0800 Subject: [PATCH] update aws deployment, have client get env from server --- src/client/ClientGameRunner.ts | 9 ++-- src/client/HostLobbyModal.ts | 29 ++++++------ src/client/JoinPrivateLobbyModal.ts | 10 ++-- src/client/LocalServer.ts | 9 +--- src/client/Main.ts | 3 ++ src/core/GameRunner.ts | 2 +- src/core/configuration/Config.ts | 58 ++++++++++++++++------- src/core/configuration/DefaultConfig.ts | 2 +- src/core/configuration/DevConfig.ts | 3 ++ src/core/configuration/PreprodConfig.ts | 3 ++ src/server/Master.ts | 19 +++++++- src/server/Worker.ts | 4 +- update-deploy-aws.sh | 11 +---- update.sh | 62 ++++++++++++++++++++++--- webpack.config.js | 1 + 15 files changed, 159 insertions(+), 66 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 985060473..66b5e8f4e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -18,12 +18,13 @@ import { } from "../core/game/GameUpdates"; import { WorkerClient } from "../core/worker/WorkerClient"; import { consolex, initRemoteSender } from "../core/Consolex"; -import { getConfig, getServerConfig } from "../core/configuration/Config"; +import { getConfig, ServerConfig } from "../core/configuration/Config"; import { GameView, PlayerView } from "../core/game/GameView"; import { GameUpdateViewData } from "../core/game/GameUpdates"; import { UserSettings } from "../core/game/UserSettings"; export interface LobbyConfig { + serverConfig: ServerConfig; flag: () => string; playerName: () => string; clientID: ClientID; @@ -51,8 +52,6 @@ export function joinLobby( `joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`, ); - const serverConfig = getServerConfig(); - const userSettings: UserSettings = new UserSettings(); let gameConfig: GameConfig = null; if (lobbyConfig.gameType == GameType.Singleplayer) { @@ -72,7 +71,7 @@ export function joinLobby( lobbyConfig, gameConfig, eventBus, - serverConfig, + lobbyConfig.serverConfig, ); const onconnect = () => { @@ -106,7 +105,7 @@ export async function createClientGame( transport: Transport, userSettings: UserSettings, ): Promise { - const config = getConfig(gameConfig, userSettings); + const config = await getConfig(gameConfig, userSettings); const gameMap = await loadTerrainMap(gameConfig.gameMap); const worker = new WorkerClient( diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index d69f0cd29..aad7ff26a 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -7,7 +7,10 @@ import "./components/Difficulties"; import { DifficultyDescription } from "./components/Difficulties"; import "./components/Maps"; import { generateID } from "../core/Util"; -import { getConfig, getServerConfig } from "../core/configuration/Config"; +import { + getConfig, + getServerConfigFromClient, +} from "../core/configuration/Config"; @customElement("host-lobby-modal") export class HostLobbyModal extends LitElement { @@ -584,8 +587,9 @@ export class HostLobbyModal extends LitElement { } private async putGameConfig() { + const config = await getServerConfigFromClient(); const response = await fetch( - `${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`, + `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}`, { method: "PUT", headers: { @@ -609,8 +613,9 @@ export class HostLobbyModal extends LitElement { `Starting private game with map: ${GameMapType[this.selectedMap]}`, ); this.close(); + const config = await getServerConfigFromClient(); const response = await fetch( - `${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/start_game/${this.lobbyId}`, + `${window.location.origin}/${config.workerPath(this.lobbyId)}/start_game/${this.lobbyId}`, { method: "POST", headers: { @@ -636,15 +641,13 @@ export class HostLobbyModal extends LitElement { } private async pollPlayers() { - fetch( - `/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + const config = await getServerConfigFromClient(); + fetch(`/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", }, - ) + }) .then((response) => response.json()) .then((data: GameInfo) => { console.log(`got response: ${data}`); @@ -654,11 +657,11 @@ export class HostLobbyModal extends LitElement { } async function createLobby(): Promise { - const serverConfig = getServerConfig(); + const config = await getServerConfigFromClient(); try { const id = generateID(); const response = await fetch( - `/${serverConfig.workerPath(id)}/create_game/${id}`, + `/${config.workerPath(id)}/create_game/${id}`, { method: "POST", headers: { diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index 6beb3ae75..32c276bfd 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -1,9 +1,9 @@ import { LitElement, css, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; -import { getServerConfig } from "../core/configuration/Config"; import { consolex } from "../core/Consolex"; import { GameMapType, GameType } from "../core/game/Game"; import { GameInfo } from "../core/Schemas"; +import { getServerConfigFromClient } from "../core/configuration/Config"; @customElement("join-private-lobby-modal") export class JoinPrivateLobbyModal extends LitElement { @@ -355,12 +355,13 @@ export class JoinPrivateLobbyModal extends LitElement { } } - private joinLobby() { + private async joinLobby() { const lobbyId = this.lobbyIdInput.value; consolex.log(`Joining lobby with ID: ${lobbyId}`); this.message = "Checking lobby..."; // Set initial message - const url = `/${getServerConfig().workerPath(lobbyId)}/game/${lobbyId}/exists`; + const config = await getServerConfigFromClient(); + const url = `/${config.workerPath(lobbyId)}/game/${lobbyId}/exists`; fetch(url, { method: "GET", headers: { @@ -398,9 +399,10 @@ export class JoinPrivateLobbyModal extends LitElement { private async pollPlayers() { if (!this.lobbyIdInput?.value) return; + const config = await getServerConfigFromClient(); fetch( - `/${getServerConfig().workerPath(this.lobbyIdInput.value)}/game/${this.lobbyIdInput.value}`, + `/${config.workerPath(this.lobbyIdInput.value)}/game/${this.lobbyIdInput.value}`, { method: "GET", headers: { diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 282e916af..099d6ca7c 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,9 +1,4 @@ -import { - Config, - GameEnv, - getServerConfig, - ServerConfig, -} from "../core/configuration/Config"; +import { Config, GameEnv, ServerConfig } from "../core/configuration/Config"; import { consolex } from "../core/Consolex"; import { GameEvent } from "../core/EventBus"; import { @@ -130,7 +125,7 @@ export class LocalServer { const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], { type: "application/json", }); - const workerPath = getServerConfig().workerPath(this.lobbyConfig.gameID); + const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID); navigator.sendBeacon(`/${workerPath}/archive_singleplayer_game`, blob); } } diff --git a/src/client/Main.ts b/src/client/Main.ts index 4ada098b9..ec033b8bb 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -19,6 +19,7 @@ import "./DarkModeButton"; import { DarkModeButton } from "./DarkModeButton"; import { HelpModal } from "./HelpModal"; import { GameType } from "../core/game/Game"; +import { getServerConfigFromClient } from "../core/configuration/Config"; class Client { private gameStop: () => void; @@ -135,9 +136,11 @@ class Client { consolex.log("joining lobby, stopping existing game"); this.gameStop(); } + const config = await getServerConfigFromClient(); const gameType = event.detail.gameType; this.gameStop = joinLobby( { + serverConfig: config, gameType: gameType, flag: (): string => this.flagInput.getCurrentFlag(), playerName: (): string => this.usernameInput.getCurrentUsername(), diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 56c0e7e65..944f7bef4 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -32,7 +32,7 @@ export async function createGameRunner( clientID: ClientID, callBack: (gu: GameUpdateViewData) => void, ): Promise { - const config = getConfig(gameConfig, null); + const config = await getConfig(gameConfig, null); const gameMap = await loadGameMap(gameConfig.gameMap); const game = createGame( gameMap.gameMap, diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index ad70b48ec..32da46374 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -22,21 +22,24 @@ import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { UserSettings } from "../game/UserSettings"; +let cachedSC: ServerConfig = null; + export enum GameEnv { Dev, Preprod, Prod, } -export function getConfig( + +export async function getConfig( gameConfig: GameConfig, userSettings: UserSettings | null = null, -): Config { - const sc = getServerConfig(); - switch (process.env.GAME_ENV) { - case "dev": +): Promise { + const sc = await getServerConfigFromClient(); + switch (sc.env()) { + case GameEnv.Dev: return new DevConfig(sc, gameConfig, userSettings); - case "preprod": - case "prod": + case GameEnv.Preprod: + case GameEnv.Prod: consolex.log("using prod config"); return new DefaultConfig(sc, gameConfig, userSettings); default: @@ -44,20 +47,43 @@ export function getConfig( } } -export function getServerConfig(): ServerConfig { - switch (process.env.GAME_ENV) { +export async function getServerConfigFromClient(): Promise { + if (cachedSC) { + return cachedSC; + } + const response = await fetch("/api/env"); + + if (!response.ok) { + throw new Error( + `Failed to fetch server config: ${response.status} ${response.statusText}`, + ); + } + const config = await response.json(); + // Log the retrieved configuration + console.log("Server config loaded:", config); + + cachedSC = getServerConfig(config.game_env); + return cachedSC; +} + +export function getServerConfigFromServer(): ServerConfig { + const gameEnv = process.env.GAME_ENV; + return getServerConfig(gameEnv); +} + +function getServerConfig(gameEnv: string) { + switch (gameEnv) { case "dev": - consolex.log("using dev config"); + consolex.log("using dev server config"); return new DevServerConfig(); - case "preprod": - consolex.log("using preprod config"); + case "staging": + consolex.log("using preprod server config"); return preprodConfig; case "prod": - default: - consolex.log("using prod config"); + consolex.log("using prod server config"); return prodConfig; - // default: - // throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`) + default: + throw Error(`unsupported server configuration: ${gameEnv}`); } } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 4d60b2984..fc4ecc8ba 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -23,7 +23,7 @@ import { pastelThemeDark } from "./PastelThemeDark"; export abstract class DefaultServerConfig implements ServerConfig { numWorkers(): number { - return 2; + return 6; } abstract env(): GameEnv; abstract discordRedirectURI(): string; diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 2bc1a0c15..abeb91ecf 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -15,6 +15,9 @@ export class DevServerConfig extends DefaultServerConfig { discordRedirectURI(): string { return "http://localhost:3000/auth/callback"; } + numWorkers(): number { + return 2; + } } export class DevConfig extends DefaultConfig { diff --git a/src/core/configuration/PreprodConfig.ts b/src/core/configuration/PreprodConfig.ts index 5204ebde8..3b1aa061f 100644 --- a/src/core/configuration/PreprodConfig.ts +++ b/src/core/configuration/PreprodConfig.ts @@ -8,4 +8,7 @@ export const preprodConfig = new (class extends DefaultServerConfig { discordRedirectURI(): string { return "https://openfront.dev/auth/callback"; } + numWorkers(): number { + return 3; + } })(); diff --git a/src/server/Master.ts b/src/server/Master.ts index 9215be323..4c106708e 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -4,14 +4,17 @@ import express from "express"; import { GameMapType, GameType, Difficulty } from "../core/game/Game"; import { generateID } from "../core/Util"; import { PseudoRandom } from "../core/PseudoRandom"; -import { GameEnv, getServerConfig } from "../core/configuration/Config"; +import { + GameEnv, + getServerConfigFromServer, +} from "../core/configuration/Config"; import { GameInfo } from "../core/Schemas"; import path from "path"; import rateLimit from "express-rate-limit"; import { fileURLToPath } from "url"; import { isHighTrafficTime } from "./Util"; -const config = getServerConfig(); +const config = getServerConfigFromServer(); const readyWorkers = new Set(); const app = express(); @@ -121,6 +124,18 @@ export async function startMaster() { }); } +app.get("/api/env", (req, res) => { + const envConfig = { + game_env: process.env.GAME_ENV || "prod", + }; + + res.set("Cache-Control", "no-cache, no-store, must-revalidate"); + res.set("Pragma", "no-cache"); + res.set("Expires", "0"); + + res.json(envConfig); +}); + // Add lobbies endpoint to list public games for this worker app.get("/public_lobbies", (req, res) => { res.send(publicLobbiesJsonStr); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 3b3932b04..5f0d34bd5 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -4,7 +4,7 @@ import { WebSocketServer } from "ws"; import path from "path"; import { fileURLToPath } from "url"; import { GameManager } from "./GameManager"; -import { getServerConfig } from "../core/configuration/Config"; +import { getServerConfigFromServer } from "../core/configuration/Config"; import { WebSocket } from "ws"; import { Client } from "./Client"; import rateLimit from "express-rate-limit"; @@ -14,7 +14,7 @@ import { slog } from "./StructuredLog"; import { GameType } from "../core/game/Game"; import { archive } from "./Archive"; -const config = getServerConfig(); +const config = getServerConfigFromServer(); // Worker setup export function startWorker() { diff --git a/update-deploy-aws.sh b/update-deploy-aws.sh index cc9cde6a3..e09f94c93 100755 --- a/update-deploy-aws.sh +++ b/update-deploy-aws.sh @@ -4,7 +4,6 @@ # 1. Builds and uploads the Docker image to ECR with appropriate tag # 2. Copies the update script to EC2 instance (staging or prod) # 3. Executes the update script on the EC2 instance - set -e # Exit immediately if a command exits with a non-zero status # Function to print section headers @@ -64,8 +63,6 @@ if [ ! -f "$UPDATE_SCRIPT" ]; then exit 1 fi - - # Step 1: Build and upload Docker image to ECR print_header "STEP 1: Building and uploading Docker image to ECR" echo "Environment: ${ENV}" @@ -73,7 +70,6 @@ echo "Using version tag: $VERSION_TAG" # Execute the build script with the version tag $BUILD_SCRIPT $VERSION_TAG - if [ $? -ne 0 ]; then echo "❌ Build and upload failed. Stopping deployment." exit 1 @@ -88,20 +84,17 @@ chmod +x $UPDATE_SCRIPT # Copy the update script to the EC2 instance scp -i $EC2_KEY $UPDATE_SCRIPT $EC2_HOST:$REMOTE_UPDATE_SCRIPT - if [ $? -ne 0 ]; then echo "❌ Failed to copy update script to EC2 instance. Stopping deployment." exit 1 fi - echo "✅ Update script successfully copied to EC2 instance." # Step 3: Execute the update script on the EC2 instance print_header "STEP 3: Executing update script on EC2 instance" -# Make the script executable on the remote server and execute it -ssh -i $EC2_KEY $EC2_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT" - +# Make the script executable on the remote server and execute it with the environment parameter +ssh -i $EC2_KEY $EC2_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT $ENV" if [ $? -ne 0 ]; then echo "❌ Failed to execute update script on EC2 instance." exit 1 diff --git a/update.sh b/update.sh index ca23324c6..6bc22e660 100755 --- a/update.sh +++ b/update.sh @@ -1,10 +1,23 @@ #!/bin/bash # Script to update Docker container +# Check if environment parameter is provided +if [ -z "$1" ]; then + echo "Error: Environment parameter is required (prod or staging)" + echo "Usage: $0 " + exit 1 +fi + +# Set environment from parameter +ENV=$1 +CONTAINER_NAME="openfront-${ENV}" +LOG_GROUP="/aws/ec2/docker-containers/${ENV}" + # Get AWS account ID AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com/openfront:latest" +echo "Deploying to ${ENV} environment..." echo "Logging in to ECR..." aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com @@ -13,29 +26,66 @@ docker pull $ECR_REPO echo "Checking for existing container..." # Check for running container -RUNNING_CONTAINER=$(docker ps | grep openfront | awk '{print $1}') +RUNNING_CONTAINER=$(docker ps | grep ${CONTAINER_NAME} | awk '{print $1}') if [ -n "$RUNNING_CONTAINER" ]; then echo "Stopping running container $RUNNING_CONTAINER..." docker stop $RUNNING_CONTAINER + echo "Waiting for container to fully stop and release resources..." + sleep 5 # Add a 5-second delay docker rm $RUNNING_CONTAINER echo "Container $RUNNING_CONTAINER stopped and removed." fi # Also check for stopped containers with the same name -STOPPED_CONTAINER=$(docker ps -a | grep openfront | awk '{print $1}') +STOPPED_CONTAINER=$(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}') if [ -n "$STOPPED_CONTAINER" ]; then echo "Removing stopped container $STOPPED_CONTAINER..." docker rm $STOPPED_CONTAINER echo "Container $STOPPED_CONTAINER removed." fi -echo "Starting new container..." +# Check if port 80 is still in use +echo "Checking if port 80 is still in use..." +if command -v lsof >/dev/null 2>&1; then + PORT_CHECK=$(lsof -i :80 | grep LISTEN) +elif command -v netstat >/dev/null 2>&1; then + PORT_CHECK=$(netstat -tuln | grep ":80 ") +else + PORT_CHECK="" + echo "Warning: Cannot check if port is in use (neither lsof nor netstat found)" +fi + +if [ -n "$PORT_CHECK" ]; then + echo "Warning: Port 80 is still in use by another process:" + echo "$PORT_CHECK" + echo "Attempting to proceed anyway..." +fi + +echo "Starting new container for ${ENV} environment..." docker run -d -p 80:80 \ --log-driver=awslogs \ --log-opt awslogs-region=eu-west-1 \ - --log-opt awslogs-group=/aws/ec2/docker-containers \ + --log-opt awslogs-group=${LOG_GROUP} \ --log-opt awslogs-create-group=true \ - --name openfront \ + --name ${CONTAINER_NAME} \ $ECR_REPO -echo "Update complete! New container is running." \ No newline at end of file +if [ $? -eq 0 ]; then + echo "Update complete! New ${ENV} container is running." +else + echo "Failed to start container. Trying alternative port 8080..." + docker run -d -p 8080:80 \ + --log-driver=awslogs \ + --log-opt awslogs-region=eu-west-1 \ + --log-opt awslogs-group=${LOG_GROUP} \ + --log-opt awslogs-create-group=true \ + --name ${CONTAINER_NAME} \ + $ECR_REPO + + if [ $? -eq 0 ]; then + echo "Container started on port 8080 instead of 80!" + else + echo "Failed to start container on alternative port as well." + exit 1 + fi +fi \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 709671737..fdf9e79ae 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -186,6 +186,7 @@ export default (env, argv) => { // Original API endpoints { context: [ + "/api/env", "/public_lobbies", "/join_game", "/start_game",