Skip to content

Commit

Permalink
throwing exception if no version property in host.json does not equal…
Browse files Browse the repository at this point in the history
… '2.0'
  • Loading branch information
brettsam committed Aug 24, 2018
1 parent 2dd52a0 commit d500b50
Show file tree
Hide file tree
Showing 29 changed files with 345 additions and 128 deletions.
1 change: 1 addition & 0 deletions sample/host.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"version": "2.0",
"watchDirectories": [ "Shared", "Test" ],
"healthMonitor": {
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.WebJobs.Script.WebHost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Script.WebHost.Features;
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -46,7 +47,10 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
builder.UseMiddleware<HttpExceptionMiddleware>();
builder.UseMiddleware<ResponseBufferingMiddleware>();
builder.UseMiddleware<HomepageMiddleware>();
builder.UseMiddleware<HttpThrottleMiddleware>();
builder.UseWhen(context => context.Features.Get<IFunctionExecutionFeature>() != null, config =>
{
config.UseMiddleware<HttpThrottleMiddleware>();
});
builder.UseMiddleware<FunctionInvocationMiddleware>();
builder.UseMiddleware<HostWarmupMiddleware>();

Expand Down
52 changes: 36 additions & 16 deletions src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Hosting;
Expand Down Expand Up @@ -76,23 +74,27 @@ public async Task StartAsync(CancellationToken cancellationToken)
}
}

private async Task StartHostAsync(CancellationToken cancellationToken, int attemptCount = 0)
private async Task StartHostAsync(CancellationToken cancellationToken, int attemptCount = 0, bool skipHostJsonConfiguration = false)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
bool isOffline = CheckAppOffline();

_host = BuildHost(isOffline);
_host = BuildHost(isOffline, skipHostJsonConfiguration);

LogInitialization(_host, isOffline, attemptCount, _hostStartCount++);

await _host.StartAsync(cancellationToken);

LastError = null;
// This means we had an error on a previous load, so we want to keep the LastError around
if (!skipHostJsonConfiguration)
{
LastError = null;
}

if (!isOffline)
if (!isOffline && !skipHostJsonConfiguration)
{
State = ScriptHostState.Running;
}
Expand Down Expand Up @@ -122,12 +124,21 @@ private async Task StartHostAsync(CancellationToken cancellationToken, int attem

cancellationToken.ThrowIfCancellationRequested();

await Utility.DelayWithBackoffAsync(attemptCount, cancellationToken, min: TimeSpan.FromSeconds(1), max: TimeSpan.FromMinutes(2))
.ContinueWith(t =>
{
cancellationToken.ThrowIfCancellationRequested();
return StartHostAsync(cancellationToken, attemptCount);
});
if (exc is HostConfigurationException)
{
// Try starting the host without parsing host.json. This will start up a
// minimal host and allow the portal to see the error. Any modification will restart again.
Task ignore = StartHostAsync(cancellationToken, attemptCount, skipHostJsonConfiguration: true);
}
else
{
await Utility.DelayWithBackoffAsync(attemptCount, cancellationToken, min: TimeSpan.FromSeconds(1), max: TimeSpan.FromMinutes(2))
.ContinueWith(t =>
{
cancellationToken.ThrowIfCancellationRequested();
return StartHostAsync(cancellationToken, attemptCount);
});
}
}
}

Expand Down Expand Up @@ -176,10 +187,19 @@ public async Task RestartHostAsync(CancellationToken cancellationToken)
_logger.LogInformation("Script host restarted.");
}

private IHost BuildHost(bool isOffline = false)
private IHost BuildHost(bool isOffline = false, bool skipHostJsonConfiguration = false)
{
var builder = new HostBuilder()
.SetAzureFunctionsEnvironment()
var builder = new HostBuilder();

if (skipHostJsonConfiguration)
{
builder.ConfigureAppConfiguration((context, _) =>
{
context.Properties[ScriptConstants.SkipHostJsonConfigurationKey] = true;
});
}

builder.SetAzureFunctionsEnvironment()
.AddWebScriptHost(_rootServiceProvider, _rootScopeFactory, _applicationHostOptions.CurrentValue);

if (isOffline)
Expand Down Expand Up @@ -238,7 +258,7 @@ private async Task Orphan(IHost instance, ILogger logger, CancellationToken canc
{
try
{
await instance?.StopAsync(cancellationToken);
await (instance?.StopAsync(cancellationToken) ?? Task.CompletedTask);
}
catch (Exception)
{
Expand Down
61 changes: 53 additions & 8 deletions src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Microsoft.Azure.WebJobs.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static System.Environment;
Expand All @@ -20,19 +19,22 @@ public class HostJsonFileConfigurationSource : IConfigurationSource
{
private readonly ILogger _logger;

public HostJsonFileConfigurationSource(ScriptApplicationHostOptions applicationHostOptions, ILoggerFactory loggerFactory)
public HostJsonFileConfigurationSource(ScriptApplicationHostOptions applicationHostOptions, IEnvironment environment, ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

HostOptions = applicationHostOptions;
Environment = environment;
_logger = loggerFactory.CreateLogger(LogCategories.Startup);
}

public ScriptApplicationHostOptions HostOptions { get; }

public IEnvironment Environment { get; }

public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new HostJsonFileConfigurationProvider(this, _logger);
Expand All @@ -42,8 +44,8 @@ private class HostJsonFileConfigurationProvider : ConfigurationProvider
{
private static readonly string[] WellKnownHostJsonProperties = new[]
{
"id", "functionTimeout", "functions", "http", "watchDirectories", "queues", "serviceBus",
"eventHub", "tracing", "singleton", "logger", "aggregator", "applicationInsights", "healthMonitor"
"version", "id", "functionTimeout", "functions", "http", "watchDirectories", "queues", "serviceBus",
"eventHub", "singleton", "logging", "aggregator", "healthMonitor"
};

private readonly HostJsonFileConfigurationSource _configurationSource;
Expand Down Expand Up @@ -133,6 +135,7 @@ private JObject LoadHostConfigurationFile()
string hostFilePath = Path.Combine(options.ScriptPath, ScriptConstants.HostMetadataFileName);
string readingFileMessage = string.Format(CultureInfo.InvariantCulture, "Reading host configuration file '{0}'", hostFilePath);
JObject hostConfigObject = LoadHostConfig(hostFilePath);
InitializeHostConfig(hostFilePath, hostConfigObject);
string sanitizedJson = SanitizeHostJson(hostConfigObject);
string readFileMessage = $"Host configuration file read:{NewLine}{sanitizedJson}";

Expand Down Expand Up @@ -190,7 +193,24 @@ private JObject LoadHostConfigurationFile()
return hostConfigObject;
}

internal static JObject LoadHostConfig(string configFilePath)
private void InitializeHostConfig(string hostJsonPath, JObject hostConfigObject)
{
// If the object is empty, initialize it to include the version and write the file.
if (!hostConfigObject.HasValues)
{
_logger.LogInformation($"Empty host configuration file found. Creating a default {ScriptConstants.HostMetadataFileName} file.");

hostConfigObject = GetDefaultHostConfigObject();
TryWriteHostJson(hostJsonPath, hostConfigObject);
}

if (hostConfigObject["version"]?.Value<string>() != "2.0")
{
throw new HostConfigurationException($"The {ScriptConstants.HostMetadataFileName} file is missing the required 'version' property. See https://aka.ms/functions-hostjson for steps to migrate the configuration file.");
}
}

internal JObject LoadHostConfig(string configFilePath)
{
JObject hostConfigObject;
try
Expand All @@ -205,14 +225,39 @@ internal static JObject LoadHostConfig(string configFilePath)
catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException)
{
// if no file exists we default the config
// TODO: DI (FACAVAL) Log
//logger.LogInformation("No host configuration file found. Using default.");
hostConfigObject = new JObject();
_logger.LogInformation($"No host configuration file found. Creating a default {ScriptConstants.HostMetadataFileName} file.");

hostConfigObject = GetDefaultHostConfigObject();
TryWriteHostJson(configFilePath, hostConfigObject);
}

return hostConfigObject;
}

private static JObject GetDefaultHostConfigObject()
{
return new JObject { { "version", "2.0" } };
}

private void TryWriteHostJson(string filePath, JObject content)
{
if (!_configurationSource.Environment.FileSystemIsReadOnly())
{
try
{
File.WriteAllText(filePath, content.ToString(Formatting.Indented));
}
catch
{
_logger.LogInformation($"Failed to create {ScriptConstants.HostMetadataFileName} file. Host execution will continue.");
}
}
else
{
_logger.LogInformation($"File system is read-only. Skipping {ScriptConstants.HostMetadataFileName} creation.");
}
}

internal static string SanitizeHostJson(JObject hostJsonObject)
{
JObject sanitizedObject = new JObject();
Expand Down
4 changes: 2 additions & 2 deletions src/WebJobs.Script/Description/FunctionDescriptorProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void VerifyResolvedBindings(FunctionMetadata functionMetadata, IEnumerabl
if (unresolvedBindings.Any())
{
string allUnresolvedBindings = string.Join(", ", unresolvedBindings);
throw new ScriptConfigurationException($"The binding type(s) '{allUnresolvedBindings}' are not registered. " +
throw new FunctionConfigurationException($"The binding type(s) '{allUnresolvedBindings}' are not registered. " +
$"Please ensure the type is correct and the binding extension is installed.");
}
}
Expand Down Expand Up @@ -135,7 +135,7 @@ protected virtual ParameterDescriptor CreateTriggerParameter(BindingMetadata tri
}
else
{
throw new ScriptConfigurationException($"The binding type '{triggerMetadata.Type}' is not registered. " +
throw new FunctionConfigurationException($"The binding type '{triggerMetadata.Type}' is not registered. " +
$"Please ensure the type is correct and the binding extension is installed.");
}

Expand Down
25 changes: 25 additions & 0 deletions src/WebJobs.Script/FunctionConfigurationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;

namespace Microsoft.Azure.WebJobs.Script
{
/// <summary>
/// An exception that indicates an issue with a function. These exceptions will be caught and
/// logged, but not cause a host to restart.
/// </summary>
[Serializable]
public class FunctionConfigurationException : Exception
{
public FunctionConfigurationException() { }

public FunctionConfigurationException(string message) : base(message) { }

public FunctionConfigurationException(string message, Exception inner) : base(message, inner) { }

protected FunctionConfigurationException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
}
8 changes: 4 additions & 4 deletions src/WebJobs.Script/Host/FunctionMetadataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ internal static bool TryParseFunctionMetadata(string functionName, JObject funct
{
functionMetadata.ScriptFile = DeterminePrimaryScriptFile(functionConfig, scriptDirectory, fileSystem);
}
catch (ScriptConfigurationException exc)
catch (FunctionConfigurationException exc)
{
error = exc.Message;
return false;
Expand Down Expand Up @@ -216,7 +216,7 @@ internal static string DeterminePrimaryScriptFile(JObject functionConfig, string
string scriptPath = fileSystem.Path.Combine(scriptDirectory, scriptFile);
if (!fileSystem.File.Exists(scriptPath))
{
throw new ScriptConfigurationException("Invalid script file name configuration. The 'scriptFile' property is set to a file that does not exist.");
throw new FunctionConfigurationException("Invalid script file name configuration. The 'scriptFile' property is set to a file that does not exist.");
}

functionPrimary = scriptPath;
Expand All @@ -229,7 +229,7 @@ internal static string DeterminePrimaryScriptFile(JObject functionConfig, string

if (functionFiles.Length == 0)
{
throw new ScriptConfigurationException("No function script files present.");
throw new FunctionConfigurationException("No function script files present.");
}

if (functionFiles.Length == 1)
Expand All @@ -249,7 +249,7 @@ internal static string DeterminePrimaryScriptFile(JObject functionConfig, string

if (string.IsNullOrEmpty(functionPrimary))
{
throw new ScriptConfigurationException("Unable to determine the primary function script. Try renaming your entry point script to 'run' (or 'index' in the case of Node), " +
throw new FunctionConfigurationException("Unable to determine the primary function script. Try renaming your entry point script to 'run' (or 'index' in the case of Node), " +
"or alternatively you can specify the name of the entry point script explicitly by adding a 'scriptFile' property to your function metadata.");
}

Expand Down
25 changes: 25 additions & 0 deletions src/WebJobs.Script/HostConfigurationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;

namespace Microsoft.Azure.WebJobs.Script
{
/// <summary>
/// An exception that indicates an issue configuring a ScriptHost. This will
/// prevent the host from starting.
/// </summary>
[Serializable]
public class HostConfigurationException : Exception
{
public HostConfigurationException() { }

public HostConfigurationException(string message) : base(message) { }

public HostConfigurationException(string message, Exception inner) : base(message, inner) { }

protected HostConfigurationException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
}
23 changes: 0 additions & 23 deletions src/WebJobs.Script/ScriptConfigurationException.cs

This file was deleted.

2 changes: 2 additions & 0 deletions src/WebJobs.Script/ScriptConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public static class ScriptConstants
public const string LogCategoryWorker = "Worker";
public const string LogCategoryMigration = "Host.Migration";

public const string SkipHostJsonConfigurationKey = "MS_SkipHostJsonConfiguration";

// Define all system parameters we inject with a prefix to avoid collisions
// with user parameters
public const string SystemTriggerParameterName = "_triggerValue";
Expand Down
Loading

0 comments on commit d500b50

Please sign in to comment.