Merge pull request #839 from vanvonlj/upstream-pr/assume-externally-provisioned-database

Support externally-provisioned databases (opt-in, no drop/create)

(cherry picked from commit 2e117c26764483fb4ee3de9540c9bf7a647d8067)
This commit is contained in:
sven-n
2026-07-22 07:30:36 +02:00
committed by Acentech Dev
parent b854565586
commit eb4c05eda2
6 changed files with 70 additions and 12 deletions

View File

@@ -152,12 +152,20 @@ public class PersistenceContextProvider : IMigratableDatabaseContextProvider
/// <summary> /// <summary>
/// Recreates the database by deleting and creating it again. /// Recreates the database by deleting and creating it again.
/// </summary> /// </summary>
/// <param name="dropExistingDatabase">
/// If <see langword="true"/> (the default), the database is dropped and created again from scratch.
/// If <see langword="false"/>, the existing database is kept and only its schema is built via
/// migrations — required when the database is provisioned externally and the connecting role is
/// not permitted to create or drop databases.
/// </param>
/// <returns>The disposable that should be disposed of when the data creation process is finished.</returns> /// <returns>The disposable that should be disposed of when the data creation process is finished.</returns>
public async Task<IDisposable> ReCreateDatabaseAsync() public async Task<IDisposable> ReCreateDatabaseAsync(bool dropExistingDatabase = true)
{ {
var changePublisher = this._changeListener; var changePublisher = this._changeListener;
this._changeListener = null; this._changeListener = null;
try try
{
if (dropExistingDatabase)
{ {
try try
{ {
@@ -168,6 +176,7 @@ public class PersistenceContextProvider : IMigratableDatabaseContextProvider
{ {
// That's expected for a fresh database // That's expected for a fresh database
} }
}
await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false); await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);

View File

@@ -63,8 +63,15 @@ public interface IMigratableDatabaseContextProvider : IPersistenceContextProvide
/// <summary> /// <summary>
/// Recreates the database by deleting and creating it again. /// Recreates the database by deleting and creating it again.
/// </summary> /// </summary>
/// <param name="dropExistingDatabase">
/// If <see langword="true"/> (the default), the database is dropped and created again from scratch.
/// If <see langword="false"/>, the existing database is kept and only its schema is built via
/// migrations. Set this to <see langword="false"/> when the database is provisioned externally
/// (e.g. by a Kubernetes operator, infrastructure-as-code, or a managed cloud database) and the
/// connecting role is not permitted to create or drop databases.
/// </param>
/// <returns>The disposable which should be disposed when the data creation process is finished.</returns> /// <returns>The disposable which should be disposed when the data creation process is finished.</returns>
Task<IDisposable> ReCreateDatabaseAsync(); Task<IDisposable> ReCreateDatabaseAsync(bool dropExistingDatabase = true);
/// <summary> /// <summary>
/// Resets the cache of this instance. /// Resets the cache of this instance.

View File

@@ -127,7 +127,7 @@ public class InMemoryPersistenceContextProvider : IMigratableDatabaseContextProv
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<IDisposable> ReCreateDatabaseAsync() public Task<IDisposable> ReCreateDatabaseAsync(bool dropExistingDatabase = true)
{ {
this._repositoryProvider = new(); this._repositoryProvider = new();
return Task.FromResult<IDisposable>(new Disposable(() => { })); return Task.FromResult<IDisposable>(new Disposable(() => { }));

View File

@@ -51,6 +51,7 @@ internal sealed class Program : IDisposable
private readonly IDictionary<int, IGameServer> _gameServers = new Dictionary<int, IGameServer>(); private readonly IDictionary<int, IGameServer> _gameServers = new Dictionary<int, IGameServer>();
private readonly IList<IManageableServer> _servers = new List<IManageableServer>(); private readonly IList<IManageableServer> _servers = new List<IManageableServer>();
private readonly Serilog.ILogger _logger; private readonly Serilog.ILogger _logger;
private readonly IConfiguration _configuration;
private IHost? _serverHost; private IHost? _serverHost;
@@ -65,8 +66,10 @@ internal sealed class Program : IDisposable
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true) .AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true, true) .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true, true)
.AddEnvironmentVariables()
.Build(); .Build();
this._configuration = configuration;
this._logger = new LoggerConfiguration() this._logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration) .ReadFrom.Configuration(configuration)
.CreateLogger(); .CreateLogger();
@@ -475,8 +478,19 @@ internal sealed class Program : IDisposable
var contextProvider = new PersistenceContextProvider(loggerFactory, changeListener); var contextProvider = new PersistenceContextProvider(loggerFactory, changeListener);
if (reinit || !await contextProvider.DatabaseExistsAsync().ConfigureAwait(false)) if (reinit || !await contextProvider.DatabaseExistsAsync().ConfigureAwait(false))
{ {
this._logger.Information("The database is getting (re-)initialized..."); // The database can be provisioned externally (e.g. by a Kubernetes operator,
using var update = await contextProvider.ReCreateDatabaseAsync().ConfigureAwait(false); // infrastructure-as-code, or a managed cloud database), where the connecting role is
// typically not permitted to create or drop databases. Enabling
// Database:AssumeExternallyProvisioned keeps the existing database and only builds its
// schema via migrations, instead of dropping and recreating it. The default (false)
// preserves the original drop-and-recreate behaviour; an explicit -reinit always recreates.
var assumeExternallyProvisioned = !reinit
&& this._configuration.GetValue<bool>("Database:AssumeExternallyProvisioned");
this._logger.Information(assumeExternallyProvisioned
? "Building the schema on the externally-provisioned database (no drop/create)..."
: "The database is getting (re-)initialized...");
using var update = await contextProvider.ReCreateDatabaseAsync(dropExistingDatabase: !assumeExternallyProvisioned).ConfigureAwait(false);
await this.InitializeDataAsync(version, loggerFactory, contextProvider).ConfigureAwait(false); await this.InitializeDataAsync(version, loggerFactory, contextProvider).ConfigureAwait(false);
this._logger.Information("...initialization finished."); this._logger.Information("...initialization finished.");
} }

View File

@@ -14,6 +14,30 @@ only actions of certain players, for example.
In the future, it might be possible to change logging settings over the admin In the future, it might be possible to change logging settings over the admin
panel, too. panel, too.
## Externally-provisioned database
By default, when no database exists yet, the server drops and (re-)creates it
before building the schema. This requires the connecting database role to be
allowed to create and drop databases (upstream the connection strings use the
`postgres` superuser, optionally overridden via `DB_ADMIN_USER`/`DB_ADMIN_PW`).
In managed environments — a Kubernetes operator, infrastructure-as-code, or a
managed cloud database — the database is often provisioned ahead of time and the
connecting role is intentionally *not* permitted to create or drop databases
(least privilege; it only owns its own database). In that case, set:
```json
"Database": {
"AssumeExternallyProvisioned": true
}
```
or, equivalently, the environment variable
`Database__AssumeExternallyProvisioned=true`. The server then keeps the existing
(empty) database and only builds its schema via migrations, instead of dropping
and recreating it. The default (`false`) preserves the original behaviour. An
explicit `-reinit` always drops and recreates, regardless of this setting.
## Parameters ## Parameters
**Please note, that the most of these parameters (except ```-demo``` and ```-adminpanel```) **Please note, that the most of these parameters (except ```-demo``` and ```-adminpanel```)
@@ -62,6 +86,7 @@ They may be helpful when running the server in a container or under linux.
| DB_HOST | Host name/address of the postgres database | | DB_HOST | Host name/address of the postgres database |
| DB_ADMIN_USER | User name of the admin user of the postgres database | | DB_ADMIN_USER | User name of the admin user of the postgres database |
| DB_ADMIN_PW | Password of the admin user of the postgres database | | DB_ADMIN_PW | Password of the admin user of the postgres database |
| Database__AssumeExternallyProvisioned | When `true`, keep an already-provisioned (empty) database and only build its schema via migrations, instead of dropping and recreating it. Useful when the connecting role may not create/drop databases. Default: `false`. See *Externally-provisioned database* above. |
## Settings priority ## Settings priority

View File

@@ -1,6 +1,9 @@
{ {
"DetailedErrors": true, "DetailedErrors": true,
"AllowedHosts": "*", "AllowedHosts": "*",
"Database": {
"AssumeExternallyProvisioned": false
},
"Serilog": { "Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": { "MinimumLevel": {