tgstation-server
The /tg/station 13 server suite
DatabaseContext.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
13 
14 namespace Tgstation.Server.Host.Models
15 {
17  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
18  {
20  public DbSet<User> Users { get; set; }
21 
23  public DbSet<Instance> Instances { get; set; }
24 
26  public DbSet<CompileJob> CompileJobs { get; set; }
27 
29  public DbSet<RevisionInformation> RevisionInformations { get; set; }
30 
32  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
33 
35  public DbSet<ChatBot> ChatBots { get; set; }
36 
38  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
39 
41  public DbSet<RepositorySettings> RepositorySettings { get; set; }
42 
44  public DbSet<InstanceUser> InstanceUsers { get; set; }
45 
47  public DbSet<ChatChannel> ChatChannels { get; set; }
48 
50  public DbSet<Job> Jobs { get; set; }
51 
53  public DbSet<ReattachInformation> ReattachInformations { get; set; }
54 
56  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
57 
61  public DbSet<TestMerge> TestMerges { get; set; }
62 
66  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
67 
71  protected ILogger Logger { get; }
72 
77 
82 
90  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
91  {
92  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
93  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
94  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
95  }
96 
98  protected override void OnModelCreating(ModelBuilder modelBuilder)
99  {
100  Logger.LogTrace("Building entity framework context...");
101  base.OnModelCreating(modelBuilder);
102 
103  var userModel = modelBuilder.Entity<User>();
104  userModel.HasIndex(x => x.CanonicalName).IsUnique();
105  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
106 
107  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
108 
109  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
110 
111  var revInfo = modelBuilder.Entity<RevisionInformation>();
112  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
113  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
114  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
115  revInfo.HasIndex(x => x.CommitSha).IsUnique();
116 
117  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
118 
119  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
120 
121  var chatChannel = modelBuilder.Entity<ChatChannel>();
122  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
123  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
124  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
125 
126  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
127 
128  var instanceModel = modelBuilder.Entity<Instance>();
129  instanceModel.HasIndex(x => x.Path).IsUnique();
130  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
134  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
135  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
136  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
137  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
138  }
139 
141  public async Task Initialize(CancellationToken cancellationToken)
142  {
144  {
145  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
146  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
147  }
148 
149  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
150  var wasEmpty = !migrations.Any();
151 
152  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
153  {
154  Logger.LogInformation("Migrating database...");
155  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
156  }
157  else
158  Logger.LogDebug("No migrations to apply.");
159 
160  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
161 
162  if (wasEmpty)
163  {
164  Logger.LogInformation("Seeding database...");
165  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
166  }
168  {
169  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
170  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
171  }
172  }
173 
175  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
176 
181  protected abstract bool UseMySQLMigrations();
182 
184  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
222  }
223 }
For initially seeding a database
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
override void OnModelCreating(ModelBuilder modelBuilder)
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
DatabaseContext(DbContextOptions< TParentContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext<TParentContext>
Configuration options for the Models.DatabaseContext<TParentContext>
bool DropDatabase
If the database should be deleted on application startup. Should not be used in production! ...
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>