Starts implementing ability to manually run pending migrations from the back office

This commit is contained in:
Shannon
2021-06-16 15:34:20 -06:00
parent 7ec01f232f
commit ad84c1591e
17 changed files with 228 additions and 49 deletions

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Manifest
{
public interface IManifestParser
@@ -14,5 +16,11 @@ namespace Umbraco.Cms.Core.Manifest
/// Parses a manifest.
/// </summary>
PackageManifest ParseManifest(string text);
/// <summary>
/// Returns all package individual manifests
/// </summary>
/// <returns></returns>
IEnumerable<PackageManifest> GetManifests();
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.Serialization;
using Umbraco.Cms.Core.PropertyEditors;
@@ -10,6 +10,19 @@ namespace Umbraco.Cms.Core.Manifest
[DataContract]
public class PackageManifest
{
[DataMember(Name = "name", IsRequired = true)]
public string PackageName { get; set; }
[DataMember(Name = "packageView", IsRequired = true)]
public string PackageView { get; set; }
// TODO: iconUrl? since we cannot retrieve from nuget
// TODO: Version since we cannot retrieve from nuget
//[DataMember(Name = "name", IsRequired = true)]
//public Guid PackageId { get; set; }
/// <summary>
/// Gets the source path of the manifest.
/// </summary>

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Packaging
{
[DataContract(Name = "installedPackage")]
public class InstalledPackage
{
[DataMember(Name = "name", IsRequired = true)]
[Required]
public string PackageName { get; set; }
// TODO: Version? Icon? Other metadata?
[DataMember(Name = "packageView")]
public string PackageView { get; set; }
[DataMember(Name = "plans")]
public IEnumerable<InstalledPackageMigrationPlans> PackageMigrationPlans { get; set; } = Enumerable.Empty<InstalledPackageMigrationPlans>();
[DataMember(Name = "hasPendingMigrations")]
public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations);
}
}

View File

@@ -0,0 +1,27 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Packaging
{
[DataContract(Name = "installedPackageMigrations")]
public class InstalledPackageMigrationPlans
{
[DataMember(Name = "hasPendingMigrations")]
public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId;
/// <summary>
/// If the package has migrations, this will be it's final migration Id
/// </summary>
/// <remarks>
/// This can be used to determine if the package advertises any migrations
/// </remarks>
[DataMember(Name = "finalMigrationId")]
public string FinalMigrationId { get; set; }
/// <summary>
/// If the package has migrations, this will be it's current migration Id
/// </summary>
[DataMember(Name = "currentMigrationId")]
public string CurrentMigrationId { get; set; }
}
}

View File

@@ -7,7 +7,13 @@ using Umbraco.Cms.Core.Models.Packaging;
namespace Umbraco.Cms.Core.Packaging
{
// This is the thing that goes in the createdPackages.config
/// <summary>
/// A created package in the back office.
/// </summary>
/// <remarks>
/// This data structure is persisted to createdPackages.config when creating packages in the back office.
/// </remarks>
[DataContract(Name = "packageInstance")]
public class PackageDefinition
{

View File

@@ -1,22 +1,38 @@
using System;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Packaging
{
{
/// <summary>
/// Base class for package migration plans
/// </summary>
public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable
{
protected PackageMigrationPlan(string name) : base(name)
/// <summary>
/// Creates a package migration plan
/// </summary>
/// <param name="packageName">The name of the package. If the package has a package.manifest these must match.</param>
protected PackageMigrationPlan(string packageName) : this(packageName, packageName)
{
}
/// <summary>
/// Create a plan for a Package Name
/// </summary>
/// <param name="packageName">The package name that the plan is for. If the package has a package.manifest these must match.</param>
/// <param name="planName">
/// The plan name for the package. This should be the same name as the
/// package name if there is only one plan in the package.
/// </param>
protected PackageMigrationPlan(string packageName, string planName) : base(planName)
{
// A call to From must be done first
From(string.Empty);
DefinePlan();
PackageName = packageName;
}
/// <summary>
@@ -25,6 +41,11 @@ namespace Umbraco.Cms.Core.Packaging
/// </summary>
public override bool IgnoreCurrentState => true;
/// <summary>
/// Returns the Package Name for this plan
/// </summary>
public string PackageName { get; }
protected abstract void DefinePlan();
}

View File

@@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.Packaging
/// These are the key/value pairs from the keyvalue storage of migration names and their final values
/// </param>
/// <returns></returns>
public IReadOnlyList<string> GetUmbracoPendingPackageMigrations(IReadOnlyDictionary<string, string> keyValues)
public IReadOnlyList<string> GetPendingPackageMigrations(IReadOnlyDictionary<string, string> keyValues)
{
var packageMigrationPlans = _packageMigrationPlans.ToList();

View File

@@ -24,10 +24,25 @@ namespace Umbraco.Cms.Core.Services
InstallationSummary InstallCompiledPackageData(XDocument packageXml, int userId = Constants.Security.SuperUserId);
IEnumerable<PackageDefinition> GetAllInstalledPackages();
/// <summary>
/// Returns the advertised installed packages
/// </summary>
/// <returns></returns>
IEnumerable<InstalledPackage> GetAllInstalledPackages();
/// <summary>
/// Returns the created packages
/// </summary>
/// <returns></returns>
IEnumerable<PackageDefinition> GetAllCreatedPackages();
/// <summary>
/// Returns a created package by id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
PackageDefinition GetCreatedPackageById(int id);
void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
/// <summary>

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.Manifest
private readonly ILocalizedTextService _localizedTextService;
private readonly IShortStringHelper _shortStringHelper;
private readonly IDataValueEditorFactory _dataValueEditorFactory;
private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
private readonly IAppPolicyCache _cache;
private readonly ILogger<ManifestParser> _logger;
@@ -97,7 +97,7 @@ namespace Umbraco.Cms.Core.Manifest
/// <summary>
/// Gets all manifests.
/// </summary>
private IEnumerable<PackageManifest> GetManifests()
public IEnumerable<PackageManifest> GetManifests()
{
var manifests = new List<PackageManifest>();
@@ -108,8 +108,11 @@ namespace Umbraco.Cms.Core.Manifest
var text = File.ReadAllText(path);
text = TrimPreamble(text);
if (string.IsNullOrWhiteSpace(text))
{
continue;
var manifest = ParseManifest(text);
}
PackageManifest manifest = ParseManifest(text);
manifest.Source = path;
manifests.Add(manifest);
}
@@ -174,8 +177,8 @@ namespace Umbraco.Cms.Core.Manifest
private static string TrimPreamble(string text)
{
// strangely StartsWith(preamble) would always return true
if (text.Substring(0, 1) == Utf8Preamble)
text = text.Remove(0, Utf8Preamble.Length);
if (text.Substring(0, 1) == s_utf8Preamble)
text = text.Remove(0, s_utf8Preamble.Length);
return text;
}

View File

@@ -212,7 +212,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
return UmbracoDatabaseState.NeedsUpgrade;
}
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetUmbracoPendingPackageMigrations(keyValues);
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues);
if (packagesRequiringMigration.Count > 0)
{
_startupState[PendingPacakgeMigrationsStateKey] = packagesRequiringMigration;

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Packaging;
using Umbraco.Cms.Core.Notifications;
@@ -19,6 +20,10 @@ namespace Umbraco.Cms.Core.Services.Implement
{
private readonly IPackageInstallation _packageInstallation;
private readonly IEventAggregator _eventAggregator;
private readonly IManifestParser _manifestParser;
private readonly IKeyValueService _keyValueService;
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
private readonly PendingPackageMigrations _pendingPackageMigrations;
private readonly IAuditService _auditService;
private readonly ICreatedPackagesRepository _createdPackages;
@@ -26,12 +31,20 @@ namespace Umbraco.Cms.Core.Services.Implement
IAuditService auditService,
ICreatedPackagesRepository createdPackages,
IPackageInstallation packageInstallation,
IEventAggregator eventAggregator)
IEventAggregator eventAggregator,
IManifestParser manifestParser,
IKeyValueService keyValueService,
PackageMigrationPlanCollection packageMigrationPlans,
PendingPackageMigrations pendingPackageMigrations)
{
_auditService = auditService;
_createdPackages = createdPackages;
_packageInstallation = packageInstallation;
_eventAggregator = eventAggregator;
_manifestParser = manifestParser;
_keyValueService = keyValueService;
_packageMigrationPlans = packageMigrationPlans;
_pendingPackageMigrations = pendingPackageMigrations;
}
#region Installation
@@ -95,10 +108,52 @@ namespace Umbraco.Cms.Core.Services.Implement
public string ExportCreatedPackage(PackageDefinition definition) => _createdPackages.ExportPackage(definition);
public IEnumerable<InstalledPackage> GetAllInstalledPackages()
{
IReadOnlyDictionary<string, string> keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
IReadOnlyList<string> pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues);
var installedPackages = new Dictionary<string, InstalledPackage>();
foreach(PackageMigrationPlan plan in _packageMigrationPlans)
{
if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage installedPackage))
{
installedPackage = new InstalledPackage
{
PackageName = plan.PackageName
};
installedPackages.Add(plan.PackageName, installedPackage);
}
var currentPlans = installedPackage.PackageMigrationPlans.ToList();
keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.PackageName, out var currentState);
currentPlans.Add(new InstalledPackageMigrationPlans
{
CurrentMigrationId = currentState,
FinalMigrationId = plan.FinalState
});
installedPackage.PackageMigrationPlans = currentPlans;
}
foreach(PackageManifest package in _manifestParser.GetManifests())
{
if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage installedPackage))
{
installedPackage = new InstalledPackage
{
PackageName = package.PackageName
};
installedPackages.Add(package.PackageName, installedPackage);
}
installedPackage.PackageView = package.PackageView;
}
return installedPackages.Values;
}
// TODO: Implement
public IEnumerable<PackageDefinition> GetAllInstalledPackages() => Enumerable.Empty<PackageDefinition>();
#endregion
}
}

