V14: Fix up install controller (#15646)

* Rename InstallVResponseModel to InstallRequestModel

* Align SettingsInstallController

* Rename split DatabaseInstallResponseModel in two

* Change UserInstallResponseModel to UserInstallViewModel

* Use PresentationModel instead of ViewModel

* Use operation status pattern when validating database

* Prepare for install to return a message

* Begin updating steps

* Make StepBase sharable between upgrade and install

* Update steps

* Use error message from install steps in install controller

* Use error message from upgrade steps in upgrade controller

* Use 500 for install/upgrade failed

It's entirely likely that it has nothing to do with the request

* Updated OpenApi.Json

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Mole
2024-01-30 13:12:08 +01:00
committed by GitHub
parent 9b454bec6b
commit 8c8405bbbf
33 changed files with 332 additions and 205 deletions

View File

@@ -1,8 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Api.Management.Filters;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Install;
@@ -13,4 +17,32 @@ namespace Umbraco.Cms.Api.Management.Controllers.Install;
[RequireRuntimeLevel(RuntimeLevel.Install)]
public abstract class InstallControllerBase : ManagementApiControllerBase
{
protected IActionResult InstallOperationResult(InstallOperationStatus status, InstallationResult? result = null) =>
status switch
{
InstallOperationStatus.Success => Ok(),
InstallOperationStatus.UnknownDatabaseProvider => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid database configuration")
.WithDetail("The database provider is unknown.")
.Build()),
InstallOperationStatus.MissingConnectionString => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid database configuration")
.WithDetail("The connection string is missing.")
.Build()),
InstallOperationStatus.MissingProviderName => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid database configuration")
.WithDetail("The provider name is missing.")
.Build()),
InstallOperationStatus.DatabaseConnectionFailed => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid database configuration")
.WithDetail("Could not connect to the database.")
.Build()),
InstallOperationStatus.InstallFailed => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Install failed")
.WithDetail(result?.ErrorMessage ?? "An unknown error occurred.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Unknown install operation status.")
.Build()),
};
}

View File

@@ -28,10 +28,9 @@ public class SettingsInstallController : InstallControllerBase
[HttpGet("settings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)]
[ProducesResponseType(typeof(InstallSettingsResponseModel), StatusCodes.Status200OK)]
public async Task<ActionResult<InstallSettingsResponseModel>> Settings()
public async Task<IActionResult> Settings()
{
// Register that the install has started
await _installHelper.SetInstallStatusAsync(false, string.Empty);
@@ -39,6 +38,6 @@ public class SettingsInstallController : InstallControllerBase
InstallSettingsModel installSettings = _installSettingsFactory.GetInstallSettings();
InstallSettingsResponseModel responseModel = _mapper.Map<InstallSettingsResponseModel>(installSettings)!;
return responseModel;
return Ok(responseModel);
}
}

View File

@@ -1,13 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Api.Management.ViewModels.Installer;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Install;
@@ -16,31 +15,24 @@ public class SetupInstallController : InstallControllerBase
{
private readonly IUmbracoMapper _mapper;
private readonly IInstallService _installService;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly GlobalSettings _globalSettings;
public SetupInstallController(
IUmbracoMapper mapper,
IInstallService installService,
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment)
IInstallService installService)
{
_mapper = mapper;
_installService = installService;
_hostingEnvironment = hostingEnvironment;
_globalSettings = globalSettings.Value;
}
[HttpPost("setup")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Setup(InstallVResponseModel installData)
public async Task<IActionResult> Setup(InstallRequestModel installData)
{
InstallData data = _mapper.Map<InstallData>(installData)!;
await _installService.Install(data);
Attempt<InstallationResult?, InstallOperationStatus> result = await _installService.InstallAsync(data);
return Ok();
return InstallOperationResult(result.Status, result.Result);
}
}

View File

@@ -5,6 +5,8 @@ using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Api.Management.ViewModels.Installer;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Install;
@@ -26,25 +28,13 @@ public class ValidateDatabaseInstallController : InstallControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> ValidateDatabase(DatabaseInstallResponseModel responseModel)
public async Task<IActionResult> ValidateDatabase(DatabaseInstallRequestModel viewModel)
{
DatabaseModel databaseModel = _mapper.Map<DatabaseModel>(responseModel)!;
DatabaseModel databaseModel = _mapper.Map<DatabaseModel>(viewModel)!;
var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true);
Attempt<InstallOperationStatus> attempt = await _databaseBuilder.ValidateDatabaseConnectionAsync(databaseModel);
if (success)
{
return await Task.FromResult(Ok());
}
var invalidModelProblem = new ProblemDetails
{
Title = "Invalid database configuration",
Detail = "The provided database configuration is invalid",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(BadRequest(invalidModelProblem));
return InstallOperationResult(attempt.Result);
}
}

