v10: Use ForceCreateDatabase during unattended install and extend GetUmbracoConnectionString extension methods (#12397)

* Add extension methods to get the Umbraco connection string/provider name from configuration

* Added tests for configuration extension methods.

* Fix issue with InstallMissingDatabase and ForceCreateDatabase

* Fix comments

* Revert casing change in GenerateConnectionString

* Re-add AddOptions (without config binding) to fix test

* Update src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs

Co-authored-by: Ronald Barendse <ronald@barend.se>

* Update src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs

* Update src/Umbraco.Infrastructure/Runtime/RuntimeState.cs

* Whitespace and documentation updates

* Add DatabaseProviderMetadataExtensions

* Filter before ordering

* Replace DataDirectory placeholder when setting connection string

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Ronald Barendse
2022-05-17 12:59:01 +02:00
committed by GitHub
parent e82bcb1b76
commit 8e6e262c7f
15 changed files with 487 additions and 243 deletions

View File

@@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -13,9 +9,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall,
"DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website",
PerformsAppRestart = true)]
[InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)]
public class DatabaseConfigureStep : InstallSetupStep<DatabaseModel>
{
private readonly DatabaseBuilder _databaseBuilder;
@@ -45,44 +39,33 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
return Task.FromResult<InstallSetupResult?>(null);
}
public override object ViewModel
public override object ViewModel => new
{
get
{
var options = _databaseProviderMetadata
.Where(x => x.IsAvailable)
.OrderBy(x => x.SortOrder)
.ToList();
databases = _databaseProviderMetadata.GetAvailable().ToList()
};
return new
{
databases = options
};
}
}
public override string View => ShouldDisplayView() ? base.View : "";
public override string View => ShouldDisplayView() ? base.View : string.Empty;
public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView();
private bool ShouldDisplayView()
{
//If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading.
// If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading.
var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName);
if (databaseSettings.IsConnectionStringConfigured())
{
try
{
//Since a connection string was present we verify the db can connect and query
_ = _databaseBuilder.ValidateSchema();
// Since a connection string was present we verify the db can connect and query
_databaseBuilder.ValidateSchema();
return false;
}
catch (Exception ex)
{
// Something went wrong, could not connect so probably need to reconfigure
_logger.LogError(ex, "An error occurred, reconfiguring...");
//something went wrong, could not connect so probably need to reconfigure
return true;
}
}

View File

