Skip to content

Commit

Permalink
Merge pull request #1923 from tgstation/SkipDMAPIValidation [APIDeplo…
Browse files Browse the repository at this point in the history
…y][NugetDeploy]

Allow DMAPI Validation to be fully skipped
  • Loading branch information
Cyberboss authored Sep 8, 2024
2 parents 2df11b4 + 6c0f9fa commit 0795258
Show file tree
Hide file tree
Showing 29 changed files with 4,892 additions and 120 deletions.
6 changes: 3 additions & 3 deletions build/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<PropertyGroup>
<TgsCoreVersion>6.10.0</TgsCoreVersion>
<TgsConfigVersion>5.2.0</TgsConfigVersion>
<TgsApiVersion>10.8.0</TgsApiVersion>
<TgsApiVersion>10.9.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>14.0.0</TgsApiLibraryVersion>
<TgsClientVersion>17.0.0</TgsClientVersion>
<TgsApiLibraryVersion>14.1.0</TgsApiLibraryVersion>
<TgsClientVersion>17.1.0</TgsClientVersion>
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
Expand Down
23 changes: 23 additions & 0 deletions src/Tgstation.Server.Api/Models/DMApiValidationMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// The DMAPI validation setting for deployments.
/// </summary>
public enum DMApiValidationMode
{
/// <summary>
/// DMAPI validation is performed but not required for the deployment to succeed.
/// </summary>
Optional,

/// <summary>
/// DMAPI validation must suceed for the deployment to succeed.
/// </summary>
Required,

/// <summary>
/// DMAPI validation will not be performed and no DMAPI features will be available in the deployment.
/// </summary>
Skipped,
}
}
11 changes: 10 additions & 1 deletion src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Tgstation.Server.Api.Models.Internal
{
Expand Down Expand Up @@ -29,11 +30,19 @@ public abstract class DreamMakerSettings
public DreamDaemonSecurity? ApiValidationSecurityLevel { get; set; }

/// <summary>
/// If API validation should be required for a deployment to succeed.
/// If API validation should be required for a deployment to succeed. Must not be set on mutation if <see cref="DMApiValidationMode"/> is set.
/// </summary>
[Required]
[NotMapped]
[Obsolete($"Use {nameof(DMApiValidationMode)} instead.")]
public bool? RequireDMApiValidation { get; set; }

/// <summary>
/// The current <see cref="Models.DMApiValidationMode"/>. Must not be set on mutation if <see cref="RequireDMApiValidation"/> is set.
/// </summary>
[Required]
public DMApiValidationMode? DMApiValidationMode { get; set; }

/// <summary>
/// Amount of time before an in-progress deployment is cancelled.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Tgstation.Server.Api/Rights/DreamMakerRights.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public enum DreamMakerRights : ulong
SetSecurityLevel = 1 << 6,

/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.RequireDMApiValidation"/>.
/// User may modify <see cref="Models.Internal.DreamMakerSettings.DMApiValidationMode"/> and <see cref="Models.Internal.DreamMakerSettings.RequireDMApiValidation"/>.
/// </summary>
SetApiValidationRequirement = 1 << 7,

Expand Down
20 changes: 16 additions & 4 deletions src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,14 +648,14 @@ await eventConsumer.HandleEvent(
ErrorCode.DeploymentExitCode,
new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));

