diff --git a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs index 50c02cbad5..4d676f68ce 100644 --- a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs +++ b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs @@ -1,5 +1,6 @@ namespace Umbraco.Cms.Core.Notifications { + /// /// Used to notify when the core runtime can do an unattended upgrade. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e216516437..7e71756379 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -61,6 +61,5 @@ - diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index 2218f14df3..990b158ef3 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -29,6 +29,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddTransient(); builder.Services.AddUnique(); + builder.Services.AddTransient(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 9e11cbd51f..aba2cdd9f4 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -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(null); + return Task.FromResult((InstallSetupResult)null); } public override bool RequiresExecution(object model) diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index a7ddf3378f..bf8aaa54ac 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -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; diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs new file mode 100644 index 0000000000..54448c68c0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -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 +{ + /// + /// Runs the package migration plans + /// + 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 _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); + } + + /// + /// Runs all migration plans for a package name if any are pending. + /// + /// + /// + public IEnumerable RunPackageMigrationsIfPending(string packageName) + { + IReadOnlyDictionary keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); + + IEnumerable packagePlans = _packageMigrationPlans.Values + .Where(x => x.PackageName.InvariantEquals(packageName)) + .Where(x => pendingMigrations.Contains(x.Name)) + .Select(x => x.Name); + + return RunPackagePlans(packagePlans); + } + + /// + /// Runs the all specified package migration plans and publishes a + /// if all are successful. + /// + /// + /// + /// If any plan fails it will throw an exception. + public IEnumerable RunPackagePlans(IEnumerable plansToRun) + { + var results = new List(); + + // 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( + "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; + } + } +} diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 24cbce273f..5f5c8f16a8 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -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 { + /// + /// Handles to execute the unattended Umbraco upgrader + /// or the unattended Package migrations runner. + /// public class UnattendedUpgrader : INotificationAsyncHandler { 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(); - 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( - "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 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 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); } } diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs new file mode 100644 index 0000000000..9979da1e40 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -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; } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs index 30d3385632..3a5a4649fe 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations { /// /// Marker interface for migration expressions diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs index 4610e02d60..41a831360a 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Umbraco.Cms.Infrastructure.Migrations; namespace Umbraco.Cms.Core.Migrations diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 691400121f..b7437f4c2d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -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; diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs index 9d0f110a74..bdb5aeb780 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs @@ -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() 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; } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 51fc613c21..09cddbc20b 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -16,7 +16,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations private readonly IMigrationBuilder _migrationBuilder; private readonly ILogger _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()); - - 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()); - 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(); - 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(); + 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; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs new file mode 100644 index 0000000000..50ee5c3582 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +{ + /// + /// Published when one or more migration plans have been successfully executed. + /// + public class MigrationPlansExecutedNotification : INotification + { + public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) + => ExecutedPlans = executedPlans; + + public IReadOnlyList ExecutedPlans { get; } + + + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index fc0e01c3d9..afc4fdec81 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade { /// - /// Represents an upgrader. + /// Used to run a /// public class Upgrader { @@ -36,13 +36,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// /// A scope provider. /// A key-value service. - 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); } } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index b41e4dce49..40dbad176e 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 26616d9d69..08bcee255e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -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 scope = Mock.Of(x => x.Notifications == Mock.Of()); Mock.Get(scope) .Setup(x => x.Database) .Returns(database); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index 2b6ff721eb..a61de49ee5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -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 scope = Mock.Of(x => x.Notifications == Mock.Of()); 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 scope = Mock.Of(x => x.Notifications == Mock.Of()); Mock.Get(scope) .Setup(x => x.Database) .Returns(database); diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index dca3a320ec..813682c70e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -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 _logger; public PackageController( IPackagingService packagingService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IKeyValueService keyValueService, - PendingPackageMigrations pendingPackageMigrations, - PackageMigrationPlanCollection packageMigrationPlans, - IMigrationPlanExecutor migrationPlanExecutor, - IScopeProvider scopeProvider, + PackageMigrationRunner packageMigrationRunner, ILogger 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> RunMigrations([FromQuery]string packageName) { - IReadOnlyDictionary keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); - IReadOnlyList 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)