Skip to content

Commit 89ed450

Browse files
authored
adding support for IWebJobsConfigurationStartup (v2.x) (#6121)
1 parent 65ecf73 commit 89ed450

20 files changed

+792
-41
lines changed

WebJobs.Script.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger", "HttpTrigger"
308308
sample\Python\HttpTrigger\function.json = sample\Python\HttpTrigger\function.json
309309
EndProjectSection
310310
EndProject
311+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobsStartupTests", "test\WebJobsStartupTests\WebJobsStartupTests.csproj", "{F5D74052-3807-410F-9A5A-B69A57127CF4}"
312+
EndProject
311313
Global
312314
GlobalSection(SharedMSBuildProjectFiles) = preSolution
313315
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
@@ -342,7 +344,10 @@ Global
342344
{5C308A72-5CF3-45E8-B64F-2C98F567054A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
343345
{5C308A72-5CF3-45E8-B64F-2C98F567054A}.Debug|Any CPU.Build.0 = Debug|Any CPU
344346
{5C308A72-5CF3-45E8-B64F-2C98F567054A}.Release|Any CPU.ActiveCfg = Release|Any CPU
345-
{5C308A72-5CF3-45E8-B64F-2C98F567054A}.Release|Any CPU.Build.0 = Release|Any CPU
347+
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
348+
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
349+
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
350+
{F5D74052-3807-410F-9A5A-B69A57127CF4}.Release|Any CPU.Build.0 = Release|Any CPU
346351
EndGlobalSection
347352
GlobalSection(SolutionProperties) = preSolution
348353
HideSolutionNode = FALSE
@@ -401,6 +406,7 @@ Global
401406
{EA8288BA-CB4D-4B9C-ADF8-F4B7C41466EF} = {FA3EB27D-D1C1-4AE0-A928-CF3882D929CD}
402407
{0AE3CE25-4CD9-4769-AE58-399FC59CF70F} = {FF9C0818-30D3-437A-A62D-7A61CA44F459}
403408
{BA45A727-34B7-484F-9B93-B1755AF09A2A} = {0AE3CE25-4CD9-4769-AE58-399FC59CF70F}
409+
{F5D74052-3807-410F-9A5A-B69A57127CF4} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
404410
EndGlobalSection
405411
GlobalSection(ExtensibilityGlobals) = postSolution
406412
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}

src/WebJobs.Script.WebHost/DependencyInjection/DependencyValidator/DependencyValidator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Azure.WebJobs.Host.Timers;
77
using Microsoft.Azure.WebJobs.Hosting;
88
using Microsoft.Azure.WebJobs.Script.ChangeAnalysis;
9+
using Microsoft.Azure.WebJobs.Script.DependencyInjection;
910
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1011
using Microsoft.Azure.WebJobs.Script.Eventing;
1112
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
@@ -42,6 +43,7 @@ private static ExpectedDependencyBuilder CreateExpectedDependencies()
4243

4344
expected.ExpectCollection<IHostedService>()
4445
.Expect<JobHostService>("Microsoft.Azure.WebJobs.Hosting.OptionsLoggingService")
46+
.ExpectFactory<ExternalConfigurationStartupValidatorService>()
4547
.Expect<PrimaryHostCoordinator>()
4648
.Expect<FileMonitoringService>()
4749
.Expect<WorkerConsoleLogService>()

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.0.3" />
5959
<PackageReference Include="Microsoft.Azure.Storage.File" Version="11.1.0" />
6060
<PackageReference Include="Microsoft.Azure.WebJobs.Host.Storage" Version="4.0.0" />
61-
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.17" />
61+
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.18-11733" />
6262
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.3" />
6363
<PackageReference Include="Microsoft.Azure.WebJobs.Logging" Version="4.0.0" />
6464
<PackageReference Include="Microsoft.Azure.WebSites.DataProtection" Version="2.1.91-alpha" />
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.Azure.WebJobs.Host;
8+
using Microsoft.Extensions.Configuration;
9+
using Newtonsoft.Json.Linq;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
12+
{
13+
internal class ExternalConfigurationStartupValidator
14+
{
15+
private readonly IConfiguration _config;
16+
private readonly IFunctionMetadataManager _metadataManager;
17+
private readonly DefaultNameResolver _nameResolver;
18+
19+
public ExternalConfigurationStartupValidator(IConfiguration config, IFunctionMetadataManager metadataManager)
20+
{
21+
_config = config ?? throw new ArgumentNullException(nameof(config));
22+
_metadataManager = metadataManager ?? throw new ArgumentNullException(nameof(metadataManager));
23+
_nameResolver = new DefaultNameResolver(config);
24+
}
25+
26+
/// <summary>
27+
/// Validates the current configuration against the original configuration. If any values for a trigger
28+
/// do not match, they are returned via the return value.
29+
/// </summary>
30+
/// <param name="originalConfig">The original configuration</param>
31+
/// <returns>A dictionary mapping function name to a list of the invalid values for that function.</returns>
32+
public IDictionary<string, IEnumerable<string>> Validate(IConfigurationRoot originalConfig)
33+
{
34+
if (originalConfig == null)
35+
{
36+
throw new ArgumentNullException(nameof(originalConfig));
37+
}
38+
39+
INameResolver originalNameResolver = new DefaultNameResolver(originalConfig);
40+
IDictionary<string, IEnumerable<string>> invalidValues = new Dictionary<string, IEnumerable<string>>();
41+
42+
var functions = _metadataManager.GetFunctionMetadata();
43+
44+
foreach (var function in functions)
45+
{
46+
var trigger = function.Bindings.SingleOrDefault(b => b.IsTrigger);
47+
48+
if (trigger == null)
49+
{
50+
continue;
51+
}
52+
53+
IList<string> invalidValuesForFunction = new List<string>();
54+
55+
// make sure none of the resolved values have changed for the trigger.
56+
foreach (KeyValuePair<string, JToken> property in trigger.Raw)
57+
{
58+
string lookup = property.Value?.ToString();
59+
60+
if (lookup != null)
61+
{
62+
string originalValue = originalConfig[lookup];
63+
string newValue = _config[lookup];
64+
if (originalValue != newValue)
65+
{
66+
invalidValuesForFunction.Add(lookup);
67+
}
68+
else
69+
{
70+
// It may be a binding expression like "%lookup%"
71+
originalNameResolver.TryResolveWholeString(lookup, out originalValue);
72+
_nameResolver.TryResolveWholeString(lookup, out newValue);
73+
74+
if (originalValue != newValue)
75+
{
76+
invalidValuesForFunction.Add(lookup);
77+
}
78+
}
79+
}
80+
}
81+
82+
if (invalidValuesForFunction.Any())
83+
{
84+
invalidValues[function.Name] = invalidValuesForFunction;
85+
}
86+
}
87+
88+
return invalidValues;
89+
}
90+
}
91+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
15+
{
16+
internal class ExternalConfigurationStartupValidatorService : IHostedService
17+
{
18+
private readonly ExternalConfigurationStartupValidator _validator;
19+
private readonly IConfigurationRoot _originalConfig;
20+
private readonly IEnvironment _environment;
21+
private readonly ILogger<ExternalConfigurationStartupValidator> _logger;
22+
23+
public ExternalConfigurationStartupValidatorService(ExternalConfigurationStartupValidator validator, IConfigurationRoot originalConfig, IEnvironment environment, ILogger<ExternalConfigurationStartupValidator> logger)
24+
{
25+
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
26+
_originalConfig = originalConfig ?? throw new ArgumentNullException(nameof(originalConfig));
27+
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
28+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
29+
}
30+
31+
public Task StartAsync(CancellationToken cancellationToken)
32+
{
33+
IDictionary<string, IEnumerable<string>> invalidValues = _validator.Validate(_originalConfig);
34+
35+
if (invalidValues.Any())
36+
{
37+
StringBuilder sb = new StringBuilder();
38+
sb.AppendLine("The Functions scale controller may not scale the following functions correctly because some configuration values were modified in an external startup class.");
39+
40+
foreach (KeyValuePair<string, IEnumerable<string>> invalidValueMap in invalidValues)
41+
{
42+
sb.AppendLine($" Function '{invalidValueMap.Key}' uses the modified key(s): {string.Join(", ", invalidValueMap.Value)}");
43+
}
44+
45+
if (_environment.IsCoreTools())
46+
{
47+
// We don't know where this will be deployed, so it may not matter,
48+
// but log this as a warning during development.
49+
_logger.LogWarning(sb.ToString());
50+
}
51+
else
52+
{
53+
throw new HostInitializationException(sb.ToString());
54+
}
55+
}
56+
57+
return Task.CompletedTask;
58+
}
59+
60+
public Task StopAsync(CancellationToken cancellationToken)
61+
{
62+
return Task.CompletedTask;
63+
}
64+
}
65+
}

src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ public class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
2929
private readonly IExtensionBundleManager _extensionBundleManager;
3030
private readonly IFunctionMetadataManager _functionMetadataManager;
3131
private readonly IMetricsLogger _metricsLogger;
32+
private readonly Lazy<IEnumerable<Type>> _startupTypes;
3233

3334
private static string[] _builtinExtensionAssemblies = GetBuiltinExtensionAssemblies();
3435

35-
public ScriptStartupTypeLocator(string rootScriptPath, ILogger<ScriptStartupTypeLocator> logger, IExtensionBundleManager extensionBundleManager, IFunctionMetadataManager functionMetadataManager, IMetricsLogger metricsLogger)
36+
public ScriptStartupTypeLocator(string rootScriptPath, ILogger<ScriptStartupTypeLocator> logger, IExtensionBundleManager extensionBundleManager,
37+
IFunctionMetadataManager functionMetadataManager, IMetricsLogger metricsLogger)
3638
{
3739
_rootScriptPath = rootScriptPath ?? throw new ArgumentNullException(nameof(rootScriptPath));
3840
_extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager));
3941
_logger = logger;
4042
_functionMetadataManager = functionMetadataManager;
4143
_metricsLogger = metricsLogger;
44+
_startupTypes = new Lazy<IEnumerable<Type>>(() => GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult());
4245
}
4346