progressReporter.StageName = "Validating DMAPI";
await VerifyApi(
launchParameters.StartupTimeout!.Value,
dreamMakerSettings.ApiValidationSecurityLevel!.Value,
job,
progressReporter,
engineLock,
dreamMakerSettings.ApiValidationPort!.Value,
dreamMakerSettings.RequireDMApiValidation!.Value,
dreamMakerSettings.DMApiValidationMode!.Value,
launchParameters.LogOutput!.Value,
cancellationToken);
}
Expand Down Expand Up @@ -767,22 +767,34 @@ async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? est
/// <param name="timeout">The timeout in seconds for validation.</param>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API.</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/>.</param>
/// <param name="engineLock">The current <see cref="IEngineExecutableLock"/>.</param>
/// <param name="portToUse">The port to use for API validation.</param>
/// <param name="requireValidate">If the API validation is required to complete the deployment.</param>
/// <param name="validationMode">The <see cref="DMApiValidationMode"/>.</param>
/// <param name="logOutput">If output should be logged to the DreamDaemon Diagnostics folder.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask VerifyApi(
uint timeout,
DreamDaemonSecurity securityLevel,
Models.CompileJob job,
JobProgressReporter progressReporter,
IEngineExecutableLock engineLock,
ushort portToUse,
bool requireValidate,
DMApiValidationMode validationMode,
bool logOutput,
CancellationToken cancellationToken)
{
if (validationMode == DMApiValidationMode.Skipped)
{
logger.LogDebug("Skipping DMAPI validation");
job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
return;
}

progressReporter.StageName = "Validating DMAPI";

var requireValidate = validationMode == DMApiValidationMode.Required;
logger.LogTrace("Verifying {possiblyRequired}DMAPI...", requireValidate ? "required " : String.Empty);
var launchParameters = new DreamDaemonLaunchParameters
{
Expand Down
20 changes: 18 additions & 2 deletions src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,28 @@ public async ValueTask<IActionResult> Update([FromBody] DreamMakerRequest model,
hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
}

if (model.RequireDMApiValidation.HasValue)
#pragma warning disable CS0618 // Type or member is obsolete
bool? legacyRequireDMApiValidation = model.RequireDMApiValidation;
#pragma warning restore CS0618 // Type or member is obsolete
if (legacyRequireDMApiValidation.HasValue)
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement))
return Forbid();

hostModel.RequireDMApiValidation = model.RequireDMApiValidation;
hostModel.DMApiValidationMode = legacyRequireDMApiValidation.Value
? DMApiValidationMode.Required
: DMApiValidationMode.Optional;
}

if (model.DMApiValidationMode.HasValue)
{
if (legacyRequireDMApiValidation.HasValue)
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));

if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement))
return Forbid();

hostModel.DMApiValidationMode = model.DMApiValidationMode;
}

