Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 6ef52c8e92 Include OPENAI_API_KEY and GPT_MODEL_NAME in .env file
Pass all required env vars from GitHub secrets to droplet .env file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 10:49:26 -08:00
adminandClaude Opus 4.5 1353f3bc82 Fix .env file permissions in deploy script
Remove existing .env before writing to handle permission issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 10:43:11 -08:00
adminandClaude Opus 4.5 aa5564c2c9 Centralize S3 config and add endpoint env var
- Move all S3 config to S3Credentials (single source of truth)
- Add DO_SPACES_ENDPOINT env var (defaults to sfo3.digitaloceanspaces.com)
- Remove duplicate config reading from LocalGamePersisterCreation and ServerSetupHelpers
- Pass S3 secrets from GitHub Actions to droplet via .env file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 07:35:49 -08:00
adminandClaude Opus 4.5 b81341ece4 Enable S3/DO Spaces persistence via environment variables
Add support for persisting game saves to DigitalOcean Spaces (S3-compatible)
via environment variables, preventing game state loss during deployments.

Configuration:
- EAGLE_ENABLE_S3=true to enable S3 persistence
- DO_SPACES_ACCESS_KEY and DO_SPACES_SECRET_KEY for credentials
- Falls back to ~/.s3cfg if env vars not set