4447
private static string[] GetBuiltinExtensionAssemblies()
@@ -52,13 +55,13 @@ private static string[] GetBuiltinExtensionAssemblies()
5255

5356
public Type[] GetStartupTypes()
5457
{
55-
IEnumerable<Type> startupTypes = GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult();
56-
57-
return startupTypes
58+
return _startupTypes.Value
5859
.Distinct(new TypeNameEqualityComparer())
5960
.ToArray();
6061
}
6162

63+
internal bool HasExternalConfigurationStartups() => _startupTypes.Value.Any(p => typeof(IWebJobsConfigurationStartup).IsAssignableFrom(p));
64+
6265
public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
6366
{
6467
string binPath;
@@ -150,16 +153,19 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
150153
_logger.ScriptStartUpLoadedExtension(startupExtensionName, assembly.GetName().Version.ToString());
151154
return assembly?.GetType(typeName, false, ignoreCase);
152155
}, false, true);
156+
153157
if (extensionType == null)
154158
{
155159
_logger.ScriptStartUpUnableToLoadExtension(startupExtensionName, extensionItem.TypeName);
156160
continue;
157161
}
158-
if (!typeof(IWebJobsStartup).IsAssignableFrom(extensionType))
162+
163+
if (!typeof(IWebJobsStartup).IsAssignableFrom(extensionType) && !typeof(IWebJobsConfigurationStartup).IsAssignableFrom(extensionType))
159164
{
160-
_logger.ScriptStartUpTypeIsNotValid(extensionItem.TypeName, nameof(IWebJobsStartup));
165+
_logger.ScriptStartUpTypeIsNotValid(extensionItem.TypeName, nameof(IWebJobsStartup), nameof(IWebJobsConfigurationStartup));
161166
continue;
162167
}
168+
163169
startupTypes.Add(extensionType);
164170
}
165171
}

