diff --git a/src/Persistence/EntityFramework/PersistenceContextProvider.cs b/src/Persistence/EntityFramework/PersistenceContextProvider.cs
index 56b55db..93b7747 100644
--- a/src/Persistence/EntityFramework/PersistenceContextProvider.cs
+++ b/src/Persistence/EntityFramework/PersistenceContextProvider.cs
@@ -152,21 +152,30 @@ public class PersistenceContextProvider : IMigratableDatabaseContextProvider
///
/// Recreates the database by deleting and creating it again.
///
+ ///
+ /// If (the default), the database is dropped and created again from scratch.
+ /// If , 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.
+ ///
/// The disposable that should be disposed of when the data creation process is finished.
- public async Task ReCreateDatabaseAsync()
+ public async Task ReCreateDatabaseAsync(bool dropExistingDatabase = true)
{
var changePublisher = this._changeListener;
this._changeListener = null;
try
{
- try
+ if (dropExistingDatabase)
{
- await using var installationContext = new EntityDataContext();
- await installationContext.Database.EnsureDeletedAsync().ConfigureAwait(false);
- }
- catch (NpgsqlException)
- {
- // That's expected for a fresh database
+ try
+ {
+ await using var installationContext = new EntityDataContext();
+ await installationContext.Database.EnsureDeletedAsync().ConfigureAwait(false);
+ }
+ catch (NpgsqlException)
+ {
+ // That's expected for a fresh database
+ }
}
await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);
diff --git a/src/Persistence/IMigratableDatabaseContextProvider.cs b/src/Persistence/IMigratableDatabaseContextProvider.cs
index 242b71f..f0a15a5 100644
--- a/src/Persistence/IMigratableDatabaseContextProvider.cs
+++ b/src/Persistence/IMigratableDatabaseContextProvider.cs
@@ -63,8 +63,15 @@ public interface IMigratableDatabaseContextProvider : IPersistenceContextProvide
///
/// Recreates the database by deleting and creating it again.
///
+ ///
+ /// If (the default), the database is dropped and created again from scratch.
+ /// If , the existing database is kept and only its schema is built via
+ /// migrations. Set this to 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.
+ ///
/// The disposable which should be disposed when the data creation process is finished.
- Task ReCreateDatabaseAsync();
+ Task ReCreateDatabaseAsync(bool dropExistingDatabase = true);
///
/// Resets the cache of this instance.
diff --git a/src/Persistence/InMemory/InMemoryPersistenceContextProvider.cs b/src/Persistence/InMemory/InMemoryPersistenceContextProvider.cs
index ace16c4..08538f5 100644
--- a/src/Persistence/InMemory/InMemoryPersistenceContextProvider.cs
+++ b/src/Persistence/InMemory/InMemoryPersistenceContextProvider.cs
@@ -127,7 +127,7 @@ public class InMemoryPersistenceContextProvider : IMigratableDatabaseContextProv
}
///
- public Task ReCreateDatabaseAsync()
+ public Task ReCreateDatabaseAsync(bool dropExistingDatabase = true)
{
this._repositoryProvider = new();
return Task.FromResult(new Disposable(() => { }));
diff --git a/src/Startup/Program.cs b/src/Startup/Program.cs
index 62745c2..d693697 100644
--- a/src/Startup/Program.cs
+++ b/src/Startup/Program.cs
@@ -51,6 +51,7 @@ internal sealed class Program : IDisposable
private readonly IDictionary _gameServers = new Dictionary();
private readonly IList _servers = new List();
private readonly Serilog.ILogger _logger;
+ private readonly IConfiguration _configuration;
private IHost? _serverHost;
@@ -65,8 +66,10 @@ internal sealed class Program : IDisposable
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true, true)
+ .AddEnvironmentVariables()
.Build();
+ this._configuration = configuration;
this._logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
@@ -475,8 +478,19 @@ internal sealed class Program : IDisposable
var contextProvider = new PersistenceContextProvider(loggerFactory, changeListener);
if (reinit || !await contextProvider.DatabaseExistsAsync().ConfigureAwait(false))
{
- this._logger.Information("The database is getting (re-)initialized...");
- using var update = await contextProvider.ReCreateDatabaseAsync().ConfigureAwait(false);
+ // The database can be provisioned externally (e.g. by a Kubernetes operator,
+ // 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("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);
this._logger.Information("...initialization finished.");
}
diff --git a/src/Startup/Readme.md b/src/Startup/Readme.md
index 66111ce..6397918 100644
--- a/src/Startup/Readme.md
+++ b/src/Startup/Readme.md
@@ -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
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
**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_ADMIN_USER | User name 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
diff --git a/src/Startup/appsettings.json b/src/Startup/appsettings.json
index 4d0436a..ed1294e 100644
--- a/src/Startup/appsettings.json
+++ b/src/Startup/appsettings.json
@@ -1,6 +1,9 @@
{
"DetailedErrors": true,
"AllowedHosts": "*",
+ "Database": {
+ "AssumeExternallyProvisioned": false
+ },
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {