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:
@@ -152,21 +152,30 @@ 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
|
||||||
{
|
{
|
||||||
try
|
if (dropExistingDatabase)
|
||||||
{
|
{
|
||||||
await using var installationContext = new EntityDataContext();
|
try
|
||||||
await installationContext.Database.EnsureDeletedAsync().ConfigureAwait(false);
|
{
|
||||||
}
|
await using var installationContext = new EntityDataContext();
|
||||||
catch (NpgsqlException)
|
await installationContext.Database.EnsureDeletedAsync().ConfigureAwait(false);
|
||||||
{
|
}
|
||||||
// That's expected for a fresh database
|
catch (NpgsqlException)
|
||||||
|
{
|
||||||
|
// That's expected for a fresh database
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);
|
await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(() => { }));
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user