diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Install/InstallControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Install/InstallControllerBase.cs index 7c597e1a13..3cc96ac26a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Install/InstallControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Install/InstallControllerBase.cs @@ -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()), + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs index aecd62ff2a..8200b5cb95 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs @@ -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> Settings() + public async Task 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(installSettings)!; - return responseModel; + return Ok(responseModel); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Install/SetupInstallController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Install/SetupInstallController.cs index b26eb7a8b0..014fd43a0d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Install/SetupInstallController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Install/SetupInstallController.cs @@ -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, - 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 Setup(InstallVResponseModel installData) + public async Task Setup(InstallRequestModel installData) { InstallData data = _mapper.Map(installData)!; - await _installService.Install(data); + Attempt result = await _installService.InstallAsync(data); - return Ok(); + return InstallOperationResult(result.Status, result.Result); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Install/ValidateDatabaseInstallController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Install/ValidateDatabaseInstallController.cs index 33ac4bb4cd..51a36833f5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Install/ValidateDatabaseInstallController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Install/ValidateDatabaseInstallController.cs @@ -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 ValidateDatabase(DatabaseInstallResponseModel responseModel) + public async Task ValidateDatabase(DatabaseInstallRequestModel viewModel) { - DatabaseModel databaseModel = _mapper.Map(responseModel)!; + DatabaseModel databaseModel = _mapper.Map(viewModel)!; - var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true); + Attempt 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); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/AuthorizeUpgradeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/AuthorizeUpgradeController.cs index 7d7f38b873..a3d3b3d92b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/AuthorizeUpgradeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/AuthorizeUpgradeController.cs @@ -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 Authorize() { - await _upgradeService.Upgrade(); - return Ok(); + Attempt result = await _upgradeService.UpgradeAsync(); + return UpgradeOperationResult(result.Status, result.Result); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/UpgradeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/UpgradeControllerBase.cs index 810fe82d3b..b49437aa4d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/UpgradeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Upgrade/UpgradeControllerBase.cs @@ -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()), + }; } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs index 33893c1ba7..fc4e89de4b 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs @@ -10,13 +10,13 @@ public class InstallerViewModelsMapDefinition : IMapDefinition { public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((source, context) => new InstallData(), Map); - mapper.Define((source, context) => new UserInstallData(), Map); - mapper.Define((source, context) => new DatabaseInstallData(), Map); - mapper.Define((source, context) => new DatabaseModel(), Map); + mapper.Define((source, context) => new InstallData(), Map); + mapper.Define((source, context) => new UserInstallData(), Map); + mapper.Define((source, context) => new DatabaseInstallData(), Map); + mapper.Define((source, context) => new DatabaseModel(), Map); mapper.Define((source, context) => new DatabaseModel(), Map); mapper.Define((source, context) => new InstallSettingsResponseModel(), Map); - mapper.Define((source, context) => new UserSettingsViewModel(), Map); + mapper.Define((source, context) => new UserSettingsPresentationModel(), Map); mapper.Define((source, context) => new DatabaseSettingsModel(), Map); mapper.Define((source, context) => new DatabaseSettingsPresentationModel(), Map); mapper.Define((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(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(source.UserSettings)!; + target.User = context.Map(source.UserSettings)!; target.Databases = context.MapEnumerable(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; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 2a0078cbe5..4358509a9a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -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", diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallPresentationModel.cs similarity index 92% rename from src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallPresentationModel.cs index 56b12ba36c..74d6c90fba 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallPresentationModel.cs @@ -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; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallRequestModel.cs new file mode 100644 index 0000000000..56e10697f0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseInstallRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Installer; + +public class DatabaseInstallRequestModel : DatabaseInstallPresentationModel +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallVResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallRequestModel.cs similarity index 66% rename from src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallVResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallRequestModel.cs index 3f25930e7f..78bcdbf401 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallVResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallRequestModel.cs @@ -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; diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallSettingsResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallSettingsResponseModel.cs index e492c92eb3..1ed1c1a874 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallSettingsResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/InstallSettingsResponseModel.cs @@ -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 Databases { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallPresentationModel.cs similarity index 91% rename from src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallPresentationModel.cs index 8a7bdf4163..4e9f3a6e11 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallPresentationModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Umbraco.Cms.Api.Management.ViewModels.Installer; -public class UserInstallResponseModel +public class UserInstallPresentationModel { [Required] [StringLength(255)] diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsPresentationModel.cs similarity index 87% rename from src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsViewModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsPresentationModel.cs index 344f8f4927..ded48d45c7 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserSettingsPresentationModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Installer; -public class UserSettingsViewModel +public class UserSettingsPresentationModel { public int MinCharLength { get; set; } diff --git a/src/Umbraco.Core/Installer/IInstallStep.cs b/src/Umbraco.Core/Installer/IInstallStep.cs index 9d3f9b9041..0ccb417bf1 100644 --- a/src/Umbraco.Core/Installer/IInstallStep.cs +++ b/src/Umbraco.Core/Installer/IInstallStep.cs @@ -12,7 +12,7 @@ public interface IInstallStep /// /// InstallData model containing the data provided by the installer UI. /// - Task ExecuteAsync(InstallData model); + Task> ExecuteAsync(InstallData model); /// /// Determines if the step is required to execute. diff --git a/src/Umbraco.Core/Installer/IUpgradeStep.cs b/src/Umbraco.Core/Installer/IUpgradeStep.cs index df375f9684..891fcc8eca 100644 --- a/src/Umbraco.Core/Installer/IUpgradeStep.cs +++ b/src/Umbraco.Core/Installer/IUpgradeStep.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Cms.Core.Installer; +using Umbraco.Cms.Core.Models.Installer; + +namespace Umbraco.Cms.Core.Installer; /// /// Defines a step that's required to upgrade Umbraco. @@ -8,7 +10,7 @@ public interface IUpgradeStep /// /// Executes the upgrade step. /// - Task ExecuteAsync(); + Task> ExecuteAsync(); /// /// Determines if the step is required to execute. diff --git a/src/Umbraco.Core/Installer/StepBase.cs b/src/Umbraco.Core/Installer/StepBase.cs new file mode 100644 index 0000000000..4ad7983e76 --- /dev/null +++ b/src/Umbraco.Core/Installer/StepBase.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models.Installer; + +namespace Umbraco.Cms.Core.Installer; + +public abstract class StepBase +{ + protected Attempt FailWithMessage(string message) + => Attempt.Fail(new InstallationResult { ErrorMessage = message }); + + + protected Attempt Success() => Attempt.Succeed(new InstallationResult()); +} diff --git a/src/Umbraco.Core/Installer/Steps/FilePermissionsStep.cs b/src/Umbraco.Core/Installer/Steps/FilePermissionsStep.cs index c7412e1122..a36d30d640 100644 --- a/src/Umbraco.Core/Installer/Steps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Installer/Steps/FilePermissionsStep.cs @@ -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> ExecuteAsync(InstallData _) => Execute(); - public Task ExecuteAsync() => Execute(); + public Task> ExecuteAsync() => Execute(); - private Task Execute() + private Task> 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 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 RequiresExecutionAsync(InstallData model) => ShouldExecute(); diff --git a/src/Umbraco.Core/Installer/Steps/RestartRuntimeStep.cs b/src/Umbraco.Core/Installer/Steps/RestartRuntimeStep.cs index b85d7dc55d..5e65175430 100644 --- a/src/Umbraco.Core/Installer/Steps/RestartRuntimeStep.cs +++ b/src/Umbraco.Core/Installer/Steps/RestartRuntimeStep.cs @@ -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> ExecuteAsync(InstallData _) => await Execute(); - public async Task ExecuteAsync() => await Execute(); + public async Task> ExecuteAsync() => await Execute(); - private async Task Execute() => await _runtime.RestartAsync(); + private async Task> Execute() + { + await _runtime.RestartAsync(); + return Success(); + } public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); diff --git a/src/Umbraco.Core/Installer/Steps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Installer/Steps/TelemetryIdentifierStep.cs index 1af4678060..18a00e4a07 100644 --- a/src/Umbraco.Core/Installer/Steps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Installer/Steps/TelemetryIdentifierStep.cs @@ -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; private readonly ISiteIdentifierService _siteIdentifierService; @@ -19,14 +18,14 @@ public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep _siteIdentifierService = siteIdentifierService; } - public Task ExecuteAsync(InstallData _) => Execute(); + public Task> ExecuteAsync(InstallData _) => Execute(); - public Task ExecuteAsync() => Execute(); + public Task> ExecuteAsync() => Execute(); - private Task Execute() + private Task> Execute() { _siteIdentifierService.TryCreateSiteIdentifier(out _); - return Task.CompletedTask; + return Task.FromResult(Success()); } public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); diff --git a/src/Umbraco.Core/Models/Installer/InstallationResult.cs b/src/Umbraco.Core/Models/Installer/InstallationResult.cs new file mode 100644 index 0000000000..7f2ef6bab7 --- /dev/null +++ b/src/Umbraco.Core/Models/Installer/InstallationResult.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.Models.Installer; + +public class InstallationResult +{ + /// + /// Gets ore sets a string specifying why the installation failed. + /// + public string? ErrorMessage { get; set; } +} diff --git a/src/Umbraco.Core/Services/Installer/IInstallService.cs b/src/Umbraco.Core/Services/Installer/IInstallService.cs index c5110f5c5d..5b0630d6e1 100644 --- a/src/Umbraco.Core/Services/Installer/IInstallService.cs +++ b/src/Umbraco.Core/Services/Installer/IInstallService.cs @@ -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 /// /// InstallData containing the required data used to install /// - Task Install(InstallData model); + Task> InstallAsync(InstallData model); } diff --git a/src/Umbraco.Core/Services/Installer/IUpgradeService.cs b/src/Umbraco.Core/Services/Installer/IUpgradeService.cs index fe36dd03d7..e92ca7e975 100644 --- a/src/Umbraco.Core/Services/Installer/IUpgradeService.cs +++ b/src/Umbraco.Core/Services/Installer/IUpgradeService.cs @@ -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 /// /// Runs all the steps in the , upgrading Umbraco. /// - Task Upgrade(); + Task> UpgradeAsync(); } diff --git a/src/Umbraco.Core/Services/Installer/InstallService.cs b/src/Umbraco.Core/Services/Installer/InstallService.cs index f3f0adb307..026cb7f98d 100644 --- a/src/Umbraco.Core/Services/Installer/InstallService.cs +++ b/src/Umbraco.Core/Services/Installer/InstallService.cs @@ -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 } /// - public async Task Install(InstallData model) + public async Task> InstallAsync(InstallData model) { if (_runtimeState.Level != RuntimeLevel.Install) { @@ -32,16 +31,19 @@ public class InstallService : IInstallService try { - await RunSteps(model); + Attempt 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> 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 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.Succeed(); } } diff --git a/src/Umbraco.Core/Services/Installer/UpgradeService.cs b/src/Umbraco.Core/Services/Installer/UpgradeService.cs index b3f597ab74..1412eab63e 100644 --- a/src/Umbraco.Core/Services/Installer/UpgradeService.cs +++ b/src/Umbraco.Core/Services/Installer/UpgradeService.cs @@ -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 } /// - public async Task Upgrade() + public async Task> UpgradeAsync() { if (_runtimeState.Level != RuntimeLevel.Upgrade) { @@ -32,16 +32,19 @@ public class UpgradeService : IUpgradeService try { - await RunSteps(); + Attempt 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> RunStepsAsync() { foreach (IUpgradeStep step in _upgradeSteps) { @@ -54,8 +57,25 @@ public class UpgradeService : IUpgradeService } _logger.LogInformation("Running {StepName}", stepName); - await step.ExecuteAsync(); + Attempt 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.Succeed(); } } diff --git a/src/Umbraco.Core/Services/OperationStatus/InstallOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/InstallOperationStatus.cs new file mode 100644 index 0000000000..44011a2d39 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/InstallOperationStatus.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum InstallOperationStatus +{ + Success, + UnknownDatabaseProvider, + MissingConnectionString, + MissingProviderName, + DatabaseConnectionFailed, + InstallFailed, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/UpgradeOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UpgradeOperationStatus.cs new file mode 100644 index 0000000000..e06b8b9bdc --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/UpgradeOperationStatus.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum UpgradeOperationStatus +{ + Success, + UpgradeFailed, +} diff --git a/src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs index 34b567f813..76e31decca 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs @@ -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> 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(); } /// diff --git a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs index fb91f4aae6..33796affeb 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs @@ -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; private readonly DatabaseBuilder _databaseBuilder; @@ -30,26 +30,21 @@ public class DatabaseConfigureStep : IInstallStep _mapper = mapper; } - public Task ExecuteAsync(InstallData model) + public Task> ExecuteAsync(InstallData model) { DatabaseModel databaseModel = _mapper.Map(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 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); } } diff --git a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseInstallStep.cs index 30c094b1ad..78c04a6389 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseInstallStep.cs @@ -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> ExecuteAsync(InstallData _) => Execute(); - public Task ExecuteAsync() => Execute(); + public Task> ExecuteAsync() => Execute(); - private Task Execute() + private Task> 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 RequiresExecutionAsync(InstallData _) => ShouldExecute(); diff --git a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs index 9d537f571a..4d1e99a27e 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs @@ -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> ExecuteAsync(InstallData _) => ExecuteInternalAsync(); - public Task ExecuteAsync() => Execute(); + public Task> ExecuteAsync() => ExecuteInternalAsync(); - private Task Execute() + private Task> 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 RequiresExecutionAsync(InstallData model) => ShouldExecute(); diff --git a/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs index bf9d7e49fa..b7717d9fc4 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs @@ -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> ExecuteAsync(InstallData _) => Execute(); - public Task ExecuteAsync() => Execute(); + public Task> ExecuteAsync() => Execute(); - private Task Execute() => _installHelper.SetInstallStatusAsync(true, string.Empty); + private async Task> Execute() + { + await _installHelper.SetInstallStatusAsync(true, string.Empty); + return Success(); + } public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index fc5f9e2ceb..702abe5575 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -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> 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);