@@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core.Configuration.Models;
@@ -118,10 +113,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
get
{
var quickInstallSettings = _databaseProviderMetadata
.Where(x => x.SupportsQuickInstall)
.Where(x => x.IsAvailable)
.OrderBy(x => x.SortOrder)
var quickInstallSettings = _databaseProviderMetadata.GetAvailable(true)
.Select(x => new
{
displayName = x.DisplayName,

View File

@@ -69,10 +69,10 @@ namespace Umbraco.Cms.Infrastructure.Install
break;
case RuntimeLevelReason.UpgradePackageMigrations:
{
if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPacakgeMigrationsStateKey, out var pm)
if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPackageMigrationsStateKey, out var pm)
|| pm is not IReadOnlyList<string> pendingMigrations)
{
throw new InvalidOperationException($"The required key {RuntimeState.PendingPacakgeMigrationsStateKey} does not exist in startup state");
throw new InvalidOperationException($"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state");
}
if (pendingMigrations.Count == 0)

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -139,20 +136,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
// if the database model is null then we will attempt quick install.
if (databaseSettings == null)
{
providerMeta = _databaseProviderMetadata
.OrderBy(x => x.SortOrder)
.Where(x => x.SupportsQuickInstall)
.FirstOrDefault(x => x.IsAvailable);
providerMeta = _databaseProviderMetadata.GetAvailable(true).FirstOrDefault();
databaseSettings = new DatabaseModel
{
DatabaseName = providerMeta?.DefaultDatabaseName!,
DatabaseName = providerMeta?.DefaultDatabaseName!
};
}
else
{
providerMeta = _databaseProviderMetadata
.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
providerMeta = _databaseProviderMetadata.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
}
if (providerMeta == null)
@@ -177,7 +169,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
return true;
}
private void Configure(string connectionString, string? providerName, bool installMissingDatabase)
{
// Update existing connection string

View File

@@ -0,0 +1,55 @@
using Umbraco.Cms.Core.Install.Models;
namespace Umbraco.Cms.Infrastructure.Persistence;
/// <summary>
/// Extension methods for <see cref="IDatabaseProviderMetadata" />.
/// </summary>
public static class DatabaseProviderMetadataExtensions
{
/// <summary>
/// Gets the available database provider metadata.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="onlyQuickInstall">If set to <c>true</c> only returns providers that support quick install.</param>
/// <returns>
/// The available database provider metadata.
/// </returns>
public static IEnumerable<IDatabaseProviderMetadata> GetAvailable(this IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata, bool onlyQuickInstall = false)
=> databaseProviderMetadata.Where(x => (!onlyQuickInstall || x.SupportsQuickInstall) && x.IsAvailable).OrderBy(x => x.SortOrder);
/// <summary>
/// Determines whether a database can be created for the specified provider name while ignoring the value of <see cref="GlobalSettings.InstallMissingDatabase" />.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="providerName">The name of the provider.</param>
/// <returns>
/// <c>true</c> if a database can be created for the specified provider name; otherwise, <c>false</c>.
/// </returns>
public static bool CanForceCreateDatabase(this IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata, string? providerName)
=> databaseProviderMetadata.FirstOrDefault(x => x.ProviderName == providerName)?.ForceCreateDatabase == true;
/// <summary>
/// Generates the connection string.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="databaseName">The name of the database, uses the default database name when <c>null</c>.</param>
/// <param name="server">The server.</param>
/// <param name="login">The login.</param>
/// <param name="password">The password.</param>
/// <param name="integratedAuth">Indicates whether integrated authentication should be used (when supported by the provider).</param>
/// <returns>
/// The generated connection string.
/// </returns>
public static string? GenerateConnectionString(this IDatabaseProviderMetadata databaseProviderMetadata, string? databaseName = null, string? server = null, string? login = null, string? password = null, bool? integratedAuth = null)
=> databaseProviderMetadata.GenerateConnectionString(new DatabaseModel()
{
DatabaseProviderMetadataId = databaseProviderMetadata.Id,
ProviderName = databaseProviderMetadata.ProviderName,
DatabaseName = databaseName ?? databaseProviderMetadata.DefaultDatabaseName,
Server = server ?? string.Empty,
Login = login ?? string.Empty,
Password = password ?? string.Empty,
IntegratedAuth = integratedAuth == true && databaseProviderMetadata.SupportsIntegratedAuthentication
});
}

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -14,7 +11,6 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Runtime
{
@@ -24,7 +20,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
/// </summary>
public class RuntimeState : IRuntimeState
{
internal const string PendingPacakgeMigrationsStateKey = "PendingPackageMigrations";
internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations";
private readonly IOptions<GlobalSettings> _globalSettings = null!;
private readonly IOptions<UnattendedSettings> _unattendedSettings = null!;
private readonly IUmbracoVersion _umbracoVersion = null!;
@@ -33,6 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private readonly PendingPackageMigrations _packageMigrationState = null!;
private readonly Dictionary<string, object> _startupState = new Dictionary<string, object>();
private readonly IConflictingRouteService _conflictingRouteService = null!;
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata = null!;
/// <summary>
/// The initial <see cref="RuntimeState"/>
@@ -41,17 +39,17 @@ namespace Umbraco.Cms.Infrastructure.Runtime
public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot };
private RuntimeState()
{
}
{ }
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService)
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService,
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata)
{
_globalSettings = globalSettings;
_unattendedSettings = unattendedSettings;
@@ -60,8 +58,29 @@ namespace Umbraco.Cms.Infrastructure.Runtime
_logger = logger;
_packageMigrationState = packageMigrationState;
_conflictingRouteService = conflictingRouteService;
_databaseProviderMetadata = databaseProviderMetadata;
}
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService)
: this(
globalSettings,
unattendedSettings,
umbracoVersion,
databaseFactory,
logger,
packageMigrationState,
StaticServiceProvider.Instance.GetRequiredService<IConflictingRouteService>(),
StaticServiceProvider.Instance.GetServices<IDatabaseProviderMetadata>())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState"/> class.
/// </summary>
@@ -81,8 +100,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
logger,
packageMigrationState,
StaticServiceProvider.Instance.GetRequiredService<IConflictingRouteService>())
{
}
{ }
/// <inheritdoc />
public Version Version => _umbracoVersion.Version;
@@ -143,7 +161,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
// cannot connect to configured database, this is bad, fail
_logger.LogDebug("Could not connect to database.");
if (_globalSettings.Value.InstallMissingDatabase || CanAutoInstallMissingDatabase(_databaseFactory))
if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory.ProviderName))
{
// ok to install on a configured but missing database
Level = RuntimeLevel.Install;
@@ -257,7 +275,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues);
if (packagesRequiringMigration.Count > 0)
{
_startupState[PendingPacakgeMigrationsStateKey] = packagesRequiringMigration;
_startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration;
return UmbracoDatabaseState.NeedsPackageMigration;
}
@@ -311,8 +329,5 @@ namespace Umbraco.Cms.Infrastructure.Runtime
return canConnect;
}
private bool CanAutoInstallMissingDatabase(IUmbracoDatabaseFactory databaseFactory)
=> databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true;
}
}