Merge pull request #10880 from umbraco/v9/bugfix/suppress-notifications-migrations
Suppress scope notifications during migrations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Notifications
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Used to notify when the core runtime can do an unattended upgrade.
|
||||
/// </summary>
|
||||
|
||||
@@ -61,6 +61,5 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="ContentEditing" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
builder.Services.AddTransient<InstallStepCollection>();
|
||||
builder.Services.AddUnique<InstallHelper>();
|
||||
|
||||
builder.Services.AddTransient<PackageMigrationRunner>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -61,7 +61,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
return Task.FromResult((InstallSetupResult)null);
|
||||
}
|
||||
|
||||
public override bool RequiresExecution(object model)
|
||||
|
||||
@@ -137,6 +137,9 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
|
||||
{
|
||||
var installState = InstallState.Unknown;
|
||||
|
||||
// TODO: we need to do a null check here since this could be entirely missing and we end up with a null ref
|
||||
// exception in the installer.
|
||||
|
||||
var databaseSettings = _connectionStrings.UmbracoConnectionString;
|
||||
|
||||
var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured;
|
||||
|
||||
108
src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs
Normal file
108
src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Migrations;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Notifications;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Install
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the package migration plans
|
||||
/// </summary>
|
||||
public class PackageMigrationRunner
|
||||
{
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly PendingPackageMigrations _pendingPackageMigrations;
|
||||
private readonly IMigrationPlanExecutor _migrationPlanExecutor;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Dictionary<string, PackageMigrationPlan> _packageMigrationPlans;
|
||||
|
||||
public PackageMigrationRunner(
|
||||
IProfilingLogger profilingLogger,
|
||||
IScopeProvider scopeProvider,
|
||||
PendingPackageMigrations pendingPackageMigrations,
|
||||
PackageMigrationPlanCollection packageMigrationPlans,
|
||||
IMigrationPlanExecutor migrationPlanExecutor,
|
||||
IKeyValueService keyValueService,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_profilingLogger = profilingLogger;
|
||||
_scopeProvider = scopeProvider;
|
||||
_pendingPackageMigrations = pendingPackageMigrations;
|
||||
_migrationPlanExecutor = migrationPlanExecutor;
|
||||
_keyValueService = keyValueService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all migration plans for a package name if any are pending.
|
||||
/// </summary>
|
||||
/// <param name="packageName"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<ExecutedMigrationPlan> RunPackageMigrationsIfPending(string packageName)
|
||||
{
|
||||
IReadOnlyDictionary<string, string> keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
|
||||
IReadOnlyList<string> pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues);
|
||||
|
||||
IEnumerable<string> packagePlans = _packageMigrationPlans.Values
|
||||
.Where(x => x.PackageName.InvariantEquals(packageName))
|
||||
.Where(x => pendingMigrations.Contains(x.Name))
|
||||
.Select(x => x.Name);
|
||||
|
||||
return RunPackagePlans(packagePlans);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the all specified package migration plans and publishes a <see cref="MigrationPlansExecutedNotification"/>
|
||||
/// if all are successful.
|
||||
/// </summary>
|
||||
/// <param name="plansToRun"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception">If any plan fails it will throw an exception.</exception>
|
||||
public IEnumerable<ExecutedMigrationPlan> RunPackagePlans(IEnumerable<string> plansToRun)
|
||||
{
|
||||
var results = new List<ExecutedMigrationPlan>();
|
||||
|
||||
// Create an explicit scope around all package migrations so they are
|
||||
// all executed in a single transaction. If one package migration fails,
|
||||
// none of them will be committed. This is intended behavior so we can
|
||||
// ensure when we publish the success notification that is is done when they all succeed.
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
foreach (var migrationName in plansToRun)
|
||||
{
|
||||
if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan plan))
|
||||
{
|
||||
throw new InvalidOperationException("Cannot find package migration plan " + migrationName);
|
||||
}
|
||||
|
||||
using (_profilingLogger.TraceDuration<PackageMigrationRunner>(
|
||||
"Starting unattended package migration for " + migrationName,
|
||||
"Unattended upgrade completed for " + migrationName))
|
||||
{
|
||||
var upgrader = new Upgrader(plan);
|
||||
// This may throw, if so the transaction will be rolled back
|
||||
results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var executedPlansNotification = new MigrationPlansExecutedNotification(results);
|
||||
_eventAggregator.Publish(executedPlansNotification);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,47 +7,40 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
|
||||
using Umbraco.Cms.Infrastructure.Runtime;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Migrations;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Install
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <see cref="RuntimeUnattendedUpgradeNotification"/> to execute the unattended Umbraco upgrader
|
||||
/// or the unattended Package migrations runner.
|
||||
/// </summary>
|
||||
public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpgradeNotification>
|
||||
{
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
private readonly IUmbracoVersion _umbracoVersion;
|
||||
private readonly DatabaseBuilder _databaseBuilder;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
|
||||
private readonly IMigrationPlanExecutor _migrationPlanExecutor;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly PackageMigrationRunner _packageMigrationRunner;
|
||||
|
||||
public UnattendedUpgrader(
|
||||
IProfilingLogger profilingLogger,
|
||||
IUmbracoVersion umbracoVersion,
|
||||
DatabaseBuilder databaseBuilder,
|
||||
IRuntimeState runtimeState,
|
||||
PackageMigrationPlanCollection packageMigrationPlans,
|
||||
IMigrationPlanExecutor migrationPlanExecutor,
|
||||
IScopeProvider scopeProvider,
|
||||
IKeyValueService keyValueService)
|
||||
PackageMigrationRunner packageMigrationRunner)
|
||||
{
|
||||
_profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger));
|
||||
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
|
||||
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
|
||||
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
|
||||
_packageMigrationPlans = packageMigrationPlans;
|
||||
_migrationPlanExecutor = migrationPlanExecutor;
|
||||
_scopeProvider = scopeProvider;
|
||||
_keyValueService = keyValueService;
|
||||
_packageMigrationRunner = packageMigrationRunner;
|
||||
}
|
||||
|
||||
public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken)
|
||||
@@ -69,7 +61,6 @@ namespace Umbraco.Cms.Infrastructure.Install
|
||||
{
|
||||
var innerException = new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message);
|
||||
_runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete;
|
||||
@@ -83,73 +74,36 @@ namespace Umbraco.Cms.Infrastructure.Install
|
||||
{
|
||||
throw new InvalidOperationException($"The required key {RuntimeState.PendingPacakgeMigrationsStateKey} does not exist in startup state");
|
||||
}
|
||||
|
||||
|
||||
if (pendingMigrations.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No pending migrations found but the runtime level reason is " + Core.RuntimeLevelReason.UpgradePackageMigrations);
|
||||
}
|
||||
|
||||
var exceptions = new List<Exception>();
|
||||
var packageMigrationsPlans = _packageMigrationPlans.ToDictionary(x => x.Name);
|
||||
|
||||
foreach (var migrationName in pendingMigrations)
|
||||
{
|
||||
if (!packageMigrationsPlans.TryGetValue(migrationName, out PackageMigrationPlan plan))
|
||||
{
|
||||
throw new InvalidOperationException("Cannot find package migration plan " + migrationName);
|
||||
}
|
||||
|
||||
using (_profilingLogger.TraceDuration<UnattendedUpgrader>(
|
||||
"Starting unattended package migration for " + migrationName,
|
||||
"Unattended upgrade completed for " + migrationName))
|
||||
{
|
||||
var upgrader = new Upgrader(plan);
|
||||
|
||||
try
|
||||
{
|
||||
upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(new UnattendedInstallException("Unattended package migration failed for " + migrationName, ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
{
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors;
|
||||
SetRuntimeErrors(exceptions);
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
IEnumerable<ExecutedMigrationPlan> result = _packageMigrationRunner.RunPackagePlans(pendingMigrations);
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete;
|
||||
}
|
||||
catch (Exception ex )
|
||||
{
|
||||
SetRuntimeError(ex);
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void SetRuntimeErrors(List<Exception> exception)
|
||||
{
|
||||
Exception innerException;
|
||||
if (exception.Count == 1)
|
||||
{
|
||||
innerException = exception[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
innerException = new AggregateException(exception);
|
||||
}
|
||||
|
||||
_runtimeState.Configure(
|
||||
private void SetRuntimeError(Exception exception)
|
||||
=> _runtimeState.Configure(
|
||||
RuntimeLevel.BootFailed,
|
||||
RuntimeLevelReason.BootFailedOnException,
|
||||
innerException);
|
||||
}
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
{
|
||||
public class ExecutedMigrationPlan
|
||||
{
|
||||
public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState)
|
||||
{
|
||||
Plan = plan;
|
||||
InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState));
|
||||
FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState));
|
||||
}
|
||||
|
||||
public MigrationPlan Plan { get; }
|
||||
public string InitialState { get; }
|
||||
public string FinalState { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface for migration expressions
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Cms.Infrastructure.Migrations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Migrations
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
using Type = System.Type;
|
||||
|
||||
@@ -217,6 +215,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
public virtual MigrationPlan AddPostMigration<TMigration>()
|
||||
where TMigration : MigrationBase
|
||||
{
|
||||
// TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead.
|
||||
// The only place we use this is to clear cookies in the installer which could be done
|
||||
// via notification. Then we can clean up all the code related to post migrations which is
|
||||
// not insignificant.
|
||||
|
||||
_postMigrationTypes.Add(typeof(TMigration));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
private readonly IMigrationBuilder _migrationBuilder;
|
||||
private readonly ILogger<MigrationPlanExecutor> _logger;
|
||||
|
||||
public MigrationPlanExecutor(IScopeProvider scopeProvider, ILoggerFactory loggerFactory, IMigrationBuilder migrationBuilder)
|
||||
public MigrationPlanExecutor(
|
||||
IScopeProvider scopeProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IMigrationBuilder migrationBuilder)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -40,51 +43,58 @@ namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
|
||||
_logger.LogInformation("Starting '{MigrationName}'...", plan.Name);
|
||||
|
||||
var origState = fromState ?? string.Empty;
|
||||
fromState ??= string.Empty;
|
||||
var nextState = fromState;
|
||||
|
||||
_logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin" : origState);
|
||||
_logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState);
|
||||
|
||||
if (!plan.Transitions.TryGetValue(origState, out MigrationPlan.Transition transition))
|
||||
if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition transition))
|
||||
{
|
||||
plan.ThrowOnUnknownInitialState(origState);
|
||||
plan.ThrowOnUnknownInitialState(nextState);
|
||||
}
|
||||
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
var context = new MigrationContext(plan, scope.Database, _loggerFactory.CreateLogger<MigrationContext>());
|
||||
|
||||
while (transition != null)
|
||||
// We want to suppress scope (service, etc...) notifications during a migration plan
|
||||
// execution. This is because if a package that doesn't have their migration plan
|
||||
// executed is listening to service notifications to perform some persistence logic,
|
||||
// that packages notification handlers may explode because that package isn't fully installed yet.
|
||||
using (scope.Notifications.Suppress())
|
||||
{
|
||||
_logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name);
|
||||
var context = new MigrationContext(plan, scope.Database, _loggerFactory.CreateLogger<MigrationContext>());
|
||||
|
||||
var migration = _migrationBuilder.Build(transition.MigrationType, context);
|
||||
migration.Run();
|
||||
|
||||
var nextState = transition.TargetState;
|
||||
origState = nextState;
|
||||
|
||||
_logger.LogInformation("At {OrigState}", origState);
|
||||
|
||||
// throw a raw exception here: this should never happen as the plan has
|
||||
// been validated - this is just a paranoid safety test
|
||||
if (!plan.Transitions.TryGetValue(origState, out transition))
|
||||
while (transition != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown state \"{origState}\".");
|
||||
_logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name);
|
||||
|
||||
var migration = _migrationBuilder.Build(transition.MigrationType, context);
|
||||
migration.Run();
|
||||
|
||||
nextState = transition.TargetState;
|
||||
|
||||
_logger.LogInformation("At {OrigState}", nextState);
|
||||
|
||||
// throw a raw exception here: this should never happen as the plan has
|
||||
// been validated - this is just a paranoid safety test
|
||||
if (!plan.Transitions.TryGetValue(nextState, out transition))
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown state \"{nextState}\".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepare and de-duplicate post-migrations, only keeping the 1st occurence
|
||||
var temp = new HashSet<Type>();
|
||||
var postMigrationTypes = context.PostMigrations
|
||||
.Where(x => !temp.Contains(x))
|
||||
.Select(x => { temp.Add(x); return x; });
|
||||
// prepare and de-duplicate post-migrations, only keeping the 1st occurence
|
||||
var temp = new HashSet<Type>();
|
||||
var postMigrationTypes = context.PostMigrations
|
||||
.Where(x => !temp.Contains(x))
|
||||
.Select(x => { temp.Add(x); return x; });
|
||||
|
||||
// run post-migrations
|
||||
foreach (var postMigrationType in postMigrationTypes)
|
||||
{
|
||||
_logger.LogInformation($"PostMigration: {postMigrationType.FullName}.");
|
||||
var postMigration = _migrationBuilder.Build(postMigrationType, context);
|
||||
postMigration.Run();
|
||||
// run post-migrations
|
||||
foreach (var postMigrationType in postMigrationTypes)
|
||||
{
|
||||
_logger.LogInformation($"PostMigration: {postMigrationType.FullName}.");
|
||||
var postMigration = _migrationBuilder.Build(postMigrationType, context);
|
||||
postMigration.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +103,12 @@ namespace Umbraco.Cms.Infrastructure.Migrations
|
||||
// safety check - again, this should never happen as the plan has been validated,
|
||||
// and this is just a paranoid safety test
|
||||
var finalState = plan.FinalState;
|
||||
if (origState != finalState)
|
||||
if (nextState != finalState)
|
||||
{
|
||||
throw new InvalidOperationException($"Internal error, reached state {origState} which is not final state {finalState}");
|
||||
throw new InvalidOperationException($"Internal error, reached state {nextState} which is not final state {finalState}");
|
||||
}
|
||||
|
||||
return origState;
|
||||
return nextState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Notifications
|
||||
{
|
||||
/// <summary>
|
||||
/// Published when one or more migration plans have been successfully executed.
|
||||
/// </summary>
|
||||
public class MigrationPlansExecutedNotification : INotification
|
||||
{
|
||||
public MigrationPlansExecutedNotification(IReadOnlyList<ExecutedMigrationPlan> executedPlans)
|
||||
=> ExecutedPlans = executedPlans;
|
||||
|
||||
public IReadOnlyList<ExecutedMigrationPlan> ExecutedPlans { get; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services;
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an upgrader.
|
||||
/// Used to run a <see cref="MigrationPlan"/>
|
||||
/// </summary>
|
||||
public class Upgrader
|
||||
{
|
||||
@@ -36,13 +36,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
/// </summary>
|
||||
/// <param name="scopeProvider">A scope provider.</param>
|
||||
/// <param name="keyValueService">A key-value service.</param>
|
||||
public void Execute(IMigrationPlanExecutor migrationPlanExecutor, IScopeProvider scopeProvider, IKeyValueService keyValueService)
|
||||
public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, IScopeProvider scopeProvider, IKeyValueService keyValueService)
|
||||
{
|
||||
if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider));
|
||||
if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService));
|
||||
|
||||
using (var scope = scopeProvider.CreateScope())
|
||||
{
|
||||
using (IScope scope = scopeProvider.CreateScope())
|
||||
{
|
||||
// read current state
|
||||
var currentState = keyValueService.GetValue(StateValueKey);
|
||||
var forceState = false;
|
||||
@@ -51,13 +51,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
{
|
||||
currentState = Plan.InitialState;
|
||||
forceState = true;
|
||||
}
|
||||
}
|
||||
|
||||
// execute plan
|
||||
var state = migrationPlanExecutor.Execute(Plan, currentState);
|
||||
if (string.IsNullOrWhiteSpace(state))
|
||||
{
|
||||
throw new Exception("Plan execution returned an invalid null or empty state.");
|
||||
throw new InvalidOperationException("Plan execution returned an invalid null or empty state.");
|
||||
}
|
||||
|
||||
// save new state
|
||||
@@ -69,8 +69,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
{
|
||||
keyValueService.SetValue(StateValueKey, currentState, state);
|
||||
}
|
||||
|
||||
|
||||
scope.Complete();
|
||||
|
||||
return new ExecutedMigrationPlan(Plan, currentState, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -11,6 +12,7 @@ using Moq;
|
||||
using NPoco;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Migrations;
|
||||
@@ -32,7 +34,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations
|
||||
NullLoggerFactory loggerFactory = NullLoggerFactory.Instance;
|
||||
|
||||
var database = new TestDatabase();
|
||||
IScope scope = Mock.Of<IScope>();
|
||||
IScope scope = Mock.Of<IScope>(x => x.Notifications == Mock.Of<IScopedNotificationPublisher>());
|
||||
Mock.Get(scope)
|
||||
.Setup(x => x.Database)
|
||||
.Returns(database);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -9,6 +10,7 @@ using Moq;
|
||||
using NPoco;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -47,7 +49,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations
|
||||
});
|
||||
|
||||
var database = new TestDatabase();
|
||||
IScope scope = Mock.Of<IScope>();
|
||||
IScope scope = Mock.Of<IScope>(x => x.Notifications == Mock.Of<IScopedNotificationPublisher>());
|
||||
Mock.Get(scope)
|
||||
.Setup(x => x.Database)
|
||||
.Returns(database);
|
||||
@@ -96,7 +98,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations
|
||||
});
|
||||
|
||||
var database = new TestDatabase();
|
||||
IScope scope = Mock.Of<IScope>();
|
||||
IScope scope = Mock.Of<IScope>(x => x.Notifications == Mock.Of<IScopedNotificationPublisher>());
|
||||
Mock.Get(scope)
|
||||
.Setup(x => x.Database)
|
||||
.Returns(database);
|
||||
|
||||
@@ -7,21 +7,15 @@ using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.BackOffice.Extensions;
|
||||
using Umbraco.Cms.Web.Common.ActionsResults;
|
||||
using Umbraco.Cms.Web.Common.Attributes;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using Umbraco.Cms.Infrastructure.Install;
|
||||
|
||||
namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
{
|
||||
@@ -34,30 +28,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
{
|
||||
private readonly IPackagingService _packagingService;
|
||||
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly PendingPackageMigrations _pendingPackageMigrations;
|
||||
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
|
||||
private readonly IMigrationPlanExecutor _migrationPlanExecutor;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly PackageMigrationRunner _packageMigrationRunner;
|
||||
private readonly ILogger<PackageController> _logger;
|
||||
|
||||
public PackageController(
|
||||
IPackagingService packagingService,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IKeyValueService keyValueService,
|
||||
PendingPackageMigrations pendingPackageMigrations,
|
||||
PackageMigrationPlanCollection packageMigrationPlans,
|
||||
IMigrationPlanExecutor migrationPlanExecutor,
|
||||
IScopeProvider scopeProvider,
|
||||
PackageMigrationRunner packageMigrationRunner,
|
||||
ILogger<PackageController> logger)
|
||||
{
|
||||
_packagingService = packagingService ?? throw new ArgumentNullException(nameof(packagingService));
|
||||
_backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor));
|
||||
_keyValueService = keyValueService;
|
||||
_pendingPackageMigrations = pendingPackageMigrations;
|
||||
_packageMigrationPlans = packageMigrationPlans;
|
||||
_migrationPlanExecutor = migrationPlanExecutor;
|
||||
_scopeProvider = scopeProvider;
|
||||
_packageMigrationRunner = packageMigrationRunner;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -119,33 +101,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
[HttpPost]
|
||||
public ActionResult<IEnumerable<InstalledPackage>> RunMigrations([FromQuery]string packageName)
|
||||
{
|
||||
IReadOnlyDictionary<string, string> keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
|
||||
IReadOnlyList<string> pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues);
|
||||
foreach(PackageMigrationPlan plan in _packageMigrationPlans.Where(x => x.PackageName.InvariantEquals(packageName)))
|
||||
try
|
||||
{
|
||||
if (pendingMigrations.Contains(plan.Name))
|
||||
{
|
||||
var upgrader = new Upgrader(plan);
|
||||
|
||||
try
|
||||
{
|
||||
upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Package migration failed on package {Package} for plan {Plan}", packageName, plan.Name);
|
||||
|
||||
return ValidationErrorResult.CreateNotificationValidationErrorResult(
|
||||
$"Package migration failed on package {packageName} for plan {plan.Name} with error: {ex.Message}. Check log for full details.");
|
||||
}
|
||||
}
|
||||
_packageMigrationRunner.RunPackageMigrationsIfPending(packageName);
|
||||
return _packagingService.GetAllInstalledPackages().ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Package migration failed on package {Package}", packageName);
|
||||
|
||||
return _packagingService.GetAllInstalledPackages().ToList();
|
||||
return ValidationErrorResult.CreateNotificationValidationErrorResult(
|
||||
$"Package migration failed on package {packageName} with error: {ex.Message}. Check log for full details.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult DownloadCreatedPackage(int id)
|
||||
public IActionResult DownloadCreatedPackage(int id)
|
||||
{
|
||||
var package = _packagingService.GetCreatedPackageById(id);
|
||||
if (package == null)
|
||||
|
||||
Reference in New Issue
Block a user