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": {