Skip to content

Commit

Permalink
adding support for IWebJobsConfigurationStartup (Azure#5991)
Browse files Browse the repository at this point in the history
  • Loading branch information
brettsam authored May 29, 2020
1 parent caa3a43 commit 86fc21a
Show file tree
Hide file tree
Showing 21 changed files with 801 additions and 41 deletions.
7 changes: 7 additions & 0 deletions WebJobs.Script.sln
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger", "HttpTrigger"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Script.Abstractions", "src\WebJobs.Script.Abstractions\WebJobs.Script.Abstractions.csproj", "{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobsStartupTests", "test\WebJobsStartupTests\WebJobsStartupTests.csproj", "{F5D74052-3807-410F-9A5A-B69A57127CF4}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
Expand Down Expand Up @@ -349,6 +351,10 @@ Global
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Release|Any CPU.Build.0 = Release|Any CPU
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -408,6 +414,7 @@ Global
{0AE3CE25-4CD9-4769-AE58-399FC59CF70F} = {FF9C0818-30D3-437A-A62D-7A61CA44F459}
{BA45A727-34B7-484F-9B93-B1755AF09A2A} = {0AE3CE25-4CD9-4769-AE58-399FC59CF70F}
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C} = {16351B76-87CA-4A8C-80A1-3DD83A0C4AA6}
{F5D74052-3807-410F-9A5A-B69A57127CF4} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Azure.WebJobs.Host.Timers;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Azure.WebJobs.Script.ChangeAnalysis;
using Microsoft.Azure.WebJobs.Script.DependencyInjection;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
Expand Down Expand Up @@ -42,6 +43,7 @@ private static ExpectedDependencyBuilder CreateExpectedDependencies()

