Skip to content

Commit 5be2a71

Browse files
HostJson validation (#10432)
HostJson validation
1 parent 5cb5b79 commit 5be2a71

File tree

8 files changed

+194
-10
lines changed

8 files changed

+194
-10
lines changed

src/WebJobs.Script/Config/ConfigurationSectionNames.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ public static class ConfigurationSectionNames
2727
public const string TelemetryMode = "telemetryMode";
2828
public const string MetadataProviderTimeout = "metadataProviderTimeout";
2929
}
30-
}
30+
}

src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ internal JObject LoadHostConfig(string configFilePath)
246246

247247
private JObject GetDefaultHostConfigObject()
248248
{
249-
var hostJsonJObj = JObject.Parse("{'version': '2.0'}");
249+
// isDefaultHostConfig is used to determine if the host.json file was created by the system
250+
var hostJsonJObj = JObject.Parse("{'version': '2.0', 'isDefaultHostConfig': true}");
250251
if (string.Equals(_configurationSource.Environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName), "powershell", StringComparison.InvariantCultureIgnoreCase)
251252
&& !_configurationSource.HostOptions.IsFileSystemReadOnly)
252253
{
@@ -287,4 +288,4 @@ private JObject TryAddBundleConfiguration(JObject content, string bundleId, stri
287288
}
288289
}
289290
}
290-
}
291+
}

src/WebJobs.Script/Config/ScriptJobHostOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ public string RootScriptPath
136136
/// </summary>
137137
internal TelemetryMode TelemetryMode { get; set; } = TelemetryMode.ApplicationInsights;
138138

139+
/// <summary>
140+
/// Gets or sets a value indicating whether the host.json file was created by the host.
141+
/// </summary>
142+
public bool IsDefaultHostConfig { get; set; }
143+
139144
/// <summary>
140145
/// Gets or sets a value indicating the timeout duration for the function metadata provider.
141146
/// </summary>

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ internal static class LoggerExtension
183183
new EventId(337, nameof(MinimumBundleVersionNotSatisfied)),
184184
"Referenced bundle {bundleId} of version {bundleVersion} does not meet the required minimum version of {minimumVersion}. Update your extension bundle reference in host.json to reference {minimumVersion2} or later.");
185185

186+
private static readonly Action<ILogger, string, Exception> _hostJsonZipDeploymentIssue =
187+
LoggerMessage.Define<string>(LogLevel.Error,
188+
new EventId(338, nameof(HostJsonZipDeploymentIssue)),
189+
"No functions were found. A valid host.json file wasn't found in the package root. However, one was located at: {hostJsonFilesPath}. This state indicates that your deployment package was created incorrectly. For deployment package requirements, see https://aka.ms/deployment-zip-push.");
190+
191+
private static readonly Action<ILogger, Exception> _noHostJsonFile =
192+
LoggerMessage.Define(LogLevel.Information,
193+
new EventId(339, nameof(NoHostJsonFile)),
194+
"No functions were found. This can occur before you deploy code to your function app or when the host.json file is missing from the most recent deployment. Make sure that your deployment package includes the host.json file in the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");
195+
186196
private static readonly Action<ILogger, string, Exception> _publishingMetrics =
187197
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(338, nameof(PublishingMetrics)), "{metrics}");
188198

@@ -367,5 +377,15 @@ public static void MinimumBundleVersionNotSatisfied(this ILogger logger, string
367377
{
368378
_minimumBundleVersionNotSatisfied(logger, bundleId, bundleVersion, minimumVersion, minimumVersion, null);
369379
}
380+
381+
public static void HostJsonZipDeploymentIssue(this ILogger logger, string path)
382+
{
383+
_hostJsonZipDeploymentIssue(logger, path, null);
384+
}
385+
386+
public static void NoHostJsonFile(this ILogger logger)
387+
{
388+
_noHostJsonFile(logger, null);
389+
}
370390
}
371-
}
391+
}

src/WebJobs.Script/Host/FunctionMetadataManager.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
8+
using System.IO;
89
using System.Linq;
910
using System.Threading;
1011
using System.Threading.Tasks;
1112
using Microsoft.Azure.WebJobs.Logging;
13+
using Microsoft.Azure.WebJobs.Script.Configuration;
1214
using Microsoft.Azure.WebJobs.Script.Description;
1315
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
1416
using Microsoft.Azure.WebJobs.Script.Workers;
1517
using Microsoft.Azure.WebJobs.Script.Workers.Http;
1618
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
19+
using Microsoft.Extensions.Configuration;
1720
using Microsoft.Extensions.DependencyInjection;
1821
using Microsoft.Extensions.Logging;
1922
using Microsoft.Extensions.Options;
@@ -180,6 +183,12 @@ internal ImmutableArray<FunctionMetadata> LoadFunctionMetadata(bool forceRefresh
180183
Errors = _functionErrors.Where(kvp => functionsAllowList.Any(functionName => functionName.Equals(kvp.Key, StringComparison.CurrentCultureIgnoreCase))).ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray());
181184
}
182185

186+
if (functionMetadataList.Count == 0 && !_environment.IsPlaceholderModeEnabled())
187+
{
188+
// Validate the host.json file if no functions are found.
189+
ValidateHostJsonFile();
190+
}
191+
183192
return functionMetadataList.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase).ToImmutableArray();
184193
}
185194

@@ -283,5 +292,33 @@ private void AddMetadataFromCustomProviders(IEnumerable<IFunctionProvider> funct
283292
}
284293
}
285294
}
295+
296+
private void ValidateHostJsonFile()
297+
{
298+
try
299+
{
300+
if (_scriptOptions.Value.RootScriptPath is not null && _scriptOptions.Value.IsDefaultHostConfig)
301+
{
302+
// Search for the host.json file within nested directories to verify scenarios where it isn't located at the root. This situation often occurs when a function app has been improperly zipped.
303+
string hostFilePath = Path.Combine(_scriptOptions.Value.RootScriptPath, ScriptConstants.HostMetadataFileName);
304+
IEnumerable<string> hostJsonFiles = Directory.GetFiles(_scriptOptions.Value.RootScriptPath, ScriptConstants.HostMetadataFileName, SearchOption.AllDirectories)
305+
.Where(file => !file.Equals(hostFilePath, StringComparison.OrdinalIgnoreCase));
306+
307+
if (hostJsonFiles != null && hostJsonFiles.Any())
308+
{
309+
string hostJsonFilesPath = string.Join(", ", hostJsonFiles).Replace(_scriptOptions.Value.RootScriptPath, string.Empty);
310+
_logger.HostJsonZipDeploymentIssue(hostJsonFilesPath);
311+
}
312+
else
313+
{
314+
_logger.NoHostJsonFile();
315+
}
316+
}
317+
}
318+
catch
319+
{
320+
// Ignore any exceptions.
321+
}
322+
}
286323
}
287324
}

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Azure.WebJobs.Script.Grpc;
1010
using Microsoft.Azure.WebJobs.Script.Workers.Http;
1111
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
12+
using Microsoft.Extensions.Configuration;
1213
using Microsoft.Extensions.DependencyInjection;
1314
using Microsoft.Extensions.Logging;
1415
using Microsoft.Extensions.Options;
@@ -42,6 +43,51 @@ public static FunctionMetadataManager GetFunctionMetadataManager(IOptions<Script
4243
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IOptionsMonitor<LanguageWorkerOptions>))).Returns(languageWorkerOptions);
4344
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(ILoggerFactory))).Returns(loggerFactory);
4445

46+
var testData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
47+
{
48+
{ "version", "2.0" }
49+
};
50+
51+
var testActiveHostConfig = new ConfigurationBuilder()
52+
.AddInMemoryCollection(testData)
53+
.Build();
54+
55+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IConfiguration))).Returns(testActiveHostConfig);
56+
57+
var options = new ScriptApplicationHostOptions()
58+
{
59+
IsSelfHost = true,
60+
ScriptPath = TestHelpers.FunctionsTestDirectory,
61+
LogPath = TestHelpers.GetHostLogFileDirectory().FullName
62+
};
63+
var factory = new TestOptionsFactory<ScriptApplicationHostOptions>(options);
64+
var source = new TestChangeTokenSource<ScriptApplicationHostOptions>();
65+
var changeTokens = new[] { source };
66+
var optionsMonitor = new OptionsMonitor<ScriptApplicationHostOptions>(factory, changeTokens, factory);
67+
return new FunctionMetadataManager(jobHostOptions, functionMetadataProvider, httpOptions, managerMock.Object, loggerFactory, SystemEnvironment.Instance);
68+
}
69+
70+
public static FunctionMetadataManager GetFunctionMetadataManagerWithDefaultHostConfig(IOptions<ScriptJobHostOptions> jobHostOptions,
71+
IFunctionMetadataProvider functionMetadataProvider, IList<IFunctionProvider> functionProviders, IOptions<HttpWorkerOptions> httpOptions, ILoggerFactory loggerFactory, IOptionsMonitor<LanguageWorkerOptions> languageWorkerOptions)
72+
{
73+
var managerMock = new Mock<IScriptHostManager>();
74+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IEnumerable<IFunctionProvider>))).Returns(functionProviders);
75+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IOptions<ScriptJobHostOptions>))).Returns(jobHostOptions);
76+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IOptions<HttpWorkerOptions>))).Returns(httpOptions);
77+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IOptionsMonitor<LanguageWorkerOptions>))).Returns(languageWorkerOptions);
78+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(ILoggerFactory))).Returns(loggerFactory);
79+
80+
var testData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
81+
{
82+
{ "AzureFunctionsJobHost:isDefaultHostConfig", "true" }
83+
};
84+
85+
var testActiveHostConfig = new ConfigurationBuilder()
86+
.AddInMemoryCollection(testData)
87+
.Build();
88+
89+
managerMock.As<IServiceProvider>().Setup(m => m.GetService(typeof(IConfiguration))).Returns(testActiveHostConfig);
90+
4591
var options = new ScriptApplicationHostOptions()
4692
{
4793
IsSelfHost = true,
@@ -55,4 +101,4 @@ public static FunctionMetadataManager GetFunctionMetadataManager(IOptions<Script
55101
return new FunctionMetadataManager(jobHostOptions, functionMetadataProvider, httpOptions, managerMock.Object, loggerFactory, SystemEnvironment.Instance);
56102
}
57103
}
58-
}
104+
}

test/WebJobs.Script.Tests/Configuration/HostJsonFileConfigurationSourceTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Configuration
1919
{
2020
public class HostJsonFileConfigurationSourceTests
2121
{
22-
private readonly string _hostJsonWithBundles = "{\r\n \"version\": \"2.0\",\r\n \"extensionBundle\": {\r\n \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\r\n \"version\": \"[4.*, 5.0.0)\"\r\n }\r\n}";
23-
private readonly string _hostJsonWithWorkFlowBundle = "{\r\n \"version\": \"2.0\",\r\n \"extensionBundle\": {\r\n \"id\": \"Microsoft.Azure.Functions.ExtensionBundle.Workflows\",\r\n \"version\": \"[1.*, 2.0.0)\"\r\n }\r\n}";
24-
private readonly string _defaultHostJson = "{\r\n \"version\": \"2.0\"\r\n}";
22+
private readonly string _hostJsonWithBundles = "{\r\n \"version\": \"2.0\",\r\n \"isDefaultHostConfig\": true,\r\n \"extensionBundle\": {\r\n \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\r\n \"version\": \"[4.*, 5.0.0)\"\r\n }\r\n}";
23+
private readonly string _hostJsonWithWorkFlowBundle = "{\r\n \"version\": \"2.0\",\r\n \"isDefaultHostConfig\": true,\r\n \"extensionBundle\": {\r\n \"id\": \"Microsoft.Azure.Functions.ExtensionBundle.Workflows\",\r\n \"version\": \"[1.*, 2.0.0)\"\r\n }\r\n}";
24+
private readonly string _defaultHostJson = "{\r\n \"version\": \"2.0\",\r\n \"isDefaultHostConfig\": true\r\n}";
2525
private readonly ScriptApplicationHostOptions _options;
2626
private readonly string _hostJsonFile;
2727
private readonly TestLoggerProvider _loggerProvider = new TestLoggerProvider();
@@ -141,7 +141,7 @@ public void ReadOnlyFileSystem_SkipsDefaultHostJsonCreation()
141141
AreExpectedMetricsGenerated(testMetricsLogger);
142142
var configList = config.AsEnumerable().ToList();
143143
Assert.Equal(config["AzureFunctionsJobHost:version"], "2.0");
144-
Assert.Equal(configList.Count, 2);
144+
Assert.Equal(configList.Count, 3);
145145
Assert.True(configList.TrueForAll((k) => !k.Key.Contains("extensionBundle")));
146146

147147
var log = _loggerProvider.GetAllLogMessages().Single(l => l.FormattedMessage == "No host configuration file found. Creating a default host.json file.");

test/WebJobs.Script.Tests/FunctionMetadataManagerTests.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,81 @@ public void FunctionMetadataManager_ResetProviders_OnRefresh()
437437
Assert.Equal("newFunction", testFunctionMetadataManager.GetFunctionMetadata(true).FirstOrDefault()?.Name);
438438
}
439439

440+
[Fact]
441+
public void FunctionMetadataManager_GetsMetadata_ValidateDefaultHostJson()
442+
{
443+
var functionMetadataCollection1 = new Collection<FunctionMetadata>
444+
{
445+
};
446+
447+
var expectedTotalFunctionsCount = 0;
448+
449+
var mockFunctionMetadataProvider = new Mock<IFunctionMetadataProvider>();
450+
mockFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(It.IsAny<IEnumerable<RpcWorkerConfig>>(), It.IsAny<SystemEnvironment>(), It.IsAny<bool>()))
451+
.Returns(Task.FromResult(new Collection<FunctionMetadata>().ToImmutableArray()));
452+
mockFunctionMetadataProvider.Setup(m => m.FunctionErrors)
453+
.Returns(new Dictionary<string, ICollection<string>>().ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray()));
454+
455+
var mockFunctionProvider = new Mock<IFunctionProvider>();
456+
mockFunctionProvider.Setup(m => m.GetFunctionMetadataAsync()).ReturnsAsync(functionMetadataCollection1.ToImmutableArray());
457+
458+
var testLoggerProvider = new TestLoggerProvider();
459+
var loggerFactory = new LoggerFactory();
460+
loggerFactory.AddProvider(testLoggerProvider);
461+
_scriptJobHostOptions.IsDefaultHostConfig = true;
462+
FunctionMetadataManager testFunctionMetadataManager = TestFunctionMetadataManager.GetFunctionMetadataManagerWithDefaultHostConfig(
463+
new OptionsWrapper<ScriptJobHostOptions>(_scriptJobHostOptions),
464+
mockFunctionMetadataProvider.Object,
465+
new List<IFunctionProvider>() { mockFunctionProvider.Object },
466+
new OptionsWrapper<HttpWorkerOptions>(_defaultHttpWorkerOptions),
467+
loggerFactory,
468+
new TestOptionsMonitor<LanguageWorkerOptions>(TestHelpers.GetTestLanguageWorkerOptions()));
469+
470+
var actualFunctionMetadata = testFunctionMetadataManager.LoadFunctionMetadata();
471+
472+
var traces = testLoggerProvider.GetAllLogMessages();
473+
Assert.Equal(expectedTotalFunctionsCount, actualFunctionMetadata.Length);
474+
Assert.Single(traces.Where(t => t.EventId.Name.Equals("NoHostJsonFile", StringComparison.OrdinalIgnoreCase)));
475+
}
476+
477+
[Fact]
478+
public void FunctionMetadataManager_GetsMetadata_ValidateHostJson()
479+
{
480+
var functionMetadataCollection1 = new Collection<FunctionMetadata>
481+
{
482+
};
483+
484+
var expectedTotalFunctionsCount = 0;
485+
486+
var mockFunctionMetadataProvider = new Mock<IFunctionMetadataProvider>();
487+
mockFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(It.IsAny<IEnumerable<RpcWorkerConfig>>(), It.IsAny<SystemEnvironment>(), It.IsAny<bool>()))
488+
.Returns(Task.FromResult(new Collection<FunctionMetadata>().ToImmutableArray()));
489+
mockFunctionMetadataProvider.Setup(m => m.FunctionErrors)
490+
.Returns(new Dictionary<string, ICollection<string>>().ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray()));
491+
492+
var mockFunctionProvider = new Mock<IFunctionProvider>();
493+
mockFunctionProvider.Setup(m => m.GetFunctionMetadataAsync()).ReturnsAsync(functionMetadataCollection1.ToImmutableArray());
494+
495+
var testLoggerProvider = new TestLoggerProvider();
496+
var loggerFactory = new LoggerFactory();
497+
loggerFactory.AddProvider(testLoggerProvider);
498+
499+
_scriptJobHostOptions.IsDefaultHostConfig = true;
500+
FunctionMetadataManager testFunctionMetadataManager = TestFunctionMetadataManager.GetFunctionMetadataManager(
501+
new OptionsWrapper<ScriptJobHostOptions>(_scriptJobHostOptions),
502+
mockFunctionMetadataProvider.Object,
503+
new List<IFunctionProvider>() { mockFunctionProvider.Object },
504+
new OptionsWrapper<HttpWorkerOptions>(_defaultHttpWorkerOptions),
505+
loggerFactory,
506+
new TestOptionsMonitor<LanguageWorkerOptions>(TestHelpers.GetTestLanguageWorkerOptions()));
507+
508+
var actualFunctionMetadata = testFunctionMetadataManager.LoadFunctionMetadata();
509+
510+
var traces = testLoggerProvider.GetAllLogMessages();
511+
Assert.Equal(expectedTotalFunctionsCount, actualFunctionMetadata.Length);
512+
Assert.Single(traces.Where(t => t.EventId.Name.Equals("NoHostJsonFile", StringComparison.OrdinalIgnoreCase)));
513+
}
514+
440515
[Theory]
441516
[InlineData("")]
442517
[InlineData(null)]
@@ -495,4 +570,4 @@ private static FunctionMetadata GetTestFunctionMetadata(string scriptFile, strin
495570
};
496571
}
497572
}
498-
}
573+
}

0 commit comments

Comments
 (0)