src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ internal static class LoggerExtension
5858
new EventId(306, nameof(ScriptStartUpUnableToLoadExtension)),
5959
"Unable to load startup extension '{startupExtensionName}' (Type: '{typeName}'). The type does not exist. Please validate the type and assembly names.");
6060

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

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

232-
public static void ScriptStartUpTypeIsNotValid(this ILogger logger, string typeName, string className)
232+
public static void ScriptStartUpTypeIsNotValid(this ILogger logger, string typeName, string startupClassName, string startupConfigurationClassName)
233233
{
234-
_scriptStartUpTypeIsNotValid(logger, typeName, className, null);
234+
_scriptStartUpTypeIsNotValid(logger, typeName, startupClassName, startupConfigurationClassName, null);
235235
}
236236

237237
public static void ScriptStartUpUnableParseMetadataMissingProperty(this ILogger logger, string metadataFilePath)

src/WebJobs.Script.WebHost/NullHostedService.cs renamed to src/WebJobs.Script/NullHostedService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
using System.Threading.Tasks;
77
using Microsoft.Extensions.Hosting;
88

9-
namespace Microsoft.Azure.WebJobs.Script.WebHost
9+
namespace Microsoft.Azure.WebJobs.Script
1010
{
1111
internal sealed class NullHostedService : IHostedService
1212
{
13-
private static readonly Lazy<NullHostedService> _instance = new Lazy<NullHostedService>(new NullHostedService());
13+
private static readonly Lazy<NullHostedService> _instance = new Lazy<NullHostedService>(() => new NullHostedService());
1414

1515
public static NullHostedService Instance => _instance.Value;
1616

0 commit comments

Comments
 (0)