View File

@@ -13,11 +13,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
{
private static readonly Guid s_step1 = Guid.NewGuid();
private static readonly Guid s_step2 = Guid.NewGuid();
private const string PackageName = "Test1";
private const string TestPackageName = "Test1";
private class TestPackageMigrationPlan : PackageMigrationPlan
{
public TestPackageMigrationPlan() : base(PackageName)
public TestPackageMigrationPlan() : base(TestPackageName)
{
}
@@ -41,7 +41,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
{
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
var registeredMigrations = new Dictionary<string, string>();
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
Assert.AreEqual(1, pending.Count);
}
@@ -51,9 +51,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
var registeredMigrations = new Dictionary<string, string>
{
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step2.ToString()
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step2.ToString()
};
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
Assert.AreEqual(0, pending.Count);
}
@@ -63,9 +63,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
var registeredMigrations = new Dictionary<string, string>
{
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step1.ToString()
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step1.ToString()
};
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
Assert.AreEqual(1, pending.Count);
}
@@ -75,9 +75,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
var registeredMigrations = new Dictionary<string, string>
{
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step1.ToString().ToUpper()
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step1.ToString().ToUpper()
};
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
Assert.AreEqual(1, pending.Count);
}
}

View File

@@ -129,10 +129,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
/// Returns all installed packages - only shows their latest versions
/// </summary>
/// <returns></returns>
public IEnumerable<PackageDefinition> GetInstalled()
{
return _packagingService.GetAllInstalledPackages()
.ToList();
}
public IEnumerable<InstalledPackage> GetInstalled()
=> _packagingService.GetAllInstalledPackages().ToList();
}
}

