Implement new backoffice installer (#12790)

* Add new BackOfficeApi project

* Add swagger

* Add and route new install controller

* Add new install steps

* Add Setup endpoint

* Add missing RequiresExecution methods

* Fix nullability of databasemodel

* Move user information to separate model

* Remove ping method

* Add view models install data

* Move mapping folder

* Move ViewModels

* Add settings endpoint

* Remove unused binderprovider

* Postfix RequiresExecution with async

* Update NewDatabaseUpgradeStep to not depend on install step

* Add installstep collection

* Move registration into backoffice project

* Add InstallService

* Use service in controller

* Add upgrade to install service and use in controller

* Correctly check is database is configured

* Reorganize

* Reorganize into new core and infrastructure

* Rename steps

* Rename BackofficeApi to MangementApi

* Make install step an interface instead of abstract class

* Rename InstallStep to create CreateUserStep

* Move restart runtime and sign in user into install steps

* Move install service into new core project

* Map controllers in composer

* Restrict access to installcontroller based on runtime level

* Use FireAndForget when logging install

* Use actionresult instead of iactionresult

* Set new projects as not packable

* Link to backoffice in 201 response when installed

* Register installations

* Add custom backoffice routing template token

* Move umbraco path trimming out of application convention

* Make it easier to route to backoffice api

* Make swagger version aware and move behind backoffice path

* Obsolete old install classes

* Move maps into single file

This is all mappint to/from viewmodels in some manner

* Remove usage of InstallSetupResult

* Move new projects to the src folder

* Remove InstallationType from IInstallStep

This upgrade steps should implement their own IUpgradeStep interface

* Remove upgrade from service and controller

This should be its own service and controller

* Add xml docs

* Remove internals visible to

* Disable package validation for new projects

Quite the gotcha here, if the projects are brand new, there is no nuget packages to compare with, this causes the build to fail.

* Add ValidateDatabase endpoint

* Remove project references to new backoffice

We don't actually want to depend on this yet, it's just needed for testing/development

* Obsolete installationtype

* Add DatabaseSettingsFactory tests

* Add InstallServiceTests

* Fix InstallServiceTests

* Test RequireRuntimeLevelAttribute

* Implement new backoffice upgrader (#12818)

* Add UpgradeSettingsModel and viewmodel

* Add upgrade/settings endpoint

* Implement upgrade steps

* Add upgrade step collection

* Add UpgradeService

* Add authorize endpoint to UpgradeController

* Fix interface

* Add upgrade service tests

* Remove runtime check in databaseinstallstep

* Move RequireRuntimeLevel to controller

* Add a readme to the new backoffice part

* BackOffice not Backoffice

* Add conditional project references

* Fixes based on review

* Fix up

* Move running of steps into its own method in UpgradeService

* Make services transient

* More fixup

* Log exceptions when running steps
This commit is contained in:
Mole
2022-08-29 09:50:48 +02:00
committed by GitHub
parent aaa0b38701
commit 748fb7d1f7
96 changed files with 2603 additions and 25 deletions

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Filters;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.ManagementApi.Filters;
[TestFixture]
public class RequireRuntimeLevelAttributeTest
{
[Test]
[TestCase(RuntimeLevel.Install, RuntimeLevel.Run, true)]
[TestCase(RuntimeLevel.Install, RuntimeLevel.Unknown, true)]
[TestCase(RuntimeLevel.Install, RuntimeLevel.Boot, true)]
[TestCase(RuntimeLevel.Install, RuntimeLevel.Upgrade, true)]
[TestCase(RuntimeLevel.Run, RuntimeLevel.Upgrade, true)]
[TestCase(RuntimeLevel.Install, RuntimeLevel.Install, false)]
[TestCase(RuntimeLevel.Upgrade, RuntimeLevel.Upgrade, false)]
public void BlocksWhenIncorrectRuntime(RuntimeLevel requiredLevel, RuntimeLevel actualLevel, bool shouldFail)
{
var executionContext = CreateActionExecutingContext(actualLevel);
var sut = new RequireRuntimeLevelAttribute(requiredLevel);
sut.OnActionExecuting(executionContext);
if (shouldFail)
{
AssertFailure(executionContext);
return;
}
// Assert success, result being null == we haven't short circuited.
Assert.IsNull(executionContext.Result);
}
private void AssertFailure(ActionExecutingContext executionContext)
{
var result = executionContext.Result;
Assert.IsInstanceOf<ObjectResult>(result);
var objectResult = (ObjectResult)result;
Assert.AreEqual(StatusCodes.Status428PreconditionRequired, objectResult?.StatusCode);
Assert.IsInstanceOf<ProblemDetails>(objectResult?.Value);
}
private ActionExecutingContext CreateActionExecutingContext(RuntimeLevel targetRuntimeLevel)
{
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor()
};
var executingContext = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new());
var fakeRuntime = new Mock<IRuntimeState>();
fakeRuntime.Setup(x => x.Level).Returns(targetRuntimeLevel);
var fakeServiceProvider = new Mock<IServiceProvider>();
fakeServiceProvider.Setup(x => x.GetService(typeof(IRuntimeState))).Returns(fakeRuntime.Object);
actionContext.HttpContext.RequestServices = fakeServiceProvider.Object;
return executingContext;
}
}

