Add deploy tooling, docs, and Aspire config
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
# dotenv files
|
# dotenv files
|
||||||
.env
|
.env
|
||||||
|
deploy/aspire-output/
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
|
|||||||
95
README.md
Normal file
95
README.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# TenWholeYears.Server
|
||||||
|
|
||||||
|
Server for the TenWholeYears Game Client. A decompilation of the first public Rec Room build.
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- Docker Desktop or another container runtime, when using `AppHost`
|
||||||
|
- A Steam Web API key, if using platform validation.
|
||||||
|
|
||||||
|
Restore and build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet restore RecNet.sln
|
||||||
|
dotnet build RecNet.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the full local stack with Aspire:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src\AppHost\AppHost.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Use user secrets for local development.
|
||||||
|
|
||||||
|
For the API:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet user-secrets set "Jwt:Secret" "replace-with-a-long-random-secret" --project src\API\API.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional API secrets:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet user-secrets set "Jwt:Issuer" "http://localhost:5155" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Jwt:Audience" "TenWholeYears" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Jwt:ExpiryMinutes" "60" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Steam:ApiKey" "your-steam-web-api-key" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Steam:AppId" "480" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Neutrino:ValidateSecret" "true" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "Neutrino:Secret" "replace-with-neutrino-secret" --project src\API\API.csproj
|
||||||
|
dotnet user-secrets set "RecNet:UseForwardedHeaders" "false" --project src\API\API.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Config With Environment Variables
|
||||||
|
|
||||||
|
.env example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Jwt__Secret = "replace-with-a-long-random-secret"
|
||||||
|
Jwt__Issuer = "https://your-api.example.com"
|
||||||
|
Jwt__Audience = "TenWholeYears"
|
||||||
|
Jwt__ExpiryMinutes = "60"
|
||||||
|
Steam__ApiKey = "your-steam-web-api-key"
|
||||||
|
Steam__AppId = "480"
|
||||||
|
Neutrino__ValidateSecret = "true"
|
||||||
|
Neutrino__Secret = "replace-with-neutrino-secret"
|
||||||
|
RecNet__UseForwardedHeaders = "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Server Configs
|
||||||
|
|
||||||
|
The server has configs in the `ServerConfigs` database table that are able to be adjusted while the server is running. Keys are strings and values are stored as JSON.
|
||||||
|
|
||||||
|
| Key | Type | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Config:MOTD` | `string` | `"Ten Whole Years!"` |
|
||||||
|
| `Profiles:IgnoreAuthValidation` | `boolean` | `true` |
|
||||||
|
| `Profiles:NameGen` | object | `DefaultNameGenConfig.Value` |
|
||||||
|
|
||||||
|
`Profiles:NameGen` expects this shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Adjectives": [
|
||||||
|
"Adorable",
|
||||||
|
"Brave"
|
||||||
|
],
|
||||||
|
"Nouns": [
|
||||||
|
"Pony",
|
||||||
|
"Tiger"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
|
||||||
|
- Set `RecNet:UseForwardedHeaders` to `true` only when the API is behind a trusted reverse proxy.
|
||||||
22
deploy/.env.example
Normal file
22
deploy/.env.example
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Copy this file to .env and fill in production values before deploying.
|
||||||
|
|
||||||
|
# Registry login used by deploy/push-images.ps1.
|
||||||
|
REGISTRY_ENDPOINT=git.recroomarchive.org
|
||||||
|
REGISTRY_REPOSITORY=recroomarchive-deploy/tenwholeyears-server
|
||||||
|
REGISTRY_USERNAME=replace-with-registry-username
|
||||||
|
REGISTRY_TOKEN=replace-with-registry-token
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# Database.
|
||||||
|
POSTGRES_PASSWORD=replace-with-long-random-postgres-password
|
||||||
|
|
||||||
|
# API configuration.
|
||||||
|
Jwt__Secret=replace-with-at-least-32-random-characters
|
||||||
|
Jwt__Issuer=https://your-api.example.com
|
||||||
|
Jwt__Audience=TenWholeYears
|
||||||
|
Jwt__ExpiryMinutes=60
|
||||||
|
Steam__ApiKey=replace-with-steam-web-api-key
|
||||||
|
Steam__AppId=480
|
||||||
|
Neutrino__ValidateSecret=true
|
||||||
|
Neutrino__Secret=replace-with-neutrino-secret
|
||||||
|
RecNet__UseForwardedHeaders=true
|
||||||
34
deploy/README.md
Normal file
34
deploy/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Build and Push Images
|
||||||
|
|
||||||
|
1. Copy `.env.example` to `.env` and fill in production values.
|
||||||
|
2. Run the Aspire-native push pipeline:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\deploy\push-images.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from a POSIX shell:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh ./deploy/push-images.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The shell script expects the Aspire CLI to be installed in that shell. In WSL, install it with the official Aspire installer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sSL https://aspire.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or as a .NET tool:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet tool install -g Aspire.Cli
|
||||||
|
```
|
||||||
|
|
||||||
|
The script loads `deploy/.env` when it exists, authenticates to `git.recroomarchive.org` using `REGISTRY_USERNAME` and `REGISTRY_TOKEN`, then runs `aspire do push`.
|
||||||
|
|
||||||
|
The included `docker-compose.yml` consumes the pushed images:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose --env-file .env up -d
|
||||||
|
```
|
||||||
52
deploy/docker-compose.yml
Normal file
52
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: tenwholeyears-server
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: recnet
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- recnet-postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d recnet"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
migrationservice:
|
||||||
|
image: ${REGISTRY_ENDPOINT:-git.recroomarchive.org}/${REGISTRY_REPOSITORY}/migrationservice:${IMAGE_TAG:-latest}
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__recnet: Host=postgres;Port=5432;Database=recnet;Username=postgres;Password=${POSTGRES_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: ${REGISTRY_ENDPOINT:-git.recroomarchive.org}/${REGISTRY_REPOSITORY}/api:${IMAGE_TAG:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${API_HTTP_PORT:-8080}:8080"
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_HTTP_PORTS: 8080
|
||||||
|
ConnectionStrings__recnet: Host=postgres;Port=5432;Database=recnet;Username=postgres;Password=${POSTGRES_PASSWORD}
|
||||||
|
Jwt__Secret: ${Jwt__Secret}
|
||||||
|
Jwt__Issuer: ${Jwt__Issuer}
|
||||||
|
Jwt__Audience: ${Jwt__Audience}
|
||||||
|
Jwt__ExpiryMinutes: ${Jwt__ExpiryMinutes}
|
||||||
|
Steam__ApiKey: ${Steam__ApiKey}
|
||||||
|
Steam__AppId: ${Steam__AppId}
|
||||||
|
Neutrino__ValidateSecret: ${Neutrino__ValidateSecret}
|
||||||
|
Neutrino__Secret: ${Neutrino__Secret}
|
||||||
|
RecNet__UseForwardedHeaders: ${RecNet__UseForwardedHeaders}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
migrationservice:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
recnet-postgres-data:
|
||||||
49
deploy/push-images.ps1
Normal file
49
deploy/push-images.ps1
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
param(
|
||||||
|
[string] $Environment = "Production",
|
||||||
|
[string] $AppHostPath = "",
|
||||||
|
[string] $OutputPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($AppHostPath)) {
|
||||||
|
$AppHostPath = Join-Path $PSScriptRoot "../src/AppHost/AppHost.csproj"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||||
|
$OutputPath = Join-Path $PSScriptRoot "aspire-output"
|
||||||
|
}
|
||||||
|
|
||||||
|
$envPath = Join-Path $PSScriptRoot ".env"
|
||||||
|
if (Test-Path $envPath) {
|
||||||
|
Get-Content $envPath | Where-Object { $_ -and $_ -notmatch '^\s*#' } | ForEach-Object {
|
||||||
|
$name, $value = $_ -split '=', 2
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($name) -and [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
|
||||||
|
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredEnvironmentVariables = @(
|
||||||
|
"REGISTRY_USERNAME",
|
||||||
|
"REGISTRY_TOKEN",
|
||||||
|
"REGISTRY_REPOSITORY"
|
||||||
|
)
|
||||||
|
|
||||||
|
$missing = $requiredEnvironmentVariables | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }
|
||||||
|
if ($missing.Count -gt 0) {
|
||||||
|
throw "Missing required environment variables: $($missing -join ', '). Load deploy/.env values before running this script."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:IMAGE_TAG)) {
|
||||||
|
$env:IMAGE_TAG = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
$env:REGISTRY_ENDPOINT = "git.recroomarchive.org"
|
||||||
|
|
||||||
|
$env:REGISTRY_TOKEN | docker login $env:REGISTRY_ENDPOINT -u $env:REGISTRY_USERNAME --password-stdin
|
||||||
|
|
||||||
|
aspire do push `
|
||||||
|
--apphost $AppHostPath `
|
||||||
|
--environment $Environment `
|
||||||
|
--output-path $OutputPath
|
||||||
75
deploy/push-images.sh
Normal file
75
deploy/push-images.sh
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||||
|
|
||||||
|
ENVIRONMENT=${ENVIRONMENT:-Production}
|
||||||
|
APPHOST_PATH=${APPHOST_PATH:-"$script_dir/../src/AppHost/AppHost.csproj"}
|
||||||
|
OUTPUT_PATH=${OUTPUT_PATH:-"$script_dir/aspire-output"}
|
||||||
|
|
||||||
|
env_file="$script_dir/.env"
|
||||||
|
if [ -f "$env_file" ]; then
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
line=$(printf '%s' "$line" | tr -d '\r')
|
||||||
|
case "$line" in
|
||||||
|
""|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
key=${line%%=*}
|
||||||
|
value=${line#*=}
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
[A-Za-z_][A-Za-z0-9_]*) ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
eval "existing=\${$key-}"
|
||||||
|
if [ -n "$key" ] && [ -z "$existing" ]; then
|
||||||
|
export "$key=$value"
|
||||||
|
fi
|
||||||
|
done < "$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
missing=""
|
||||||
|
for name in REGISTRY_USERNAME REGISTRY_TOKEN REGISTRY_REPOSITORY; do
|
||||||
|
eval "value=\${$name-}"
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
if [ -z "$missing" ]; then
|
||||||
|
missing=$name
|
||||||
|
else
|
||||||
|
missing="$missing, $name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "Missing required environment variables: $missing. Load deploy/.env values before running this script." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE_TAG=${IMAGE_TAG:-latest}
|
||||||
|
REGISTRY_ENDPOINT=git.recroomarchive.org
|
||||||
|
export IMAGE_TAG REGISTRY_ENDPOINT
|
||||||
|
|
||||||
|
if [ -n "${ASPIRE_CLI-}" ]; then
|
||||||
|
aspire_cli=$ASPIRE_CLI
|
||||||
|
elif command -v aspire >/dev/null 2>&1; then
|
||||||
|
aspire_cli=aspire
|
||||||
|
elif [ -x "$HOME/.aspire/bin/aspire" ]; then
|
||||||
|
aspire_cli="$HOME/.aspire/bin/aspire"
|
||||||
|
elif [ -x "$HOME/.dotnet/tools/aspire" ]; then
|
||||||
|
aspire_cli="$HOME/.dotnet/tools/aspire"
|
||||||
|
else
|
||||||
|
echo "Aspire CLI was not found in this shell." >&2
|
||||||
|
echo "Install it in WSL with: curl -sSL https://aspire.dev/install.sh | bash" >&2
|
||||||
|
echo "Or install it as a .NET tool with: dotnet tool install -g Aspire.Cli" >&2
|
||||||
|
echo "If it is installed somewhere else, rerun with: ASPIRE_CLI=/path/to/aspire sh ./deploy/push-images.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY_ENDPOINT" -u "$REGISTRY_USERNAME" --password-stdin
|
||||||
|
|
||||||
|
"$aspire_cli" do push \
|
||||||
|
--apphost "$APPHOST_PATH" \
|
||||||
|
--environment "$ENVIRONMENT" \
|
||||||
|
--output-path "$OUTPUT_PATH"
|
||||||
@@ -2,6 +2,16 @@ var builder = DistributedApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
var postgresPassword = builder.AddParameter("postgres-password", secret: true);
|
var postgresPassword = builder.AddParameter("postgres-password", secret: true);
|
||||||
|
|
||||||
|
var imageTag = builder.Configuration["IMAGE_TAG"] ?? "latest";
|
||||||
|
var registryRepository = builder.Configuration["REGISTRY_REPOSITORY"] ?? "recroomarchive/tenwholeyears-server";
|
||||||
|
|
||||||
|
#pragma warning disable ASPIRECOMPUTE003
|
||||||
|
var registry = builder.AddContainerRegistry(
|
||||||
|
"recroomarchive-registry",
|
||||||
|
"git.recroomarchive.org",
|
||||||
|
registryRepository);
|
||||||
|
#pragma warning restore ASPIRECOMPUTE003
|
||||||
|
|
||||||
// Infra
|
// Infra
|
||||||
var postgres = builder.AddPostgres("postgres")
|
var postgres = builder.AddPostgres("postgres")
|
||||||
.WithDataVolume("recnet-postgres-data")
|
.WithDataVolume("recnet-postgres-data")
|
||||||
@@ -12,14 +22,22 @@ var postgres = builder.AddPostgres("postgres")
|
|||||||
|
|
||||||
var db = postgres.AddDatabase("recnet");
|
var db = postgres.AddDatabase("recnet");
|
||||||
|
|
||||||
|
#pragma warning disable ASPIRECOMPUTE003, ASPIREPIPELINES003
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
var migrationService = builder.AddProject<Projects.RecNet_MigrationService>("migrationservice")
|
var migrationService = builder.AddProject<Projects.RecNet_MigrationService>("migrationservice")
|
||||||
.WithReference(db)
|
.WithReference(db)
|
||||||
.WaitFor(db);
|
.WaitFor(db)
|
||||||
|
.WithContainerRegistry(registry)
|
||||||
|
.WithRemoteImageTag(imageTag);
|
||||||
|
|
||||||
builder.AddProject<Projects.API>("api")
|
builder.AddProject<Projects.API>("api")
|
||||||
.WithReference(db)
|
.WithReference(db)
|
||||||
.WaitFor(db)
|
.WaitFor(db)
|
||||||
.WaitForCompletion(migrationService);
|
.WaitForCompletion(migrationService)
|
||||||
|
.WithContainerRegistry(registry)
|
||||||
|
.WithRemoteImageTag(imageTag);
|
||||||
|
|
||||||
builder.Build().Run();
|
builder.Build().Run();
|
||||||
|
|
||||||
|
#pragma warning restore ASPIRECOMPUTE003, ASPIREPIPELINES003
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ public class SteamOptions
|
|||||||
{
|
{
|
||||||
public string ApiKey { get; set; } = string.Empty;
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
public uint AppId { get; set; } = 480; // Spacewar
|
public uint AppId { get; set; } = 480; // Spacewar
|
||||||
public string Identity { get; set; } = "recnet";
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user