View File

@@ -1,7 +1,7 @@
(function () {
"use strict";
function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) {
function PackagesInstalledController($location, packageResource, localizationService) {
var vm = this;

View File

@@ -9,7 +9,7 @@
<table class="table">
<tbody>
<tr ng-repeat="installedPackage in vm.installedPackages track by installedPackage.id">
<tr ng-repeat="installedPackage in vm.installedPackages track by installedPackage.name">
<td class="flex items-center">
<div class="umb-package-list__item-icon">
<umb-icon ng-if="!installedPackage.iconUrl" icon="icon-box" class="icon-box"></umb-icon>
@@ -17,25 +17,30 @@
</div>
<div class="umb-package-list__item-content">
<div class="umb-package-list__item-name">{{ installedPackage.name }}</div>
<div class="umb-package-list__item-description">
{{ installedPackage.version }} | <a href="{{ installedPackage.url }}" target="_blank" rel="noopener">{{ installedPackage.url }}</a> | {{ installedPackage.author }}
</div>
</div>
</td>
<td style="text-align: right;">
<umb-button ng-show="installedPackage.packageView"
type="button"
button-style="info"
size="xxs"
label-key="packager_packageOptions"
action="vm.packageOptions(installedPackage)">
</umb-button>
<umb-button ng-show="installedPackage.hasPendingMigrations"
type="button"
button-style="info"
size="xxs"
label-key="packager_packageMigrationsRun"
action="vm.packageOptions(installedPackage)">
</umb-button>
<umb-button type="button"
button-style="danger"
label-key="packager_packageUninstallHeader"
action="vm.confirmUninstall(installedPackage)">
</umb-button>
<umb-button ng-show="installedPackage.packageView"
type="button"
button-style="info"
size="xxs"
label-key="packager_packageOptions"
action="vm.packageOptions(installedPackage)">
</umb-button>
<umb-button type="button"
button-style="danger"
label-key="packager_packageUninstallHeader"
action="vm.confirmUninstall(installedPackage)">
</umb-button>
</td>
</tr>
</tbody>

View File

@@ -1264,6 +1264,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="packageNoItemsText"><![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
You can safely remove this from the system by clicking "uninstall package" below.]]></key>
<key alias="packageOptions">Package options</key>
<key alias="packageMigrationsRun">Run pending package migrations</key>
<key alias="packageReadme">Package readme</key>
<key alias="packageRepository">Package repository</key>
<key alias="packageUninstallConfirm">Confirm package uninstall</key>

View File

@@ -1276,6 +1276,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="packageNoItemsText"><![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
You can safely remove this from the system by clicking "uninstall package" below.]]></key>
<key alias="packageOptions">Package options</key>
<key alias="packageMigrationsRun">Run pending package migrations</key>
<key alias="packageReadme">Package readme</key>
<key alias="packageRepository">Package repository</key>
<key alias="packageUninstallConfirm">Confirm package uninstall</key>