if (model.Timeout.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ public async ValueTask<IActionResult> GrantPermissions(long id, CancellationToke
{
ApiValidationPort = dmPort,
ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
RequireDMApiValidation = true,
DMApiValidationMode = DMApiValidationMode.Required,
Timeout = TimeSpan.FromHours(1),
CompilerAdditionalArguments = null,
},
Expand Down
128 changes: 70 additions & 58 deletions src/Tgstation.Server.Host/Database/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,63 @@ public async ValueTask<bool> Migrate(ILogger<DatabaseContext> logger, Cancellati
return wasEmpty;
}

/// <inheritdoc />
public async ValueTask SchemaDowngradeForServerVersion(
ILogger<DatabaseContext> logger,
Version targetVersion,
DatabaseType currentDatabaseType,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(targetVersion);
if (targetVersion < new Version(4, 0))
throw new ArgumentOutOfRangeException(nameof(targetVersion), targetVersion, "Cannot migrate below version 4.0.0!");

if (currentDatabaseType == DatabaseType.PostgresSql && targetVersion < new Version(4, 3, 0))
throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");

if (currentDatabaseType == DatabaseType.MariaDB)
currentDatabaseType = DatabaseType.MySql; // Keeping switch expressions while avoiding `or` syntax from C#9

if (targetVersion < new Version(4, 1, 0))
throw new NotSupportedException("Cannot migrate below version 4.1.0!");

var targetMigration = GetTargetMigration(targetVersion, currentDatabaseType);

if (targetMigration == null)
{
logger.LogDebug("No down migration required.");
return;
}

// already setup
var migrationSubstitution = currentDatabaseType switch
{
DatabaseType.SqlServer => null, // already setup
DatabaseType.MySql => "MY{0}",
DatabaseType.Sqlite => "SL{0}",
DatabaseType.PostgresSql => "PG{0}",
_ => throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"),
};

if (migrationSubstitution != null)
targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration[2..]);

// even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();

logger.LogInformation("Migrating down to version {targetVersion}. Target: {targetMigration}", targetVersion, targetMigration);
try
{
await migrator.MigrateAsync(targetMigration, cancellationToken);
}
catch (Exception e)
{
logger.LogCritical(e, "Failed to migrate!");
}
}

/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand Down Expand Up @@ -393,45 +450,31 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
/// <summary>
/// Used by unit tests to remind us to setup the correct MSSQL migration downgrades.
/// </summary>
internal static readonly Type MSLatestMigration = typeof(MSAddOpenDreamTopicPort);
internal static readonly Type MSLatestMigration = typeof(MSAddDMApiValidationMode);

/// <summary>
/// Used by unit tests to remind us to setup the correct MYSQL migration downgrades.
/// </summary>
internal static readonly Type MYLatestMigration = typeof(MYAddOpenDreamTopicPort);
internal static readonly Type MYLatestMigration = typeof(MYAddDMApiValidationMode);

/// <summary>
/// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades.
/// </summary>
internal static readonly Type PGLatestMigration = typeof(PGAddOpenDreamTopicPort);
internal static readonly Type PGLatestMigration = typeof(PGAddDMApiValidationMode);

/// <summary>
/// Used by unit tests to remind us to setup the correct SQLite migration downgrades.
/// </summary>
internal static readonly Type SLLatestMigration = typeof(SLAddOpenDreamTopicPort);
internal static readonly Type SLLatestMigration = typeof(SLAddDMApiValidationMode);

/// <inheritdoc />
#pragma warning disable CA1502 // Cyclomatic complexity
public async ValueTask SchemaDowngradeForServerVersion(
ILogger<DatabaseContext> logger,
Version targetVersion,
DatabaseType currentDatabaseType,
CancellationToken cancellationToken)
/// <summary>
/// Gets the name of the migration to run for migrating down to a given <paramref name="targetVersion"/> for the <paramref name="currentDatabaseType"/>.
/// </summary>
/// <param name="targetVersion">The <see cref="Version"/> TGS is being migratied down to.</param>
/// <param name="currentDatabaseType">The currently running <see cref="DatabaseType"/>.</param>
/// <returns>The name of the migration to run on success, <see langword="null"/> otherwise.</returns>
private string? GetTargetMigration(Version targetVersion, DatabaseType currentDatabaseType)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(targetVersion);
if (targetVersion < new Version(4, 0))
throw new ArgumentOutOfRangeException(nameof(targetVersion), targetVersion, "Cannot migrate below version 4.0.0!");

if (currentDatabaseType == DatabaseType.PostgresSql && targetVersion < new Version(4, 3, 0))
throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");

if (currentDatabaseType == DatabaseType.MariaDB)
currentDatabaseType = DatabaseType.MySql; // Keeping switch expressions while avoiding `or` syntax from C#9

if (targetVersion < new Version(4, 1, 0))
throw new NotSupportedException("Cannot migrate below version 4.1.0!");

// Update this with new migrations as they are made
string? targetMigration = null;

Expand Down Expand Up @@ -603,42 +646,11 @@ public async ValueTask SchemaDowngradeForServerVersion(
DatabaseType.Sqlite => nameof(SLRemoveSoftColumns),
_ => BadDatabaseType(),
};

if (targetVersion < new Version(4, 2, 0))
targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);

if (targetMigration == null)
{
logger.LogDebug("No down migration required.");
return;
}

// already setup
var migrationSubstitution = currentDatabaseType switch
{
DatabaseType.SqlServer => null, // already setup
DatabaseType.MySql => "MY{0}",
DatabaseType.Sqlite => "SL{0}",
DatabaseType.PostgresSql => "PG{0}",
_ => throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"),
};

if (migrationSubstitution != null)
targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration[2..]);

// even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();

logger.LogInformation("Migrating down to version {targetVersion}. Target: {targetMigration}", targetVersion, targetMigration);
try
{
await migrator.MigrateAsync(targetMigration, cancellationToken);
}
catch (Exception e)
{
logger.LogCritical(e, "Failed to migrate!");
}
return targetMigration;
}
#pragma warning restore CA1502 // Cyclomatic complexity
}
}
Loading

0 comments on commit 0795258

Please sign in to comment.