diff --git a/build/Version.props b/build/Version.props index 12ef300ad6..e68888800a 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,7 +2,7 @@ - 4.3.2 + 4.4.0 7.0.0 7.3.0 5.2.2 diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index ba23173db1..2a82485356 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task Disconnect(CancellationToken cancellationToken) + protected override async Task DisconnectImpl(CancellationToken cancellationToken) { Logger.LogTrace("Disconnecting..."); if (!Connected) @@ -337,7 +337,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Title = "Code Deployment", Footer = new EmbedFooterBuilder { - Text = "In progress... ETA" + Text = $"In progress...{(estimatedCompletionTime.HasValue ? " ETA" : String.Empty)}" }, Timestamp = estimatedCompletionTime }; @@ -350,7 +350,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } var message = await channel.SendMessageAsync( - String.Empty, + "DM: Deployment in Progress...", false, builder.Build(), new RequestOptions @@ -361,7 +361,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return async (errorMessage, dreamMakerOutput) => { - builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed"; + var completionString = errorMessage == null ? "Succeeded" : "Failed"; + builder.Footer.Text = completionString; builder.Color = errorMessage == null ? Color.Green : Color.Red; builder.Timestamp = DateTimeOffset.Now; builder.Description = errorMessage == null @@ -394,7 +395,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers try { await channel.SendMessageAsync( - String.Empty, + $"DM: Deployment {completionString}!", false, builder.Build()) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index c06c22844f..cca006f663 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Task Connect(CancellationToken cancellationToken); /// - /// Gracefully disconnects the provider. Implies a call to + /// Gracefully disconnects the provider. Permanently stops the reconnection timer. /// /// The for the operation /// A representing the running operation diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 67e85e152a..014a5d9d63 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -346,7 +346,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public override async Task Disconnect(CancellationToken cancellationToken) + protected override async Task DisconnectImpl(CancellationToken cancellationToken) { if (!Connected) return; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 9f686bedca..070db5a8ef 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -87,8 +87,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public abstract Task Connect(CancellationToken cancellationToken); + /// + /// Gracefully disconnects the provider. + /// + /// The for the operation. + /// A representing the running operation. + protected abstract Task DisconnectImpl(CancellationToken cancellationToken); + /// - public abstract Task Disconnect(CancellationToken cancellationToken); + public async Task Disconnect(CancellationToken cancellationToken) + { + await StopReconnectionTimer().ConfigureAwait(false); + await DisconnectImpl(cancellationToken).ConfigureAwait(false); + } /// public abstract Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index e0045ccbc9..d76ab47826 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -67,6 +67,11 @@ namespace Tgstation.Server.Host.Components /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the + /// + readonly IDatabaseSeeder databaseSeeder; + /// /// The for the /// @@ -113,6 +118,7 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The value of public InstanceManager( @@ -124,6 +130,7 @@ namespace Tgstation.Server.Host.Components IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, + IDatabaseSeeder databaseSeeder, IOptions generalConfigurationOptions, ILogger logger) { @@ -135,6 +142,7 @@ namespace Tgstation.Server.Host.Components this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -274,7 +282,7 @@ namespace Tgstation.Server.Host.Components { CheckSystemCompatibility(); var factoryStartup = instanceFactory.StartAsync(cancellationToken); - await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); + await databaseSeeder.Initialize(databaseContext, cancellationToken).ConfigureAwait(false); await jobManager.StartAsync(cancellationToken).ConfigureAwait(false); var dbInstances = databaseContext .Instances @@ -323,7 +331,7 @@ namespace Tgstation.Server.Host.Components // downgrade the db if necessary if (downgradeVersion != null) - await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false); + await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 772083a05c..5c47fbd619 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers { var query = DatabaseContext.ChatBots .AsQueryable() - .Where(x => x.Id == id) + .Where(x => x.Id == id && x.InstanceId == Instance.Id) .Include(x => x.Channels); var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 5e96354cfb..3d75f56a7a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -18,6 +18,7 @@ using Serilog.Formatting.Display; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; +using System.Reflection; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -207,10 +208,23 @@ namespace Tgstation.Server.Host.Core void AddTypedContext() where TContext : DatabaseContext { - services.AddDbContext(builder => + // HACK HACK HACK HACK HACK + const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith); + var configureFunction = typeof(TContext).GetMethod( + nameof(SqlServerDatabaseContext.ConfigureWith), + BindingFlags.Public | BindingFlags.Static); + + if (configureFunction == null) + throw new InvalidOperationException($"Context type {typeof(TContext).FullName} missing static {ConfigureMethodName} function!"); + + services.AddDbContextPool((serviceProvider, builder) => { if (hostingEnvironment.IsDevelopment()) builder.EnableSensitiveDataLogging(); + + var databaseConfigOptions = serviceProvider.GetRequiredService>(); + var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!"); + configureFunction.Invoke(null, new object[] { builder, databaseConfig }); }); services.AddScoped(x => x.GetRequiredService()); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index b75baf3e09..17cfaa3323 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Globalization; using System.Linq; @@ -19,11 +18,8 @@ namespace Tgstation.Server.Host.Database /// Backend abstract implementation of /// #pragma warning disable CA1506 // TODO: Decomplexify - abstract class DatabaseContext : DbContext, IDatabaseContext + public abstract class DatabaseContext : DbContext, IDatabaseContext { - /// - public DatabaseType DatabaseType => DatabaseConfiguration.DatabaseType; - /// /// The s in the . /// @@ -99,16 +95,6 @@ namespace Tgstation.Server.Host.Database /// public DbSet RevInfoTestMerges { get; set; } - /// - /// The for the - /// - protected ILogger Logger { get; } - - /// - /// The for the - /// - protected DatabaseConfiguration DatabaseConfiguration { get; } - /// /// The for the / foreign key. /// @@ -153,11 +139,6 @@ namespace Tgstation.Server.Host.Database /// IDatabaseCollection IDatabaseContext.WatchdogReattachInformations => watchdogReattachInformationsCollection; - /// - /// The for the - /// - readonly IDatabaseSeeder databaseSeeder; - /// /// Backing field for . /// @@ -227,15 +208,8 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the . - /// The containing the value of - /// The value of - /// The value of - public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions) + public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { - DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); - this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - usersCollection = new DatabaseCollection(Users); instancesCollection = new DatabaseCollection(Instances); instanceUsersCollection = new DatabaseCollection(InstanceUsers); @@ -254,8 +228,9 @@ namespace Tgstation.Server.Host.Database /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - // Setup our more complex database relations - Logger.LogTrace("Building entity framework context..."); + if (modelBuilder == null) + throw new ArgumentNullException(nameof(modelBuilder)); + base.OnModelCreating(modelBuilder); var userModel = modelBuilder.Entity(); @@ -306,52 +281,41 @@ namespace Tgstation.Server.Host.Database } /// - public async Task Initialize(CancellationToken cancellationToken) + public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken); + + /// + public Task Drop(CancellationToken cancellationToken) => Database.EnsureDeletedAsync(cancellationToken); + + /// + public async Task Migrate(ILogger logger, CancellationToken cancellationToken) { - ValidateDatabaseType(); - - if (DatabaseConfiguration.DropDatabase) - { - Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database..."); - await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false); - } - + if (logger == null) + throw new ArgumentNullException(nameof(logger)); var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); var wasEmpty = !migrations.Any(); if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any()) { - Logger.LogInformation("Migrating database..."); + logger.LogInformation("Migrating database..."); await Database.MigrateAsync(cancellationToken).ConfigureAwait(false); } else - Logger.LogDebug("No migrations to apply."); + logger.LogDebug("No migrations to apply"); wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0; - if (wasEmpty) - { - Logger.LogInformation("Seeding database..."); - await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false); - } - else - { - if (DatabaseConfiguration.ResetAdminPassword) - { - Logger.LogWarning("Enabling and resetting admin password due to configuration!"); - await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false); - } - - await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false); - } + return wasEmpty; } /// - public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken); - - /// - public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken) + public async Task SchemaDowngradeForServerVersion( + ILogger logger, + Version version, + DatabaseType currentDatabaseType, + CancellationToken cancellationToken) { + if(logger == null) + throw new ArgumentNullException(nameof(logger)); if (version == null) throw new ArgumentNullException(nameof(version)); if (version < new Version(4, 0)) @@ -360,23 +324,23 @@ namespace Tgstation.Server.Host.Database // Update this with new migrations as they are made string targetMigration = null; - if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0)) + if (currentDatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0)) throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!"); if (version < new Version(4, 1, 0)) throw new NotSupportedException("Cannot migrate below version 4.1.0!"); if (version < new Version(4, 2, 0)) - targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); + targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); if (targetMigration == null) { - Logger.LogDebug("No down migration required."); + logger.LogDebug("No down migration required."); return; } string migrationSubstitution; - switch (DatabaseType) + switch (currentDatabaseType) { case DatabaseType.SqlServer: // already setup @@ -393,7 +357,7 @@ namespace Tgstation.Server.Host.Database migrationSubstitution = "PG{0}"; break; default: - throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}"); + throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"); } if (migrationSubstitution != null) @@ -403,20 +367,15 @@ namespace Tgstation.Server.Host.Database var dbServiceProvider = ((IInfrastructure)Database).Instance; var migrator = dbServiceProvider.GetRequiredService(); - Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration); + logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration); try { await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false); } catch (Exception e) { - Logger.LogCritical("Failed to migrate! Exception: {0}", e); + logger.LogCritical("Failed to migrate! Exception: {0}", e); } } - - /// - /// Ensure the is correct for the . - /// - protected abstract void ValidateDatabaseType(); } } diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index e7173b0067..4bbc995197 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -1,9 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -23,15 +26,41 @@ namespace Tgstation.Server.Host.Database /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The used for s. + /// + readonly ILogger databaseLogger; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly DatabaseConfiguration databaseConfiguration; + /// /// Construct a /// /// The value of /// The value of . - public DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier) + /// The containing the value of . + /// The value of + /// The value of . + public DatabaseSeeder( + ICryptographySuite cryptographySuite, + IPlatformIdentifier platformIdentifier, + IOptions databaseConfigurationOptions, + ILogger databaseLogger, + ILogger logger) { this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); + this.databaseLogger = databaseLogger ?? throw new ArgumentNullException(nameof(databaseLogger)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// @@ -53,15 +82,25 @@ namespace Tgstation.Server.Host.Database databaseContext.Users.Add(admin); } - /// - public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Initially seed a given + /// + /// The to seed + /// The for the operation + /// A representing the running operation + async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { SeedAdminUser(databaseContext); await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - /// - public async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Correct invalid database data caused by previous versions. + /// + /// The to sanitize. + /// The for the operation. + /// A representing the running operation. + async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false); if (admin != null) @@ -88,8 +127,13 @@ namespace Tgstation.Server.Host.Database await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - /// - public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Changes the admin password in back to it's default and enables the account + /// + /// The to reset the admin password for + /// The for the operation + /// A representing the running operation + async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false); if (admin != null) @@ -120,5 +164,46 @@ namespace Tgstation.Server.Host.Database return admin; } + + /// + public async Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken) + { + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); + + if (databaseConfiguration.DropDatabase) + { + logger.LogCritical("DropDatabase configuration option set! Dropping any existing database..."); + await databaseContext.Drop(cancellationToken).ConfigureAwait(false); + } + + var wasEmpty = await databaseContext.Migrate(databaseLogger, cancellationToken).ConfigureAwait(false); + if (wasEmpty) + { + logger.LogInformation("Seeding database..."); + await SeedDatabase(databaseContext, cancellationToken).ConfigureAwait(false); + } + else + { + if (databaseConfiguration.ResetAdminPassword) + { + logger.LogWarning("Enabling and resetting admin password due to configuration!"); + await ResetAdminPassword(databaseContext, cancellationToken).ConfigureAwait(false); + } + + await SanitizeDatabase(databaseContext, cancellationToken).ConfigureAwait(false); + } + } + + /// + public Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken) + { + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); + if (downgradeVersion == null) + throw new ArgumentNullException(nameof(downgradeVersion)); + + return databaseContext.SchemaDowngradeForServerVersion(databaseLogger, downgradeVersion, databaseConfiguration.DatabaseType, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs deleted file mode 100644 index 5b39d9e63d..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - /// for creating s. - /// - sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public MySqlDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new MySqlDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.MariaDB, - "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs deleted file mode 100644 index 2af7d7c939..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public PostgresSqlDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new PostgresSqlDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.PostgresSql, - "Application Name=tgstation-server;Host=127.0.0.1;Password=qCkWimNgLfWwpr7TnUHs;Username=postgres;Database=TGS_Design"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs deleted file mode 100644 index 427d84ba8a..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - /// for creating s. - /// - sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public SqlServerDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new SqlServerDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.SqlServer, - "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs index 91de1284f2..330cbc39f3 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs @@ -1,11 +1,7 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Database.Design { @@ -18,16 +14,13 @@ namespace Tgstation.Server.Host.Database.Design public SqliteDatabaseContext CreateDbContext(string[] args) { using var loggerFactory = new LoggerFactory(); - return new SqliteDatabaseContext( - new DbContextOptions(), + var config = DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( DatabaseType.Sqlite, - "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); + "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"); + SqliteDatabaseContext.DesignTime = config.Value.DesignTime; + return new SqliteDatabaseContext( + new DbContextOptions()); } } } diff --git a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs index 90c472d5b7..36ce44583b 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -12,11 +13,6 @@ namespace Tgstation.Server.Host.Database /// public interface IDatabaseContext { - /// - /// The . - /// - DatabaseType DatabaseType { get; } - /// /// The s in the /// @@ -89,19 +85,33 @@ namespace Tgstation.Server.Host.Database /// A representing the running operation Task Save(CancellationToken cancellationToken); + /// + /// Attempts to delete all tables and drop the database in use. + /// + /// The for the operation. + /// A representing the running operation. + Task Drop(CancellationToken cancellationToken); + /// /// Creates and migrates the /// + /// The to use. /// The for the operation - /// A representing the running operation - Task Initialize(CancellationToken cancellationToken); + /// A resulting in if the database should be seeded, otherwise. + Task Migrate(ILogger logger, CancellationToken cancellationToken); /// /// Attempt to downgrade the schema to the migration used for a given server /// + /// The to use. /// The tgstation-server that the schema should downgrade for + /// The in use. /// The for the operation /// A representing the running operation - Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken); + Task SchemaDowngradeForServerVersion( + ILogger logger, + Version version, + DatabaseType currentDatabaseType, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs index 7ff210979a..8e218c5e61 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs @@ -1,35 +1,29 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.Database { /// - /// For initially seeding a database + /// For initially setting up a database. /// interface IDatabaseSeeder { /// - /// Initially seed a given + /// Setup up a given . /// - /// The to seed - /// The for the operation - /// A representing the running operation - Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken); - - /// - /// Correct invalid database data caused by previous versions. - /// - /// The to sanitize. + /// The to setup. /// The for the operation. /// A representing the running operation. - Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken); + Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken); /// - /// Changes the admin password in back to it's default and enables the account + /// Migrate a given down. /// - /// The to reset the admin password for - /// The for the operation - /// A representing the running operation - Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken); + /// The to downgrade. + /// The migration to downgrade the to. + /// The for the operation. + /// A representing the running operation. + Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs index 78e936352c..46bd93309c 100644 --- a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs @@ -1,7 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MySql.Data.MySqlClient; using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using System; using Tgstation.Server.Host.Configuration; @@ -20,42 +17,37 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public MySqlDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public MySqlDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - var stringDeconstructor = new MySqlConnectionStringBuilder - { - ConnectionString = DatabaseConfiguration.ConnectionString - }; - if (stringDeconstructor.Server == "localhost") - Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!"); + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.MariaDB && databaseConfiguration.DatabaseType != DatabaseType.MySql) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(MySqlDatabaseContext)}!"); + options.UseMySql( - DatabaseConfiguration.ConnectionString, + databaseConfiguration.ConnectionString, mySqlOptions => { mySqlOptions.EnableRetryOnFailure(); - if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) + if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion)) mySqlOptions.ServerVersion( - Version.Parse(DatabaseConfiguration.ServerVersion), - DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB + Version.Parse(databaseConfiguration.ServerVersion), + databaseConfiguration.DatabaseType == DatabaseType.MariaDB ? ServerType.MariaDb : ServerType.MySql); }); } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.MariaDB && DatabaseType != DatabaseType.MySql) - throw new InvalidOperationException("Invalid DatabaseType for MySqlDatabaseContext!"); - } } } diff --git a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs index 6514bc5333..090ec770ca 100644 --- a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using Tgstation.Server.Host.Configuration; @@ -18,36 +16,34 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the public PostgresSqlDatabaseContext( - DbContextOptions dbContextOptions, - IOptions databaseConfiguration, - IDatabaseSeeder databaseSeeder, - ILogger logger) - : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + DbContextOptions dbContextOptions) + : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseNpgsql(DatabaseConfiguration.ConnectionString, options => + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.PostgresSql) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(PostgresSqlDatabaseContext)}!"); + + options.UseNpgsql(databaseConfiguration.ConnectionString, options => { options.EnableRetryOnFailure(); - if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) + if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion)) options.SetPostgresVersion( - Version.Parse(DatabaseConfiguration.ServerVersion)); + Version.Parse(databaseConfiguration.ServerVersion)); }); } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.PostgresSql) - throw new InvalidOperationException("Invalid DatabaseType for PostgresSqlDatabaseContext!"); - } } } diff --git a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs index 42c6aedc4e..3b5eb77b83 100644 --- a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using Tgstation.Server.Host.Configuration; @@ -15,24 +13,25 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public SqlServerDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public SqlServerDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseSqlServer(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); - } + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.SqlServer) - throw new InvalidOperationException("Invalid DatabaseType for SqlServerDatabaseContext!"); + if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqlServerDatabaseContext)}!"); + + options.UseSqlServer(databaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); } } } diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 229a30a8dc..f23aedca95 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -1,7 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Linq; using Tgstation.Server.Host.Configuration; @@ -9,25 +7,39 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for Sqlite. + /// for SQLite. /// sealed class SqliteDatabaseContext : DatabaseContext { + /// + /// Static property to receive the configured value of . + /// + public static bool DesignTime { get; set; } + /// /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public SqliteDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public SqliteDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseSqlite(DatabaseConfiguration.ConnectionString); + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!"); + + DesignTime = databaseConfiguration.DesignTime; + options.UseSqlite(databaseConfiguration.ConnectionString); } /// @@ -43,7 +55,7 @@ namespace Tgstation.Server.Host.Database // use the DateTimeOffsetToBinaryConverter // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 // This only supports millisecond precision, but should be sufficient for most use cases. - if (!DatabaseConfiguration.DesignTime) + if (!DesignTime) foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType @@ -57,12 +69,5 @@ namespace Tgstation.Server.Host.Database .HasConversion(new DateTimeOffsetToBinaryConverter()); } } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.Sqlite) - throw new InvalidOperationException("Invalid DatabaseType for SqliteDatabaseContext!"); - } } }