diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 1488b9ae9..4260eba9b 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -34,6 +34,7 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Microsoft.Azure.WebJobs.Script.Description; namespace Azure.Functions.Cli.Actions.HostActions { @@ -235,6 +236,7 @@ public override async Task RunAsync() await PreRunConditions(); Utilities.PrintLogo(); Utilities.PrintVersion(); + ValidateHostJsonConfiguration(); var settings = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); @@ -255,6 +257,21 @@ public override async Task RunAsync() await runTask; } + private void ValidateHostJsonConfiguration() + { + bool IsPreCompiledApp = IsPreCompiledFunctionApp(); + var hostJsonPath = Path.Combine(Environment.CurrentDirectory, Constants.HostJsonFileName); + if (IsPreCompiledApp && !File.Exists(hostJsonPath)) + { + throw new CliException($"Host.json file in missing. Please make sure host.json file is preset at {Environment.CurrentDirectory}"); + } + + if (IsPreCompiledApp && BundleConfigurationExists(hostJsonPath)) + { + throw new CliException($"Extension bundle configuration should not be present for the function app with pre-compiled functions. Please remove extension bundle configuration from host.json: {Path.Combine(Environment.CurrentDirectory, "host.json")}"); + } + } + private async Task PreRunConditions() { if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.python) @@ -288,6 +305,33 @@ private async Task PreRunConditions() } } + private bool BundleConfigurationExists(string hostJsonPath) + { + var hostJson = FileSystemHelpers.ReadAllTextFromFile(hostJsonPath); + return hostJson.Contains(Constants.ExtensionBundleConfigPropertyName, StringComparison.OrdinalIgnoreCase); + } + + private bool IsPreCompiledFunctionApp() + { + bool isPrecompiled = false; + foreach (var directory in FileSystemHelpers.GetDirectories(Environment.CurrentDirectory)) + { + var functionMetadataFile = Path.Combine(directory, Constants.FunctionJsonFileName); + if (File.Exists(functionMetadataFile)) + { + var functionMetadataFileContent = FileSystemHelpers.ReadAllTextFromFile(functionMetadataFile); + var functionMetadata = JsonConvert.DeserializeObject(functionMetadataFileContent); + string extension = Path.GetExtension(functionMetadata?.ScriptFile)?.ToLowerInvariant().TrimStart('.'); + isPrecompiled = isPrecompiled || (!string.IsNullOrEmpty(extension) && extension == "dll"); + } + if (isPrecompiled) + { + break; + } + } + return isPrecompiled; + } + private void DisplayDisabledFunctions(IScriptJobHost scriptHost) { if (scriptHost != null) diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 89c5e9004..609e0c2f5 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -21,6 +21,7 @@ internal static class Constants public const string FunctionsWorkerRuntimeVersion = "FUNCTIONS_WORKER_RUNTIME_VERSION"; public const string RequirementsTxt = "requirements.txt"; public const string FunctionJsonFileName = "function.json"; + public const string HostJsonFileName = "host.json"; public const string ExtenstionsCsProjFile = "extensions.csproj"; public const string DefaultVEnvName = "worker_env"; public const string ExternalPythonPackages = ".python_packages"; @@ -44,6 +45,7 @@ internal static class Constants public const string TelemetrySentinelFile = "telemetryDefaultOn.sentinel"; public const string DefaultManagementURL = "https://management.azure.com/"; public const string AzureManagementAccessToken = "AZURE_MANAGEMENT_ACCESS_TOKEN"; + public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index e99484685..eb2ee14c1 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -145,6 +145,76 @@ await CliTester.Run(new RunConfiguration[] }, _output); } + [Fact] + public async Task start_displays_error_on_invalid_host_json() + { + var functionName = "HttpTriggerCSharp"; + + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet", + $"new --template Httptrigger --name {functionName}", + + }, + Test = async (workingDir, p) => + { + var filePath = Path.Combine(workingDir, "host.json"); + string hostJsonContent = "{ \"version\": \"2.0\", \"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\", \"version\": \"[1.*, 2.0.0)\" }}"; + await File.WriteAllTextAsync(filePath, hostJsonContent); + }, + }, + new RunConfiguration + { + Commands = new[] + { + "start" + }, + ExpectExit = true, + ExitInError = true, + ErrorContains = new[] { "Extension bundle configuration should not be present" }, + }, + }, _output, startHost: true); + } + + + [Fact] + public async Task start_displays_error_on_missing_host_json() + { + var functionName = "HttpTriggerCSharp"; + + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet", + $"new --template Httptrigger --name {functionName}", + }, + Test = async (workingDir, p) => + { + var hostJsonPath = Path.Combine(workingDir, "host.json"); + File.Delete(hostJsonPath); + + }, + }, + new RunConfiguration + { + Commands = new[] + { + "start" + }, + ExpectExit = true, + ExitInError = true, + ErrorContains = new[] { "Host.json file in missing" }, + }, + }, _output); + } + [Fact] public async Task start_host_port_in_use() {