View File

@@ -1,7 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Upgrade;
@@ -19,7 +22,7 @@ public class AuthorizeUpgradeController : UpgradeControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Authorize()
{
await _upgradeService.Upgrade();
return Ok();
Attempt<InstallationResult?, UpgradeOperationStatus> result = await _upgradeService.UpgradeAsync();
return UpgradeOperationResult(result.Status, result.Result);
}
}

View File

@@ -1,8 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Api.Management.Filters;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Upgrade;
@@ -14,5 +18,16 @@ namespace Umbraco.Cms.Api.Management.Controllers.Upgrade;
[Authorize(Policy = "New" + AuthorizationPolicies.RequireAdminAccess)]
public abstract class UpgradeControllerBase : ManagementApiControllerBase
{
protected IActionResult UpgradeOperationResult(UpgradeOperationStatus status, InstallationResult? result = null) =>
status switch
{
UpgradeOperationStatus.Success => Ok(),
UpgradeOperationStatus.UpgradeFailed => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Upgrade failed")
.WithDetail(result?.ErrorMessage ?? "An unknown error occurred.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Unknown upgrade operation status.")
.Build()),
};
}

View File

@@ -10,13 +10,13 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<InstallVResponseModel, InstallData>((source, context) => new InstallData(), Map);
mapper.Define<UserInstallResponseModel, UserInstallData>((source, context) => new UserInstallData(), Map);
mapper.Define<DatabaseInstallResponseModel, DatabaseInstallData>((source, context) => new DatabaseInstallData(), Map);
mapper.Define<DatabaseInstallResponseModel, DatabaseModel>((source, context) => new DatabaseModel(), Map);
mapper.Define<InstallRequestModel, InstallData>((source, context) => new InstallData(), Map);
mapper.Define<UserInstallPresentationModel, UserInstallData>((source, context) => new UserInstallData(), Map);
mapper.Define<DatabaseInstallPresentationModel, DatabaseInstallData>((source, context) => new DatabaseInstallData(), Map);
mapper.Define<DatabaseInstallPresentationModel, DatabaseModel>((source, context) => new DatabaseModel(), Map);
mapper.Define<DatabaseInstallData, DatabaseModel>((source, context) => new DatabaseModel(), Map);
mapper.Define<InstallSettingsModel, InstallSettingsResponseModel>((source, context) => new InstallSettingsResponseModel(), Map);
mapper.Define<UserSettingsModel, UserSettingsViewModel>((source, context) => new UserSettingsViewModel(), Map);
mapper.Define<UserSettingsModel, UserSettingsPresentationModel>((source, context) => new UserSettingsPresentationModel(), Map);
mapper.Define<IDatabaseProviderMetadata, DatabaseSettingsModel>((source, context) => new DatabaseSettingsModel(), Map);
mapper.Define<DatabaseSettingsModel, DatabaseSettingsPresentationModel>((source, context) => new DatabaseSettingsPresentationModel(), Map);
mapper.Define<ConsentLevelModel, ConsentLevelPresentationModel>((source, context) => new ConsentLevelPresentationModel(), Map);
@@ -33,7 +33,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
private void Map(DatabaseInstallResponseModel source, DatabaseModel target, MapperContext context)
private void Map(DatabaseInstallPresentationModel source, DatabaseModel target, MapperContext context)
{
target.ConnectionString = source.ConnectionString;
target.DatabaseName = source.Name ?? string.Empty;
@@ -47,7 +47,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
private static void Map(InstallVResponseModel source, InstallData target, MapperContext context)
private static void Map(InstallRequestModel source, InstallData target, MapperContext context)
{
target.TelemetryLevel = source.TelemetryLevel;
target.User = context.Map<UserInstallData>(source.User)!;
@@ -55,7 +55,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
private static void Map(UserInstallResponseModel source, UserInstallData target, MapperContext context)
private static void Map(UserInstallPresentationModel source, UserInstallData target, MapperContext context)
{
target.Email = source.Email;
target.Name = source.Name;
@@ -64,7 +64,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
private static void Map(DatabaseInstallResponseModel source, DatabaseInstallData target, MapperContext context)
private static void Map(DatabaseInstallPresentationModel source, DatabaseInstallData target, MapperContext context)
{
target.Id = source.Id;
target.ProviderName = source.ProviderName;
@@ -94,12 +94,12 @@ public class InstallerViewModelsMapDefinition : IMapDefinition
// Umbraco.Code.MapAll
private static void Map(InstallSettingsModel source, InstallSettingsResponseModel target, MapperContext context)
{
target.User = context.Map<UserSettingsViewModel>(source.UserSettings)!;
target.User = context.Map<UserSettingsPresentationModel>(source.UserSettings)!;
target.Databases = context.MapEnumerable<DatabaseSettingsModel, DatabaseSettingsPresentationModel>(source.DatabaseSettings);
}
// Umbraco.Code.MapAll
private static void Map(UserSettingsModel source, UserSettingsViewModel target, MapperContext context)
private static void Map(UserSettingsModel source, UserSettingsPresentationModel target, MapperContext context)
{
target.MinCharLength = source.PasswordSettings.MinCharLength;
target.MinNonAlphaNumericLength = source.PasswordSettings.MinNonAlphaNumericLength;

View File

@@ -7207,26 +7207,6 @@
],
"operationId": "GetInstallSettings",
"responses": {
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"428": {
"description": "Client Error",
"content": {
@@ -7294,7 +7274,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/InstallVResponseModel"
"$ref": "#/components/schemas/InstallRequestModel"
}
]
}
@@ -7303,7 +7283,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/InstallVResponseModel"
"$ref": "#/components/schemas/InstallRequestModel"
}
]
}
@@ -7312,7 +7292,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/InstallVResponseModel"
"$ref": "#/components/schemas/InstallRequestModel"
}
]
}
@@ -7320,26 +7300,6 @@
}
},
"responses": {
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"428": {
"description": "Client Error",
"content": {
@@ -7378,7 +7338,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DatabaseInstallResponseModel"
"$ref": "#/components/schemas/DatabaseInstallRequestModel"
}
]
}
@@ -7387,7 +7347,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DatabaseInstallResponseModel"
"$ref": "#/components/schemas/DatabaseInstallRequestModel"
}
]
}
@@ -7396,7 +7356,7 @@
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DatabaseInstallResponseModel"
"$ref": "#/components/schemas/DatabaseInstallRequestModel"
}
]
}
@@ -23764,7 +23724,7 @@
},
"additionalProperties": false
},
"DatabaseInstallResponseModel": {
"DatabaseInstallPresentationModel": {
"required": [
"id",
"providerName",
@@ -23810,6 +23770,15 @@
},
"additionalProperties": false
},
"DatabaseInstallRequestModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/DatabaseInstallPresentationModel"
}
],
"additionalProperties": false
},
"DatabaseSettingsPresentationModel": {
"required": [
"defaultDatabaseName",
@@ -25325,6 +25294,37 @@
},
"additionalProperties": false
},
"InstallRequestModel": {
"required": [
"database",
"telemetryLevel",
"user"
],
"type": "object",
"properties": {
"user": {
"oneOf": [
{
"$ref": "#/components/schemas/UserInstallPresentationModel"
}
]
},
"database": {
"oneOf": [
{
"$ref": "#/components/schemas/DatabaseInstallPresentationModel"
},
{
"$ref": "#/components/schemas/DatabaseInstallRequestModel"
}
]
},
"telemetryLevel": {
"$ref": "#/components/schemas/TelemetryLevelModel"
}
},
"additionalProperties": false
},
"InstallSettingsResponseModel": {
"required": [
"databases",
@@ -25335,7 +25335,7 @@
"user": {
"oneOf": [
{
"$ref": "#/components/schemas/UserSettingsModel"
"$ref": "#/components/schemas/UserSettingsPresentationModel"
}
]
},
@@ -25352,34 +25352,6 @@
},
"additionalProperties": false
},
"InstallVResponseModel": {
"required": [
"database",
"telemetryLevel",
"user"
],
"type": "object",
"properties": {
"user": {
"oneOf": [
{
"$ref": "#/components/schemas/UserInstallResponseModel"
}
]
},
"database": {
"oneOf": [
{
"$ref": "#/components/schemas/DatabaseInstallResponseModel"
}
]
},
"telemetryLevel": {
"$ref": "#/components/schemas/TelemetryLevelModel"
}
},
"additionalProperties": false
},
"InviteUserRequestModel": {
"type": "object",
"allOf": [
@@ -29563,7 +29535,7 @@
},
"additionalProperties": false
},
"UserInstallResponseModel": {
"UserInstallPresentationModel": {
"required": [
"email",
"name",
@@ -29766,7 +29738,7 @@
},
"additionalProperties": false
},
"UserSettingsModel": {
"UserSettingsPresentationModel": {
"required": [
"consentLevels",
"minCharLength",

View File

@@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class DatabaseInstallResponseModel
public class DatabaseInstallPresentationModel
{
[Required]
public Guid Id { get; set; }

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class DatabaseInstallRequestModel : DatabaseInstallPresentationModel
{
}

View File

@@ -4,13 +4,13 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class InstallVResponseModel
public class InstallRequestModel
{
[Required]
public UserInstallResponseModel User { get; set; } = null!;
public UserInstallPresentationModel User { get; set; } = null!;
[Required]
public DatabaseInstallResponseModel Database { get; set; } = null!;
public DatabaseInstallPresentationModel Database { get; set; } = null!;
[JsonConverter(typeof(JsonStringEnumConverter))]
public TelemetryLevel TelemetryLevel { get; set; } = TelemetryLevel.Basic;

View File

@@ -5,7 +5,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class InstallSettingsResponseModel
{
[Required]
public UserSettingsViewModel User { get; set; } = null!;
public UserSettingsPresentationModel User { get; set; } = null!;
public IEnumerable<DatabaseSettingsPresentationModel> Databases { get; set; } = Enumerable.Empty<DatabaseSettingsPresentationModel>();
}

View File

@@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class UserInstallResponseModel
public class UserInstallPresentationModel
{
[Required]
[StringLength(255)]

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
public class UserSettingsViewModel
public class UserSettingsPresentationModel
{
public int MinCharLength { get; set; }

View File

@@ -12,7 +12,7 @@ public interface IInstallStep
/// </summary>
/// <param name="model">InstallData model containing the data provided by the installer UI.</param>
/// <returns></returns>
Task ExecuteAsync(InstallData model);
Task<Attempt<InstallationResult>> ExecuteAsync(InstallData model);
/// <summary>
/// Determines if the step is required to execute.

View File

@@ -1,4 +1,6 @@
namespace Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
namespace Umbraco.Cms.Core.Installer;
/// <summary>
/// Defines a step that's required to upgrade Umbraco.
@@ -8,7 +10,7 @@ public interface IUpgradeStep
/// <summary>
/// Executes the upgrade step.
/// </summary>
Task ExecuteAsync();
Task<Attempt<InstallationResult>> ExecuteAsync();
/// <summary>
/// Determines if the step is required to execute.

View File

@@ -0,0 +1,12 @@
using Umbraco.Cms.Core.Models.Installer;
namespace Umbraco.Cms.Core.Installer;
public abstract class StepBase
{
protected Attempt<InstallationResult> FailWithMessage(string message)
=> Attempt<InstallationResult>.Fail(new InstallationResult { ErrorMessage = message });
protected Attempt<InstallationResult> Success() => Attempt<InstallationResult>.Succeed(new InstallationResult());
}

View File

@@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.Installer;
namespace Umbraco.Cms.Core.Installer.Steps;
public class FilePermissionsStep : IInstallStep, IUpgradeStep
public class FilePermissionsStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly IFilePermissionHelper _filePermissionHelper;
private readonly ILocalizedTextService _localizedTextService;
@@ -18,11 +18,11 @@ public class FilePermissionsStep : IInstallStep, IUpgradeStep
_localizedTextService = localizedTextService;
}
public Task ExecuteAsync(InstallData _) => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => Execute();
public Task ExecuteAsync() => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync() => Execute();
private Task Execute()
private Task<Attempt<InstallationResult>> Execute()
{
// validate file permissions
var permissionsOk =
@@ -33,10 +33,11 @@ public class FilePermissionsStep : IInstallStep, IUpgradeStep
report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value);
if (permissionsOk == false)
{
throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors });
IEnumerable<string> errorstring = translatedErrors.Select(x => $"{x.Key}: {string.Join(", ", x.Value)}");
return Task.FromResult(FailWithMessage("Permission check failed:\n " + string.Join("\n", errorstring)));
}
return Task.CompletedTask;
return Task.FromResult(Success());
}
public Task<bool> RequiresExecutionAsync(InstallData model) => ShouldExecute();

View File

@@ -3,17 +3,21 @@ using Umbraco.Cms.Core.Models.Installer;
namespace Umbraco.Cms.Core.Installer.Steps;
public class RestartRuntimeStep : IInstallStep, IUpgradeStep
public class RestartRuntimeStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly IRuntime _runtime;
public RestartRuntimeStep(IRuntime runtime) => _runtime = runtime;
public async Task ExecuteAsync(InstallData _) => await Execute();
public async Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => await Execute();
public async Task ExecuteAsync() => await Execute();
public async Task<Attempt<InstallationResult>> ExecuteAsync() => await Execute();
private async Task Execute() => await _runtime.RestartAsync();
private async Task<Attempt<InstallationResult>> Execute()
{
await _runtime.RestartAsync();
return Success();
}
public Task<bool> RequiresExecutionAsync(InstallData _) => ShouldExecute();

View File

@@ -1,12 +1,11 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Core.Models.Installer;
namespace Umbraco.Cms.Core.Installer.Steps;
public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep
public class TelemetryIdentifierStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly IOptions<GlobalSettings> _globalSettings;
private readonly ISiteIdentifierService _siteIdentifierService;
@@ -19,14 +18,14 @@ public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep
_siteIdentifierService = siteIdentifierService;
}
public Task ExecuteAsync(InstallData _) => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => Execute();
public Task ExecuteAsync() => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync() => Execute();
private Task Execute()
private Task<Attempt<InstallationResult>> Execute()
{
_siteIdentifierService.TryCreateSiteIdentifier(out _);
return Task.CompletedTask;
return Task.FromResult(Success());
}
public Task<bool> RequiresExecutionAsync(InstallData _) => ShouldExecute();

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Core.Models.Installer;
public class InstallationResult
{
/// <summary>
/// Gets ore sets a string specifying why the installation failed.
/// </summary>
public string? ErrorMessage { get; set; }
}

View File

@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.Installer;
@@ -10,5 +11,5 @@ public interface IInstallService
/// </summary>
/// <param name="model">InstallData containing the required data used to install</param>
/// <returns></returns>
Task Install(InstallData model);
Task<Attempt<InstallationResult?, InstallOperationStatus>> InstallAsync(InstallData model);
}

View File

@@ -1,4 +1,6 @@
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.Installer;
@@ -7,5 +9,5 @@ public interface IUpgradeService
/// <summary>
/// Runs all the steps in the <see cref="UpgradeStepCollection"/>, upgrading Umbraco.
/// </summary>
Task Upgrade();
Task<Attempt<InstallationResult?, UpgradeOperationStatus>> UpgradeAsync();
}

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.Installer;
@@ -23,7 +22,7 @@ public class InstallService : IInstallService
}
/// <inheritdoc/>
public async Task Install(InstallData model)
public async Task<Attempt<InstallationResult?, InstallOperationStatus>> InstallAsync(InstallData model)
{
if (_runtimeState.Level != RuntimeLevel.Install)
{
@@ -32,16 +31,19 @@ public class InstallService : IInstallService
try
{
await RunSteps(model);
Attempt<InstallationResult?> result = await RunStepsAsync(model);
return result.Success
? Attempt.SucceedWithStatus(InstallOperationStatus.Success, result.Result)
: Attempt.FailWithStatus(InstallOperationStatus.InstallFailed, result.Result);
}
catch (Exception exception)
{
_logger.LogError(exception, "Encountered an error when running the install steps");
_logger.LogError(exception, "Encountered an unexpected error when running the install steps");
throw;
}
}
private async Task RunSteps(InstallData model)
private async Task<Attempt<InstallationResult?>> RunStepsAsync(InstallData model)
{
foreach (IInstallStep step in _installSteps)
{
@@ -54,8 +56,25 @@ public class InstallService : IInstallService
}
_logger.LogInformation("Running {StepName}", stepName);
await step.ExecuteAsync(model);
Attempt<InstallationResult> result = await step.ExecuteAsync(model);
if (result.Success is false)
{
if (result.Result?.ErrorMessage is not null)
{
_logger.LogError("Failed {StepName}, with the message: {Message}", stepName, result.Result?.ErrorMessage);
}
else
{
_logger.LogError("Failed {StepName}", stepName);
}
return Attempt.Fail(result.Result);
}
_logger.LogInformation("Finished {StepName}", stepName);
}
return Attempt<InstallationResult?>.Succeed();
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.Installer;
@@ -22,7 +22,7 @@ public class UpgradeService : IUpgradeService
}
/// <inheritdoc/>
public async Task Upgrade()
public async Task<Attempt<InstallationResult?, UpgradeOperationStatus>> UpgradeAsync()
{
if (_runtimeState.Level != RuntimeLevel.Upgrade)
{
@@ -32,16 +32,19 @@ public class UpgradeService : IUpgradeService
try
{
await RunSteps();
Attempt<InstallationResult?> result = await RunStepsAsync();
return result.Success
? Attempt.SucceedWithStatus(UpgradeOperationStatus.Success, result.Result)
: Attempt.FailWithStatus(UpgradeOperationStatus.UpgradeFailed, result.Result);
}
catch (Exception exception)
{
_logger.LogError(exception, "Encountered an error when running the upgrade steps");
_logger.LogError(exception, "Encountered an unexpected error when running the upgrade steps");
throw;
}
}
private async Task RunSteps()
private async Task<Attempt<InstallationResult?>> RunStepsAsync()
{
foreach (IUpgradeStep step in _upgradeSteps)
{
@@ -54,8 +57,25 @@ public class UpgradeService : IUpgradeService
}
_logger.LogInformation("Running {StepName}", stepName);
await step.ExecuteAsync();
Attempt<InstallationResult> result = await step.ExecuteAsync();
if (result.Success is false)
{
if (result.Result?.ErrorMessage is not null)
{
_logger.LogError("Failed {StepName}, with the message: {Message}", stepName, result.Result?.ErrorMessage);
}
else
{
_logger.LogError("Failed {StepName}", stepName);
}
return Attempt.Fail(result.Result);
}
_logger.LogInformation("Finished {StepName}", stepName);
}
return Attempt<InstallationResult?>.Succeed();
}
}

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum InstallOperationStatus
{
Success,
UnknownDatabaseProvider,
MissingConnectionString,
MissingProviderName,
DatabaseConnectionFailed,
InstallFailed,
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum UpgradeOperationStatus
{
Success,
UpgradeFailed,
}

View File

@@ -4,8 +4,8 @@ using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Models.Membership;
@@ -20,7 +20,7 @@ using HttpResponseMessage = System.Net.Http.HttpResponseMessage;
namespace Umbraco.Cms.Infrastructure.Installer.Steps;
public class CreateUserStep : IInstallStep
public class CreateUserStep : StepBase, IInstallStep
{
private readonly IUserService _userService;
private readonly DatabaseBuilder _databaseBuilder;
@@ -54,12 +54,12 @@ public class CreateUserStep : IInstallStep
_metricsConsentService = metricsConsentService;
}
public async Task ExecuteAsync(InstallData model)
public async Task<Attempt<InstallationResult>> ExecuteAsync(InstallData model)
{
IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId);
if (admin == null)
if (admin is null)
{
throw new InvalidOperationException("Could not find the super user!");
return FailWithMessage("Could not find the super user");
}
UserInstallData user = model.User;
@@ -72,21 +72,21 @@ public class CreateUserStep : IInstallStep
BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString);
if (membershipUser == null)
{
throw new InvalidOperationException(
return FailWithMessage(
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
}
//To change the password here we actually need to reset it since we don't have an old one to use to change
// To change the password here we actually need to reset it since we don't have an old one to use to change
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser);
if (string.IsNullOrWhiteSpace(resetToken))
{
throw new InvalidOperationException("Could not reset password: unable to generate internal reset token");
return FailWithMessage("Could not reset password: unable to generate internal reset token");
}
IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
if (!resetResult.Succeeded)
{
throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
}
_metricsConsentService.SetConsentLevel(model.TelemetryLevel);
@@ -104,6 +104,8 @@ public class CreateUserStep : IInstallStep
}
catch { /* fail in silence */ }
}
return Success();
}
/// <inheritdoc/>

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Mapping;
@@ -11,7 +11,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Installer.Steps;
public class DatabaseConfigureStep : IInstallStep
public class DatabaseConfigureStep : StepBase, IInstallStep
{
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly DatabaseBuilder _databaseBuilder;
@@ -30,26 +30,21 @@ public class DatabaseConfigureStep : IInstallStep
_mapper = mapper;
}
public Task ExecuteAsync(InstallData model)
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData model)
{
DatabaseModel databaseModel = _mapper.Map<DatabaseModel>(model.Database)!;
if (!_databaseBuilder.ConfigureDatabaseConnection(databaseModel, false))
{
throw new InstallException("Could not connect to the database");
return Task.FromResult(FailWithMessage("Could not connect to the database"));
}
return Task.CompletedTask;
return Task.FromResult(Success());
}
public Task<bool> RequiresExecutionAsync(InstallData model)
{
// If the connection string is already present in config we don't need to configure it again
if (_connectionStrings.CurrentValue.IsConnectionStringConfigured())
{
return Task.FromResult(false);
}
return Task.FromResult(true);
return Task.FromResult(_connectionStrings.CurrentValue.IsConnectionStringConfigured() is false);
}
}

View File

@@ -7,7 +7,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install;
namespace Umbraco.Cms.Infrastructure.Installer.Steps;
public class DatabaseInstallStep : IInstallStep, IUpgradeStep
public class DatabaseInstallStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly IRuntimeState _runtime;
private readonly DatabaseBuilder _databaseBuilder;
@@ -18,11 +18,11 @@ public class DatabaseInstallStep : IInstallStep, IUpgradeStep
_databaseBuilder = databaseBuilder;
}
public Task ExecuteAsync(InstallData _) => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => Execute();
public Task ExecuteAsync() => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync() => Execute();
private Task Execute()
private Task<Attempt<InstallationResult>> Execute()
{
if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase)
@@ -34,10 +34,10 @@ public class DatabaseInstallStep : IInstallStep, IUpgradeStep
if (result?.Success == false)
{
throw new InstallException("The database failed to install. ERROR: " + result.Message);
return Task.FromResult(FailWithMessage("The database failed to install. ERROR: " + result.Message));
}
return Task.CompletedTask;
return Task.FromResult(Success());
}
public Task<bool> RequiresExecutionAsync(InstallData _) => ShouldExecute();

View File

@@ -1,17 +1,15 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Install;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Migrations.PostMigrations;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
namespace Umbraco.Cms.Infrastructure.Installer.Steps;
public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep
public class DatabaseUpgradeStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly DatabaseBuilder _databaseBuilder;
private readonly IRuntimeState _runtime;
@@ -33,11 +31,11 @@ public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep
_keyValueService = keyValueService;
}
public Task ExecuteAsync(InstallData _) => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => ExecuteInternalAsync();
public Task ExecuteAsync() => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync() => ExecuteInternalAsync();
private Task Execute()
private Task<Attempt<InstallationResult>> ExecuteInternalAsync()
{
_logger.LogInformation("Running 'Upgrade' service");
@@ -48,10 +46,10 @@ public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep
if (result?.Success == false)
{
throw new InstallException("The database failed to upgrade. ERROR: " + result.Message);
return Task.FromResult(FailWithMessage("The database failed to upgrade. ERROR: " + result.Message));
}
return Task.CompletedTask;
return Task.FromResult(Success());
}
public Task<bool> RequiresExecutionAsync(InstallData model) => ShouldExecute();

