diff --git a/release_notes.md b/release_notes.md index 99bd2089df..7658be4911 100644 --- a/release_notes.md +++ b/release_notes.md @@ -2,4 +2,5 @@ \ No newline at end of file +--> +- Implement changes needed in the Host to decouple workers from the Host release (#11111) \ No newline at end of file diff --git a/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs b/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs index 0d5eb6d79e..398de5b0d3 100644 --- a/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs +++ b/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs @@ -29,7 +29,9 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies; using Microsoft.Azure.WebJobs.Script.WebHost.Standby; using Microsoft.Azure.WebJobs.Script.Workers.FunctionDataCache; +using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -217,6 +219,7 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi services.ConfigureOptions(); services.AddSingleton, ScriptApplicationHostOptionsChangeTokenSource>(); services.ConfigureOptions(); + services.ConfigureOptions(); services.ConfigureOptions(); services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptionsWithChangeTokenSource>(); @@ -228,9 +231,22 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi services.AddHostingConfigOptions(configuration); services.ConfigureOptions(); - // Refresh LanguageWorkerOptions when HostBuiltChangeTokenSource is triggered. + // Refresh WorkerConfigurationResolverOptions and LanguageWorkerOptions when HostBuiltChangeTokenSource is triggered. + services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptionsWithChangeTokenSource>(); + services.AddSingleton(p => + { + var environment = p.GetService(); + var workerConfigurationResolverOptions = p.GetService>(); + var workerProfileManager = p.GetService(); + var loggerFactory = p.GetService(); + + return Utility.IsDynamicWorkerResolutionEnabled(environment, workerConfigurationResolverOptions) ? + new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, workerProfileManager, workerConfigurationResolverOptions) : + new DefaultWorkerConfigurationResolver(loggerFactory, workerConfigurationResolverOptions); + }); + services.TryAddSingleton(); services.TryAddSingleton(s => DefaultMiddlewarePipeline.Empty); diff --git a/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs b/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs index 8fb10c55db..71ef8ee2a7 100644 --- a/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs +++ b/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs @@ -30,6 +30,7 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -66,6 +67,7 @@ public class WebJobsScriptHostService : IHostedService, IScriptHostManager, ISer private readonly bool _originalStandbyModeValue; private readonly string _originalFunctionsWorkerRuntime; private readonly string _originalFunctionsWorkerRuntimeVersion; + private readonly IOptionsChangeTokenSource _workerConfigResolverOptionsChangeTokenSource; private readonly IOptionsChangeTokenSource _languageWorkerOptionsChangeTokenSource; // we're only using this dictionary's keys so it acts as a "ConcurrentHashSet" @@ -89,7 +91,8 @@ public WebJobsScriptHostService(IOptionsMonitor ap HostPerformanceManager hostPerformanceManager, IOptions healthMonitorOptions, IMetricsLogger metricsLogger, IApplicationLifetime applicationLifetime, IConfiguration config, IScriptEventManager eventManager, IHostMetrics hostMetrics, IOptions hostingConfigOptions, - IOptionsChangeTokenSource languageWorkerOptionsChangeTokenSource) + IOptionsChangeTokenSource languageWorkerOptionsChangeTokenSource, + IOptionsChangeTokenSource workerConfigResolverOptionsChangeTokenSource) { ArgumentNullException.ThrowIfNull(loggerFactory); @@ -100,6 +103,7 @@ public WebJobsScriptHostService(IOptionsMonitor ap RegisterApplicationLifetimeEvents(); _metricsLogger = metricsLogger; + _workerConfigResolverOptionsChangeTokenSource = workerConfigResolverOptionsChangeTokenSource ?? throw new ArgumentNullException(nameof(workerConfigResolverOptionsChangeTokenSource)); _languageWorkerOptionsChangeTokenSource = languageWorkerOptionsChangeTokenSource ?? throw new ArgumentNullException(nameof(languageWorkerOptionsChangeTokenSource)); _applicationHostOptions = applicationHostOptions ?? throw new ArgumentNullException(nameof(applicationHostOptions)); _scriptWebHostEnvironment = scriptWebHostEnvironment ?? throw new ArgumentNullException(nameof(scriptWebHostEnvironment)); @@ -378,6 +382,11 @@ private async Task UnsynchronizedStartHostAsync(ScriptHostStartupOperation activ } } + if (_workerConfigResolverOptionsChangeTokenSource is HostBuiltChangeTokenSource { } hostBuiltChangeToken) + { + hostBuiltChangeToken.TriggerChange(); + } + if (_languageWorkerOptionsChangeTokenSource is HostBuiltChangeTokenSource { } hostBuiltChangeTokenSource) { hostBuiltChangeTokenSource.TriggerChange(); diff --git a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs index f2192c8b6b..818586517e 100644 --- a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs +++ b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs @@ -91,6 +91,17 @@ internal string WorkerIndexingDisabledApps } } + /// + /// Gets a string delimited by '|' that contains the names of the language workers available through probing paths outside of the Host. + /// + internal string WorkersAvailableForDynamicResolution + { + get + { + return GetFeature(RpcWorkerConstants.WorkersAvailableForDynamicResolution) ?? string.Empty; + } + } + /// /// Gets a value indicating whether Linux Log Backoff is disabled in the hosting config. /// diff --git a/src/WebJobs.Script/Environment/EnvironmentExtensions.cs b/src/WebJobs.Script/Environment/EnvironmentExtensions.cs index a1611530aa..5728a97727 100644 --- a/src/WebJobs.Script/Environment/EnvironmentExtensions.cs +++ b/src/WebJobs.Script/Environment/EnvironmentExtensions.cs @@ -278,6 +278,16 @@ public static bool IsWindowsElasticPremium(this IEnvironment environment) return string.Equals(value, ScriptConstants.ElasticPremiumSku, StringComparison.OrdinalIgnoreCase); } + /// + /// Gets a value indicating whether the application is running in a Windows App Service environment. + /// + /// The environment to verify. + /// if running in a Windows App Service app; otherwise, false. + public static bool IsHostedWindowsEnvironment(this IEnvironment environment) + { + return environment.IsWindowsAzureManagedHosting() || environment.IsWindowsConsumption() || environment.IsWindowsElasticPremium(); + } + public static bool IsDynamicSku(this IEnvironment environment) { return environment.IsConsumptionSku() || environment.IsWindowsElasticPremium(); @@ -687,6 +697,11 @@ public static bool IsInProc(this IEnvironment environment, string workerRuntime return string.IsNullOrEmpty(workerRuntime) || string.Equals(workerRuntime, RpcWorkerConstants.DotNetLanguageWorkerName, StringComparison.OrdinalIgnoreCase); } + public static string GetPlatformReleaseChannel(this IEnvironment environment) + { + return environment.GetEnvironmentVariable(AntaresPlatformReleaseChannel) ?? ScriptConstants.LatestPlatformChannelNameUpper; + } + public static bool IsApplicationInsightsAgentEnabled(this IEnvironment environment) { // cache the value of the environment variable diff --git a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs index cea779be92..80795be2a9 100644 --- a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs +++ b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs @@ -35,6 +35,7 @@ public static class EnvironmentSettingNames public const string AppInsightsAgent = "APPLICATIONINSIGHTS_ENABLE_AGENT"; public const string FunctionsExtensionVersion = "FUNCTIONS_EXTENSION_VERSION"; public const string FunctionWorkerRuntime = "FUNCTIONS_WORKER_RUNTIME"; + public const string WorkerProbingPaths = "WORKER_PROBING_PATHS"; public const string ContainerName = "CONTAINER_NAME"; public const string WebsitePodName = "WEBSITE_POD_NAME"; public const string LegionServiceHost = "LEGION_SERVICE_HOST"; diff --git a/src/WebJobs.Script/ScriptConstants.cs b/src/WebJobs.Script/ScriptConstants.cs index 857c5bd912..8223697aa8 100644 --- a/src/WebJobs.Script/ScriptConstants.cs +++ b/src/WebJobs.Script/ScriptConstants.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using NuGet.Versioning; @@ -63,6 +64,7 @@ public static class ScriptConstants public const string LogCategoryHost = "Host"; public const string LogCategoryFunction = "Function"; public const string LogCategoryWorker = "Worker"; + public const string LogCategoryWorkerConfig = "Host.LanguageWorkerConfig"; public const string SkipHostJsonConfigurationKey = "MS_SkipHostJsonConfiguration"; public const string SkipHostInitializationKey = "MS_SkipHostInitialization"; @@ -132,6 +134,7 @@ public static class ScriptConstants public const string FeatureFlagDisableWebHostLogForwarding = "DisableWebHostLogForwarding"; public const string FeatureFlagDisableMergedWebHostScriptHostConfiguration = "DisableMergedConfiguration"; public const string FeatureFlagEnableWorkerIndexing = "EnableWorkerIndexing"; + public const string FeatureFlagDisableDynamicWorkerResolution = "DisableDynamicWorkerResolution"; public const string FeatureFlagEnableDebugTracing = "EnableDebugTracing"; public const string FeatureFlagEnableProxies = "EnableProxies"; public const string FeatureFlagStrictHISModeEnabled = "StrictHISModeEnabled"; @@ -247,6 +250,7 @@ public static class ScriptConstants public static readonly long DefaultMaxRequestBodySize = 104857600; public static readonly ImmutableArray SystemLogCategoryPrefixes = ImmutableArray.Create("Microsoft.Azure.WebJobs.", "Function.", "Worker.", "Host."); + public static readonly HashSet HostCapabilities = new HashSet(StringComparer.OrdinalIgnoreCase); public static readonly string FunctionMetadataDirectTypeKey = "DirectType"; public static readonly string LiveLogsSessionAIKey = "#AzFuncLiveLogsSessionId"; diff --git a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs index 61ed8de519..f73d070afa 100644 --- a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs +++ b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs @@ -33,6 +33,7 @@ using Microsoft.Azure.WebJobs.Script.Workers.Http; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -338,7 +339,11 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp if (applicationHostOptions.HasParentScope) { - // Forward the host LanguageWorkerOptions to the Job Host. + // Forward the host WorkerConfigurationResolverOptions and LanguageWorkerOptions to the Job Host. + var workerResolverOptions = applicationHostOptions.RootServiceProvider.GetService>(); + services.AddSingleton(workerResolverOptions); + services.AddSingleton>(s => new OptionsWrapper(workerResolverOptions.CurrentValue)); + var languageWorkerOptions = applicationHostOptions.RootServiceProvider.GetService>(); services.AddSingleton(languageWorkerOptions); services.AddSingleton>(s => new OptionsWrapper(languageWorkerOptions.CurrentValue)); @@ -346,6 +351,7 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp } else { + services.ConfigureOptions(); services.ConfigureOptions(); AddCommonServices(services); } diff --git a/src/WebJobs.Script/Utility.cs b/src/WebJobs.Script/Utility.cs index 30c343535d..c69717dae3 100644 --- a/src/WebJobs.Script/Utility.cs +++ b/src/WebJobs.Script/Utility.cs @@ -23,6 +23,7 @@ using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions; using Microsoft.Azure.WebJobs.Script.Models; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -1065,6 +1066,28 @@ public static bool CanWorkerIndex(IEnumerable workerConfigs, IE return workerIndexingEnabled && workerIndexingAvailable; } + // Users can disable dynamic worker resolution via setting the appropriate feature flag. + // Worker resolution can be enabled for specific workers at the stamp level via hosting config options. + // Feature flag takes precedence over hosting config options. + public static bool IsDynamicWorkerResolutionEnabled(IEnvironment environment, IOptionsMonitor options) + { + if (FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagDisableDynamicWorkerResolution, environment) || options.CurrentValue.WorkersAvailableForResolution is null) + { + return false; + } + + string workerRuntime = options.CurrentValue.WorkerRuntime; + + if (!options.CurrentValue.IsMultiLanguageWorkerEnvironment && + !string.IsNullOrWhiteSpace(workerRuntime) && + !options.CurrentValue.IsPlaceholderModeEnabled) + { + return options.CurrentValue.WorkersAvailableForResolution.Contains(workerRuntime); + } + + return options.CurrentValue.WorkersAvailableForResolution.Any(); + } + public static void LogAutorestGeneratedJsonIfExists(string rootScriptPath, ILogger logger) { string autorestGeneratedJsonPath = Path.Combine(rootScriptPath, ScriptConstants.AutorestGeenratedMetadataFileName); diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs new file mode 100644 index 0000000000..55dc64a7df --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + // This class resolves worker configurations by scanning the "workers" directory within the Host for worker config files. + internal sealed class DefaultWorkerConfigurationResolver : IWorkerConfigurationResolver + { + private readonly ILogger _logger; + private readonly IOptionsMonitor _workerConfigurationResolverOptions; + + public DefaultWorkerConfigurationResolver(ILoggerFactory loggerFactory, IOptionsMonitor workerConfigurationResolverOptions) + { + _logger = loggerFactory is not null ? loggerFactory.CreateLogger(ScriptConstants.LogCategoryWorkerConfig) : throw new ArgumentNullException(nameof(loggerFactory)); + _workerConfigurationResolverOptions = workerConfigurationResolverOptions ?? throw new ArgumentNullException(nameof(workerConfigurationResolverOptions)); + } + + public List GetWorkerConfigPaths() + { + // "workers" directory path within the Host + var workersDirPath = _workerConfigurationResolverOptions.CurrentValue.WorkersDirPath; + + _logger.LogDebug("Workers Directory set to: {workersDirPath}", workersDirPath); + + List workerConfigs = new(); + + foreach (var workerDir in Directory.EnumerateDirectories(workersDirPath)) + { + string workerConfigPath = Path.Combine(workerDir, RpcWorkerConstants.WorkerConfigFileName); + + if (File.Exists(workerConfigPath)) + { + workerConfigs.Add(workerDir); + } + } + + return workerConfigs; + } + } +} diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/DynamicWorkerConfigurationResolver.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/DynamicWorkerConfigurationResolver.cs new file mode 100644 index 0000000000..e0464b9238 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/DynamicWorkerConfigurationResolver.cs @@ -0,0 +1,323 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Workers.Profiles; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + // This class resolves worker configurations dynamically based on the current environment and configuration settings. + // It searches for worker configs in specified probing paths and the fallback path, and returns a list of worker configuration paths. + internal sealed class DynamicWorkerConfigurationResolver : IWorkerConfigurationResolver + { + private readonly ILogger _logger; + private readonly IWorkerProfileManager _profileManager; + private readonly IFileSystem _fileSystem; + private readonly HashSet _workersAvailableForResolutionViaHostingConfig; + private readonly List _workerProbingPaths; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; + private readonly IOptionsMonitor _workerConfigurationResolverOptions; + + public DynamicWorkerConfigurationResolver(ILoggerFactory loggerFactory, + IFileSystem fileSystem, + IWorkerProfileManager workerProfileManager, + IOptionsMonitor workerConfigResolverOptions) + { + _logger = loggerFactory is not null ? loggerFactory.CreateLogger(ScriptConstants.LogCategoryWorkerConfig) : throw new ArgumentNullException(nameof(loggerFactory)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _profileManager = workerProfileManager ?? throw new ArgumentNullException(nameof(workerProfileManager)); + _workerConfigurationResolverOptions = workerConfigResolverOptions ?? throw new ArgumentNullException(nameof(workerConfigResolverOptions)); + _workerProbingPaths = workerConfigResolverOptions.CurrentValue.ProbingPaths; + _workersAvailableForResolutionViaHostingConfig = workerConfigResolverOptions.CurrentValue.WorkersAvailableForResolution ?? new HashSet(StringComparer.OrdinalIgnoreCase); + } + + public List GetWorkerConfigPaths() + { + // Dictionary of { FUNCTIONS_WORKER_RUNTIME environment variable value : path of workerConfig } + // Example: outputDict = {"java": "path1", "node": "path2", "dotnet-isolated": "path3"} for multilanguage worker scenario + // Sample path: "///" + var outputDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var workerRuntime = _workerConfigurationResolverOptions.CurrentValue.WorkerRuntime; + + // Search for worker configs in probing paths + ResolveWorkerConfigsFromProbingPaths(workerRuntime, outputDict); + + if (!_workerConfigurationResolverOptions.CurrentValue.IsMultiLanguageWorkerEnvironment && + !_workerConfigurationResolverOptions.CurrentValue.IsPlaceholderModeEnabled && + workerRuntime is not null && + outputDict.ContainsKey(workerRuntime)) + { + return outputDict.Values.ToList(); + } + + // Search in fallback path if worker cannot be found in probing paths + ResolveWorkerConfigsFromWithinHost(workerRuntime, outputDict); + + return outputDict.Values.ToList(); + } + + private void ResolveWorkerConfigsFromProbingPaths(string workerRuntime, Dictionary outputDict) + { + _logger.LogDebug("Workers probing paths set to: {probingPaths}", _workerProbingPaths is null ? null : string.Join(", ", _workerProbingPaths)); + + if (_workerProbingPaths is null) + { + return; + } + + string releaseChannel = _workerConfigurationResolverOptions.CurrentValue.ReleaseChannel; + + // probing path directory structure is: /// + foreach (var probingPath in _workerProbingPaths) + { + if (string.IsNullOrWhiteSpace(probingPath)) + { + continue; + } + + if (!_fileSystem.Directory.Exists(probingPath)) + { + _logger.LogDebug("Worker probing path directory does not exist: {probingPath}", probingPath); + continue; + } + + foreach (var workerRuntimePath in _fileSystem.Directory.EnumerateDirectories(probingPath)) + { + string workerRuntimeDir = Path.GetFileName(workerRuntimePath); + + // If probing paths are malformed and have duplicate directories of the same language worker (eg. due to different casing) + if (outputDict.ContainsKey(workerRuntimeDir)) + { + continue; + } + + // Skip worker directories that don't match the current runtime or are not enabled via hosting config + // Do not load all worker directories after the specialization is done and if it is not a multi-language runtime environment + if (!_workersAvailableForResolutionViaHostingConfig.Contains(workerRuntimeDir) || + (!_workerConfigurationResolverOptions.CurrentValue.IsMultiLanguageWorkerEnvironment && + !_workerConfigurationResolverOptions.CurrentValue.IsPlaceholderModeEnabled && + ShouldSkipWorkerDirectory(workerRuntime, workerRuntimeDir))) + { + continue; + } + + ResolveWorkerConfigsFromVersionsDirs(workerRuntimePath, workerRuntimeDir, releaseChannel, outputDict); + } + } + } + + private void ResolveWorkerConfigsFromVersionsDirs(string languageWorkerPath, string languageWorkerFolder, string releaseChannel, Dictionary outputDict) + { + var workerVersionPaths = _fileSystem.Directory.EnumerateDirectories(languageWorkerPath); + + // Map of: (parsed worker version, worker path) + // Example: [ (1.0.0, "/java/1.0.0"), (2.0.0, "/java/2.0.0") ] + var versionPathMap = GetWorkerVersionsDescending(workerVersionPaths); + + int compatibleWorkerCount = 0; + + bool isStandardOrExtendedChannel = + !string.IsNullOrWhiteSpace(releaseChannel) && + (releaseChannel.Equals(ScriptConstants.StandardPlatformChannelNameUpper, StringComparison.OrdinalIgnoreCase) || + releaseChannel.Equals(ScriptConstants.ExtendedPlatformChannelNameUpper, StringComparison.OrdinalIgnoreCase)); + + foreach (var versionPair in versionPathMap) + { + string languageWorkerVersionPath = versionPair.Value; + + if (IsWorkerCompatibleWithHost(languageWorkerVersionPath)) + { + compatibleWorkerCount++; + outputDict[languageWorkerFolder] = languageWorkerVersionPath; + + if (string.IsNullOrEmpty(releaseChannel) || !isStandardOrExtendedChannel) + { + return; // latest version is the default + } + + if (compatibleWorkerCount > 1) + { + outputDict[languageWorkerFolder] = languageWorkerVersionPath; + return; + } + } + } + } + + private void ResolveWorkerConfigsFromWithinHost(string workerRuntime, Dictionary outputDict) + { + var fallbackPath = _workerConfigurationResolverOptions.CurrentValue.WorkersDirPath; + + _logger.LogDebug("Searching for worker configs in the fallback directory: {fallbackPath}", fallbackPath); + + if (!string.IsNullOrEmpty(fallbackPath) && _fileSystem.Directory.Exists(fallbackPath)) + { + foreach (var workerPath in _fileSystem.Directory.EnumerateDirectories(fallbackPath)) + { + string workerDir = Path.GetFileName(workerPath).ToLower(); + + if (outputDict.ContainsKey(workerDir) || + (!_workerConfigurationResolverOptions.CurrentValue.IsMultiLanguageWorkerEnvironment && + !_workerConfigurationResolverOptions.CurrentValue.IsPlaceholderModeEnabled && + ShouldSkipWorkerDirectory(workerRuntime, workerDir))) + { + continue; + } + + string workerConfigPath = Path.Combine(workerPath, RpcWorkerConstants.WorkerConfigFileName); + if (File.Exists(workerConfigPath)) + { + outputDict[workerDir] = workerPath; + } + + if (!_workerConfigurationResolverOptions.CurrentValue.IsMultiLanguageWorkerEnvironment && + !_workerConfigurationResolverOptions.CurrentValue.IsPlaceholderModeEnabled && + workerRuntime is not null && + outputDict.ContainsKey(workerRuntime)) + { + return; + } + } + } + } + + private SortedList GetWorkerVersionsDescending(IEnumerable workerVersionPaths) + { + // Map of: (parsed worker version, worker path) + // Example: [ (1.0.0, "/java/1.0.0"), (2.0.0, "/java/2.0.0") ] + var versionPathMap = new SortedList(new DescendingVersionComparer()); + + if (!workerVersionPaths.Any()) + { + return versionPathMap; + } + + foreach (var workerVersionPath in workerVersionPaths) + { + string versionDir = Path.GetFileName(workerVersionPath); + string formattedVersion = FormatVersion(versionDir); + + if (Version.TryParse(formattedVersion, out Version version)) + { + versionPathMap[version] = workerVersionPath; + } + else + { + _logger.LogTrace("Failed to parse worker version '{versionDir}' as a valid version.", versionDir); + } + } + + return versionPathMap; + } + + private bool IsWorkerCompatibleWithHost(string workerDir) + { + string workerConfigPath = Path.Combine(workerDir, RpcWorkerConstants.WorkerConfigFileName); + if (!File.Exists(workerConfigPath)) + { + return false; + } + + JsonElement workerConfig = WorkerConfigurationHelper.GetWorkerConfigJsonElement(workerConfigPath); + + if (workerConfig.ValueKind == JsonValueKind.Undefined) + { + return false; + } + + // static capability resolution + bool hostHasRequiredCapabilities = DoesHostHasRequiredCapabilities(workerConfig); + + if (!hostHasRequiredCapabilities) + { + return false; + } + + // profiles evaluation + RpcWorkerDescription workerDescription = WorkerConfigurationHelper.GetWorkerDescription( + workerConfig: workerConfig, + jsonSerializerOptions: _jsonSerializerOptions, + workerDir: workerDir, + profileManager: _profileManager, + languageWorkersSettings: _workerConfigurationResolverOptions.CurrentValue.LanguageWorkersSettings, + logger: _logger); + + if (workerDescription.IsDisabled == true) + { + return false; + } + + return true; + } + + /// + /// Extracts host requirements from the worker configuration JSON element. + /// + /// Worker config: { "hostRequirements": [ "test-capability1", "test-capability2" ] }. + /// HashSet { "test-capability1", "test-capability2" }. + private HashSet GetHostRequirementsFromWorker(JsonElement workerConfig) + { + HashSet hostRequirements = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (workerConfig.TryGetProperty(RpcWorkerConstants.HostRequirementsSectionName, out JsonElement configSection)) + { + var requirements = configSection.EnumerateArray(); + + foreach (var requirement in requirements) + { + hostRequirements.Add(requirement.GetString()); + } + } + + return hostRequirements; + } + + private bool DoesHostHasRequiredCapabilities(JsonElement workerConfig) + { + HashSet hostCapabilities = ScriptConstants.HostCapabilities; + HashSet hostRequirements = GetHostRequirementsFromWorker(workerConfig); + + foreach (var hostRequirement in hostRequirements) + { + if (!hostCapabilities.Contains(hostRequirement)) + { + return false; + } + } + + return true; + } + + internal static bool ShouldSkipWorkerDirectory(string workerRuntime, string workerDir) + { + return workerRuntime is not null && !workerRuntime.Equals(workerDir, StringComparison.OrdinalIgnoreCase); + } + + private string FormatVersion(string version) + { + if (!version.Contains('.')) + { + version = version + ".0"; // Handle versions like '1' as '1.0' + } + + return version; + } + + private class DescendingVersionComparer : IComparer + { + public int Compare(Version x, Version y) + { + return y.CompareTo(x); // Inverted comparison for descending order + } + } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs index 92bd078295..f39b8b95ec 100644 --- a/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -21,13 +22,17 @@ internal class LanguageWorkerOptionsSetup : IConfigureOptions _workerConfigResolverOptions; + private readonly IWorkerConfigurationResolver _workerConfigurationResolver; public LanguageWorkerOptionsSetup(IConfiguration configuration, ILoggerFactory loggerFactory, IEnvironment environment, IMetricsLogger metricsLogger, IWorkerProfileManager workerProfileManager, - IScriptHostManager scriptHostManager) + IScriptHostManager scriptHostManager, + IOptionsMonitor workerConfigResolverOptions, + IWorkerConfigurationResolver workerConfigurationResolver) { if (loggerFactory is null) { @@ -39,8 +44,10 @@ public LanguageWorkerOptionsSetup(IConfiguration configuration, _environment = environment ?? throw new ArgumentNullException(nameof(environment)); _metricsLogger = metricsLogger ?? throw new ArgumentNullException(nameof(metricsLogger)); _workerProfileManager = workerProfileManager ?? throw new ArgumentNullException(nameof(workerProfileManager)); + _workerConfigResolverOptions = workerConfigResolverOptions ?? throw new ArgumentNullException(nameof(workerConfigResolverOptions)); + _workerConfigurationResolver = workerConfigurationResolver ?? throw new ArgumentNullException(nameof(workerConfigurationResolver)); - _logger = loggerFactory.CreateLogger("Host.LanguageWorkerConfig"); + _logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryWorkerConfig); } public void Configure(LanguageWorkerOptions options) @@ -72,7 +79,7 @@ public void Configure(LanguageWorkerOptions options) } } - var configFactory = new RpcWorkerConfigFactory(configuration, _logger, SystemRuntimeInformation.Instance, _environment, _metricsLogger, _workerProfileManager); + var configFactory = new RpcWorkerConfigFactory(configuration, _logger, SystemRuntimeInformation.Instance, _environment, _metricsLogger, _workerProfileManager, _workerConfigurationResolver, _workerConfigResolverOptions); options.WorkerConfigs = configFactory.GetConfigs(); } } diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/RpcWorkerConfigFactory.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/RpcWorkerConfigFactory.cs index 3071b87b37..aa2b9a197e 100644 --- a/src/WebJobs.Script/Workers/Rpc/Configuration/RpcWorkerConfigFactory.cs +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/RpcWorkerConfigFactory.cs @@ -7,12 +7,13 @@ using System.IO; using System.Linq; using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc { @@ -26,6 +27,8 @@ internal class RpcWorkerConfigFactory private readonly IMetricsLogger _metricsLogger; private readonly string _workerRuntime; private readonly IEnvironment _environment; + private readonly IWorkerConfigurationResolver _workerConfigurationResolver; + private readonly IOptionsMonitor _workerConfigurationResolverOptions; private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true @@ -38,7 +41,9 @@ public RpcWorkerConfigFactory(IConfiguration config, ISystemRuntimeInformation systemRuntimeInfo, IEnvironment environment, IMetricsLogger metricsLogger, - IWorkerProfileManager workerProfileManager) + IWorkerProfileManager workerProfileManager, + IWorkerConfigurationResolver workerConfigurationResolver, + IOptionsMonitor workerConfigurationResolverOptions) { _config = config ?? throw new ArgumentNullException(nameof(config)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -47,14 +52,10 @@ public RpcWorkerConfigFactory(IConfiguration config, _metricsLogger = metricsLogger ?? throw new ArgumentNullException(nameof(metricsLogger)); _profileManager = workerProfileManager ?? throw new ArgumentNullException(nameof(workerProfileManager)); _workerRuntime = _environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName); + _workerConfigurationResolver = workerConfigurationResolver ?? throw new ArgumentNullException(nameof(workerConfigurationResolver)); + _workerConfigurationResolverOptions = workerConfigurationResolverOptions ?? throw new ArgumentNullException(nameof(workerConfigurationResolverOptions)); - WorkersDirPath = GetDefaultWorkersDirectory(Directory.Exists); - var workersDirectorySection = _config.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"); - - if (!string.IsNullOrEmpty(workersDirectorySection.Value)) - { - WorkersDirPath = workersDirectorySection.Value; - } + WorkersDirPath = WorkerConfigurationHelper.GetWorkersDirPath(config); } public string WorkersDirPath { get; } @@ -68,20 +69,6 @@ public IList GetConfigs() } } - internal static string GetDefaultWorkersDirectory(Func directoryExists) - { -#pragma warning disable SYSLIB0012 // Type or member is obsolete - string assemblyLocalPath = Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.CodeBase).LocalPath); -#pragma warning restore SYSLIB0012 // Type or member is obsolete - string workersDirPath = Path.Combine(assemblyLocalPath, RpcWorkerConstants.DefaultWorkersDirectoryName); - if (!directoryExists(workersDirPath)) - { - // Site Extension. Default to parent directory - workersDirPath = Path.Combine(Directory.GetParent(assemblyLocalPath).FullName, RpcWorkerConstants.DefaultWorkersDirectoryName); - } - return workersDirPath; - } - internal void BuildWorkerProviderDictionary() { AddProviders(); @@ -90,15 +77,11 @@ internal void BuildWorkerProviderDictionary() internal void AddProviders() { - _logger.LogDebug("Workers Directory set to: {WorkersDirPath}", WorkersDirPath); + List workerConfigs = _workerConfigurationResolver.GetWorkerConfigPaths(); - foreach (var workerDir in Directory.EnumerateDirectories(WorkersDirPath)) + foreach (var workerConfig in workerConfigs) { - string workerConfigPath = Path.Combine(workerDir, RpcWorkerConstants.WorkerConfigFileName); - if (File.Exists(workerConfigPath)) - { - AddProvider(workerDir); - } + AddProvider(workerConfig); } } @@ -144,30 +127,9 @@ internal void AddProvider(string workerDir) _logger.LogDebug("Found worker config: {workerConfigPath}", workerConfigPath); - var workerConfig = GetWorkerConfigJsonElement(workerConfigPath); - var workerDescriptionElement = workerConfig.GetProperty(WorkerConstants.WorkerDescription); - var workerDescription = workerDescriptionElement.Deserialize(_jsonSerializerOptions); - workerDescription.WorkerDirectory = workerDir; - - // Read the profiles from worker description and load the profile for which the conditions match - if (workerConfig.TryGetProperty(WorkerConstants.WorkerDescriptionProfiles, out var profiles)) - { - List workerDescriptionProfiles = ReadWorkerDescriptionProfiles(profiles); - if (workerDescriptionProfiles.Count > 0) - { - _profileManager.SetWorkerDescriptionProfiles(workerDescriptionProfiles, workerDescription.Language); - _profileManager.LoadWorkerDescriptionFromProfiles(workerDescription, out workerDescription); - } - } - - // Check if any app settings are provided for that language - var languageSection = _config.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}:{workerDescription.Language}"); - workerDescription.Arguments ??= new List(); - GetWorkerDescriptionFromAppSettings(workerDescription, languageSection); - AddArgumentsFromAppSettings(workerDescription, languageSection); + var workerConfig = WorkerConfigurationHelper.GetWorkerConfigJsonElement(workerConfigPath); - // Validate workerDescription - workerDescription.ApplyDefaultsAndValidate(Directory.GetCurrentDirectory(), _logger); + RpcWorkerDescription workerDescription = WorkerConfigurationHelper.GetWorkerDescription(workerConfig, _jsonSerializerOptions, workerDir, _profileManager, _workerConfigurationResolverOptions.CurrentValue.LanguageWorkersSettings, _logger); if (workerDescription.IsDisabled == true) { @@ -203,7 +165,7 @@ internal void AddProvider(string workerDir) _workerDescriptionDictionary[workerDescription.Language] = rpcWorkerConfig; ReadLanguageWorkerFile(arguments.WorkerPath); - _logger.LogDebug("Added WorkerConfig for language: {language}", workerDescription.Language); + _logger.LogDebug("Added WorkerConfig for language: {language} with worker path: {path}", workerDescription.Language, workerDescription.DefaultWorkerPath); } } catch (Exception ex) when (!ex.IsFatal()) @@ -213,61 +175,6 @@ internal void AddProvider(string workerDir) } } - private static JsonElement GetWorkerConfigJsonElement(string workerConfigPath) - { - ReadOnlySpan jsonSpan = File.ReadAllBytes(workerConfigPath); - - if (jsonSpan.StartsWith([0xEF, 0xBB, 0xBF])) - { - jsonSpan = jsonSpan[3..]; // Skip UTF-8 Byte Order Mark (BOM) if present at the beginning of the file. - } - - var reader = new Utf8JsonReader(jsonSpan, isFinalBlock: true, state: default); - using var doc = JsonDocument.ParseValue(ref reader); - - return doc.RootElement.Clone(); - } - - private List ReadWorkerDescriptionProfiles(JsonElement profilesElement) - { - var profiles = profilesElement.Deserialize>(_jsonSerializerOptions); - - if (profiles == null || profiles.Count <= 0) - { - return new List(0); - } - - var descriptionProfiles = new List(profiles.Count); - - try - { - foreach (var profile in profiles) - { - var profileConditions = new List(profile.Conditions.Count); - - foreach (var descriptor in profile.Conditions) - { - if (!_profileManager.TryCreateWorkerProfileCondition(descriptor, out IWorkerProfileCondition condition)) - { - // Failed to resolve condition. This profile will be disabled using a mock false condition - _logger.LogInformation("Profile {name} is disabled. Cannot resolve the profile condition {condition}", profile.ProfileName, descriptor.Type); - condition = new FalseCondition(); - } - - profileConditions.Add(condition); - } - - descriptionProfiles.Add(new(profile.ProfileName, profileConditions, profile.Description)); - } - } - catch (Exception) - { - throw new FormatException("Failed to parse profiles in worker config."); - } - - return descriptionProfiles; - } - internal WorkerProcessCountOptions GetWorkerProcessCount(JsonElement workerConfig) { WorkerProcessCountOptions workerProcessCount = null; @@ -310,24 +217,6 @@ internal WorkerProcessCountOptions GetWorkerProcessCount(JsonElement workerConfi return workerProcessCount; } - private static void GetWorkerDescriptionFromAppSettings(RpcWorkerDescription workerDescription, IConfigurationSection languageSection) - { - var defaultExecutablePathSetting = languageSection.GetSection($"{WorkerConstants.WorkerDescriptionDefaultExecutablePath}"); - workerDescription.DefaultExecutablePath = defaultExecutablePathSetting.Value != null ? defaultExecutablePathSetting.Value : workerDescription.DefaultExecutablePath; - - var defaultRuntimeVersionAppSetting = languageSection.GetSection($"{WorkerConstants.WorkerDescriptionDefaultRuntimeVersion}"); - workerDescription.DefaultRuntimeVersion = defaultRuntimeVersionAppSetting.Value != null ? defaultRuntimeVersionAppSetting.Value : workerDescription.DefaultRuntimeVersion; - } - - internal static void AddArgumentsFromAppSettings(RpcWorkerDescription workerDescription, IConfigurationSection languageSection) - { - var argumentsSection = languageSection.GetSection($"{WorkerConstants.WorkerDescriptionArguments}"); - if (argumentsSection.Value != null) - { - ((List)workerDescription.Arguments).AddRange(Regex.Split(argumentsSection.Value, @"\s+")); - } - } - internal bool ShouldAddWorkerConfig(string workerDescriptionLanguage) { if (_environment.IsPlaceholderModeEnabled()) @@ -398,4 +287,4 @@ private void ReadLanguageWorkerFile(string workerPath) }); } } -} +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationHelper.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationHelper.cs new file mode 100644 index 0000000000..c2dfa623a3 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationHelper.cs @@ -0,0 +1,167 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.Azure.WebJobs.Script.Workers.Profiles; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + internal static class WorkerConfigurationHelper + { + internal static RpcWorkerDescription GetWorkerDescription( + JsonElement workerConfig, + JsonSerializerOptions jsonSerializerOptions, + string workerDir, + IWorkerProfileManager profileManager, + Dictionary languageWorkersSettings, + ILogger logger) + { + var workerDescriptionElement = workerConfig.GetProperty(WorkerConstants.WorkerDescription); + var workerDescription = workerDescriptionElement.Deserialize(jsonSerializerOptions); + workerDescription.WorkerDirectory = workerDir; + + // Read the profiles from worker description and load the profile for which the conditions match + if (workerConfig.TryGetProperty(WorkerConstants.WorkerDescriptionProfiles, out var profiles)) + { + List workerDescriptionProfiles = ReadWorkerDescriptionProfiles(profiles, jsonSerializerOptions, profileManager, logger); + if (workerDescriptionProfiles.Count > 0) + { + profileManager.SetWorkerDescriptionProfiles(workerDescriptionProfiles, workerDescription.Language); + profileManager.LoadWorkerDescriptionFromProfiles(workerDescription, out workerDescription); + } + } + + workerDescription.Arguments ??= new List(); + + if (languageWorkersSettings is not null) + { + // Check if any app settings are provided for that language + GetWorkerDescriptionFromAppSettings(workerDescription, languageWorkersSettings); + AddArgumentsFromAppSettings(workerDescription, languageWorkersSettings); + } + + // Validate workerDescription + workerDescription.ApplyDefaultsAndValidate(Directory.GetCurrentDirectory(), logger); + + return workerDescription; + } + + internal static JsonElement GetWorkerConfigJsonElement(string workerConfigPath) + { + ReadOnlySpan jsonSpan = File.ReadAllBytes(workerConfigPath); + + if (jsonSpan.StartsWith([0xEF, 0xBB, 0xBF])) + { + jsonSpan = jsonSpan[3..]; // Skip UTF-8 Byte Order Mark (BOM) if present at the beginning of the file. + } + + if (jsonSpan.IsEmpty) + { + return default; // Return default JsonElement if the file is empty. + } + + var reader = new Utf8JsonReader(jsonSpan, isFinalBlock: true, state: default); + using var doc = JsonDocument.ParseValue(ref reader); + + return doc.RootElement.Clone(); + } + + private static List ReadWorkerDescriptionProfiles(JsonElement profilesElement, + JsonSerializerOptions jsonSerializerOptions, + IWorkerProfileManager profileManager, + ILogger logger) + { + var profiles = profilesElement.Deserialize>(jsonSerializerOptions); + + if (profiles == null || profiles.Count <= 0) + { + return new List(0); + } + + var descriptionProfiles = new List(profiles.Count); + + try + { + foreach (var profile in profiles) + { + var profileConditions = new List(profile.Conditions.Count); + + foreach (var descriptor in profile.Conditions) + { + if (!profileManager.TryCreateWorkerProfileCondition(descriptor, out IWorkerProfileCondition condition)) + { + // Failed to resolve condition. This profile will be disabled using a mock false condition + logger.LogInformation("Profile {name} is disabled. Cannot resolve the profile condition {condition}", profile.ProfileName, descriptor.Type); + condition = new FalseCondition(); + } + + profileConditions.Add(condition); + } + + descriptionProfiles.Add(new(profile.ProfileName, profileConditions, profile.Description)); + } + } + catch (Exception) + { + throw new FormatException("Failed to parse profiles in worker config."); + } + + return descriptionProfiles; + } + + private static void GetWorkerDescriptionFromAppSettings(RpcWorkerDescription workerDescription, Dictionary languageWorkersSettings) + { + if (languageWorkersSettings.TryGetValue($"{RpcWorkerConstants.LanguageWorkersSectionName}:{workerDescription.Language}:{WorkerConstants.WorkerDescriptionDefaultExecutablePath}", out string defaultExecutablePathSetting)) + { + workerDescription.DefaultExecutablePath = defaultExecutablePathSetting; + } + + if (languageWorkersSettings.TryGetValue($"{RpcWorkerConstants.LanguageWorkersSectionName}:{workerDescription.Language}:{WorkerConstants.WorkerDescriptionDefaultRuntimeVersion}", out string defaultRuntimeVersionAppSetting)) + { + workerDescription.DefaultRuntimeVersion = defaultRuntimeVersionAppSetting; + } + } + + internal static void AddArgumentsFromAppSettings(RpcWorkerDescription workerDescription, Dictionary languageWorkersSettings) + { + if (languageWorkersSettings.TryGetValue($"{RpcWorkerConstants.LanguageWorkersSectionName}:{workerDescription.Language}:{WorkerConstants.WorkerDescriptionArguments}", out string argumentsValue)) + { + ((List)workerDescription.Arguments).AddRange(Regex.Split(argumentsValue, @"\s+")); + } + } + + internal static string GetDefaultWorkersDirectory(Func directoryExists) + { +#pragma warning disable SYSLIB0012 // Type or member is obsolete + string assemblyLocalPath = Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.CodeBase).LocalPath); +#pragma warning restore SYSLIB0012 // Type or member is obsolete + string workersDirPath = Path.Combine(assemblyLocalPath, RpcWorkerConstants.DefaultWorkersDirectoryName); + if (!directoryExists(workersDirPath)) + { + // Site Extension. Default to parent directory + workersDirPath = Path.Combine(Directory.GetParent(assemblyLocalPath).FullName, RpcWorkerConstants.DefaultWorkersDirectoryName); + } + return workersDirPath; + } + + internal static string GetWorkersDirPath(IConfiguration configuration) + { + var workersDirectorySection = configuration?.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"); + + string workersDirPath = GetDefaultWorkersDirectory(Directory.Exists); + + if (!string.IsNullOrEmpty(workersDirectorySection?.Value)) + { + workersDirPath = workersDirectorySection.Value; + } + + return workersDirPath; + } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs new file mode 100644 index 0000000000..31e34363e8 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + public class WorkerConfigurationResolverOptions + { + // Gets or sets the value of platform release channel. + public string ReleaseChannel { get; set; } + + // Gets or sets the value of worker runtime. + public string WorkerRuntime { get; set; } + + // Gets or sets a value indicating whether placeholder mode is enabled. + public bool IsPlaceholderModeEnabled { get; set; } + + // Gets or sets a value indicating whether it is a multi-language worker environment. + public bool IsMultiLanguageWorkerEnvironment { get; set; } + + // Gets or sets the workers directory path within the Host. + public string WorkersDirPath { get; set; } + + // Gets or sets the list of probing paths for worker resolution. + public List ProbingPaths { get; set; } + + // Gets or sets the worker runtimes available for resolution via Hosting configuration. + public HashSet WorkersAvailableForResolution { get; set; } + + // Gets or sets the dictionary containing language workers related settings in configuration. + public Dictionary LanguageWorkersSettings { get; set; } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs new file mode 100644 index 0000000000..0a74684e3a --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + internal sealed class WorkerConfigurationResolverOptionsSetup : IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly IEnvironment _environment; + private readonly IScriptHostManager _scriptHostManager; + private readonly IOptions _functionsHostingConfigOptions; + + public WorkerConfigurationResolverOptionsSetup(IConfiguration configuration, + IEnvironment environment, + IScriptHostManager scriptHostManager, + IOptions functionsHostingConfigOptions) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _scriptHostManager = scriptHostManager ?? throw new ArgumentNullException(nameof(scriptHostManager)); + _functionsHostingConfigOptions = functionsHostingConfigOptions ?? throw new ArgumentNullException(nameof(functionsHostingConfigOptions)); + } + + public void Configure(WorkerConfigurationResolverOptions options) + { + var configuration = _configuration; + if (_scriptHostManager is IServiceProvider scriptHostManagerServiceProvider) + { + var latestConfiguration = scriptHostManagerServiceProvider.GetService(); + if (latestConfiguration is not null) + { + configuration = new ConfigurationBuilder() + .AddConfiguration(_configuration) + .AddConfiguration(latestConfiguration) + .Build(); + } + } + + options.WorkerRuntime = _environment.GetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime); + options.ReleaseChannel = EnvironmentExtensions.GetPlatformReleaseChannel(_environment); + options.IsPlaceholderModeEnabled = _environment.IsPlaceholderModeEnabled(); + options.IsMultiLanguageWorkerEnvironment = _environment.IsMultiLanguageRuntimeEnvironment(); + options.WorkersDirPath = WorkerConfigurationHelper.GetWorkersDirPath(configuration); + options.ProbingPaths = GetWorkerProbingPaths(); + options.WorkersAvailableForResolution = GetWorkersAvailableForResolutionViaHostingConfig(_functionsHostingConfigOptions); + options.LanguageWorkersSettings = GetLanguageWorkersSettings(configuration); + } + + internal List GetWorkerProbingPaths() + { + // If Configuration section is set, read probing paths from configuration. + IConfigurationSection probingPathsSection = _configuration.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}")?.GetSection($"{RpcWorkerConstants.WorkerProbingPathsSectionName}"); + + var probingPathsList = probingPathsSection?.AsEnumerable(); + + List probingPaths = new List(); + + if (probingPathsList is not null) + { + for (int i = 0; i < probingPathsList.Count(); i++) + { + var path = probingPathsSection.GetSection($"{i}").Value; + if (!string.IsNullOrWhiteSpace(path)) + { + probingPaths.Add(path); + } + } + } + else + { + if (_environment.IsHostedWindowsEnvironment()) + { + // Harcoded site extensions path for Windows until Antares sets it as an Environment variable. + // Example probing path for Windows: "c:\\home\\SiteExtensions\\workers" + string windowsSiteExtensionsPath = GetWindowsSiteExtensionsPath(); + + if (!string.IsNullOrWhiteSpace(windowsSiteExtensionsPath)) + { + var windowsWorkerFullProbingPath = Path.Combine(windowsSiteExtensionsPath, RpcWorkerConstants.DefaultWorkersDirectoryName); + probingPaths.Add(windowsWorkerFullProbingPath); + } + } + } + + return probingPaths; + } + + internal static HashSet GetWorkersAvailableForResolutionViaHostingConfig(IOptions functionsHostingConfigOptions) => + (functionsHostingConfigOptions.Value?.WorkersAvailableForDynamicResolution ?? string.Empty) + .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + internal static string GetWindowsSiteExtensionsPath() + { + var assemblyPath = Assembly.GetExecutingAssembly().Location; + var assemblyDir = Path.GetDirectoryName(assemblyPath); + + //Move 2 directories up to get to the SiteExtensions directory + return Directory.GetParent(assemblyDir)?.Parent?.FullName; + } + + internal Dictionary GetLanguageWorkersSettings(IConfiguration configuration) + { + // Convert the required configuration sections to Dictionary + var languageWorkersSettings = new Dictionary(); + + foreach (var kvp in configuration.AsEnumerable()) + { + if (kvp.Key.StartsWith(RpcWorkerConstants.LanguageWorkersSectionName)) + { + languageWorkersSettings[kvp.Key] = kvp.Value; + } + } + + return languageWorkersSettings; + } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs b/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs new file mode 100644 index 0000000000..a11b384d94 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + /// + /// Interface to resolve the Worker Configs. + /// + internal interface IWorkerConfigurationResolver + { + List GetWorkerConfigPaths(); + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs index b7a41b980b..3976c7f54c 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs @@ -26,6 +26,8 @@ public static class RpcWorkerConstants // Section names in host.json or AppSettings public const string LanguageWorkersSectionName = "languageWorkers"; + public const string WorkerProbingPathsSectionName = "probingPaths"; + public const string HostRequirementsSectionName = "hostRequirements"; // Worker description constants public const string WorkerDescriptionLanguage = "language"; @@ -90,6 +92,7 @@ public static class RpcWorkerConstants public const string WorkerIndexingEnabled = "WORKER_INDEXING_ENABLED"; public const string WorkerIndexingDisabledApps = "WORKER_INDEXING_DISABLED_APPS"; + public const string WorkersAvailableForDynamicResolution = "WORKERS_AVAILABLE_FOR_DYNAMIC_RESOLUTION"; public const string RevertWorkerShutdownBehavior = "REVERT_WORKER_SHUTDOWN_BEHAVIOR"; public const string ShutdownWebhostWorkerChannelsOnHostShutdown = "ShutdownWebhostWorkerChannelsOnHostShutdown"; public const string ThrowOnMissingFunctionsWorkerRuntime = "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME"; diff --git a/test/TestWorkers/ProbingPaths/workers/dotnet-isolated/1.0.0/worker.config.json b/test/TestWorkers/ProbingPaths/workers/dotnet-isolated/1.0.0/worker.config.json new file mode 100644 index 0000000000..aef262654c --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/dotnet-isolated/1.0.0/worker.config.json @@ -0,0 +1,8 @@ +{ + "description": { + "language": "dotnet-isolated", + "extensions": [ ".dll" ], + "workerIndexing": "true", + "defaultExecutablePath": "worker.config.json" + } +} \ No newline at end of file diff --git a/test/TestWorkers/ProbingPaths/workers/java/2.18.0/worker.config.json b/test/TestWorkers/ProbingPaths/workers/java/2.18.0/worker.config.json new file mode 100644 index 0000000000..f38fd2dd93 --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/java/2.18.0/worker.config.json @@ -0,0 +1,10 @@ +{ + "description": { + "language": "java", + "extensions": [".jar"], + "defaultExecutablePath": "%JAVA_HOME%/bin/java", + "defaultWorkerPath": "worker.config.json", + "arguments": [] + }, + "profiles": [] +} \ No newline at end of file diff --git a/test/TestWorkers/ProbingPaths/workers/java/2.18.1/worker.config.json b/test/TestWorkers/ProbingPaths/workers/java/2.18.1/worker.config.json new file mode 100644 index 0000000000..13ddd8acf5 --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/java/2.18.1/worker.config.json @@ -0,0 +1,14 @@ +{ + "description": { + "language": "java", + "extensions": [".jar"], + "defaultExecutablePath": "%JAVA_HOME%/bin/java", + "defaultWorkerPath": "azure-functions-java-worker.jar", + "arguments": [] + }, + "hostRequirements": [ + "test-capability1", + "test-capability2" + ], + "profiles": [] +} \ No newline at end of file diff --git a/test/TestWorkers/ProbingPaths/workers/java/2.18.2/worker.config.json b/test/TestWorkers/ProbingPaths/workers/java/2.18.2/worker.config.json new file mode 100644 index 0000000000..a35215ccf2 --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/java/2.18.2/worker.config.json @@ -0,0 +1,12 @@ +{ + "description": { + "isDisabled": true, + "language": "java", + "extensions": [".jar"], + "defaultExecutablePath": "%JAVA_HOME%/bin/java", + "defaultWorkerPath": "azure-functions-java-worker.jar", + "arguments": [] + }, + "hostRequirements": [], + "profiles": [] +} \ No newline at end of file diff --git a/test/TestWorkers/ProbingPaths/workers/java/2.19.0/worker.config.json b/test/TestWorkers/ProbingPaths/workers/java/2.19.0/worker.config.json new file mode 100644 index 0000000000..c50bb3919b --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/java/2.19.0/worker.config.json @@ -0,0 +1,11 @@ +{ + "description": { + "language": "java", + "extensions": [".jar"], + "defaultExecutablePath": "%JAVA_HOME%/bin/java", + "defaultWorkerPath": "worker.config.json", + "arguments": [] + }, + "hostRequirements": [], + "profiles": [] +} \ No newline at end of file diff --git a/test/TestWorkers/ProbingPaths/workers/node/3.10.1/worker.config.json b/test/TestWorkers/ProbingPaths/workers/node/3.10.1/worker.config.json new file mode 100644 index 0000000000..564977d92c --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/node/3.10.1/worker.config.json @@ -0,0 +1,14 @@ +{ + "description": { + "language": "node", + "extensions": [".js", ".mjs", ".cjs"], + "defaultExecutablePath": "node", + "defaultWorkerPath": "worker.config.json", + "workerIndexing": "true" + }, + "hostRequirements": [], + "processOptions": { + "initializationTimeout": "00:02:00", + "environmentReloadTimeout": "00:02:00" + } +} diff --git a/test/TestWorkers/ProbingPaths/workers/powershell/7.4/worker.config.json b/test/TestWorkers/ProbingPaths/workers/powershell/7.4/worker.config.json new file mode 100644 index 0000000000..3734952966 --- /dev/null +++ b/test/TestWorkers/ProbingPaths/workers/powershell/7.4/worker.config.json @@ -0,0 +1,11 @@ +{ + "description":{ + "language":"powershell", + "extensions":[".ps1", ".psm1"], + "defaultExecutablePath":"dotnet", + "defaultWorkerPath":"%FUNCTIONS_WORKER_RUNTIME_VERSION%/worker.config.json", + "supportedRuntimeVersions":["7", "7.2", "7.4"], + "defaultRuntimeVersion": "7.4", + "sanitizeRuntimeVersionRegex":"\\d+\\.?\\d*" + } +} diff --git a/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs b/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs index 9dc13e2eec..fc0268db67 100644 --- a/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs +++ b/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs @@ -20,6 +20,7 @@ using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -83,6 +84,8 @@ public static IHostBuilder ConfigureDefaultTestWebScriptHost(this IHostBuilder b services.AddFunctionMetadataManager(); services.AddHostMetrics(); services.AddConfiguration(); + services.ConfigureOptions(); + services.AddSingleton(); services.ConfigureOptions(); configureRootServices?.Invoke(services); diff --git a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs index 0463807939..e6b3ef74f8 100644 --- a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs +++ b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs @@ -67,7 +67,9 @@ public static IEnumerable PropertyValues yield return [nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=True", true]; yield return [nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=1", true]; yield return [nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=0", false]; - + yield return [nameof(FunctionsHostingConfigOptions.WorkersAvailableForDynamicResolution), "WORKERS_AVAILABLE_FOR_DYNAMIC_RESOLUTION=java|node", "java|node"]; + yield return [nameof(FunctionsHostingConfigOptions.WorkersAvailableForDynamicResolution), "WORKERS_AVAILABLE_FOR_DYNAMIC_RESOLUTION=java", "java"]; + yield return [nameof(FunctionsHostingConfigOptions.WorkersAvailableForDynamicResolution), "WORKERS_AVAILABLE_FOR_DYNAMIC_RESOLUTION=java|dotnet-isolated|node", "java|dotnet-isolated|node"]; yield return [nameof(FunctionsHostingConfigOptions.IsTestDataSuppressionEnabled), "EnableTestDataSuppression=1", true]; #pragma warning restore SA1011 // Closing square brackets should be spaced correctly diff --git a/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs b/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs index d3f2dea6c8..39b61e099e 100644 --- a/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs +++ b/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs @@ -2,10 +2,19 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; using Moq; using Xunit; @@ -13,6 +22,9 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Configuration { public class LanguageWorkerOptionsSetupTests { + private readonly string _probingPath1 = Path.GetFullPath("..\\..\\..\\..\\test\\TestWorkers\\ProbingPaths\\workers\\"); + private readonly string _fallbackPath = Path.GetFullPath("workers"); + [Theory] [InlineData("DotNet")] [InlineData("dotnet")] @@ -20,6 +32,7 @@ public class LanguageWorkerOptionsSetupTests [InlineData("node")] public void LanguageWorkerOptions_Expected_ListOfConfigs(string workerRuntime) { + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); var testEnvironment = new TestEnvironment(); var testMetricLogger = new TestMetricsLogger(); var configurationBuilder = new ConfigurationBuilder() @@ -53,8 +66,12 @@ public void LanguageWorkerOptions_Expected_ListOfConfigs(string workerRuntime) } }); - LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, NullLoggerFactory.Instance, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object); - LanguageWorkerOptions options = new LanguageWorkerOptions(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, null); + + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + + var setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + var options = new LanguageWorkerOptions(); setup.Configure(options); @@ -71,5 +88,211 @@ public void LanguageWorkerOptions_Expected_ListOfConfigs(string workerRuntime) Assert.Equal(1, options.WorkerConfigs.Count); } } + + [Theory] + [InlineData("java", "java", "LATEST", "2.19.0")] + [InlineData("java", "java", "STANDARD", "2.18.0")] + [InlineData("node", "node", "LATEST", "3.10.1")] + [InlineData("node", "java|node", "STANDARD", "3.10.1")] + [InlineData("java", "java", "EXTENDED", "2.18.0")] + [InlineData("node", "java|node", "EXTENDED", "3.10.1")] + public void LanguageWorkerOptions_EnabledWorkerResolution_Expected_ListOfConfigs(string workerRuntime, string hostingOptionsSetting, string releaseChannel, string expectedVersion) + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var testMetricLogger = new TestMetricsLogger(); + var testProfileManager = new Mock(); + var testScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel, releaseChannel); + + var probingPaths = new List() { _probingPath1, string.Empty, "path-not-exists" }; + var configuration = WorkerConfigurationResolverTestsHelper.GetConfigurationWithProbingPaths(probingPaths); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, hostingOptionsSetting); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + var resolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, testProfileManager.Object, optionsMonitor); + + LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + LanguageWorkerOptions options = new LanguageWorkerOptions(); + + setup.Configure(options); + + Assert.Equal(1, options.WorkerConfigs.Count); + Assert.True(options.WorkerConfigs.First().Arguments.WorkerPath.Contains(expectedVersion)); + + var logs = loggerProvider.GetAllLogMessages(); + + string path = Path.Combine(_probingPath1, workerRuntime, expectedVersion); + string expectedLog = $"Added WorkerConfig for language: {workerRuntime} with worker path: {path}"; + Assert.True(logs.Any(l => l.FormattedMessage.Contains(expectedLog))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Workers probing paths set to:"))); + } + + [Theory] + [InlineData("java", "java", "LATEST")] + [InlineData("java", "java", "STANDARD")] + [InlineData("node", "node", "LATEST")] + [InlineData("node", "java|node", "STANDARD")] + public void LanguageWorkerOptions_FallbackPath_Expected_ListOfConfigs(string workerRuntime, string hostingOptionsSetting, string releaseChannel) + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var testMetricLogger = new TestMetricsLogger(); + var configuration = new ConfigurationBuilder().Add(new ScriptEnvironmentVariablesConfigurationSource()).Build(); + var testProfileManager = new Mock(); + var testScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel, releaseChannel); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, hostingOptionsSetting); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + var resolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, testProfileManager.Object, optionsMonitor); + + LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + LanguageWorkerOptions options = new LanguageWorkerOptions(); + + setup.Configure(options); + + Assert.Equal(1, options.WorkerConfigs.Count); + + var logs = loggerProvider.GetAllLogMessages(); + + string path = Path.Combine(_fallbackPath, workerRuntime); + string expectedLog = $"Added WorkerConfig for language: {workerRuntime} with worker path: {path}"; + Assert.True(logs.Any(l => l.FormattedMessage.Contains(expectedLog))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Workers probing paths set to:"))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Searching for worker configs in the fallback directory"))); + } + + [Theory] + [InlineData("java", null, "LATEST")] + [InlineData("java", "", "STANDARD")] + [InlineData("node", " ", "LATEST")] + public void LanguageWorkerOptions_NullHostingConfig_FeatureDisabled_ListOfConfigs(string workerRuntime, string hostingOptionsSetting, string releaseChannel) + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var testMetricLogger = new TestMetricsLogger(); + var configuration = new ConfigurationBuilder().Add(new ScriptEnvironmentVariablesConfigurationSource()).Build(); + var testProfileManager = new Mock(); + var testScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel, releaseChannel); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, hostingOptionsSetting); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + + LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + LanguageWorkerOptions options = new LanguageWorkerOptions(); + + setup.Configure(options); + + Assert.Equal(1, options.WorkerConfigs.Count); + + var logs = loggerProvider.GetAllLogMessages(); + + string path = Path.Combine(_fallbackPath, workerRuntime); + string expectedLog = $"Added WorkerConfig for language: {workerRuntime} with worker path: {path}"; + Assert.True(logs.Any(l => l.FormattedMessage.Contains(expectedLog))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Workers Directory set to:"))); + } + + [Theory] + [InlineData("java", "LATEST")] + [InlineData("java", "STANDARD")] + [InlineData("node", "LATEST")] + [InlineData("node", "STANDARD")] + public void LanguageWorkerOptions_DisabledWorkerResolution_Expected_ListOfConfigs(string workerRuntime, string releaseChannel) + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var testMetricLogger = new TestMetricsLogger(); + var configuration = new ConfigurationBuilder().Add(new ScriptEnvironmentVariablesConfigurationSource()).Build(); + var testProfileManager = new Mock(); + var testScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel, releaseChannel); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, null); + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + + LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + LanguageWorkerOptions options = new LanguageWorkerOptions(); + + setup.Configure(options); + + Assert.Equal(1, options.WorkerConfigs.Count); + + var logs = loggerProvider.GetAllLogMessages(); + + string path = Path.Combine(_fallbackPath, workerRuntime); + string expectedLog = $"Added WorkerConfig for language: {workerRuntime} with worker path: {path}"; + Assert.True(logs.Any(l => l.FormattedMessage.Contains(expectedLog))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Workers Directory set to:"))); + } + + [Theory] + [InlineData("java")] + [InlineData("node")] + public void LanguageWorkerOptions_FallbackPath_NullHostingConfig(string workerRuntime) + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var testMetricLogger = new TestMetricsLogger(); + var configuration = new ConfigurationBuilder().Add(new ScriptEnvironmentVariablesConfigurationSource()).Build(); + var testProfileManager = new Mock(); + var testScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, workerRuntime); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + var resolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, testProfileManager.Object, optionsMonitor); + + LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, optionsMonitor, resolver); + LanguageWorkerOptions options = new LanguageWorkerOptions(); + + setup.Configure(options); + + Assert.Equal(1, options.WorkerConfigs.Count); + + var logs = loggerProvider.GetAllLogMessages(); + + string path = Path.Combine(_fallbackPath, workerRuntime); + string expectedLog = $"Added WorkerConfig for language: {workerRuntime} with worker path: {path}"; + Assert.True(logs.Any(l => l.FormattedMessage.Contains(expectedLog))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Workers probing paths set to:"))); + Assert.True(logs.Any(l => l.FormattedMessage.Contains("Searching for worker configs in the fallback directory"))); + } } -} +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/UtilityTests.cs b/test/WebJobs.Script.Tests/UtilityTests.cs index 4a44afc296..849a2e4aab 100644 --- a/test/WebJobs.Script.Tests/UtilityTests.cs +++ b/test/WebJobs.Script.Tests/UtilityTests.cs @@ -16,6 +16,7 @@ using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.Models; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.WebJobs.Script.Tests; @@ -1016,6 +1017,60 @@ public void WorkerIndexingDecisionLogic_NullWorkerIndexingProperty(bool workerIn Assert.Equal(expected, workerShouldIndex); } + [Theory] + [InlineData(null, "node", true)] + [InlineData(null, "java|node", true)] + [InlineData(null, "", false)] + [InlineData(null, "| ", false)] + [InlineData(null, null, false)] + [InlineData(ScriptConstants.FeatureFlagDisableDynamicWorkerResolution, "node", false)] + [InlineData(ScriptConstants.FeatureFlagDisableDynamicWorkerResolution, "java|node", false)] + [InlineData(ScriptConstants.FeatureFlagDisableDynamicWorkerResolution, "| ", false)] + + public void IsDynamicWorkerResolutionEnabled_HostingConfigAndFeatureFlags_WorksAsExpected(string featureFlagValue, string hostingConfigSetting, bool expected) + { + var mockConfiguration = new Mock(); + var mockScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, hostingConfigSetting); + + var testEnvironment = new TestEnvironment(); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, featureFlagValue); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(mockConfiguration.Object, testEnvironment, mockScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + bool result = Utility.IsDynamicWorkerResolutionEnabled(testEnvironment, optionsMonitor); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("node", "node", null, true)] + [InlineData("node", "java", null, false)] + [InlineData("java|node", null, null, true)] + [InlineData("node", "node", "workflowapp", true)] + [InlineData("java|node", null, "workflowapp", true)] + [InlineData("| ", null, "workflowapp", false)] + public void IsDynamicWorkerResolutionEnabled_WorkerRuntimeAndMultiLanguage_WorksAsExpected(string hostingConfigSetting, string workerRuntime, string multilanguageApp, bool expected) + { + var mockConfiguration = new Mock(); + var mockScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, hostingConfigSetting); + + var testEnvironment = new TestEnvironment(); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AppKind, multilanguageApp); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, workerRuntime); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(mockConfiguration.Object, testEnvironment, mockScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + bool result = Utility.IsDynamicWorkerResolutionEnabled(testEnvironment, optionsMonitor); + + Assert.Equal(expected, result); + } + [Theory] [InlineData("True", true, true)] [InlineData("False", false, true)] diff --git a/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs b/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs index 092beeabf5..f1bf71437d 100644 --- a/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs +++ b/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs @@ -16,6 +16,7 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Configuration; using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -40,6 +41,7 @@ public class WebJobsScriptHostServiceTests private Mock _mockScriptWebHostEnvironment; private Mock _mockEnvironment; private HostBuiltChangeTokenSource _hostBuiltChangeTokenSource = new(); + private HostBuiltChangeTokenSource _hostBuiltChangeTokenSourceResolverOptions = new(); private IConfiguration _mockConfig; private OptionsWrapper _healthMonitorOptions; private HostPerformanceManager _hostPerformanceManager; @@ -119,7 +121,8 @@ public async Task StartAsync_Succeeds() _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, mockEventManager.Object, _hostMetrics, _functionsHostingConfigOptions, - _hostBuiltChangeTokenSource); + _hostBuiltChangeTokenSource, + _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); @@ -151,7 +154,8 @@ public async Task HostInitialization_OnInitializationException_MaintainsErrorInf _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, - _hostBuiltChangeTokenSource); + _hostBuiltChangeTokenSource, + _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); Assert.True(AreRequiredMetricsGenerated(metricsLogger)); @@ -181,7 +185,7 @@ public async Task HostRestart_Specialization_Succeeds() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); Assert.True(AreRequiredMetricsGenerated(metricsLogger)); @@ -236,7 +240,7 @@ public async Task HostRestart_DuringInitializationWithError_Recovers() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); TestLoggerProvider hostALogger = hostA.Object.GetTestLoggerProvider(); TestLoggerProvider hostBLogger = hostB.Object.GetTestLoggerProvider(); @@ -312,7 +316,7 @@ public async Task HostRestart_DuringInitialization_Cancels() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); TestLoggerProvider hostALogger = hostA.Object.GetTestLoggerProvider(); @@ -382,7 +386,7 @@ public async Task DisposedHost_ServicesNotExposed() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); Task startTask = _hostService.StartAsync(CancellationToken.None); @@ -433,7 +437,7 @@ public async Task DisposesScriptHost() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); var hostLogger = host.Object.GetTestLoggerProvider(); @@ -471,7 +475,7 @@ public async Task HostRestart_BeforeStart_WaitsForStartToContinue() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, - new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); // Simulate a call to specialize coming from the PlaceholderSpecializationMiddleware. This // can happen before we ever start the service, which could create invalid state. @@ -527,7 +531,7 @@ public void ShouldEnforceSequentialRestart_WithCorrectConfig(string value, bool _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, config, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); Assert.Equal(expectedResult, _hostService.ShouldEnforceSequentialRestart()); } @@ -565,7 +569,7 @@ public async Task DependencyTrackingTelemetryModule_Race() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, new TestMetricsLogger(), new Mock().Object, _mockConfig, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource)) + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions)) { await _hostService.StartAsync(CancellationToken.None); diff --git a/test/WebJobs.Script.Tests/WorkerConfigurationResolverTestsHelper.cs b/test/WebJobs.Script.Tests/WorkerConfigurationResolverTestsHelper.cs new file mode 100644 index 0000000000..8e9a6f77c8 --- /dev/null +++ b/test/WebJobs.Script.Tests/WorkerConfigurationResolverTestsHelper.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; + +namespace Microsoft.Azure.WebJobs.Script.Tests +{ + internal static class WorkerConfigurationResolverTestsHelper + { + internal static IOptionsMonitor GetTestWorkerConfigurationResolverOptions(IConfiguration configuration, + IEnvironment environment, + IScriptHostManager scriptHostManager, + IOptions functionsHostingConfigOptions = null) + { + if (functionsHostingConfigOptions is null) + { + var hostingOptions = new FunctionsHostingConfigOptions(); + functionsHostingConfigOptions = new OptionsWrapper(new FunctionsHostingConfigOptions()); + } + + var resolverOptionssetup = new WorkerConfigurationResolverOptionsSetup(configuration, environment, scriptHostManager, functionsHostingConfigOptions); + var resolverOptions = new WorkerConfigurationResolverOptions(); + resolverOptionssetup.Configure(resolverOptions); + + var factory = new TestOptionsFactory(resolverOptions); + var source = new TestChangeTokenSource(); + var changeTokens = new[] { source }; + var optionsMonitor = new OptionsMonitor(factory, changeTokens, factory); + + return optionsMonitor; + } + + internal static IConfiguration GetConfigurationWithProbingPaths(List probingPaths) + { + var jsonObj = new + { + languageWorkers = new + { + probingPaths + } + }; + + var jsonString = JsonSerializer.Serialize(jsonObj, new JsonSerializerOptions { WriteIndented = true }); + var jsonStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)); + + var configurationBuilder = new ConfigurationBuilder() + .Add(new ScriptEnvironmentVariablesConfigurationSource()) + .AddJsonStream(jsonStream); + + return configurationBuilder.Build(); + } + + internal static LoggerFactory GetTestLoggerFactory() + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + return loggerFactory; + } + } +} diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs index e2df3f80df..aedaa019ed 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs @@ -10,7 +10,12 @@ using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; +using Moq; using Xunit; namespace Microsoft.Azure.WebJobs.Script.Tests.Workers.Rpc @@ -39,7 +44,13 @@ public void DefaultLanguageWorkersDir() var expectedWorkersDir = Path.Combine(Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.Location).LocalPath), RpcWorkerConstants.DefaultWorkersDirectoryName); var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); } @@ -57,8 +68,8 @@ public void GetDefaultWorkersDirectory_Returns_Expected() var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - Assert.Equal(expectedWorkersDirIsCurrentDir, RpcWorkerConfigFactory.GetDefaultWorkersDirectory(Directory.Exists)); - Assert.Equal(expectedWorkersDirIsParentDir, RpcWorkerConfigFactory.GetDefaultWorkersDirectory(testDirectoryExists)); + Assert.Equal(expectedWorkersDirIsCurrentDir, WorkerConfigurationHelper.GetDefaultWorkersDirectory(Directory.Exists)); + Assert.Equal(expectedWorkersDirIsParentDir, WorkerConfigurationHelper.GetDefaultWorkersDirectory(testDirectoryExists)); } [Fact] @@ -72,7 +83,13 @@ public void LanguageWorker_WorkersDir_Set() }) .Build(); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var testScriptHostManager = new Mock(); + var mockLogger = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); } @@ -88,7 +105,13 @@ public void LanguageWorker_WorkersDir_NotSet() var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); } @@ -112,7 +135,13 @@ public void WorkerDescription_Skipped_When_Profile_Disables_Worker() var scriptSettingsManager = new ScriptSettingsManager(config); var testLogger = new TestLogger("test"); _testEnvironment.SetEnvironmentVariable("ENV_VAR_BAR", "True"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); var workerConfigs = configFactory.GetConfigs(); var errors = testLogger.GetLogMessages().Where(m => m.Exception != null).ToList(); @@ -133,8 +162,14 @@ public void JavaPath_FromEnvVars() var configBuilder = ScriptSettingsManager.CreateDefaultConfigurationBuilder(); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); var workerConfigs = configFactory.GetConfigs(); var javaPath = workerConfigs.FirstOrDefault(c => c.Description.Language.Equals("java", StringComparison.OrdinalIgnoreCase)).Description.DefaultExecutablePath; Assert.DoesNotContain(@"%JAVA_HOME%", javaPath); @@ -152,11 +187,21 @@ public void DefaultWorkerConfigs_Overrides_DefaultWorkerRuntimeVersion_AppSettin .AddInMemoryCollection(testEnvVariables); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); + + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + var testLogger = loggerFactory.CreateLogger("test"); + _testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1"); using var variables = new TestScopedSettings(scriptSettingsManager, testEnvVariables); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); var workerConfigs = configFactory.GetConfigs(); var pythonWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("python", StringComparison.OrdinalIgnoreCase)); var powershellWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("powershell", StringComparison.OrdinalIgnoreCase)); @@ -176,8 +221,14 @@ public void DefaultWorkerConfigs_Overrides_VersionAppSetting() var configBuilder = ScriptSettingsManager.CreateDefaultConfigurationBuilder(); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, resolver, optionsMonitor); var workerConfigs = configFactory.GetConfigs(); var powershellWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("powershell", StringComparison.OrdinalIgnoreCase)); Assert.Equal(1, workerConfigs.Count); @@ -203,10 +254,17 @@ public void ShouldAddProvider_Returns_Expected(string workerLanguage, string wor _testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); } var config = new ConfigurationBuilder().Build(); - var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); _testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); - Assert.Equal(expectedResult, rpcWorkerConfigFactory.ShouldAddWorkerConfig(workerLanguage)); + Assert.Equal(expectedResult, configFactory.ShouldAddWorkerConfig(workerLanguage)); } [Theory] @@ -248,8 +306,13 @@ public void GetWorkerProcessCount_Tests(bool defaultWorkerConfig, bool setProces var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); - var result = rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); + var result = configFactory.GetWorkerProcessCount(workerConfig); if (defaultWorkerConfig) { @@ -288,16 +351,22 @@ public void GetWorkerProcessCount_ThrowsException_Tests() var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); - var resultEx1 = Assert.Throws(() => rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig)); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver, optionsMonitor); + var resultEx1 = Assert.Throws(() => configFactory.GetWorkerProcessCount(workerConfig)); Assert.Contains("ProcessCount must be greater than 0", resultEx1.Message); workerConfig = CreateWorkerConfig(40, 10, "00:10:00", false); - var resultEx2 = Assert.Throws(() => rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig)); + var resultEx2 = Assert.Throws(() => configFactory.GetWorkerProcessCount(workerConfig)); Assert.Contains("ProcessCount must not be greater than MaxProcessCount", resultEx2.Message); workerConfig = CreateWorkerConfig(10, 10, "-800", false); - var resultEx3 = Assert.Throws(() => rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig)); + var resultEx3 = Assert.Throws(() => configFactory.GetWorkerProcessCount(workerConfig)); Assert.Contains("value could not be converted to System.TimeSpan", resultEx3.Message); } diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs index 1f696ad16b..165da137a6 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs @@ -7,13 +7,17 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using Microsoft.Azure.WebJobs.Script; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; using Moq; using Xunit; @@ -683,7 +687,13 @@ private IEnumerable TestReadWorkerProviderFromConfig(IEnumerabl var scriptHostOptions = new ScriptJobHostOptions(); var scriptSettingsManager = new ScriptSettingsManager(config); var workerProfileManager = new Mock(); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, testMetricsLogger, workerProfileManager.Object); + var testScriptHostManager = new Mock(); + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), workerProfileManager.Object, workerConfigurationResolver, optionsMonitor); if (appSvcEnv) { @@ -757,4 +767,4 @@ private bool AreRequiredMetricsEmitted(TestMetricsLogger metricsLogger) return hasBegun && hasEnded && (metricsLogger.EventsBegan.Contains(MetricEventNames.GetConfigs) && metricsLogger.EventsEnded.Contains(MetricEventNames.GetConfigs)); } } -} +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs new file mode 100644 index 0000000000..c955cde698 --- /dev/null +++ b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Workers; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Workers.Rpc +{ + public class WorkerConfigurationResolverOptionsSetupTests + { + [Fact] + public void Configure_WithRealEnvironmentValues_SetsCorrectDefaults() + { + // Arrange + var testEnvironment = new TestEnvironment(); + var configBuilder = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = "/default/workers", + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{RpcWorkerConstants.WorkerProbingPathsSectionName}:0"] = "testPath1", + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{RpcWorkerConstants.WorkerProbingPathsSectionName}:1"] = "testPath2", + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{RpcWorkerConstants.WorkerProbingPathsSectionName}:2"] = " ", + }); + var configuration = configBuilder.Build(); + var mockScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + + var setup = new WorkerConfigurationResolverOptionsSetup(configuration, testEnvironment, mockScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + var options = new WorkerConfigurationResolverOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.Null(options.WorkerRuntime); + Assert.Equal(ScriptConstants.LatestPlatformChannelNameUpper, options.ReleaseChannel); + Assert.False(options.IsPlaceholderModeEnabled); + Assert.False(options.IsMultiLanguageWorkerEnvironment); + Assert.Equal("/default/workers", options.WorkersDirPath); + Assert.NotNull(options.LanguageWorkersSettings); + + Assert.Equal(2, options.ProbingPaths.Count); + Assert.True(options.ProbingPaths.Contains("testPath1")); + Assert.True(options.ProbingPaths.Contains("testPath2")); + + Assert.False(options.WorkersAvailableForResolution.Any()); + } + + [Fact] + public void Configure_WithRealEnvironmentValues_SetsCorrectDefaults1() + { + // Arrange + var testEnvironment = new TestEnvironment(); + var configBuilder = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = "/default/workers", + }); + var configuration = configBuilder.Build(); + var mockScriptHostManager = new Mock(); + + testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, "java"); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel, "standard"); + testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AppKind, "workflowapp"); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, "java|node"); + + var setup = new WorkerConfigurationResolverOptionsSetup(configuration, testEnvironment, mockScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + var options = new WorkerConfigurationResolverOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.Equal("java", options.WorkerRuntime); + Assert.Equal("standard", options.ReleaseChannel); + Assert.False(options.IsPlaceholderModeEnabled); + Assert.False(options.IsMultiLanguageWorkerEnvironment); + Assert.Equal("/default/workers", options.WorkersDirPath); + Assert.NotNull(options.LanguageWorkersSettings); + + Assert.NotNull(options.ProbingPaths); + Assert.False(options.ProbingPaths.Any()); + + Assert.True(options.WorkersAvailableForResolution.Count == 2); + Assert.True(options.WorkersAvailableForResolution.Contains("java")); + Assert.True(options.WorkersAvailableForResolution.Contains("node")); + } + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTests.cs new file mode 100644 index 0000000000..51b8f071ab --- /dev/null +++ b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTests.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Workers.Profiles; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Workers.Rpc +{ + public class WorkerConfigurationResolverTests + { + private readonly Mock _mockProfileManager; + private readonly string _probingPath1 = Path.GetFullPath("..\\..\\..\\..\\test\\TestWorkers\\ProbingPaths\\workers\\"); + private readonly string _fallbackPath = Path.GetFullPath("workers"); + + public WorkerConfigurationResolverTests() + { + _mockProfileManager = new Mock(); + } + + [Theory] + [InlineData("LATEST", "java\\2.19.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + [InlineData("STANDARD", "java\\2.18.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + [InlineData("EXTENDED", "java\\2.18.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + [InlineData("laTest", "java\\2.19.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + [InlineData("abc", "java\\2.19.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + [InlineData("Standard", "java\\2.18.0", "node\\3.10.1", "powershell", "dotnet-isolated", "python")] + public void GetWorkerConfigs_MultiLanguageWorker_ReturnsExpectedConfigs(string releaseChannel, string java, string node, string powershell, string dotnetIsolated, string python) + { + // Arrange + var probingPaths = new List() { _probingPath1, string.Empty, "path-not-exists" }; + + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var mockEnvironment = new Mock(); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel)).Returns(releaseChannel); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AppKind)).Returns(ScriptConstants.WorkFlowAppKind); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime)).Returns((string)null); + + var config = WorkerConfigurationResolverTestsHelper.GetConfigurationWithProbingPaths(probingPaths); + + var testScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, "java|node"); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, mockEnvironment.Object, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + // Act + var workerConfigurationResolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, _mockProfileManager.Object, optionsMonitor); + + var result = workerConfigurationResolver.GetWorkerConfigPaths(); + + // Assert + Assert.Equal(result.Count, 5); + Assert.True(result.Any(r => r.Contains(Path.Combine(_probingPath1, java)))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_probingPath1, node)))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, powershell)))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, dotnetIsolated)))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, python)))); + } + + [Theory] + [InlineData(null, "LATEST")] + [InlineData(null, "STANDARD")] + [InlineData("Empty", "LATEST")] + [InlineData("Empty", "abc")] + public void GetWorkerConfigs_MultiLanguageWorker_NullOREmptyProbingPath_ReturnsExpectedConfigs(string probingPathValue, string releaseChannel) + { + // Arrange + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var mockEnvironment = new Mock(); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel)).Returns(releaseChannel); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AppKind)).Returns(ScriptConstants.WorkFlowAppKind); + mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime)).Returns((string)null); + + List probingPaths = null; + + if (probingPathValue == "Empty") + { + probingPaths = new List(); + } + + var config = WorkerConfigurationResolverTestsHelper.GetConfigurationWithProbingPaths(probingPaths); + + var testScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, "java|node|powershell"); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, mockEnvironment.Object, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + // Act + var workerConfigurationResolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, _mockProfileManager.Object, optionsMonitor); + + var result = workerConfigurationResolver.GetWorkerConfigPaths(); + + // Assert + Assert.Equal(result.Count, 5); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, "java")))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, "node")))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, "powershell")))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, "dotnet-isolated")))); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, "python")))); + } + + [Theory] + [InlineData(null, "LATEST", "java")] + [InlineData(null, "STANDARD", "java")] + [InlineData("Empty", "LATEST", "java")] + [InlineData("Empty", "STANDARD", "java")] + [InlineData(null, "STANDARD", "node")] + [InlineData("Empty", "LATEST", "node")] + [InlineData(null, "STANDARD", "powershell")] + [InlineData("Empty", "LATEST", "powershell")] + [InlineData(null, "LATEST", "dotnet-isolated")] + [InlineData(null, "STANDARD", "dotnet-isolated")] + [InlineData("Empty", "LATEST", "dotnet-isolated")] + [InlineData("Empty", "STANDARD", "dotnet-isolated")] + public void GetWorkerConfigs_NullOREmptyProbingPath_ReturnsExpectedConfigs(string probingPathValue, string releaseChannel, string languageWorker) + { + // Arrange + var mockEnv = new Mock(); + mockEnv.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime)).Returns(languageWorker); + mockEnv.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AntaresPlatformReleaseChannel)).Returns(releaseChannel); + + List probingPaths = null; + + if (probingPathValue == "Empty") + { + probingPaths = new List(); + } + + var config = WorkerConfigurationResolverTestsHelper.GetConfigurationWithProbingPaths(probingPaths); + + var mockProfileManager = new Mock(); + var mockConfig = new Mock(); + + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testScriptHostManager = new Mock(); + + var hostingOptions = new FunctionsHostingConfigOptions(); + hostingOptions.Features.Add(RpcWorkerConstants.WorkersAvailableForDynamicResolution, "java|node|powershell"); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, mockEnv.Object, testScriptHostManager.Object, new OptionsWrapper(hostingOptions)); + + // Act + var workerConfigurationResolver = new DynamicWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, _mockProfileManager.Object, optionsMonitor); + + var result = workerConfigurationResolver.GetWorkerConfigPaths(); + + // Assert + Assert.Equal(result.Count, 1); + Assert.True(result.Any(r => r.Contains(Path.Combine(_fallbackPath, languageWorker)))); + } + } +} \ No newline at end of file