Merge pull request #10449 from umbraco/v9/task/package-refactor-startup-checks-PR1

Implements package migration startup checks
This commit is contained in:
Shannon Deminick
2021-06-12 03:54:52 +10:00
committed by GitHub
41 changed files with 530 additions and 163 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Umbraco.Cms.Core.Logging;
@@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Composing
/// file properties (false) or the file contents (true).</remarks>
private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders)
{
using (_logger.DebugDuration<TypeLoader>("Determining hash of code files on disk", "Hash determined"))
using (_logger.DebugDuration<RuntimeHash>("Determining hash of code files on disk", "Hash determined"))
{
// get the distinct file infos to hash
var uniqInfos = new HashSet<string>();

View File

@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Core.Configuration.Models
{
@@ -24,6 +24,13 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
public bool UpgradeUnattended { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether unattended package migrations are enabled.
/// </summary>
/// <remarks>
/// This is true by default.
/// </remarks>
public bool PackageMigrationsUnattended { get; set; } = true;
/// <summary>
/// Gets or sets a value to use for creating a user with a name for Unattended Installs

View File

@@ -7,6 +7,13 @@ namespace Umbraco.Cms.Core
/// </summary>
public static class Conventions
{
public static class Migrations
{
public const string UmbracoUpgradePlanName = "Umbraco.Core";
public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+";
public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName;
}
public static class PermissionCategories
{
public const string ContentCategory = "content";

View File

@@ -1,4 +1,4 @@
 namespace Umbraco.Cms.Core
namespace Umbraco.Cms.Core
{
public static partial class Constants
{
@@ -59,8 +59,7 @@
public const string RecycleBinMediaPathPrefix = "-1,-21,";
public const int DefaultLabelDataTypeId = -92;
public const string UmbracoConnectionName = "umbracoDbDSN";
public const string UmbracoUpgradePlanName = "Umbraco.Core";
public const string UmbracoConnectionName = "umbracoDbDSN";
}
}
}

View File

@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.HealthChecks;
using Umbraco.Cms.Core.HealthChecks.NotificationMethods;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Media.EmbedProviders;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Routing;
@@ -32,7 +33,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
{
builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers());
builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors());
builder.Actions().Add(() => builder.TypeLoader.GetTypes<IAction>());
builder.Actions().Add(() => builder.TypeLoader.GetActions());
builder.PackageMigrationPlans().Add(() => builder.TypeLoader.GetPackageMigrationPlans());
// register known content apps
builder.ContentApps()
.Append<ListViewContentAppFactory>()
@@ -42,6 +45,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
.Append<ContentTypeListViewContentAppFactory>()
.Append<ContentTypePermissionsContentAppFactory>()
.Append<ContentTypeTemplatesContentAppFactory>();
// all built-in finders in the correct order,
// devs can then modify this list on application startup
builder.ContentFinders()
@@ -116,6 +120,13 @@ namespace Umbraco.Cms.Core.DependencyInjection
builder.BackOfficeAssets();
}
/// <summary>
/// Gets the package migration plans collection builder.
/// </summary>
/// <param name="builder">The builder.</param>
public static PackageMigrationPlanCollectionBuilder PackageMigrationPlans(this IUmbracoBuilder builder)
=> builder.WithCollectionBuilder<PackageMigrationPlanCollectionBuilder>();
/// <summary>
/// Gets the actions collection builder.
/// </summary>

View File

@@ -5,9 +5,26 @@ namespace Umbraco.Extensions
{
public static class RuntimeStateExtensions
{
/// <summary>
/// Returns true if the installer is enabled based on the current runtime state
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
public static bool EnableInstaller(this IRuntimeState state)
=> state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations;
/// <summary>
/// Returns true if Umbraco <see cref="IRuntimeState"/> is greater than <see cref="RuntimeLevel.BootFailed"/>
/// </summary>
public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed;
/// <summary>
/// Returns true if the runtime state indicates that unattended boot logic should execute
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
public static bool RunUnattendedBootLogic(this IRuntimeState state)
=> (state.Reason == RuntimeLevelReason.UpgradeMigrations || state.Reason == RuntimeLevelReason.UpgradePackageMigrations)
&& state.Level == RuntimeLevel.Run;
}
}

View File

@@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Extensions
@@ -12,19 +14,27 @@ namespace Umbraco.Extensions
public static class TypeLoaderExtensions
{
/// <summary>
/// Gets all classes implementing <see cref="IDataEditor"/>.
/// Gets all types implementing <see cref="IDataEditor"/>.
/// </summary>
public static IEnumerable<Type> GetDataEditors(this TypeLoader mgr)
{
return mgr.GetTypes<IDataEditor>();
}
public static IEnumerable<Type> GetDataEditors(this TypeLoader mgr) => mgr.GetTypes<IDataEditor>();
/// <summary>
/// Gets all classes implementing ICacheRefresher.
/// Gets all types implementing ICacheRefresher.
/// </summary>
public static IEnumerable<Type> GetCacheRefreshers(this TypeLoader mgr)
{
return mgr.GetTypes<ICacheRefresher>();
}
public static IEnumerable<Type> GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes<ICacheRefresher>();
/// <summary>
/// Gets all types implementing <see cref="PackageMigrationPlan"/>
/// </summary>
/// <param name="mgr"></param>
/// <returns></returns>
public static IEnumerable<Type> GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes<PackageMigrationPlan>();
/// <summary>
/// Gets all types implementing <see cref="IAction"/>
/// </summary>
/// <param name="mgr"></param>
/// <returns></returns>
public static IEnumerable<Type> GetActions(this TypeLoader mgr) => mgr.GetTypes<IAction>();
}
}

View File

@@ -25,9 +25,14 @@ namespace Umbraco.Cms.Core.Migrations
public MigrationPlan(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
}
Name = name;
}
@@ -48,7 +53,7 @@ namespace Umbraco.Cms.Core.Migrations
private MigrationPlan Add(string sourceState, string targetState, Type migration)
{
if (sourceState == null)
throw new ArgumentNullException(nameof(sourceState));
throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called.");
if (targetState == null)
throw new ArgumentNullException(nameof(targetState));
if (string.IsNullOrWhiteSpace(targetState))
@@ -90,6 +95,9 @@ namespace Umbraco.Cms.Core.Migrations
public MigrationPlan To(string targetState)
=> To<NoopMigration>(targetState);
public MigrationPlan To(Guid targetState)
=> To<NoopMigration>(targetState.ToString());
/// <summary>
/// Adds a transition to a target state through a migration.
/// </summary>
@@ -97,12 +105,19 @@ namespace Umbraco.Cms.Core.Migrations
where TMigration : IMigration
=> To(targetState, typeof(TMigration));
public MigrationPlan To<TMigration>(Guid targetState)
where TMigration : IMigration
=> To(targetState, typeof(TMigration));
/// <summary>
/// Adds a transition to a target state through a migration.
/// </summary>
public MigrationPlan To(string targetState, Type migration)
=> Add(_prevState, targetState, migration);
public MigrationPlan To(Guid targetState, Type migration)
=> Add(_prevState, targetState.ToString(), migration);
/// <summary>
/// Sets the starting state.
/// </summary>

View File

@@ -1,14 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Migrations;
namespace Umbraco.Cms.Core.Packaging
{
public abstract class PackageMigrationPlan : MigrationPlan
/// <summary>
/// Base class for package migration plans
/// </summary>
public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable
{
protected PackageMigrationPlan(string name) : base(name)
{
// A call to From must be done first
From(string.Empty);
DefinePlan();
}
protected abstract void DefinePlan();
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Packaging
{
/// <summary>
/// A collection of <see cref="PackageMigrationPlan"/>
/// </summary>
public class PackageMigrationPlanCollection : BuilderCollectionBase<PackageMigrationPlan>
{
public PackageMigrationPlanCollection(IEnumerable<PackageMigrationPlan> items) : base(items)
{
}
}
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Packaging
{
public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase<PackageMigrationPlanCollectionBuilder, PackageMigrationPlanCollection, PackageMigrationPlan>
{
protected override PackageMigrationPlanCollectionBuilder This => this;
}
}

View File

@@ -1,8 +1,15 @@
using Umbraco.Cms.Core.Models;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Persistence.Repositories
{
public interface IKeyValueRepository : IReadRepository<string, IKeyValue>, IWriteRepository<IKeyValue>
{
/// <summary>
/// Returns key/value pairs for all keys with the specified prefix.
/// </summary>
/// <param name="keyPrefix"></param>
/// <returns></returns>
IReadOnlyDictionary<string, string> FindByKeyPrefix(string keyPrefix);
}
}

View File

@@ -32,9 +32,14 @@
/// </summary>
Upgrade = 3,
/// <summary>
/// The runtime has detected that Package Migrations need to be executed.
/// </summary>
PackageMigrations = 4,
/// <summary>
/// The runtime has detected an up-to-date Umbraco install and is running.
/// </summary>
Run = 4
Run = 100
}
}

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core
namespace Umbraco.Cms.Core
{
/// <summary>
/// Describes the reason for the runtime level.
@@ -65,6 +65,11 @@
/// </summary>
UpgradeMigrations,
/// <summary>
/// Umbraco runs the current version but some package migrations have not run.
/// </summary>
UpgradePackageMigrations,
/// <summary>
/// Umbraco is running.
/// </summary>

View File

@@ -1,4 +1,7 @@
namespace Umbraco.Cms.Core.Services
using System.Collections;
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Services
{
/// <summary>
/// Manages the simplified key/value store.
@@ -11,6 +14,13 @@
/// <remarks>Returns <c>null</c> if no value was found for the key.</remarks>
string GetValue(string key);
/// <summary>
/// Returns key/value pairs for all keys with the specified prefix.
/// </summary>
/// <param name="keyPrefix"></param>
/// <returns></returns>
IReadOnlyDictionary<string, string> FindByKeyPrefix(string keyPrefix);
/// <summary>
/// Sets a value.
/// </summary>

View File

@@ -38,7 +38,7 @@ namespace Umbraco.Cms.Core.Cache
/// <inheritdoc/>
public void Handle(UmbracoApplicationStartingNotification notification)
{
if (_runtimeState.Level < RuntimeLevel.Run)
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}

View File

@@ -112,7 +112,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
}
}
private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level >= RuntimeLevel.Run;
private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run;
private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken)
{

View File

@@ -47,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
/// <param name="notification"></param>
public void Handle(UmbracoRequestBeginNotification notification)
{
if (_runtimeState.Level < RuntimeLevel.Run)
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}

View File

@@ -412,7 +412,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
/// configured and it is possible to connect to the database.</para>
/// <para>Runs whichever migrations need to run.</para>
/// </remarks>
public Result UpgradeSchemaAndData(MigrationPlan plan)
public Result UpgradeSchemaAndData(UmbracoPlan plan)
{
try
{

View File

@@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
/// Initializes a new instance of the <see cref="UmbracoPlan"/> class.
/// </summary>
public UmbracoPlan(IUmbracoVersion umbracoVersion)
: base(Cms.Core.Constants.System.UmbracoUpgradePlanName)
: base(Core.Constants.Conventions.Migrations.UmbracoUpgradePlanName)
{
_umbracoVersion = umbracoVersion;
DefinePlan();

View File

@@ -1,4 +1,5 @@
using System;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
@@ -13,10 +14,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
/// <summary>
/// Initializes a new instance of the <see ref="Upgrader"/> class.
/// </summary>
public Upgrader(MigrationPlan plan)
{
Plan = plan;
}
public Upgrader(MigrationPlan plan) => Plan = plan;
/// <summary>
/// Gets the name of the migration plan.
@@ -31,7 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
/// <summary>
/// Gets the key for the state value.
/// </summary>
public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name;
public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name;
/// <summary>
/// Executes.

View File

@@ -1,9 +1,10 @@
using System;
using System;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
{
[MapperFor(typeof(PublicAccessEntry))]
public sealed class AccessMapper : BaseMapper
{

View File

@@ -0,0 +1,22 @@
using System;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
{
[MapperFor(typeof(KeyValue))]
[MapperFor(typeof(IKeyValue))]
public sealed class KeyValueMapper : BaseMapper
{
public KeyValueMapper(Lazy<ISqlContext> sqlContext, MapperConfigurationStore maps)
: base(sqlContext, maps)
{ }
protected override void DefineMaps()
{
DefineMap<KeyValue, KeyValueDto>(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key));
DefineMap<KeyValue, KeyValueDto>(nameof(KeyValue.Value), nameof(KeyValueDto.Value));
DefineMap<KeyValue, KeyValueDto>(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate));
}
}
}

View File

@@ -32,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
Add<DictionaryMapper>();
Add<DictionaryTranslationMapper>();
Add<DomainMapper>();
Add<KeyValueMapper>();
Add<LanguageMapper>();
Add<MacroMapper>();
Add<MediaMapper>();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -453,19 +453,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying
else
goto case "Contains**String";
case "SqlWildcard":
case nameof(SqlExpressionExtensions.SqlWildcard):
case "StartsWith":
case "EndsWith":
case "Contains**String": // see "Contains" above
case "Equals":
case "SqlStartsWith":
case "SqlEndsWith":
case "SqlContains":
case "SqlEquals":
case "InvariantStartsWith":
case "InvariantEndsWith":
case "InvariantContains":
case "InvariantEquals":
case nameof(SqlExpressionExtensions.SqlStartsWith):
case nameof(SqlExpressionExtensions.SqlEndsWith):
case nameof(SqlExpressionExtensions.SqlContains):
case nameof(SqlExpressionExtensions.SqlEquals):
case nameof(StringExtensions.InvariantStartsWith):
case nameof(StringExtensions.InvariantEndsWith):
case nameof(StringExtensions.InvariantContains):
case nameof(StringExtensions.InvariantEquals):
string compareValue;
@@ -699,31 +699,31 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying
{
switch (verb)
{
case "SqlWildcard":
case nameof(SqlExpressionExtensions.SqlWildcard):
SqlParameters.Add(RemoveQuote(val));
return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType);
case "Equals":
case "InvariantEquals":
case "SqlEquals":
case nameof(StringExtensions.InvariantEquals):
case nameof(SqlExpressionExtensions.SqlEquals):
SqlParameters.Add(RemoveQuote(val));
return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType);
case "StartsWith":
case "InvariantStartsWith":
case "SqlStartsWith":
case nameof(StringExtensions.InvariantStartsWith):
case nameof(SqlExpressionExtensions.SqlStartsWith):
SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder());
return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType);
case "EndsWith":
case "InvariantEndsWith":
case "SqlEndsWith":
case nameof(StringExtensions.InvariantEndsWith):
case nameof(SqlExpressionExtensions.SqlEndsWith):
SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val));
return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType);
case "Contains":
case "InvariantContains":
case "SqlContains":
case nameof(StringExtensions.InvariantContains):
case nameof(SqlExpressionExtensions.SqlContains):
var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder();
SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder);
return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType);

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Umbraco.Extensions;
@@ -25,10 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying
return (value ?? fallbackValue).Equals(other ?? fallbackValue);
}
public static bool SqlIn<T>(this IEnumerable<T> collection, T item)
{
return collection.Contains(item);
}
public static bool SqlIn<T>(this IEnumerable<T> collection, T item) => collection.Contains(item);
public static bool SqlWildcard(this string str, string txt, TextColumnType columnType)
{
@@ -39,24 +36,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying
return wildcardmatch.IsMatch(str);
}
public static bool SqlContains(this string str, string txt, TextColumnType columnType)
{
return str.InvariantContains(txt);
}
#pragma warning disable IDE0060 // Remove unused parameter
public static bool SqlContains(this string str, string txt, TextColumnType columnType) => str.InvariantContains(txt);
public static bool SqlEquals(this string str, string txt, TextColumnType columnType)
{
return str.InvariantEquals(txt);
}
public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt);
public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType)
{
return str.InvariantStartsWith(txt);
}
public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) => str.InvariantStartsWith(txt);
public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType)
{
return str.InvariantEndsWith(txt);
}
public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => str.InvariantEndsWith(txt);
#pragma warning restore IDE0060 // Remove unused parameter
}
}