View File

@@ -1,20 +1,25 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Installer;
using Umbraco.Cms.Core.Models.Installer;
using Umbraco.Cms.Infrastructure.Install;
namespace Umbraco.Cms.Infrastructure.Installer.Steps;
public class RegisterInstallCompleteStep : IInstallStep, IUpgradeStep
public class RegisterInstallCompleteStep : StepBase, IInstallStep, IUpgradeStep
{
private readonly InstallHelper _installHelper;
public RegisterInstallCompleteStep(InstallHelper installHelper) => _installHelper = installHelper;
public Task ExecuteAsync(InstallData _) => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync(InstallData _) => Execute();
public Task ExecuteAsync() => Execute();
public Task<Attempt<InstallationResult>> ExecuteAsync() => Execute();
private Task Execute() => _installHelper.SetInstallStatusAsync(true, string.Empty);
private async Task<Attempt<InstallationResult>> Execute()
{
await _installHelper.SetInstallStatusAsync(true, string.Empty);
return Success();
}
public Task<bool> RequiresExecutionAsync(InstallData _) => ShouldExecute();

View File

@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Infrastructure.Migrations.Notifications;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
@@ -231,6 +232,36 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
return true;
}
public Task<Attempt<InstallOperationStatus>> ValidateDatabaseConnectionAsync(DatabaseModel databaseSettings)
{
IDatabaseProviderMetadata? providerMeta = _databaseProviderMetadata.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
if (providerMeta is null)
{
return Task.FromResult(Attempt.Fail(InstallOperationStatus.UnknownDatabaseProvider));
}
var connectionString = providerMeta.GenerateConnectionString(databaseSettings);
var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName;
if (string.IsNullOrEmpty(connectionString))
{
return Task.FromResult(Attempt.Fail(InstallOperationStatus.MissingConnectionString));
}
if (string.IsNullOrEmpty(providerName))
{
return Task.FromResult(Attempt.Fail(InstallOperationStatus.MissingProviderName));
}
if (providerMeta.RequiresConnectionTest && CanConnect(connectionString, providerName) is false)
{
return Task.FromResult(Attempt.Fail(InstallOperationStatus.DatabaseConnectionFailed));
}
return Task.FromResult(Attempt.Succeed(InstallOperationStatus.Success));
}
private void Configure(bool installMissingDatabase)
{
_databaseFactory.Configure(_connectionStrings.CurrentValue);