Skip to content

Commit 1a6e822

Browse files
authored
Introduce host configuration profile feature (#11324)
* Introduce host configuration profile feature * http:routePrefix -> extensions:http:routePrefix * Fix tests, update exception * Add unit tests for HostConfigurationProfile * Address unit tests * Fix test * Remove unused usings * mcp -> mcp-customer-handler * Fix test * Address PR comments * Fix mcp-custom-handler typo * Add release notes * Fix merge
1 parent efbc6b1 commit 1a6e822

15 files changed

+487
-113
lines changed

release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- Add JitTrace Files for v4.1043 (#11281)
1010
- Update `Microsoft.Azure.WebJobs` reference to `3.0.42` (#11309)
1111
- Adding Activity wrapper to create a function-level request telemetry when none exists (#11311)
12+
- Introduce 'configurationProfile' functionality (#11324)
13+
- Remove request size limit for Host <--> Worker communication (#11295)
1214
- Setting current activity status for failed invocations (#11313)
1315
- Adding test coverage for `Utility.IsAzureMonitorLoggingEnabled` (#11322)
1416
- Reduce allocations in `Utility.IsAzureMonitorLoggingEnabled` (#11323)

src/WebJobs.Script/Config/ConfigurationSectionNames.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
namespace Microsoft.Azure.WebJobs.Script.Configuration
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.Extensions.Configuration;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Configuration
10+
{
11+
public sealed class HostConfigurationProfile
12+
{
13+
public const string SectionKey = "configurationProfile";
14+
15+
// note: profile name consts are intentionally private.
16+
// This ensures tests will fail if these values are changed without updating the test also.
17+
private const string DefaultProfile = "default";
18+
19+
private const string McpCustomerHandlerProfile = "mcp-custom-handler";
20+
21+
// Make sure to update this as new profiles are added.
22+
private const string SupportedValues = $"'', '{DefaultProfile}', '{McpCustomerHandlerProfile}'";
23+
24+
public static readonly HostConfigurationProfile Default = new(DefaultProfile, []);
25+
26+
public static readonly HostConfigurationProfile McpCustomHandler = new(
27+
McpCustomerHandlerProfile,
28+
[
29+
KeyValuePair.Create(ConfigurationPath.Combine(
30+
ConfigurationSectionNames.CustomHandler, "enableHttpProxyingRequest"), "true"),
31+
KeyValuePair.Create(ConfigurationPath.Combine(
32+
ConfigurationSectionNames.Http, "routePrefix"), string.Empty),
33+
]);
34+
35+
private HostConfigurationProfile(
36+
string name,
37+
IEnumerable<KeyValuePair<string, string>> configuration)
38+
{
39+
Name = name;
40+
Configuration = [.. configuration, KeyValuePair.Create(SectionKey, name)];
41+
}
42+
43+
public string Name { get; }
44+
45+
public IEnumerable<KeyValuePair<string, string>> Configuration { get; }
46+
47+
public static HostConfigurationProfile Get(string name)
48+
{
49+
ArgumentNullException.ThrowIfNull(name);
50+
return name.ToLowerInvariant() switch
51+
{
52+
McpCustomerHandlerProfile => McpCustomHandler,
53+
"" or DefaultProfile => Default,
54+
_ => throw new NotSupportedException(
55+
$"Configuration profile '{name}' is not supported. Supported values: {SupportedValues}."),
56+
};
57+
}
58+
}
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
#nullable enable
5+
6+
using System;
7+
using Newtonsoft.Json.Linq;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Configuration
10+
{
11+
public sealed class HostJsonFileConfigurationOptions
12+
{
13+
private const string ConfigProfileKey = "configurationProfile";
14+
private const string ConfigProfileEnvKey = $"{ConfigurationSectionNames.JobHost}__{ConfigProfileKey}";
15+
16+
public HostJsonFileConfigurationOptions(ScriptApplicationHostOptions hostOptions)
17+
{
18+
ArgumentNullException.ThrowIfNull(hostOptions);
19+
Host = hostOptions;
20+
}
21+
22+
public HostJsonFileConfigurationOptions(
23+
IEnvironment environment, ScriptApplicationHostOptions hostOptions)
24+
: this(hostOptions)
25+
{
26+
ArgumentNullException.ThrowIfNull(environment);
27+
ArgumentNullException.ThrowIfNull(hostOptions);
28+
29+
// Right now we explicitly read config profile from environment variable only.
30+
// At the time of this commit there was 0 config sources already loaded. Environment
31+
// vars are added to IConfiguration later in the pipeline. If we do eventually have
32+
// config sources earlier we will need to consider if we want to read from those as well
33+
// here.
34+
ConfigProfile = environment.GetEnvironmentVariable(ConfigProfileEnvKey);
35+
WorkerRuntime = environment.GetFunctionsWorkerRuntime();
36+
IsLogicApp = environment.IsLogicApp();
37+
}
38+
39+
public string? ConfigProfile { get; init; }
40+
41+
public string WorkerRuntime { get; init; } = string.Empty;
42+
43+
public bool IsLogicApp { get; init; }
44+
45+
public ScriptApplicationHostOptions Host { get; }
46+
47+
public HostConfigurationProfile GetConfigProfile(JObject hostFile)
48+
{
49+
ArgumentNullException.ThrowIfNull(hostFile);
50+
51+
// Right now this is ONLY set via env variable, which will always take precedence over host.json.
52+
// If in the future we allow this to be set via other means (e.g. CLI arg), we may need to revisit precedence.
53+
// If config profile is not set via env, check host.json for the value.
54+
string profile = ConfigProfile ?? hostFile.GetValue(ConfigProfileKey)?.Value<string>() ?? string.Empty;
55+
56+
return HostConfigurationProfile.Get(profile);
57+
}
58+
}
59+
}

src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -10,7 +10,6 @@
1010
using Microsoft.Azure.WebJobs.Logging;
1111
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1212
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
13-
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
1413
using Microsoft.Extensions.Configuration;
1514
using Microsoft.Extensions.Logging;
1615
using Newtonsoft.Json;
@@ -23,22 +22,23 @@ public class HostJsonFileConfigurationSource : IConfigurationSource
2322
private readonly ILogger _logger;
2423
private readonly IMetricsLogger _metricsLogger;
2524

26-
public HostJsonFileConfigurationSource(ScriptApplicationHostOptions applicationHostOptions, IEnvironment environment, ILoggerFactory loggerFactory, IMetricsLogger metricsLogger)
25+
public HostJsonFileConfigurationSource(
26+
HostJsonFileConfigurationOptions options,
27+
ILoggerFactory loggerFactory,
28+
IMetricsLogger metricsLogger)
2729
{
28-
if (loggerFactory == null)
29-
{
30-
throw new ArgumentNullException(nameof(loggerFactory));
31-
}
30+
ArgumentNullException.ThrowIfNull(options);
31+
ArgumentNullException.ThrowIfNull(loggerFactory);
32+
ArgumentNullException.ThrowIfNull(metricsLogger);
3233

33-
HostOptions = applicationHostOptions;
34-
Environment = environment;
35-
_metricsLogger = metricsLogger;
34+
Options = options;
3635
_logger = loggerFactory.CreateLogger(LogCategories.Startup);
36+
_metricsLogger = metricsLogger;
3737
}
3838

39-
public ScriptApplicationHostOptions HostOptions { get; }
39+
private HostJsonFileConfigurationOptions Options { get; }
4040

41-
public IEnvironment Environment { get; }
41+
private ScriptApplicationHostOptions HostOptions => Options.Host;
4242

4343
public IConfigurationProvider Build(IConfigurationBuilder builder)
4444
{
@@ -47,30 +47,41 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
4747

4848
public class HostJsonFileConfigurationProvider : ConfigurationProvider
4949
{
50-
private static readonly string[] WellKnownHostJsonProperties = new[]
51-
{
50+
private static readonly string[] WellKnownHostJsonProperties =
51+
[
5252
"version", "functionTimeout", "retry", "functions", "http", "watchDirectories", "watchFiles", "queues", "serviceBus",
5353
"eventHub", "singleton", "logging", "aggregator", "healthMonitor", "extensionBundle", "managedDependencies",
5454
"customHandler", "httpWorker", "extensions", "concurrency", "telemetryMode", ConfigurationSectionNames.SendCanceledInvocationsToWorker,
5555
ConfigurationSectionNames.MetadataProviderTimeout, "isDefaultHostConfig"
56-
};
56+
];
5757

5858
private readonly HostJsonFileConfigurationSource _configurationSource;
59-
private readonly Stack<string> _path;
59+
private readonly Stack<string> _path = new();
6060
private readonly ILogger _logger;
6161
private readonly IMetricsLogger _metricsLogger;
6262

63-
public HostJsonFileConfigurationProvider(HostJsonFileConfigurationSource configurationSource, ILogger logger, IMetricsLogger metricsLogger)
63+
public HostJsonFileConfigurationProvider(
64+
HostJsonFileConfigurationSource configurationSource, ILogger logger, IMetricsLogger metricsLogger)
6465
{
6566
_configurationSource = configurationSource;
66-
_path = new Stack<string>();
6767
_logger = logger;
6868
_metricsLogger = metricsLogger;
6969
}
7070

71+
private HostJsonFileConfigurationOptions Options => _configurationSource.Options;
72+
7173
public override void Load()
7274
{
7375
JObject hostJson = LoadHostConfigurationFile();
76+
77+
// Apply profile settings first, so that they can be overridden by host.json settings.
78+
HostConfigurationProfile profile = GetConfigProfile(hostJson);
79+
_logger.LogDebug("Loading host configuration profile '{profileName}'.", profile.Name);
80+
foreach ((string key, string value) in profile.Configuration)
81+
{
82+
Data[ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, key)] = value;
83+
}
84+
7485
ProcessObject(hostJson);
7586
}
7687

@@ -141,9 +152,7 @@ private JObject LoadHostConfigurationFile()
141152
// to the startup logger until we've read configuration settings and can create the real logger.
142153
// The "startup" logger is used in this class for startup related logs. The public logger is used
143154
// for all other logging after startup.
144-
145-
ScriptApplicationHostOptions options = _configurationSource.HostOptions;
146-
string hostFilePath = Path.Combine(options.ScriptPath, ScriptConstants.HostMetadataFileName);
155+
string hostFilePath = Path.Combine(Options.Host.ScriptPath, ScriptConstants.HostMetadataFileName);
147156
JObject hostConfigObject = LoadHostConfig(hostFilePath);
148157
hostConfigObject = InitializeHostConfig(hostFilePath, hostConfigObject);
149158

@@ -192,10 +201,11 @@ private JObject InitializeHostConfig(string hostJsonPath, JObject hostConfigObje
192201
throw new HostConfigurationException(errorMsg.ToString());
193202
}
194203
}
204+
195205
return hostConfigObject;
196206
}
197207

198-
internal JObject LoadHostConfig(string configFilePath)
208+
private JObject LoadHostConfig(string configFilePath)
199209
{
200210
using (_metricsLogger.LatencyEvent(MetricEventNames.LoadHostConfiguration))
201211
{
@@ -227,11 +237,11 @@ internal JObject LoadHostConfig(string configFilePath)
227237
// So a newly created function app from the portal would have no host.json. In that case we need to
228238
// create a new function app with host.json that includes a matching extension bundle based on the app kind.
229239
hostConfigObject = GetDefaultHostConfigObject();
230-
string bundleId = _configurationSource.Environment.IsLogicApp() ?
240+
string bundleId = Options.IsLogicApp ?
231241
ScriptConstants.WorkFlowExtensionBundleId :
232242
ScriptConstants.DefaultExtensionBundleId;
233243

234-
string bundleVersion = _configurationSource.Environment.IsLogicApp() ?
244+
string bundleVersion = Options.IsLogicApp ?
235245
ScriptConstants.LogicAppDefaultExtensionBundleVersion :
236246
ScriptConstants.DefaultExtensionBundleVersion;
237247

@@ -248,15 +258,29 @@ private JObject GetDefaultHostConfigObject()
248258
{
249259
// isDefaultHostConfig is used to determine if the host.json file was created by the system
250260
var hostJsonJObj = JObject.Parse("{'version': '2.0', 'isDefaultHostConfig': true}");
251-
if (string.Equals(_configurationSource.Environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName), "powershell", StringComparison.InvariantCultureIgnoreCase)
252-
&& !_configurationSource.HostOptions.IsFileSystemReadOnly)
261+
if (string.Equals(Options.WorkerRuntime, "powershell", StringComparison.InvariantCultureIgnoreCase)
262+
&& !Options.Host.IsFileSystemReadOnly)
253263
{
254264
hostJsonJObj.Add("managedDependency", JToken.Parse("{'Enabled': true}"));
255265
}
256266

257267
return hostJsonJObj;
258268
}
259269

270+
private HostConfigurationProfile GetConfigProfile(JObject hostFile)
271+
{
272+
ArgumentNullException.ThrowIfNull(hostFile);
273+
274+
try
275+
{
276+
return Options.GetConfigProfile(hostFile);
277+
}
278+
catch (NotSupportedException ex)
279+
{
280+
throw new HostConfigurationException(ex.Message, ex);
281+
}
282+
}
283+
260284
private void TryWriteHostJson(string filePath, JObject content)
261285
{
262286
if (!_configurationSource.HostOptions.IsFileSystemReadOnly)
@@ -278,7 +302,7 @@ private void TryWriteHostJson(string filePath, JObject content)
278302

279303
private JObject TryAddBundleConfiguration(JObject content, string bundleId, string bundleVersion)
280304
{
281-
if (!_configurationSource.HostOptions.IsFileSystemReadOnly)
305+
if (!Options.Host.IsFileSystemReadOnly)
282306
{
283307
string bundleConfiguration = "{ 'id': '" + bundleId + "', 'version': '" + bundleVersion + "'}";
284308
content.Add("extensionBundle", JToken.Parse(bundleConfiguration));

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static IHostBuilder AddScriptHost(this IHostBuilder builder,
8080
IMetricsLogger metricsLogger,
8181
Action<IWebJobsBuilder> configureWebJobs = null)
8282
{
83-
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
83+
loggerFactory ??= NullLoggerFactory.Instance;
8484

8585
builder.SetAzureFunctionsConfigurationRoot();
8686
// Host configuration
@@ -103,7 +103,8 @@ public static IHostBuilder AddScriptHost(this IHostBuilder builder,
103103
{
104104
if (!context.Properties.ContainsKey(ScriptConstants.SkipHostJsonConfigurationKey))
105105
{
106-
configBuilder.Add(new HostJsonFileConfigurationSource(applicationOptions, SystemEnvironment.Instance, loggerFactory, metricsLogger));
106+
HostJsonFileConfigurationOptions hostJsonConfigOptions = new(SystemEnvironment.Instance, applicationOptions);
107+
configBuilder.Add(new HostJsonFileConfigurationSource(hostJsonConfigOptions, loggerFactory, metricsLogger));
107108
}
108109
// Adding hosting config into job host configuration
109110
configBuilder.Add(new FunctionsHostingConfigSource(SystemEnvironment.Instance));
@@ -128,18 +129,24 @@ public static IHostBuilder AddScriptHost(this IHostBuilder builder,
128129
builder.ConfigureAppConfiguration((context, configBuilder) =>
129130
{
130131
// Pre-build configuration here to load bundles and to store for later validation.
131-
var config = configBuilder.Build();
132-
var extensionBundleOptions = GetExtensionBundleOptions(config);
133-
FunctionsHostingConfigOptions configOption = new FunctionsHostingConfigOptions();
134-
var optionsSetup = new FunctionsHostingConfigOptionsSetup(config);
132+
IConfigurationRoot config = configBuilder.Build();
133+
ExtensionBundleOptions extensionBundleOptions = GetExtensionBundleOptions(config);
134+
FunctionsHostingConfigOptions configOption = new();
135+
FunctionsHostingConfigOptionsSetup optionsSetup = new(config);
135136
optionsSetup.Configure(configOption);
136137

137138
var extensionRequirementOptions = applicationOptions.RootServiceProvider.GetService<IOptions<ExtensionRequirementOptions>>();
138139

139-
var bundleManager = new ExtensionBundleManager(extensionBundleOptions, SystemEnvironment.Instance, loggerFactory, configOption);
140+
ExtensionBundleManager bundleManager = new(extensionBundleOptions, SystemEnvironment.Instance, loggerFactory, configOption);
140141
var metadataServiceManager = applicationOptions.RootServiceProvider.GetService<IFunctionMetadataManager>();
141142

142-
var locator = new ScriptStartupTypeLocator(applicationOptions.ScriptPath, loggerFactory.CreateLogger<ScriptStartupTypeLocator>(), bundleManager, metadataServiceManager, metricsLogger, extensionRequirementOptions);
143+
ScriptStartupTypeLocator locator = new(
144+
applicationOptions.ScriptPath,
145+
loggerFactory.CreateLogger<ScriptStartupTypeLocator>(),
146+
bundleManager,
147+
metadataServiceManager,
148+
metricsLogger,
149+
extensionRequirementOptions);
143150

144151
// The locator (and thus the bundle manager) need to be created now in order to configure app configuration.
145152
// Store them so they do not need to be re-created later when configuring services.

test/WebJobs.Script.Tests.Shared/TestEnvironment.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Linq;
8-
using System.Text;
98

109
namespace Microsoft.Azure.WebJobs.Script.Tests
1110
{
@@ -31,6 +30,12 @@ public TestEnvironment(IDictionary<string, string> variables, bool is64BitProces
3130

3231
public bool Is64BitProcess => _is64BitProcess;
3332

33+
public string this[string key]
34+
{
35+
get => GetEnvironmentVariable(key);
36+
set => SetEnvironmentVariable(key, value);
37+
}
38+
3439
public void Clear()
3540
{
3641
_variables.Clear();

0 commit comments

Comments
 (0)