Changes:
- S3Credentials: Load credentials from env vars or ~/.s3cfg
- LocalGamePersisterCreation: Read EAGLE_ENABLE_S3 from env
- ServerSetupHelpers: Enable S3 persister based on env var
- S3Utils: Fix transferManager to use credentials
- docker-compose.prod.yml: Add S3 environment variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 07:22:59 -08:00
8 changed files with 97 additions and 20 deletions
+17 -1
View File
@@ -285,6 +285,11 @@ jobs:
env:
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -305,11 +310,22 @@ jobs:
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
script_stop: true
envs: EAGLE_IMAGE,SHARDOK_IMAGE
envs: EAGLE_IMAGE,SHARDOK_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY
script: |
set -x
cd /opt/eagle0
# Write env vars to .env file for docker-compose
rm -f .env 2>/dev/null || true
cat > .env << EOF
OPENAI_API_KEY=${OPENAI_API_KEY:-}
GPT_MODEL_NAME=${GPT_MODEL_NAME:-gpt-4o}
EAGLE_ENABLE_S3=${EAGLE_ENABLE_S3:-false}
DO_SPACES_ACCESS_KEY=${DO_SPACES_ACCESS_KEY:-}
DO_SPACES_SECRET_KEY=${DO_SPACES_SECRET_KEY:-}
EOF
chmod 600 .env
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
+4
View File
@@ -20,6 +20,10 @@ services:
- "40032:40032"
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
volumes:
- ./saves:/app/saves
depends_on:
@@ -314,6 +314,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/service/persistence:s3_persister",
"//src/main/scala/net/eagle0/eagle/service/persistence:s3_utils",
"//src/main/scala/net/eagle0/eagle/service/persistence:save_directory",
"//src/main/scala/net/eagle0/eagle/service/persistence/credentials",
],
)
@@ -395,6 +396,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/service/persistence:s3_persister",
"//src/main/scala/net/eagle0/eagle/service/persistence:s3_utils",
"//src/main/scala/net/eagle0/eagle/service/persistence:save_directory",
"//src/main/scala/net/eagle0/eagle/service/persistence/credentials",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update_receiver",
@@ -8,11 +8,10 @@ import net.eagle0.eagle.service.persistence.{
S3Utils,
SaveDirectory
}
import net.eagle0.eagle.service.persistence.credentials.S3Credentials
import net.eagle0.eagle.GameId
object LocalGamePersisterCreation extends GamePersisterCreation {
private val includeS3 = false
private def localFilePersister(gameId: GameId) = LocalFilePersister(
SaveDirectory.saveDirectoryForGame(gameId)
)
@@ -20,7 +19,7 @@ object LocalGamePersisterCreation extends GamePersisterCreation {
S3Persister(S3Utils.mainBucketName, S3Utils.makeS3Prefix(gameId))
def persisterForGame(gameId: GameId): Persister =
if includeS3 then
if S3Credentials.isEnabled then
CompoundPersister(
Vector(localFilePersister(gameId), s3Persister(gameId))
)
@@ -12,6 +12,7 @@ import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
import net.eagle0.eagle.service.new_game_creation.FixedNewGameCreation
import net.eagle0.eagle.service.persistence.{CompoundPersister, LocalFilePersister, S3Persister, S3Utils, SaveDirectory}
import net.eagle0.eagle.service.persistence.credentials.S3Credentials
import net.eagle0.eagle.shardok_interface.ShardokInterfaceGrpcClient
import net.eagle0.shardok.common.hex_map.HexMap
@@ -59,16 +60,19 @@ object ServerSetupHelpers {
.get
val localPersister = LocalFilePersister(SaveDirectory.saveDirectory)
val s3Persister =
S3Persister(S3Utils.mainBucketName, S3Utils.runningGamesKeyPrefix)
val _ = s3Persister
val persisters = if S3Credentials.isEnabled then {
val s3Persister =
S3Persister(S3Utils.mainBucketName, S3Utils.runningGamesKeyPrefix)
Vector(localPersister, s3Persister)
} else {
Vector(localPersister)
}
GamesManager(
shardokInternalInterface = shardokInternalInterface,
randomGenerator = new SecureRandom,
persister = CompoundPersister(
Vector(localPersister)
),
persister = CompoundPersister(persisters),
gameCreation = FixedNewGameCreation,
gamePersisterCreation = LocalGamePersisterCreation,
gptModelName = gptModelName,
@@ -24,18 +24,19 @@ object S3Utils {
private def syncClient() = S3Client
.builder()
.endpointOverride(URI.create("https://sfo3.digitaloceanspaces.com"))
.endpointOverride(URI.create(S3Credentials.endpoint))
.region(Region.US_EAST_1)
.credentialsProvider(S3Credentials.credentialsProvider)
.build
private val transferManager: S3TransferManager =
private lazy val transferManager: S3TransferManager =
S3TransferManager.builder
.s3Client(
S3AsyncClient
.builder()
.endpointOverride(URI.create("https://sfo3.digitaloceanspaces.com"))
.endpointOverride(URI.create(S3Credentials.endpoint))
.region(Region.US_EAST_1)
.credentialsProvider(S3Credentials.credentialsProvider)
.build()
)
.build
@@ -4,7 +4,7 @@ scala_library(
name = "credentials",
srcs = ["S3Credentials.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/service/persistence:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
],
deps = [
"@maven//:software_amazon_awssdk_auth",
@@ -7,6 +7,15 @@ import scala.util.{Try, Using}
import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, AwsCredentialsProvider, StaticCredentialsProvider}
/**
* Centralized S3/DO Spaces configuration.
*
* Environment variables:
* - EAGLE_ENABLE_S3: Set to "true" to enable S3 persistence
* - DO_SPACES_ENDPOINT: S3 endpoint URL (default: https://sfo3.digitaloceanspaces.com)
* - DO_SPACES_ACCESS_KEY: Access key (falls back to ~/.s3cfg)
* - DO_SPACES_SECRET_KEY: Secret key (falls back to ~/.s3cfg)
*/
object S3Credentials {
private def makeS3cfgMap(): Try[Map[String, String]] = {
val s3cfgFile = new File(System.getProperty("user.home"), ".s3cfg")
@@ -32,12 +41,54 @@ object S3Credentials {
}
}
private val s3cfgMap = makeS3cfgMap().get
/** Whether S3 persistence is enabled (EAGLE_ENABLE_S3=true and credentials available). */
val isEnabled: Boolean = {
val envValue = Option(System.getenv("EAGLE_ENABLE_S3"))
.map(_.toLowerCase)
.contains("true")
if envValue && !hasCredentials then {
System.err.println(
"WARNING: EAGLE_ENABLE_S3=true but no S3 credentials configured. S3 persistence disabled."
)
false
} else envValue
}
/** S3 endpoint URL. Defaults to DO Spaces SFO3 region. */
val endpoint: String = Option(System.getenv("DO_SPACES_ENDPOINT"))
.getOrElse("https://sfo3.digitaloceanspaces.com")
/** Check if S3 credentials are available (either from env vars or ~/.s3cfg). */
private def hasCredentials: Boolean = {
val hasEnvVars = Option(System.getenv("DO_SPACES_ACCESS_KEY")).isDefined &&
Option(System.getenv("DO_SPACES_SECRET_KEY")).isDefined
val hasS3cfg = {
val s3cfgFile = new File(System.getProperty("user.home"), ".s3cfg")
s3cfgFile.exists()
}
hasEnvVars || hasS3cfg
}
/** Lazily load credentials from environment variables or ~/.s3cfg file. */
lazy val credentialsProvider: AwsCredentialsProvider = {
val accessKey = Option(System.getenv("DO_SPACES_ACCESS_KEY"))
val secretKey = Option(System.getenv("DO_SPACES_SECRET_KEY"))
val credentials = (accessKey, secretKey) match {
case (Some(ak), Some(sk)) =>
AwsBasicCredentials.create(ak, sk)
case _ =>
// Fall back to ~/.s3cfg file
val s3cfgMap = makeS3cfgMap().get
AwsBasicCredentials.create(
s3cfgMap("access_key"),
s3cfgMap("secret_key")
)
}
private val credentials = AwsBasicCredentials.create(
s3cfgMap("access_key"),
s3cfgMap("secret_key")
)
val credentialsProvider: AwsCredentialsProvider =
StaticCredentialsProvider.create(credentials)
}
}