expected.ExpectCollection<IHostedService>()
.Expect<JobHostService>("Microsoft.Azure.WebJobs.Hosting.OptionsLoggingService")
.ExpectFactory<ExternalConfigurationStartupValidatorService>()
.Expect<PrimaryHostCoordinator>()
.Expect<FileMonitoringService>()
.Expect<WorkerConsoleLogService>()
Expand Down
2 changes: 1 addition & 1 deletion src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.0.3" />
<PackageReference Include="Microsoft.Azure.Storage.File" Version="11.1.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Host.Storage" Version="4.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.17" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.18-11733" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.3" />
<PackageReference Include="Microsoft.Azure.WebJobs.Logging" Version="4.0.0" />
<PackageReference Include="Microsoft.Azure.WebSites.DataProtection" Version="2.1.91-alpha" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
{
internal class ExternalConfigurationStartupValidator
{
private readonly IConfiguration _config;
private readonly IFunctionMetadataManager _metadataManager;
private readonly DefaultNameResolver _nameResolver;

public ExternalConfigurationStartupValidator(IConfiguration config, IFunctionMetadataManager metadataManager)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_metadataManager = metadataManager ?? throw new ArgumentNullException(nameof(metadataManager));
_nameResolver = new DefaultNameResolver(config);
}

/// <summary>
/// Validates the current configuration against the original configuration. If any values for a trigger
/// do not match, they are returned via the return value.
/// </summary>
/// <param name="originalConfig">The original configuration</param>
/// <returns>A dictionary mapping function name to a list of the invalid values for that function.</returns>
public IDictionary<string, IEnumerable<string>> Validate(IConfigurationRoot originalConfig)
{
if (originalConfig == null)
{
throw new ArgumentNullException(nameof(originalConfig));
}

INameResolver originalNameResolver = new DefaultNameResolver(originalConfig);
IDictionary<string, IEnumerable<string>> invalidValues = new Dictionary<string, IEnumerable<string>>();

var functions = _metadataManager.GetFunctionMetadata();

foreach (var function in functions)
{
var trigger = function.Bindings.SingleOrDefault(b => b.IsTrigger);

if (trigger == null)
{
continue;
}

IList<string> invalidValuesForFunction = new List<string>();

// make sure none of the resolved values have changed for the trigger.
foreach (KeyValuePair<string, JToken> property in trigger.Raw)
{
string lookup = property.Value?.ToString();

if (lookup != null)
{
string originalValue = originalConfig[lookup];
string newValue = _config[lookup];
if (originalValue != newValue)
{
invalidValuesForFunction.Add(lookup);
}
else
{
// It may be a binding expression like "%lookup%"
originalNameResolver.TryResolveWholeString(lookup, out originalValue);
_nameResolver.TryResolveWholeString(lookup, out newValue);

if (originalValue != newValue)
{
invalidValuesForFunction.Add(lookup);
}
}
}
}

if (invalidValuesForFunction.Any())
{
invalidValues[function.Name] = invalidValuesForFunction;
}
}

return invalidValues;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
{
internal class ExternalConfigurationStartupValidatorService : IHostedService
{
private readonly ExternalConfigurationStartupValidator _validator;
private readonly IConfigurationRoot _originalConfig;
private readonly IEnvironment _environment;
private readonly ILogger<ExternalConfigurationStartupValidator> _logger;

public ExternalConfigurationStartupValidatorService(ExternalConfigurationStartupValidator validator, IConfigurationRoot originalConfig, IEnvironment environment, ILogger<ExternalConfigurationStartupValidator> logger)
{
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
_originalConfig = originalConfig ?? throw new ArgumentNullException(nameof(originalConfig));
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public Task StartAsync(CancellationToken cancellationToken)
{
IDictionary<string, IEnumerable<string>> invalidValues = _validator.Validate(_originalConfig);

if (invalidValues.Any())
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("The Functions scale controller may not scale the following functions correctly because some configuration values were modified in an external startup class.");

foreach (KeyValuePair<string, IEnumerable<string>> invalidValueMap in invalidValues)
{
sb.AppendLine($" Function '{invalidValueMap.Key}' uses the modified key(s): {string.Join(", ", invalidValueMap.Value)}");
}

if (_environment.IsCoreTools())
{
// We don't know where this will be deployed, so it may not matter,
// but log this as a warning during development.
_logger.LogWarning(sb.ToString());
}
else
{
throw new HostInitializationException(sb.ToString());
}
}

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
18 changes: 12 additions & 6 deletions src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ public class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
private readonly IExtensionBundleManager _extensionBundleManager;
private readonly IFunctionMetadataManager _functionMetadataManager;
private readonly IMetricsLogger _metricsLogger;
private readonly Lazy<IEnumerable<Type>> _startupTypes;

private static string[] _builtinExtensionAssemblies = GetBuiltinExtensionAssemblies();

public ScriptStartupTypeLocator(string rootScriptPath, ILogger<ScriptStartupTypeLocator> logger, IExtensionBundleManager extensionBundleManager, IFunctionMetadataManager functionMetadataManager, IMetricsLogger metricsLogger)
public ScriptStartupTypeLocator(string rootScriptPath, ILogger<ScriptStartupTypeLocator> logger, IExtensionBundleManager extensionBundleManager,
IFunctionMetadataManager functionMetadataManager, IMetricsLogger metricsLogger)
{
_rootScriptPath = rootScriptPath ?? throw new ArgumentNullException(nameof(rootScriptPath));
_extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager));
_logger = logger;
_functionMetadataManager = functionMetadataManager;
_metricsLogger = metricsLogger;
_startupTypes = new Lazy<IEnumerable<Type>>(() => GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult());
}

private static string[] GetBuiltinExtensionAssemblies()
Expand All @@ -52,13 +55,13 @@ private static string[] GetBuiltinExtensionAssemblies()

public Type[] GetStartupTypes()
{
IEnumerable<Type> startupTypes = GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult();

return startupTypes
return _startupTypes.Value
.Distinct(new TypeNameEqualityComparer())
.ToArray();
}

internal bool HasExternalConfigurationStartups() => _startupTypes.Value.Any(p => typeof(IWebJobsConfigurationStartup).IsAssignableFrom(p));

public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
{
string binPath;
Expand Down Expand Up @@ -150,16 +153,19 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
_logger.ScriptStartUpLoadedExtension(startupExtensionName, assembly.GetName().Version.ToString());
return assembly?.GetType(typeName, false, ignoreCase);
}, false, true);

if (extensionType == null)
{
_logger.ScriptStartUpUnableToLoadExtension(startupExtensionName, extensionItem.TypeName);
continue;
}
if (!typeof(IWebJobsStartup).IsAssignableFrom(extensionType))

if (!typeof(IWebJobsStartup).IsAssignableFrom(extensionType) && !typeof(IWebJobsConfigurationStartup).IsAssignableFrom(extensionType))
{
_logger.ScriptStartUpTypeIsNotValid(extensionItem.TypeName, nameof(IWebJobsStartup));
_logger.ScriptStartUpTypeIsNotValid(extensionItem.TypeName, nameof(IWebJobsStartup), nameof(IWebJobsConfigurationStartup));
continue;
}

startupTypes.Add(extensionType);
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ internal static class LoggerExtension
new EventId(306, nameof(ScriptStartUpUnableToLoadExtension)),
"Unable to load startup extension '{startupExtensionName}' (Type: '{typeName}'). The type does not exist. Please validate the type and assembly names.");

private static readonly Action<ILogger, string, string, Exception> _scriptStartUpTypeIsNotValid =
LoggerMessage.Define<string, string>(
private static readonly Action<ILogger, string, string, string, Exception> _scriptStartUpTypeIsNotValid =
LoggerMessage.Define<string, string, string>(
LogLevel.Warning,
new EventId(307, nameof(ScriptStartUpTypeIsNotValid)),
"Type '{typeName}' is not a valid startup extension. The type does not implement {className}.");
"Type '{typeName}' is not a valid startup extension. The type does not implement {startupClassName} or {startupConfigurationClassName}.");

private static readonly Action<ILogger, string, Exception> _scriptStartUpUnableParseMetadataMissingProperty =
LoggerMessage.Define<string>(
Expand Down Expand Up @@ -229,9 +229,9 @@ public static void ScriptStartUpUnableToLoadExtension(this ILogger logger, strin
_scriptStartUpUnableToLoadExtension(logger, startupExtensionName, typeName, null);
}

public static void ScriptStartUpTypeIsNotValid(this ILogger logger, string typeName, string className)
public static void ScriptStartUpTypeIsNotValid(this ILogger logger, string typeName, string startupClassName, string startupConfigurationClassName)
{
_scriptStartUpTypeIsNotValid(logger, typeName, className, null);
_scriptStartUpTypeIsNotValid(logger, typeName, startupClassName, startupConfigurationClassName, null);
}

public static void ScriptStartUpUnableParseMetadataMissingProperty(this ILogger logger, string metadataFilePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Azure.WebJobs.Script.WebHost
namespace Microsoft.Azure.WebJobs.Script
{
internal sealed class NullHostedService : IHostedService
{
private static readonly Lazy<NullHostedService> _instance = new Lazy<NullHostedService>(new NullHostedService());
private static readonly Lazy<NullHostedService> _instance = new Lazy<NullHostedService>(() => new NullHostedService());

public static NullHostedService Instance => _instance.Value;

Expand Down
Loading

0 comments on commit 86fc21a

Please sign in to comment.