View File

@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
@@ -19,6 +20,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
: base(scopeAccessor, AppCaches.NoCache, logger)
{ }
/// <inheritdoc />
public IReadOnlyDictionary<string, string> FindByKeyPrefix(string keyPrefix)
=> Get(Query<IKeyValue>().Where(entity => entity.Identifier.StartsWith(keyPrefix)))
.ToDictionary(x => x.Identifier, x => x.Value);
#region Overrides of IReadWriteQueryRepository<string, IKeyValue>
public override void Save(IKeyValue entity)
@@ -47,15 +53,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
return sql;
}
protected override string GetBaseWhereClause()
{
return Cms.Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id";
}
protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id";
protected override IEnumerable<string> GetDeleteClauses()
{
return Enumerable.Empty<string>();
}
protected override IEnumerable<string> GetDeleteClauses() => Enumerable.Empty<string>();
protected override IKeyValue PerformGet(string id)
{
@@ -73,7 +73,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
protected override IEnumerable<IKeyValue> PerformGetByQuery(IQuery<IKeyValue> query)
{
throw new NotSupportedException();
var sqlClause = GetBaseQuery(false);
var translator = new SqlTranslator<IKeyValue>(sqlClause, query);
var sql = translator.Translate();
return Database.Fetch<KeyValueDto>(sql).Select(Map);
}
protected override void PersistNewItem(IKeyValue entity)

View File

@@ -1,6 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence
@@ -9,24 +11,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence
{
public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database)
{
var asDatabase = database as UmbracoDatabase;
if (asDatabase == null) throw new Exception("oops: database.");
if (database is not UmbracoDatabase asDatabase)
{
throw new Exception("oops: database.");
}
return asDatabase;
}
/// <summary>
/// Gets a key/value directly from the database, no scope, nothing.
/// Gets a dictionary of key/values directly from the database, no scope, nothing.
/// </summary>
/// <remarks>Used by <see cref="CoreRuntimeBootstrapper"/> to determine the runtime state.</remarks>
public static string GetFromKeyValueTable(this IUmbracoDatabase database, string key)
public static IReadOnlyDictionary<string, string> GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix)
{
if (database is null) return null;
// create the wildcard where clause
ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax;
var whereParam = sqlSyntax.GetStringColumnWildcardComparison(
sqlSyntax.GetQuotedColumnName("key"),
0,
Querying.TextColumnType.NVarchar);
var sql = database.SqlContext.Sql()
.Select<KeyValueDto>()
.From<KeyValueDto>()
.Where<KeyValueDto>(x => x.Key == key);
return database.FirstOrDefault<KeyValueDto>(sql)?.Value;
.Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder());
return database.Fetch<KeyValueDto>(sql)
.ToDictionary(x => x.Key, x => x.Value);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Reflection;
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
@@ -33,6 +33,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence
{
var columnInfo = base.GetColumnInfo(mi, type);
// TODO: Is this upgrade flag still relevant? It's a lot of hacking to just set this value
// including the interface method ConfigureForUpgrade for this one circumstance.
if (_upgrading)
{
if (type == typeof(UserDto) && mi.Name == "TourData") columnInfo.IgnoreColumn = true;

View File

@@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Runtime
{
@@ -106,7 +107,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
DoUnattendedInstall();
DetermineRuntimeLevel();
if (State.Level <= RuntimeLevel.BootFailed)
if (!State.UmbracoCanBoot())
{
return; // The exception will be rethrown by BootFailedMiddelware
}
@@ -117,17 +118,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime
throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}");
}
// if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade
if (State.Reason == RuntimeLevelReason.UpgradeMigrations && State.Level == RuntimeLevel.Run)
if (State.RunUnattendedBootLogic())
{
// do the upgrade
DoUnattendedUpgrade();
// upgrade is done, set reason to Run
DetermineRuntimeLevel();
}
// create & initialize the components

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -7,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
@@ -27,6 +30,7 @@ namespace Umbraco.Cms.Core
private readonly ILogger<RuntimeState> _logger;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEventAggregator _eventAggregator;
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
/// <summary>
/// The initial <see cref="RuntimeState"/>
@@ -48,7 +52,8 @@ namespace Umbraco.Cms.Core
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
IEventAggregator eventAggregator)
IEventAggregator eventAggregator,
PackageMigrationPlanCollection packageMigrationPlans)
{
_globalSettings = globalSettings;
_unattendedSettings = unattendedSettings;
@@ -57,6 +62,7 @@ namespace Umbraco.Cms.Core
_logger = logger;
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
_eventAggregator = eventAggregator;
_packageMigrationPlans = packageMigrationPlans;
}
@@ -101,55 +107,62 @@ namespace Umbraco.Cms.Core
switch (GetUmbracoDatabaseState(_databaseFactory))
{
case UmbracoDatabaseState.CannotConnect:
{
// cannot connect to configured database, this is bad, fail
_logger.LogDebug("Could not connect to database.");
case UmbracoDatabaseState.CannotConnect:
{
// cannot connect to configured database, this is bad, fail
_logger.LogDebug("Could not connect to database.");
if (_globalSettings.Value.InstallMissingDatabase)
{
// ok to install on a configured but missing database
Level = RuntimeLevel.Install;
Reason = RuntimeLevelReason.InstallMissingDatabase;
return;
}
// else it is bad enough that we want to throw
Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase;
BootFailedException =new BootFailedException("A connection string is configured but Umbraco could not connect to the database.");
throw BootFailedException;
}
case UmbracoDatabaseState.NotInstalled:
if (_globalSettings.Value.InstallMissingDatabase)
{
// ok to install on an empty database
// ok to install on a configured but missing database
Level = RuntimeLevel.Install;
Reason = RuntimeLevelReason.InstallEmptyDatabase;
Reason = RuntimeLevelReason.InstallMissingDatabase;
return;
}
case UmbracoDatabaseState.NeedsUpgrade:
{
// the db version does not match... but we do have a migration table
// so, at least one valid table, so we quite probably are installed & need to upgrade
// although the files version matches the code version, the database version does not
// which means the local files have been upgraded but not the database - need to upgrade
_logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco.");
Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade;
Reason = RuntimeLevelReason.UpgradeMigrations;
}
// else it is bad enough that we want to throw
Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase;
BootFailedException = new BootFailedException("A connection string is configured but Umbraco could not connect to the database.");
throw BootFailedException;
}
case UmbracoDatabaseState.NotInstalled:
{
// ok to install on an empty database
Level = RuntimeLevel.Install;
Reason = RuntimeLevelReason.InstallEmptyDatabase;
return;
}
case UmbracoDatabaseState.NeedsUpgrade:
{
// the db version does not match... but we do have a migration table
// so, at least one valid table, so we quite probably are installed & need to upgrade
// although the files version matches the code version, the database version does not
// which means the local files have been upgraded but not the database - need to upgrade
_logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco.");
Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade;
Reason = RuntimeLevelReason.UpgradeMigrations;
}
break;
case UmbracoDatabaseState.NeedsPackageMigration:
_logger.LogDebug("Package migrations need to execute.");
Level = _unattendedSettings.Value.PackageMigrationsUnattended ? RuntimeLevel.Run : RuntimeLevel.PackageMigrations;
Reason = RuntimeLevelReason.UpgradePackageMigrations;
break;
case UmbracoDatabaseState.Ok:
default:
{
// if we already know we want to upgrade, exit here
if (Level == RuntimeLevel.Upgrade)
return;
{
// if we already know we want to upgrade, exit here
if (Level == RuntimeLevel.Upgrade)
return;
// the database version matches the code & files version, all clear, can run
Level = RuntimeLevel.Run;
Reason = RuntimeLevelReason.Run;
}
break;
// the database version matches the code & files version, all clear, can run
Level = RuntimeLevel.Run;
Reason = RuntimeLevelReason.Run;
}
break;
}
}
@@ -158,7 +171,8 @@ namespace Umbraco.Cms.Core
Ok,
CannotConnect,
NotInstalled,
NeedsUpgrade
NeedsUpgrade,
NeedsPackageMigration
}
private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory)
@@ -178,10 +192,24 @@ namespace Umbraco.Cms.Core
return UmbracoDatabaseState.NotInstalled;
}
if (DoesUmbracoRequireUpgrade(database))
// Make ONE SQL call to determine Umbraco upgrade vs package migrations state.
// All will be prefixed with the same key.
IReadOnlyDictionary<string, string> keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix);
// This could need both an upgrade AND package migrations to execute but
// we will process them one at a time, first the upgrade, then the package migrations.
if (DoesUmbracoRequireUpgrade(keyValues))
{
return UmbracoDatabaseState.NeedsUpgrade;
}
// TODO: Can we save the result of this since we'll need to re-use it?
IReadOnlyList<string> packagesRequiringMigration = DoesUmbracoRequirePackageMigrations(keyValues);
if (packagesRequiringMigration.Count > 0)
{
return UmbracoDatabaseState.NeedsPackageMigration;
}
}
return UmbracoDatabaseState.Ok;
@@ -206,31 +234,46 @@ namespace Umbraco.Cms.Core
public void DoUnattendedInstall()
{
// unattended install is not enabled
if (_unattendedSettings.Value.InstallUnattended == false) return;
// unattended install is not enabled
if (_unattendedSettings.Value.InstallUnattended == false)
{
return;
}
// no connection string set
if (_databaseFactory.Configured == false) return;
if (_databaseFactory.Configured == false)
{
return;
}
var connect = false;
var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5;
for (var i = 0;;)
bool connect;
for (var i = 0; ;)
{
connect = _databaseFactory.CanConnect;
if (connect || ++i == tries) break;
if (connect || ++i == tries)
{
break;
}
_logger.LogDebug("Could not immediately connect to database, trying again.");
Thread.Sleep(1000);
}
// could not connect to the database
if (connect == false) return;
if (connect == false)
{
return;
}
using (var database = _databaseFactory.CreateDatabase())
{
var hasUmbracoTables = database.IsUmbracoInstalled();
// database has umbraco tables, assume Umbraco is already installed
if (hasUmbracoTables) return;
if (hasUmbracoTables)
return;
// all conditions fulfilled, do the install
_logger.LogInformation("Starting unattended install.");
@@ -263,12 +306,14 @@ namespace Umbraco.Cms.Core
}
}
private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database)
private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary<string, string> keyValues)
{
var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion));
var stateValueKey = upgrader.StateValueKey;
CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey);
_ = keyValues.TryGetValue(stateValueKey, out var value);
CurrentMigrationState = value;
FinalMigrationState = upgrader.Plan.FinalState;
_logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? "<null>");
@@ -276,6 +321,41 @@ namespace Umbraco.Cms.Core
return CurrentMigrationState != FinalMigrationState;
}
private IReadOnlyList<string> DoesUmbracoRequirePackageMigrations(IReadOnlyDictionary<string, string> keyValues)
{
var packageMigrationPlans = _packageMigrationPlans.ToList();
var result = new List<string>(packageMigrationPlans.Count);
foreach(PackageMigrationPlan plan in packageMigrationPlans)
{
string currentMigrationState = null;
var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name;
if (keyValues.TryGetValue(planKeyValueKey, out var value))
{
currentMigrationState = value;
if (plan.FinalState != value)
{
// Not equal so we need to run
result.Add(plan.Name);
}
}
else
{
// If there is nothing in the DB then we need to run
result.Add(plan.Name);
}
_logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}",
plan.Name,
plan.FinalState,
currentMigrationState ?? "<null>");
}
return result;
}
private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory)
{
// anything other than install wants a database - see if we can connect
@@ -285,7 +365,8 @@ namespace Umbraco.Cms.Core
for (var i = 0; ;)
{
canConnect = databaseFactory.CanConnect;
if (canConnect || ++i == tries) break;
if (canConnect || ++i == tries)
break;
_logger.LogDebug("Could not immediately connect to database, trying again.");
Thread.Sleep(1000);
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
@@ -25,6 +26,15 @@ namespace Umbraco.Cms.Core.Services.Implement
}
}
/// <inheritdoc />
public IReadOnlyDictionary<string, string> FindByKeyPrefix(string keyPrefix)
{
using (var scope = _scopeProvider.CreateScope(autoComplete: true))
{
return _repository.FindByKeyPrefix(keyPrefix);
}
}
/// <inheritdoc />
public void SetValue(string key, string value)
{

View File

@@ -0,0 +1,100 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class RuntimeStateTests : UmbracoIntegrationTest
{
private protected IRuntimeState RuntimeState { get; private set; }
public override void Configure(IApplicationBuilder app)
{
base.Configure(app);
RuntimeState = Services.GetRequiredService<IRuntimeState>();
}
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
PackageMigrationPlanCollectionBuilder migrations = builder.PackageMigrationPlans();
migrations.Clear();
migrations.Add<TestMigrationPlan>();
}
[Test]
public void GivenPackageMigrationsExist_WhenLatestStateIsRegistered_ThenLevelIsRun()
{
// Add the final state to the keyvalue storage
IKeyValueService keyValueService = Services.GetRequiredService<IKeyValueService>();
keyValueService.SetValue(
Constants.Conventions.Migrations.KeyValuePrefix + TestMigrationPlan.TestMigrationPlanName,
TestMigrationPlan.TestMigrationFinalState.ToString());
RuntimeState.DetermineRuntimeLevel();
Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level);
Assert.AreEqual(RuntimeLevelReason.Run, RuntimeState.Reason);
}
[Test]
public void GivenPackageMigrationsExist_WhenUnattendedMigrations_ThenLevelIsRun()
{
RuntimeState.DetermineRuntimeLevel();
Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level);
Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason);
}
[Test]
public void GivenPackageMigrationsExist_WhenNotUnattendedMigrations_ThenLevelIsPackageMigrations()
{
var unattendedOptions = Services.GetRequiredService<IOptions<UnattendedSettings>>();
unattendedOptions.Value.PackageMigrationsUnattended = false;
RuntimeState.DetermineRuntimeLevel();
Assert.AreEqual(RuntimeLevel.PackageMigrations, RuntimeState.Level);
Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason);
}
private class TestMigrationPlan : PackageMigrationPlan
{
public const string TestMigrationPlanName = "Test";
public static Guid TestMigrationFinalState => new Guid("BB02C392-4007-4A6C-A550-28BA2FF7E43D");
public TestMigrationPlan() : base(TestMigrationPlanName)
{
}
protected override void DefinePlan()
{
To<TestMigration>(TestMigrationFinalState);
}
}
private class TestMigration : MigrationBase
{
public TestMigration(IMigrationContext context) : base(context)
{
}
public override void Migrate()
{
}
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using System.Threading;
using NUnit.Framework;
using Umbraco.Cms.Core.Services;
@@ -19,6 +20,27 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
{
private IKeyValueService KeyValueService => GetRequiredService<IKeyValueService>();
[Test]
public void Can_Query_For_Key_Prefix()
{
// Arrange
KeyValueService.SetValue("test1", "hello1");
KeyValueService.SetValue("test2", "hello2");
KeyValueService.SetValue("test3", "hello3");
KeyValueService.SetValue("test4", "hello4");
KeyValueService.SetValue("someotherprefix1", "helloagain1");
// Act
IReadOnlyDictionary<string, string> value = KeyValueService.FindByKeyPrefix("test");
// Assert
Assert.AreEqual(4, value.Count);
Assert.AreEqual("hello1", value["test1"]);
Assert.AreEqual("hello2", value["test2"]);
Assert.AreEqual("hello3", value["test3"]);
Assert.AreEqual("hello4", value["test4"]);
}
[Test]
public void GetValue_ForMissingKey_ReturnsNull()
{

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Authorization
{
@@ -30,8 +31,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization
switch (_runtimeState.Level)
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
case var _ when _runtimeState.EnableInstaller():
return Task.FromResult(true);
default:
if (!_backOfficeSecurity.BackOfficeSecurity.IsAuthenticated())

View File

@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Umbraco.Cms.Core;
@@ -29,9 +29,8 @@ namespace Umbraco.Cms.Web.BackOffice.Install
switch (_runtime.Level)
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
case var _ when _runtime.EnableInstaller():
endpoints.MapUmbracoRoute<InstallApiController>(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, "api", includeControllerNameInRoute: false);
endpoints.MapUmbracoRoute<InstallController>(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, string.Empty, includeControllerNameInRoute: false);

View File

@@ -1,9 +1,10 @@
using System;
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Install
{
@@ -44,8 +45,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install
{
// if not configured (install or upgrade) then we can continue
// otherwise we need to ensure that a user is logged in
return _runtimeState.Level == RuntimeLevel.Install
|| _runtimeState.Level == RuntimeLevel.Upgrade
return _runtimeState.EnableInstaller()
|| (authorizationFilterContext.HttpContext.User?.Identity?.IsAuthenticated ?? false);
}
catch (Exception ex)

View File

@@ -48,6 +48,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
case RuntimeLevel.PackageMigrations:
case RuntimeLevel.Run:
MapMinimalBackOffice(endpoints);

View File

@@ -36,6 +36,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
case RuntimeLevel.PackageMigrations:
case RuntimeLevel.Run:
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute());
endpoints.MapUmbracoRoute<PreviewController>(_umbracoPathSegment, Constants.Web.Mvc.BackOfficeArea, null);

View File

@@ -42,7 +42,7 @@ namespace Umbraco.Cms.Web.Common.Profiler
public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel)
{
if (runtimeLevel < RuntimeLevel.Run)
if (runtimeLevel != RuntimeLevel.Run)
{
return;
}
@@ -55,7 +55,7 @@ namespace Umbraco.Cms.Web.Common.Profiler
public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel)
{
if (runtimeLevel < RuntimeLevel.Run)
if (runtimeLevel != RuntimeLevel.Run)
{
return;
}