Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow DMAPI Validation to be fully skipped #1923

Merged
merged 5 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading