Implement minimum extension version enforcement
MODIFY
src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs
src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
+using Microsoft.Azure.WebJobs.Script.ExtensionRequirements;
using Microsoft.Azure.WebJobs.Script.Models;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -31,6 +33,7 @@ public class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
private readonly IMetricsLogger _metricsLogger;
private readonly Lazy<IEnumerable<Type>> _startupTypes;
+ private static readonly ExtensionRequirementsInfo _extensionRequirements = DependencyHelper.GetExtensionRequirements();
private static string[] _builtinExtensionAssemblies = GetBuiltinExtensionAssemblies();
public ScriptStartupTypeLocator(string rootScriptPath, ILogger<ScriptStartupTypeLocator> logger, IExtensionBundleManager extensionBundleManager,
@@ -72,6 +75,9 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
if (bundleConfigured)
{
+ ExtensionBundleDetails bundleDetails = await _extensionBundleManager.GetExtensionBundleDetails();
+ ValidateBundleRequirements(bundleDetails);
+
bindingsSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Generate a Hashset of all the binding types used in the function app
@@ -199,6 +205,8 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
}
}
+ ValidateExtensionRequirements(startupTypes);
+
return startupTypes;
}
@@ -233,6 +241,88 @@ private ExtensionReference[] ParseExtensions(string metadataFilePath)
}
}
+ private void ValidateBundleRequirements(ExtensionBundleDetails bundleDetails)
+ {
+ if (_extensionRequirements.BundleRequirementsByBundleId.TryGetValue(bundleDetails.Id, out BundleRequirement requirement))
+ {
+ var bundleVersion = new Version(bundleDetails.Version);
+ var minimumVersion = new Version(requirement.MinimumVersion);
+
+ if (bundleVersion < minimumVersion)
+ {
+ _logger.MinimumBundleVersionNotSatisfied(bundleDetails.Id, bundleDetails.Version, requirement.MinimumVersion);
+ throw new HostInitializationException($"Referenced bundle {bundleDetails.Id} of version {bundleDetails.Version} does not meet the required minimum version of {requirement.MinimumVersion}. Update your extension bundle reference in host.json to reference {requirement.MinimumVersion} or later. For more information see https://aka.ms/func-min-bundle-versions.");
+ }
+ }
+ }
+
+ private void ValidateExtensionRequirements(List<Type> startupTypes)
+ {
+ var errors = new List<string>();
+
+ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTypeRequirement requirement)
+ {
+ _logger.MinimumExtensionVersionNotSatisfied(extensionType.Name, extensionType.Assembly.FullName, minimumVersion, requirement.PackageName, requirement.MinimumPackageVersion);
+ string requirementNotMetError = $"ExtensionStartupType {extensionType.Name} from assembly '{extensionType.Assembly.FullName}' does not meet the required minimum version of {minimumVersion}. Update your NuGet package reference for {requirement.PackageName} to {requirement.MinimumPackageVersion} or later.";
+ errors.Add(requirementNotMetError);
+ }
+
+ foreach (var extensionType in startupTypes)
+ {
+ if (_extensionRequirements.ExtensionRequirementsByStartupType.TryGetValue(extensionType.Name, out ExtensionStartupTypeRequirement requirement))
+ {
+ Version minimumAssemblyVersion = new Version(requirement.MinimumAssemblyVersion);
+
+ AssemblyName assemblyName = extensionType.Assembly.GetName();
+ Version extensionAssemblyVersion = assemblyName.Version;
+ string extensionAssemblySimpleName = assemblyName.Name;
+
+ if (extensionAssemblySimpleName != requirement.AssemblyName)
+ {
+ // We recognized this type, but it did not come from the assembly that we expect, skipping validation
+ continue;
+ }
+
+ if (requirement.MinimumAssemblyFileVersion != null && !string.IsNullOrEmpty(extensionType.Assembly.Location))
+ {
+ Version minimumAssemblyFileVersion = new Version(requirement.MinimumAssemblyFileVersion);
+
+ try
+ {
+ Version extensionAssemblyFileVersion = new Version(System.Diagnostics.FileVersionInfo.GetVersionInfo(extensionType.Assembly.Location).FileVersion);
+ if (extensionAssemblyFileVersion < minimumAssemblyFileVersion)
+ {
+ CollectError(extensionType, minimumAssemblyFileVersion, requirement);
+ continue;
+ }
+ }
+ catch (FormatException)
+ {
+ // We failed to parse the assembly file version as a Version. We can't do a version check - give up
+ continue;
+ }
+ }
+
+ if (extensionAssemblyVersion < minimumAssemblyVersion)
+ {
+ CollectError(extensionType, minimumAssemblyVersion, requirement);
+ }
+ }
+ }
+
+ if (errors.Count > 0)
+ {
+ var builder = new System.Text.StringBuilder();
+ builder.AppendLine("One or more loaded extensions do not meet the minimum requirements. For more information see https://aka.ms/func-min-extension-versions.");
+ foreach (string e in errors)
+ {
+ builder.AppendLine(e);
+ }
+
+ throw new HostInitializationException(builder.ToString());
+ }
+ }
+
private class TypeNameEqualityComparer : IEqualityComparer<Type>
{
public bool Equals(Type x, Type y)

MODIFY
src/WebJobs.Script/Description/DotNet/DependencyHelper.cs
src/WebJobs.Script/Description/DotNet/DependencyHelper.cs
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using Microsoft.Azure.WebJobs.Script.ExtensionRequirements;
using Microsoft.Extensions.DependencyModel;
using Newtonsoft.Json.Linq;
using static Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
@@ -90,6 +91,20 @@ private static string GetResourceFileContents(string fileName)
.ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase);
}
+ internal static ExtensionRequirementsInfo GetExtensionRequirements()
+ {
+ string requirementsJson = GetResourceFileContents("extensionrequirements.json");
+ JObject requirements = JObject.Parse(requirementsJson);
+
+ var bundleRequirements = requirements["bundles"]
+ .ToObject<BundleRequirement[]>();
+
+ var extensionRequirements = requirements["types"]
+ .ToObject<ExtensionStartupTypeRequirement[]>();
+
+ return new ExtensionRequirementsInfo(bundleRequirements, extensionRequirements);
+ }
+
/// <summary>
/// Gets the default runtime fallback RIDs for a given RID.
/// The graph used to build the fallback list is static and

MODIFY
src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs
src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs
@@ -197,6 +197,18 @@ internal static class LoggerExtension
new EventId(333, nameof(IncorrectAutorestGeneratedJsonFile)),
"autorest_generated.json file found is incorrect (https://aka.ms/stencil) | exception:\n{contents}");
+ private static readonly Action<ILogger, string, string, Version, string, string, Exception> _minimumExtensionVersionNotSatisfied =
+ LoggerMessage.Define<string, string, Version, string, string>(
+ LogLevel.Error,
+ new EventId(334, nameof(MinimumExtensionVersionNotSatisfied)),
+ "ExtensionStartupType {extensionTypeName} from assembly '{extensionTypeAssemblyFullName}' does not meet the required minimum version of {minimumAssemblyVersion}. Update your NuGet package reference for {requirementPackageName} to {requirementMinimumPackageVersion} or later.");
+
+ private static readonly Action<ILogger, string, string, string, string, Exception> _minimumBundleVersionNotSatisfied =
+ LoggerMessage.Define<string, string, string, string>(
+ LogLevel.Error,
+ new EventId(335, nameof(MinimumBundleVersionNotSatisfied)),
+ "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.");
+
public static void ExtensionsManagerRestoring(this ILogger logger)
{
_extensionsManagerRestoring(logger, null);
@@ -358,5 +370,15 @@ public static void IncorrectAutorestGeneratedJsonFile(this ILogger logger, strin
{
_incorrectAutorestGeneratedJSONFile(logger, contents, null);
}
+
+ public static void MinimumExtensionVersionNotSatisfied(this ILogger logger, string extensionTypeName, string extensionTypeAssemblyFullName, Version minimumAssemblyVersion, string requirementPackageName, string requirementMinimumPackageVersion)
+ {
+ _minimumExtensionVersionNotSatisfied(logger, extensionTypeName, extensionTypeAssemblyFullName, minimumAssemblyVersion, requirementPackageName, requirementMinimumPackageVersion, null);
+ }
+
+ public static void MinimumBundleVersionNotSatisfied(this ILogger logger, string bundleId, string bundleVersion, string minimumVersion)
+ {
+ _minimumBundleVersionNotSatisfied(logger, bundleId, bundleVersion, minimumVersion, minimumVersion, null);
+ }
}
}

MODIFY
src/WebJobs.Script/WebJobs.Script.csproj
src/WebJobs.Script/WebJobs.Script.csproj
<None Remove="runtimes.json" />
</ItemGroup>
<ItemGroup>
+ <EmbeddedResource Include="extensionrequirements.json">
+ <CopyToOutputDirectory>Never</CopyToOutputDirectory>
+ </EmbeddedResource>
<EmbeddedResource Include="FileProvisioning\PowerShell\profile.ps1" />
<EmbeddedResource Include="FileProvisioning\PowerShell\requirements.psd1" />
<EmbeddedResource Include="runtimeassemblies-relaxed.json">

MODIFY
test/CSharpPrecompiledTestProjects/ReferenceOlderRuntimeAssembly/ReferenceOlderRuntimeAssembly.csproj
test/CSharpPrecompiledTestProjects/ReferenceOlderRuntimeAssembly/ReferenceOlderRuntimeAssembly.csproj
<_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.3" />
+ <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
</ItemGroup>

ADD
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.DurableTask.dll
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.DurableTask.dll
Binary files /dev/null and b/test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.DurableTask.dll differ

ADD
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.Storage.dll
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.Storage.dll
Binary files /dev/null and b/test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.Extensions.Storage.dll differ

ADD
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.ServiceBus.dll
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.ServiceBus.dll
Binary files /dev/null and b/test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/Microsoft.Azure.WebJobs.ServiceBus.dll differ

ADD
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/bin/extensions.json
+{
+ "extensions":[
+ { "name": "AzureStorage", "typeName":"Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage, Version=3.0.10.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"},
+ { "name": "ServiceBus", "typeName":"Microsoft.Azure.WebJobs.ServiceBus.ServiceBusWebJobsStartup, Microsoft.Azure.WebJobs.ServiceBus, Version=4.2.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"},
+ { "name": "DurableTask", "typeName":"Microsoft.Azure.WebJobs.Extensions.DurableTask.DurableTaskWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.DurableTask, Version=2.0.0.0, Culture=neutral, PublicKeyToken=014045d636e89289"}
+ ]
+}
\ No newline at end of file

ADD
test/WebJobs.Script.Tests.Integration/TestScripts/OutOfDateExtension/host.json
+{
+ "version": "2.0"
+}
\ No newline at end of file

Implement minimum extension version enforcement