View File

@@ -24,10 +24,10 @@ public class DistributedCacheTests
var cacheRefresherCollection = new CacheRefresherCollection(() => new[] { new TestCacheRefresher() });
_distributedCache = new Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection);
_distributedCache = new global::Umbraco.Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection);
}
private Cms.Core.Cache.DistributedCache _distributedCache;
private global::Umbraco.Cms.Core.Cache.DistributedCache _distributedCache;
private IServerRoleAccessor ServerRegistrar { get; set; }

View File

@@ -346,7 +346,7 @@ public class DefaultShortStringHelperTestsWithoutSetup
public void Utf8ToAsciiConverter()
{
const string str = "a\U00010F00z\uA74Ftéô";
var output = Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str);
var output = global::Umbraco.Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str);
Assert.AreEqual("a?zooteo", output);
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.New.Cms.Core.Installer;
using Umbraco.New.Cms.Core.Models.Installer;
using Umbraco.New.Cms.Core.Services.Installer;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services;
[TestFixture]
public class InstallServiceTests
{
[Test]
public void RequiresInstallRuntimeToInstall()
{
var runtimeStateMock = new Mock<IRuntimeState>();
runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run);
var stepCollection = new NewInstallStepCollection(Enumerable.Empty<IInstallStep>);
var sut = new InstallService(Mock.Of<ILogger<InstallService>>(), stepCollection, runtimeStateMock.Object);
Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.Install(new InstallData()));
}
[Test]
public async Task OnlyRunsStepsThatRequireExecution()
{
var steps = new[]
{
new TestInstallStep { ShouldRun = true },
new TestInstallStep { ShouldRun = false },
new TestInstallStep { ShouldRun = true },
};
var sut = CreateInstallService(steps);
await sut.Install(new InstallData());
foreach (var step in steps)
{
Assert.AreEqual(step.ShouldRun, step.HasRun);
}
}
[Test]
public async Task StepsRunInCollectionOrder()
{
List<TestInstallStep> runOrder = new List<TestInstallStep>();
var steps = new[]
{
new TestInstallStep { Id = 1 },
new TestInstallStep { Id = 2 },
new TestInstallStep { Id = 3 },
};
// Add an method delegate that will add the step itself, that way we can know the executed order.
foreach (var step in steps)
{
step.AdditionalExecution = _ =>
{
runOrder.Add(step);
return Task.CompletedTask;
};
}
var sut = CreateInstallService(steps);
await sut.Install(new InstallData());
// The ID's are strictly not necessary, but it makes potential debugging easier.
var expectedRunOrder = steps.Select(x => x.Id);
var actualRunOrder = runOrder.Select(x => x.Id);
Assert.AreEqual(expectedRunOrder, actualRunOrder);
}
private InstallService CreateInstallService(IEnumerable<IInstallStep> steps)
{
var logger = Mock.Of<ILogger<InstallService>>();
var stepCollection = new NewInstallStepCollection(() => steps);
var runtimeStateMock = new Mock<IRuntimeState>();
runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Install);
return new InstallService(logger, stepCollection, runtimeStateMock.Object);
}
private class TestInstallStep : IInstallStep
{
public bool HasRun;
public bool ShouldRun = true;
public int Id;
public Func<InstallData, Task> AdditionalExecution;
public Task ExecuteAsync(InstallData model)
{
HasRun = true;
AdditionalExecution?.Invoke(model);
return Task.CompletedTask;
}
public Task<bool> RequiresExecutionAsync(InstallData model) => Task.FromResult(ShouldRun);
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.New.Cms.Core.Installer;
using UpgradeService = Umbraco.New.Cms.Core.Services.Installer.UpgradeService;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services;
[TestFixture]
public class UpgradeServiceTests
{
[Test]
[TestCase(RuntimeLevel.Install)]
[TestCase(RuntimeLevel.Boot)]
[TestCase(RuntimeLevel.Run)]
[TestCase(RuntimeLevel.Unknown)]
public void RequiresUpgradeRuntimeToUpgrade(RuntimeLevel level)
{
var sut = CreateUpgradeService(Enumerable.Empty<IUpgradeStep>(), level);
Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.Upgrade());
}
[Test]
public async Task OnlyRunsStepsThatRequireExecution()
{
var steps = new[]
{
new TestUpgradeStep { ShouldRun = true },
new TestUpgradeStep { ShouldRun = false },
new TestUpgradeStep { ShouldRun = true },
};
var sut = CreateUpgradeService(steps);
await sut.Upgrade();
foreach (var step in steps)
{
Assert.AreEqual(step.ShouldRun, step.HasRun);
}
}
[Test]
public async Task StepsRunInCollectionOrder()
{
List<TestUpgradeStep> runOrder = new List<TestUpgradeStep>();
var steps = new[]
{
new TestUpgradeStep { Id = 1 },
new TestUpgradeStep { Id = 2 },
new TestUpgradeStep { Id = 3 },
};
// Add an method delegate that will add the step itself, that way we can know the executed order.
foreach (var step in steps)
{
step.AdditionalExecution = () => runOrder.Add(step);
}
var sut = CreateUpgradeService(steps);
await sut.Upgrade();
// The ID's are strictly not necessary, but it makes potential debugging easier.
var expectedRunOrder = steps.Select(x => x.Id);
var actualRunOrder = runOrder.Select(x => x.Id);
Assert.AreEqual(expectedRunOrder, actualRunOrder);
}
private UpgradeService CreateUpgradeService(IEnumerable<IUpgradeStep> steps, RuntimeLevel runtimeLevel = RuntimeLevel.Upgrade)
{
var logger = Mock.Of<ILogger<UpgradeService>>();
var stepCollection = new UpgradeStepCollection(() => steps);
var runtimeStateMock = new Mock<IRuntimeState>();
runtimeStateMock.Setup(x => x.Level).Returns(runtimeLevel);
return new UpgradeService(stepCollection, runtimeStateMock.Object, logger);
}
private class TestUpgradeStep : IUpgradeStep
{
public bool HasRun;
public bool ShouldRun = true;
public int Id;
public Action AdditionalExecution;
public Task ExecuteAsync()
{
HasRun = true;
AdditionalExecution?.Invoke();
return Task.CompletedTask;
}
public Task<bool> RequiresExecutionAsync() => Task.FromResult(ShouldRun);
}
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.ManagementApi.Mapping.Installer;
using Umbraco.Cms.Tests.Common;
using Umbraco.New.Cms.Core.Models.Installer;
using Umbraco.New.Cms.Infrastructure.Factories.Installer;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Infrastructure.Factories;
[TestFixture]
public class DatabaseSettingsFactoryTests
{
[Test]
public void CanBuildDatabaseSettings()
{
var metadata = CreateTestMetadata();
var connectionString = new TestOptionsMonitor<ConnectionStrings>(new ConnectionStrings());
var mapper = CreateMapper();
var factory = new DatabaseSettingsFactory(metadata, connectionString, mapper);
var settingsModels = factory.GetDatabaseSettings();
Assert.AreEqual(metadata.Count, settingsModels.Count);
AssertMapping(metadata, settingsModels);
}
[Test]
public void IsConfiguredSetCorrectly()
{
var connectionString = new ConnectionStrings
{
ConnectionString = "SomeConnectionString",
ProviderName = "HostedTestMeta",
};
var optionsMonitor = new TestOptionsMonitor<ConnectionStrings>(connectionString);
var mapper = CreateMapper();
var metadata = CreateTestMetadata();
var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper);
var settingsModels = factory.GetDatabaseSettings();
Assert.AreEqual(1, settingsModels.Count, "Expected only one database settings model, if a database is preconfigured we should only return the configured one.");
AssertMapping(metadata, settingsModels);
Assert.IsTrue(settingsModels.First().IsConfigured);
}
[Test]
public void SpecifiedProviderMustExist()
{
var connectionString = new ConnectionStrings
{
ConnectionString = "SomeConnectionString",
ProviderName = "NoneExistentProvider",
};
var optionsMonitor = new TestOptionsMonitor<ConnectionStrings>(connectionString);
var mapper = CreateMapper();
var metadata = CreateTestMetadata();
var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper);
Assert.Throws<InvalidOperationException>(() => factory.GetDatabaseSettings());
}
/// <summary>
/// Asserts that the mapping is correct, in other words that the values in DatabaseSettingsModel is as expected.
/// </summary>
private void AssertMapping(
IEnumerable<IDatabaseProviderMetadata> expected,
ICollection<DatabaseSettingsModel> actual)
{
expected = expected.ToList();
foreach (var model in actual)
{
var metadata = expected.FirstOrDefault(x => x.Id == model.Id);
Assert.IsNotNull(metadata);
Assert.Multiple(() =>
{
Assert.AreEqual(metadata?.SortOrder, model.SortOrder);
Assert.AreEqual(metadata.DisplayName, model.DisplayName);
Assert.AreEqual(metadata.DefaultDatabaseName, model.DefaultDatabaseName);
Assert.AreEqual(metadata.ProviderName ?? string.Empty, model.ProviderName);
Assert.AreEqual(metadata.RequiresServer, model.RequiresServer);
Assert.AreEqual(metadata.ServerPlaceholder ?? string.Empty, model.ServerPlaceholder);
Assert.AreEqual(metadata.RequiresCredentials, model.RequiresCredentials);
Assert.AreEqual(metadata.SupportsIntegratedAuthentication, model.SupportsIntegratedAuthentication);
Assert.AreEqual(metadata.RequiresConnectionTest, model.RequiresConnectionTest);
});
}
}
private IUmbracoMapper CreateMapper()
{
var mapper = new UmbracoMapper(
new MapDefinitionCollection(Enumerable.Empty<IMapDefinition>),
Mock.Of<ICoreScopeProvider>());
var definition = new InstallerViewModelsMapDefinition();
definition.DefineMaps(mapper);
return mapper;
}
private List<IDatabaseProviderMetadata> CreateTestMetadata()
{
var metadata = new List<IDatabaseProviderMetadata>
{
new TestDatabaseProviderMetadata
{
Id = Guid.Parse("EC8ACD63-8CDE-4CA5-B2A3-06322720F274"),
SortOrder = 1,
DisplayName = "FirstMetadata",
DefaultDatabaseName = "TestDatabase",
IsAvailable = true,
GenerateConnectionStringDelegate = _ => "FirstTestMetadataConnectionString",
ProviderName = "SimpleTestMeta"
},
new TestDatabaseProviderMetadata
{
Id = Guid.Parse("C5AB4E1D-B7E4-47E5-B1A4-C9343B5F59CA"),
SortOrder = 2,
DisplayName = "SecondMetadata",
DefaultDatabaseName = "HostedTest",
IsAvailable = true,
RequiresServer = true,
ServerPlaceholder = "SomeServerPlaceholder",
RequiresCredentials = true,
RequiresConnectionTest = true,
ForceCreateDatabase = true,
GenerateConnectionStringDelegate = _ => "HostedDatabaseConnectionString",
ProviderName = "HostedTestMeta"
},
};
return metadata;
}
#nullable enable
public class TestDatabaseProviderMetadata : IDatabaseProviderMetadata
{
public Guid Id { get; set; }
public int SortOrder { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string DefaultDatabaseName { get; set; } = string.Empty;
public string? ProviderName { get; set; }
public bool SupportsQuickInstall { get; set; }
public bool IsAvailable { get; set; }
public bool RequiresServer { get; set; }
public string? ServerPlaceholder { get; set; }
public bool RequiresCredentials { get; set; }
public bool SupportsIntegratedAuthentication { get; set; }
public bool RequiresConnectionTest { get; set; }
public bool ForceCreateDatabase { get; set; }
public Func<DatabaseModel, string> GenerateConnectionStringDelegate { get; set; } =
_ => "ConnectionString";
public string? GenerateConnectionString(DatabaseModel databaseModel) => GenerateConnectionStringDelegate(databaseModel);
}
}

View File

@@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Umbraco.Cms.ManagementApi\Umbraco.Cms.ManagementApi.csproj" />
<ProjectReference Include="..\..\src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj" />
<ProjectReference Include="..\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj" />
<ProjectReference Include="..\..\src\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj" />

View File

@@ -562,7 +562,7 @@ public class MemberControllerUnitTests
var map = new MapDefinitionCollection(() => new List<IMapDefinition>
{
new Cms.Core.Models.Mapping.MemberMapDefinition(),
new global::Umbraco.Cms.Core.Models.Mapping.MemberMapDefinition(),
memberMapDefinition,
new ContentTypeMapDefinition(
commonMapper,