From 316129b70b10a5f1e605aff3b36db6efd186f8b9 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 10 Mar 2025 17:02:15 -0700 Subject: [PATCH] update to hetzner deployment --- deploy.sh | 4 ++ src/core/configuration/Config.ts | 4 ++ src/core/configuration/DefaultConfig.ts | 10 ++++ src/core/configuration/DevConfig.ts | 3 ++ src/core/configuration/PreprodConfig.ts | 3 ++ src/core/configuration/ProdConfig.ts | 3 ++ src/server/Archive.ts | 65 ++++++++++++++----------- update.sh | 15 +----- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/deploy.sh b/deploy.sh index ca41ddf92..dba9fffc1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -27,6 +27,9 @@ if [ $# -ne 1 ] || ([ "$1" != "staging" ] && [ "$1" != "prod" ]); then exit 1 fi +# TODO: fix this - need to build before creating the image +bun run build-prod + ENV=$1 VERSION_TAG="latest" DOCKER_REPO="" @@ -71,6 +74,7 @@ GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown") echo "Git commit: $GIT_COMMIT" docker buildx build \ + --no-cache \ --platform linux/amd64 \ --build-arg GIT_COMMIT=$GIT_COMMIT \ -t $DOCKER_USERNAME/$DOCKER_REPO:$VERSION_TAG \ diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 77499c1be..b53b29aba 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -100,6 +100,10 @@ export interface ServerConfig { env(): GameEnv; adminToken(): string; adminHeader(): string; + r2Bucket(): string; + r2Endpoint(): string; + r2AccessKey(): string; + r2SecretKey(): string; } export interface Config { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index f3fbdbf11..022fdac65 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -22,6 +22,16 @@ import { pastelTheme } from "./PastelTheme"; import { pastelThemeDark } from "./PastelThemeDark"; export abstract class DefaultServerConfig implements ServerConfig { + r2Endpoint(): string { + return process.env.R2_ENDPOINT; + } + r2AccessKey(): string { + return process.env.R2_ACCESS_KEY; + } + r2SecretKey(): string { + return process.env.R2_SECRET_KEY; + } + abstract r2Bucket(): string; adminHeader(): string { return "x-admin-key"; } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 04fa9bf50..4ff96faae 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -5,6 +5,9 @@ import { GameEnv, ServerConfig } from "./Config"; import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig"; export class DevServerConfig extends DefaultServerConfig { + r2Bucket(): string { + return "openfront-staging"; + } adminToken(): string { return "WARNING_DEV_ADMIN_KEY_DO_NOT_USE_IN_PRODUCTION"; } diff --git a/src/core/configuration/PreprodConfig.ts b/src/core/configuration/PreprodConfig.ts index 3b1aa061f..9340053d4 100644 --- a/src/core/configuration/PreprodConfig.ts +++ b/src/core/configuration/PreprodConfig.ts @@ -2,6 +2,9 @@ import { GameEnv } from "./Config"; import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig"; export const preprodConfig = new (class extends DefaultServerConfig { + r2Bucket(): string { + return "openfront-staging"; + } env(): GameEnv { return GameEnv.Preprod; } diff --git a/src/core/configuration/ProdConfig.ts b/src/core/configuration/ProdConfig.ts index 71b9bd122..8d6860358 100644 --- a/src/core/configuration/ProdConfig.ts +++ b/src/core/configuration/ProdConfig.ts @@ -2,6 +2,9 @@ import { GameEnv } from "./Config"; import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig"; export const prodConfig = new (class extends DefaultServerConfig { + r2Bucket(): string { + return "openfront-prod"; + } numWorkers(): number { return 6; } diff --git a/src/server/Archive.ts b/src/server/Archive.ts index 01094599f..143fc018d 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -1,6 +1,5 @@ import { GameRecord, GameID } from "../core/Schemas"; import { S3 } from "@aws-sdk/client-s3"; -import { RedshiftData } from "@aws-sdk/client-redshift-data"; import { GameEnv, getServerConfigFromServer, @@ -8,22 +7,31 @@ import { const config = getServerConfigFromServer(); -const s3 = new S3({ region: "eu-west-1" }); +// R2 client configuration +const r2 = new S3({ + region: "auto", // R2 ignores region, but it's required by the SDK + endpoint: config.r2Endpoint(), // You'll need to add this to your config + credentials: { + accessKeyId: config.r2AccessKey(), // You'll need to add these + secretAccessKey: config.r2SecretKey(), // credential methods to your config + }, +}); -const gameBucket = "openfront-games"; -const analyticsBucket = "openfront-analytics"; +const bucket = config.r2Bucket(); +const gameFolder = "games"; +const analyticsFolder = "analytics"; export async function archive(gameRecord: GameRecord) { try { - // Archive to Redshift Serverless - await archiveAnalyticsToS3(gameRecord); + // Archive to R2 + await archiveAnalyticsToR2(gameRecord); - // Archive to S3 if there are turns + // Archive full game if there are turns if (gameRecord.turns.length > 0) { console.log( - `${gameRecord.id}: game has more than zero turns, attempting to write to full game to S3`, + `${gameRecord.id}: game has more than zero turns, attempting to write to full game to R2`, ); - await archiveFullGameToS3(gameRecord); + await archiveFullGameToR2(gameRecord); } } catch (error) { console.error(`${gameRecord.id}: Final archive error: ${error}`, { @@ -35,8 +43,8 @@ export async function archive(gameRecord: GameRecord) { } } -async function archiveAnalyticsToS3(gameRecord: GameRecord) { - // Create analytics data object (similar to what was going to Redshift) +async function archiveAnalyticsToR2(gameRecord: GameRecord) { + // Create analytics data object const analyticsData = { id: gameRecord.id, env: config.env(), @@ -60,17 +68,17 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) { // Store analytics data using just the game ID as the key const analyticsKey = `${gameRecord.id}.json`; - await s3.putObject({ - Bucket: analyticsBucket, - Key: analyticsKey, + await r2.putObject({ + Bucket: bucket, + Key: `${analyticsFolder}/${analyticsKey}`, Body: JSON.stringify(analyticsData), ContentType: "application/json", }); - console.log(`${gameRecord.id}: successfully wrote game analytics to S3`); + console.log(`${gameRecord.id}: successfully wrote game analytics to R2`); } catch (error) { console.error( - `${gameRecord.id}: Error writing game analytics to S3: ${error}`, + `${gameRecord.id}: Error writing game analytics to R2: ${error}`, { message: error?.message || error, stack: error?.stack, @@ -82,7 +90,7 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) { } } -async function archiveFullGameToS3(gameRecord: GameRecord) { +async function archiveFullGameToR2(gameRecord: GameRecord) { // Create a deep copy to avoid modifying the original const recordCopy = JSON.parse(JSON.stringify(gameRecord)); @@ -93,9 +101,9 @@ async function archiveFullGameToS3(gameRecord: GameRecord) { }); try { - await s3.putObject({ - Bucket: gameBucket, - Key: recordCopy.id, + await r2.putObject({ + Bucket: bucket, + Key: `${gameFolder}/${recordCopy.id}`, Body: JSON.stringify(recordCopy), ContentType: "application/json", }); @@ -104,15 +112,15 @@ async function archiveFullGameToS3(gameRecord: GameRecord) { throw error; } - console.log(`${gameRecord.id}: game record successfully written to S3`); + console.log(`${gameRecord.id}: game record successfully written to R2`); } export async function readGameRecord(gameId: GameID): Promise { try { // Check if file exists and download in one operation - const response = await s3.getObject({ - Bucket: gameBucket, - Key: gameId, + const response = await r2.getObject({ + Bucket: bucket, + Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder }); // Parse the response body @@ -121,7 +129,8 @@ export async function readGameRecord(gameId: GameID): Promise { return gameRecord as GameRecord; } catch (error) { - console.error(`${gameId}: Error reading game record from S3: ${error}`, { + // Log the error for monitoring purposes + console.error(`${gameId}: Error reading game record from R2: ${error}`, { message: error?.message || error, stack: error?.stack, name: error?.name, @@ -133,9 +142,9 @@ export async function readGameRecord(gameId: GameID): Promise { export async function gameRecordExists(gameId: GameID): Promise { try { - await s3.headObject({ - Bucket: gameBucket, - Key: gameId, + await r2.headObject({ + Bucket: bucket, + Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder }); return true; } catch (error) { diff --git a/update.sh b/update.sh index 7d297094f..9df8191c1 100755 --- a/update.sh +++ b/update.sh @@ -31,10 +31,6 @@ if [ -f /root/.env ]; then export $(grep -v '^#' /root/.env | xargs) fi -# Set the Loki URL -LOKI_URL=${LOKI_URL:-"http://localhost:3100/loki/api/v1/push"} -echo "Using Loki URL: ${LOKI_URL}" - echo "Pulling latest image from Docker Hub..." docker pull $FULL_IMAGE_NAME @@ -90,17 +86,9 @@ docker run -d -p 80:80 \ --restart=always \ $VOLUME_MOUNTS \ $NETWORK_FLAGS \ - --env APP_ENV=${ENV} \ + --env GAME_ENV=${ENV} \ --env-file /root/.env \ --name ${CONTAINER_NAME} \ - --log-driver=loki \ - --log-opt loki-url="${LOKI_URL}" \ - --log-opt loki-batch-size="400" \ - --log-opt loki-min-backoff="100ms" \ - --log-opt loki-max-backoff="10s" \ - --log-opt loki-retries="5" \ - --log-opt loki-timeout="10s" \ - --log-opt loki-external-labels="job=openfront,env=${ENV},container=${CONTAINER_NAME}" \ $FULL_IMAGE_NAME if [ $? -eq 0 ]; then @@ -121,5 +109,4 @@ echo "======================================================" echo "✅ SERVER UPDATE COMPLETED SUCCESSFULLY" echo "Container name: ${CONTAINER_NAME}" echo "Image: ${FULL_IMAGE_NAME}" -echo "Logs: Streaming to Loki at ${LOKI_URL}" echo "======================================================" \ No newline at end of file