From ca549be3eb033cd5a9b25577389e8c8dca1b091c Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 29 Jan 2020 10:48:52 -0800 Subject: [PATCH 001/127] Update PowerShell Worker to v2.0.215 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 96191ae6a..5bf34daf2 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -118,7 +118,7 @@ - + From 703cd55d23822f65950c63741e5e61651b44c1de Mon Sep 17 00:00:00 2001 From: Graham Zuber Date: Mon, 3 Feb 2020 17:23:14 -0800 Subject: [PATCH 002/127] Extensions install will fetch latest version if not specified. --- .../LocalActions/InstallExtensionAction.cs | 6 +- src/Azure.Functions.Cli/Common/Constants.cs | 101 +++++++++--------- .../ExtensionsTests/InstallExtensionsTests.cs | 41 ++++++- 3 files changed, 95 insertions(+), 53 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InstallExtensionAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InstallExtensionAction.cs index 90af7543f..7d187745a 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InstallExtensionAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InstallExtensionAction.cs @@ -196,7 +196,11 @@ private bool NeedsExtensionsInstall() private async Task AddPackage(string extensionsProj, string packageName, string version) { - var args = $"add \"{extensionsProj}\" package {packageName} --version {version}"; + var args = $"add \"{extensionsProj}\" package {packageName}"; + if (!string.IsNullOrEmpty(version)) + { + args += $" --version {version}"; + } if (!string.IsNullOrEmpty(Source)) { args += $" --source {Source}"; diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 6277dc92f..20785c425 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -114,117 +114,116 @@ public static class StaticResourcesNames public const string ZipToSquashfs = "ziptofs.sh"; } - public static ExtensionPackage ExtensionsMetadataGeneratorPackage => new ExtensionPackage { Name = "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator", Version = "1.0.2" }; + public static ExtensionPackage ExtensionsMetadataGeneratorPackage => new ExtensionPackage { Name = "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator" }; public static IDictionary BindingPackageMap { get; } = new ReadOnlyDictionary( new Dictionary { { "blobtrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Storage", - Version = "3.0.4" } + Name = "Microsoft.Azure.WebJobs.Extensions.Storage" } }, { "blob", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Storage", - Version = "3.0.4" } + Name = "Microsoft.Azure.WebJobs.Extensions.Storage" } }, { "queue", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Storage", - Version = "3.0.4" } + Name = "Microsoft.Azure.WebJobs.Extensions.Storage" } }, { "queuetrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Storage", - Version = "3.0.4" } + Name = "Microsoft.Azure.WebJobs.Extensions.Storage" } }, { "table", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Storage", - Version = "3.0.4" } + Name = "Microsoft.Azure.WebJobs.Extensions.Storage" } }, { "servicebustrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.ServiceBus", - Version = "3.0.3" } + Name = "Microsoft.Azure.WebJobs.Extensions.ServiceBus" } }, { "servicebus", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.ServiceBus", - Version = "3.0.3" } + Name = "Microsoft.Azure.WebJobs.Extensions.ServiceBus" } }, { "eventhubtrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.EventHubs", - Version = "3.0.3"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.EventHubs" } + }, { "eventhub", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.EventHubs", - Version = "3.0.3"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.EventHubs" } + }, { "sendgrid", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.SendGrid", - Version = "3.0.0" } }, + Name = "Microsoft.Azure.WebJobs.Extensions.SendGrid" } + }, { "token", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.AuthTokens", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.AuthTokens", + Version = "1.0.0-beta6" } + }, { "excel", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", + Version = "1.0.0-beta6" } + }, { "outlook", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", + Version = "1.0.0-beta6" } + }, { "graphwebhooksubscription", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", + Version = "1.0.0-beta6" } + }, { "onedrive", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", + Version = "1.0.0-beta6" } + }, { "graphwebhooktrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", - Version = "1.0.0-beta6"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph", + Version = "1.0.0-beta6" } + }, { "activitytrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask", - Version = "1.8.2"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask" } + }, { "orchestrationtrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask", - Version = "1.8.2"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask" } + }, { "orchestrationclient", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask", - Version = "1.8.2"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.DurableTask" } + }, { "eventgridtrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.EventGrid", - Version = "2.0.0"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.EventGrid" } + }, { "cosmosdbtrigger", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.CosmosDB", - Version = "3.0.3"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.CosmosDB" } + }, { "cosmosdb", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.CosmosDB", - Version = "3.0.3"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.CosmosDB" } + }, { "signalr", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.SignalRService", - Version = "1.0.0"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.SignalRService" } + }, { "signalrconnectioninfo", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.SignalRService", - Version = "1.0.0"} }, + Name = "Microsoft.Azure.WebJobs.Extensions.SignalRService" } + }, { "twiliosms", new ExtensionPackage() { - Name = "Microsoft.Azure.WebJobs.Extensions.Twilio", - Version = "3.0.0"} } + Name = "Microsoft.Azure.WebJobs.Extensions.Twilio" } + } }); } } diff --git a/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs b/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs index 1a4fcb628..5315bbbf1 100644 --- a/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs +++ b/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs @@ -55,7 +55,8 @@ public Task try_install_with_a_valid_trigger() }, OutputContains = new[] { - "Restoring packages for" + "Restoring packages for", + "Restore completed" }, OutputDoesntContain = new[] { @@ -77,5 +78,43 @@ public Task try_install_with_a_valid_trigger() CommandTimeout = TimeSpan.FromMinutes(1) }, _output); } + + [Fact] + public Task try_install_with_a_version() + { + return CliTester.Run(new RunConfiguration + { + Commands = new[] { + "init . --worker-runtime node --no-bundle", + "new --template SendGrid --name testfunc", + "extensions install", + "extensions install -p Microsoft.Azure.WebJobs.Extensions.Storage -v 3.0.8" + }, + OutputContains = new[] + { + "Restoring packages for", + "Restore completed" + }, + OutputDoesntContain = new[] + { + "No action performed because no functions in your app require extensions" + }, + CheckFiles = new[] + { + new FileResult + { + Name = "extensions.csproj", + Exists = true, + ContentContains = new[] + { + "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator", + "Microsoft.Azure.WebJobs.Extensions.SendGrid", + "Include=\"Microsoft.Azure.WebJobs.Extensions.Storage\" Version=\"3.0.8\"" + } + } + }, + CommandTimeout = TimeSpan.FromMinutes(1) + }, _output); + } } } From b4fb2d839db1e5e2f2c88fd682c7fecaeb1eaa15 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Tue, 4 Feb 2020 14:37:59 -0800 Subject: [PATCH 003/127] Fetching templates from cdn at build time instead of including it as static resource --- build/BuildSteps.cs | 21 +++++++++++++++++++ build/FileHelpers.cs | 14 +++++++++++++ build/Program.cs | 1 + build/Settings.cs | 7 +++++-- .../Azure.Functions.Cli.csproj | 3 --- .../Common/TemplatesManager.cs | 14 ++++++++++++- .../StaticResources/StaticResources.cs | 2 -- .../StaticResources/templates.json | 1 - 8 files changed, 54 insertions(+), 9 deletions(-) delete mode 100644 src/Azure.Functions.Cli/StaticResources/templates.json diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index efcde3848..6ea9f6049 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -180,6 +180,27 @@ public static void AddTemplatesNupkgs() } } + public static void AddTemplatesJson() + { + var tempDirectoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + using (var client = new WebClient()) + { + FileHelpers.EnsureDirectoryExists(tempDirectoryPath); + var zipFilePath = Path.Combine(tempDirectoryPath, "templates.zip"); + client.DownloadFile(Settings.TemplatesJsonZip, zipFilePath); + FileHelpers.ExtractZipToDirectory(zipFilePath, tempDirectoryPath); + } + + string templatesJsonPath = Path.Combine(tempDirectoryPath, "templates", "templates.json"); + if (File.Exists(templatesJsonPath)) + { + foreach (var runtime in Settings.TargetRuntimes) + { + File.Copy(templatesJsonPath, Path.Combine(Settings.OutputDir, runtime, "templates", "templates.json")); + } + } + } + public static void Test() { var funcPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) diff --git a/build/FileHelpers.cs b/build/FileHelpers.cs index 8e56e0ea7..2d73efcdc 100644 --- a/build/FileHelpers.cs +++ b/build/FileHelpers.cs @@ -6,6 +6,15 @@ namespace Build { public static class FileHelpers { + + public static void EnsureDirectoryExists(string dirPath) + { + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + } + // https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories public static void RecursiveCopy(string sourcePath, string destinationPath) { @@ -105,5 +114,10 @@ public static void ExtractZipFileForce(string zipFile, string to) } } } + + public static void ExtractZipToDirectory(string zipFile, string to) + { + ZipFile.ExtractToDirectory(zipFile, to); + } } } \ No newline at end of file diff --git a/build/Program.cs b/build/Program.cs index 50c6d62a0..c2b91f883 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -20,6 +20,7 @@ static void Main(string[] args) .Then(FilterPowershellRuntimes) .Then(AddDistLib) .Then(AddTemplatesNupkgs) + .Then(AddTemplatesJson) .Then(AddGoZip) .Then(GenerateZipToSign, skip: !args.Contains("--sign")) .Then(UploadZipToSign, skip: !args.Contains("--sign")) diff --git a/build/Settings.cs b/build/Settings.cs index 5104663de..2291c9557 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -22,6 +22,7 @@ private static string config(string @default = null, [CallerMemberName] string k public const string ItemTemplatesVersion = "2.0.10369"; public const string ProjectTemplatesVersion = "2.0.10369"; + public const string TemplateJsonVersion = "2.0.10344"; public static readonly string SrcProjectPath = Path.GetFullPath("../src/Azure.Functions.Cli/"); @@ -48,7 +49,7 @@ private static string config(string @default = null, [CallerMemberName] string k { "min.win-x64", "Windows" }, }; - private static readonly string[] _winPowershellRuntimes = new [] { "win-x86", "win", "win10-x86", "win8-x86", "win81-x86", "win7-x86", "win-x64", "win10-x64", "win8-x64", "win81-x64", "win7-x64" }; + private static readonly string[] _winPowershellRuntimes = new[] { "win-x86", "win", "win10-x86", "win8-x86", "win81-x86", "win7-x86", "win-x64", "win10-x64", "win8-x64", "win81-x64", "win7-x64" }; public static readonly Dictionary ToolsRuntimeToPowershellRuntimes = new Dictionary { { "win-x86", _winPowershellRuntimes }, @@ -77,6 +78,8 @@ private static string config(string @default = null, [CallerMemberName] string k public static readonly string ProjectTemplates = $"https://www.nuget.org/api/v2/package/Microsoft.Azure.WebJobs.ProjectTemplates/{ProjectTemplatesVersion}"; + public static readonly string TemplatesJsonZip = $"https://functionscdn.azureedge.net/public/TemplatesApi/{TemplateJsonVersion}.zip"; + public static readonly string TelemetryKeyToReplace = "00000000-0000-0000-0000-000000000000"; public static string BuildNumber => config(null, "devops_buildNumber") ?? config("9999", "APPVEYOR_BUILD_NUMBER"); @@ -99,7 +102,7 @@ public class SignInfo public static readonly string SignedDir = "signed"; public static readonly string Authenticode = "SignAuthenticode"; public static readonly string ThirdParty = "Sign3rdParty"; - public static readonly string[] RuntimesToSign = new[] { "min.win-x86" }; + public static readonly string[] RuntimesToSign = new[] { "min.win-x86", "min.win-x64" }; public static readonly string[] FilterExtenstionsSign = new[] { ".json", ".spec", ".cfg", ".pdb", ".config", ".nupkg", ".py" }; public static readonly string SigcheckDownloadURL = "https://functionsbay.blob.core.windows.net/public/tools/sigcheck64.exe"; diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 5bf34daf2..d722afcc2 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -69,9 +69,6 @@ $(AssemblyName).profile.ps1 - - $(AssemblyName).templates.json - $(AssemblyName).package.json diff --git a/src/Azure.Functions.Cli/Common/TemplatesManager.cs b/src/Azure.Functions.Cli/Common/TemplatesManager.cs index 0803a125b..93c521191 100644 --- a/src/Azure.Functions.Cli/Common/TemplatesManager.cs +++ b/src/Azure.Functions.Cli/Common/TemplatesManager.cs @@ -10,6 +10,7 @@ using Azure.Functions.Cli.Actions.LocalActions; using Azure.Functions.Cli.ExtensionBundle; using System.Linq; +using System.Reflection; namespace Azure.Functions.Cli.Common { @@ -42,11 +43,22 @@ private static async Task> GetTemplates() } else { - templatesJson = await StaticResources.TemplatesJson; + templatesJson = GetTemplatesJson(); } return JsonConvert.DeserializeObject>(templatesJson); } + private static string GetTemplatesJson() + { + var templatesLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "templates", "templates.json"); + if (!FileSystemHelpers.FileExists(templatesLocation)) + { + throw new CliException($"Can't find templates location. Looked at '{templatesLocation}'"); + } + + return FileSystemHelpers.ReadAllTextFromFile(templatesLocation); + } + public async Task Deploy(string Name, Template template) { var path = Path.Combine(Environment.CurrentDirectory, Name); diff --git a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs index f9c1fbb33..d4bfe655b 100644 --- a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs +++ b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs @@ -57,8 +57,6 @@ private static async Task GetValue(string name) public static Task PowerShellProfilePs1 => GetValue("profile.ps1"); - public static Task TemplatesJson => GetValue("templates.json"); - public static Task FuncIgnore => GetValue("funcignore"); public static Task PackageJson => GetValue("package.json"); diff --git a/src/Azure.Functions.Cli/StaticResources/templates.json b/src/Azure.Functions.Cli/StaticResources/templates.json deleted file mode 100644 index b4464b250..000000000 --- a/src/Azure.Functions.Cli/StaticResources/templates.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"BlobTrigger-CSharp","runtime":"2","files":{"readme.md":"# BlobTrigger - C#\r\n\r\nThe `BlobTrigger` makes it incredibly easy to react to new Blobs inside of Azure Blob Storage. This sample demonstrates a simple use case of processing data from a given Blob using C#.\r\n\r\n## How it works\r\n\r\nFor a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with \".png\" at the end of their name.\r\n\r\n## Learn more\r\n\r\n Documentation","run.csx":"public static void Run(Stream myBlob, string name, ILogger log)\r\n{\r\n log.LogInformation($\"C# Blob trigger function Processed blob\\n Name:{name} \\n Size: {myBlob.Length} Bytes\");\r\n}\r\n","sample.dat":"samples-workitems/workitem.txt"},"function":{"bindings":[{"name":"myBlob","type":"blobTrigger","direction":"in","path":"samples-workitems/{name}","connection":""}]},"metadata":{"defaultFunctionName":"BlobTrigger","description":"$BlobTrigger_description","name":"Azure Blob Storage trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"blob","enabledInTryMode":true,"userPrompt":["connection","path"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"BlobTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, myBlob) {\r\n context.log(\"JavaScript blob trigger function processed blob \\n Name:\", context.bindingData.name, \"\\n Blob Size:\", myBlob.length, \"Bytes\");\r\n};","readme.md":"# BlobTrigger - JavaScript\r\n\r\nThe `BlobTrigger` makes it incredibly easy to react to new Blobs inside of Azure Blob Storage. This sample demonstrates a simple use case of processing data from a given Blob using JavaScript.\r\n\r\n## How it works\r\n\r\nFor a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with \".png\" at the end of their name.\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":"samples-workitems/workitem.txt"},"function":{"bindings":[{"name":"myBlob","type":"blobTrigger","direction":"in","path":"samples-workitems/{name}","connection":""}]},"metadata":{"defaultFunctionName":"BlobTrigger","description":"$BlobTrigger_description","name":"Azure Blob Storage trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"blob","enabledInTryMode":true,"userPrompt":["connection","path"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"BlobTrigger-PowerShell","runtime":"2","files":{"readme.md":"# BlobTrigger - PowerShell\r\n\r\nThe `BlobTrigger` makes it incredibly easy to react to new Blobs inside of [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/).\r\nThis sample demonstrates a simple use case of processing data from a given Blob using PowerShell.\r\n\r\n## How it works\r\n\r\nFor a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with \".png\" at the end of their name.\r\n\r\n## Learn more\r\n\r\n Documentation\r\n","run.ps1":"# Input bindings are passed in via param block.\r\nparam([byte[]] $InputBlob, $TriggerMetadata)\r\n\r\n# Write out the blob name and size to the information log.\r\nWrite-Host \"PowerShell Blob trigger function Processed blob! Name: $($TriggerMetadata.Name) Size: $($InputBlob.Length) bytes\"\r\n","sample.dat":"samples-workitems/workitem.txt"},"function":{"bindings":[{"name":"InputBlob","type":"blobTrigger","direction":"in","path":"samples-workitems/{name}","connection":""}]},"metadata":{"defaultFunctionName":"BlobTrigger","description":"$BlobTrigger_description","name":"Azure Blob Storage trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"blob","enabledInTryMode":true,"userPrompt":["connection","path"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"BlobTrigger-Python","runtime":"2","files":{"readme.md":"# BlobTrigger - Python\r\n\r\nThe `BlobTrigger` makes it incredibly easy to react to new Blobs inside of Azure Blob Storage. This sample demonstrates a simple use case of processing data from a given Blob using Python.\r\n\r\n## How it works\r\n\r\nFor a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with \".png\" at the end of their name.\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":"samples-workitems/workitem.txt","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(myblob: func.InputStream):\r\n logging.info(f\"Python blob trigger function processed blob \\n\"\r\n f\"Name: {myblob.name}\\n\"\r\n f\"Blob Size: {myblob.length} bytes\")\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"name":"myblob","type":"blobTrigger","direction":"in","path":"samples-workitems/{name}","connection":""}]},"metadata":{"defaultFunctionName":"BlobTrigger","description":"$BlobTrigger_description","name":"Azure Blob Storage trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"blob","enabledInTryMode":true,"userPrompt":["connection","path"],"filters":["Python3"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"BlobTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst blobTrigger: AzureFunction = async function (context: Context, myBlob: any): Promise {\r\n context.log(\"Blob trigger function processed blob \\n Name:\", context.bindingData.name, \"\\n Blob Size:\", myBlob.length, \"Bytes\");\r\n};\r\n\r\nexport default blobTrigger;\r\n","readme.md":"# BlobTrigger - TypeScript\r\n\r\nThe `BlobTrigger` makes it incredibly easy to react to new Blobs inside of Azure Blob Storage. This sample demonstrates a simple use case of processing data from a given Blob using TypeScript.\r\n\r\n## How it works\r\n\r\nFor a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with \".png\" at the end of their name.\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":"samples-workitems/workitem.txt"},"function":{"bindings":[{"name":"myBlob","type":"blobTrigger","direction":"in","path":"samples-workitems/{name}","connection":""}]},"metadata":{"defaultFunctionName":"BlobTrigger","description":"$BlobTrigger_description","name":"Azure Blob Storage trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"blob","enabledInTryMode":true,"userPrompt":["connection","path"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"CosmosDBTrigger-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Azure.DocumentDB.Core\"\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Microsoft.Azure.Documents;\r\n\r\npublic static void Run(IReadOnlyList input, ILogger log)\r\n{\r\n if (input != null && input.Count > 0)\r\n {\r\n log.LogInformation(\"Documents modified \" + input.Count);\r\n log.LogInformation(\"First document Id \" + input[0].Id);\r\n }\r\n}\r\n"},"function":{"bindings":[{"type":"cosmosDBTrigger","name":"input","direction":"in","connectionStringSetting":"","databaseName":"","collectionName":"","leaseCollectionName":"leases","createLeaseCollectionIfNotExists":true}]},"metadata":{"defaultFunctionName":"CosmosTrigger","description":"$CosmosDBTrigger_description","name":"Azure Cosmos DB trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"cosmosDB","enabledInTryMode":false,"userPrompt":["connectionStringSetting","databaseName","collectionName","leaseCollectionName","createLeaseCollectionIfNotExists"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.3"}]}},{"id":"CosmosDBTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, documents) {\r\n if (!!documents && documents.length > 0) {\r\n context.log('Document Id: ', documents[0].id);\r\n }\r\n}\r\n","sample.dat":"[\r\n\t{\r\n\t\t\"id\": \"sample\"\r\n\t}\r\n]"},"function":{"bindings":[{"type":"cosmosDBTrigger","name":"documents","direction":"in","leaseCollectionName":"leases","connectionStringSetting":"","databaseName":"","collectionName":"","createLeaseCollectionIfNotExists":true}]},"metadata":{"defaultFunctionName":"CosmosTrigger","description":"$CosmosDBTrigger_description","name":"Azure Cosmos DB trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"cosmosDB","enabledInTryMode":false,"userPrompt":["connectionStringSetting","databaseName","collectionName","leaseCollectionName","createLeaseCollectionIfNotExists"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.3"}]}},{"id":"CosmosDBTrigger-PowerShell","runtime":"2","files":{"run.ps1":"# Input bindings are passed in via param block.\nparam($Documents, $TriggerMetadata)\n\nif ($Documents.Count -gt 0) {\n Write-Host \"Document Id: $($Documents[0].id)\"\n}\n","sample.dat":"[\r\n\t{\r\n\t\t\"id\": \"sample\"\r\n\t}\r\n]\r\n"},"function":{"bindings":[{"type":"cosmosDBTrigger","name":"Documents","direction":"in","leaseCollectionName":"leases","connectionStringSetting":"","databaseName":"","collectionName":"","createLeaseCollectionIfNotExists":true}]},"metadata":{"defaultFunctionName":"CosmosTrigger","description":"$CosmosDBTrigger_description","name":"Azure Cosmos DB trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"cosmosDB","enabledInTryMode":false,"userPrompt":["connectionStringSetting","databaseName","collectionName","leaseCollectionName","createLeaseCollectionIfNotExists"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.3"}]}},{"id":"CosmosDBTrigger-Python","runtime":"2","files":{"sample.dat":"[\r\n\t{\r\n\t\t\"id\": \"sample\"\r\n\t}\r\n]","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(documents: func.DocumentList) -> str:\r\n if documents:\r\n logging.info('Document id: %s', documents[0]['id'])\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"type":"cosmosDBTrigger","name":"documents","direction":"in","leaseCollectionName":"leases","connectionStringSetting":"","databaseName":"","collectionName":"","createLeaseCollectionIfNotExists":true}]},"metadata":{"defaultFunctionName":"CosmosTrigger","description":"$CosmosDBTrigger_description","name":"Azure Cosmos DB trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"cosmosDB","enabledInTryMode":false,"userPrompt":["connectionStringSetting","databaseName","collectionName","leaseCollectionName","createLeaseCollectionIfNotExists"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.3"}],"filters":["Python3"]}},{"id":"CosmosDBTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst cosmosDBTrigger: AzureFunction = async function (context: Context, documents: any[]): Promise {\r\n if (!!documents && documents.length > 0) {\r\n context.log('Document Id: ', documents[0].id);\r\n }\r\n}\r\n\r\nexport default cosmosDBTrigger;\r\n","sample.dat":"[\r\n\t{\r\n\t\t\"id\": \"sample\"\r\n\t}\r\n]"},"function":{"bindings":[{"type":"cosmosDBTrigger","name":"documents","direction":"in","leaseCollectionName":"leases","connectionStringSetting":"","databaseName":"","collectionName":"","createLeaseCollectionIfNotExists":true}]},"metadata":{"defaultFunctionName":"CosmosTrigger","description":"$CosmosDBTrigger_description","name":"Azure Cosmos DB trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"cosmosDB","enabledInTryMode":false,"userPrompt":["connectionStringSetting","databaseName","collectionName","leaseCollectionName","createLeaseCollectionIfNotExists"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.3"}]}},{"id":"DurableFunctionsActivity-CSharp","runtime":"2","files":{"run.csx":"/*\r\n * This function is not intended to be invoked directly. Instead it will be\r\n * triggered by an orchestrator function.\r\n * \r\n * Before running this sample, please:\r\n * - create a Durable orchestration function\r\n * - create a Durable HTTP starter function\r\n */\r\n\r\n#r \"Microsoft.Azure.WebJobs.Extensions.DurableTask\"\r\n\r\npublic static string Run(string name)\r\n{\r\n return $\"Hello {name}!\";\r\n}"},"function":{"bindings":[{"name":"name","type":"activityTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"Hello","description":"$DurableFunctionsActivity_description","name":"Durable Functions activity","language":"C#","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsActivity-JavaScript","runtime":"2","files":{"index.js":"/*\n * This function is not intended to be invoked directly. Instead it will be\n * triggered by an orchestrator function.\n * \n * Before running this sample, please:\n * - create a Durable orchestration function\n * - create a Durable HTTP starter function\n * - run 'npm install durable-functions' from the wwwroot folder of your\n * function app in Kudu\n */\n\nmodule.exports = async function (context) {\n return `Hello ${context.bindings.name}!`;\n};"},"function":{"bindings":[{"name":"name","type":"activityTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"Hello","description":"$DurableFunctionsActivity_description","name":"Durable Functions activity","language":"JavaScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsActivity-TypeScript","runtime":"2","files":{"index.ts":"/*\n * This function is not intended to be invoked directly. Instead it will be\n * triggered by an orchestrator function.\n * \n * Before running this sample, please:\n * - create a Durable orchestration function\n * - create a Durable HTTP starter function\n * - run 'npm install durable-functions' from the wwwroot folder of your\n * function app in Kudu\n */\n\nimport { AzureFunction, Context } from \"@azure/functions\"\n\nconst activityFunction: AzureFunction = async function (context: Context): Promise {\n return `Hello ${context.bindings.name}!`;\n};\n\nexport default activityFunction;\n"},"function":{"bindings":[{"name":"name","type":"activityTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"Hello","description":"$DurableFunctionsActivity_description","name":"Durable Functions activity","language":"TypeScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsHttpStart-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Azure.WebJobs.Extensions.DurableTask\"\r\n#r \"Newtonsoft.Json\"\r\n\r\nusing System.Net;\r\n\r\npublic static async Task Run(\r\n HttpRequestMessage req,\r\n DurableOrchestrationClient starter,\r\n string functionName,\r\n ILogger log)\r\n{\r\n // Function input comes from the request content.\r\n dynamic eventData = await req.Content.ReadAsAsync();\r\n\r\n // Pass the function name as part of the route \r\n string instanceId = await starter.StartNewAsync(functionName, eventData);\r\n\r\n log.LogInformation($\"Started orchestration with ID = '{instanceId}'.\");\r\n\r\n return starter.CreateCheckStatusResponse(req, instanceId);\r\n}"},"function":{"bindings":[{"authLevel":"function","name":"req","type":"httpTrigger","direction":"in","route":"orchestrators/{functionName}","methods":["post","get"]},{"name":"$return","type":"http","direction":"out"},{"name":"starter","type":"orchestrationClient","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsHttpStart","description":"$DurableFunctionsHttpStart_description","name":"Durable Functions HTTP starter","language":"C#","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsHttpStart-JavaScript","runtime":"2","files":{"index.js":"const df = require(\"durable-functions\");\n\nmodule.exports = async function (context, req) {\n const client = df.getClient(context);\n const instanceId = await client.startNew(req.params.functionName, undefined, req.body);\n\n context.log(`Started orchestration with ID = '${instanceId}'.`);\n\n return client.createCheckStatusResponse(context.bindingData.req, instanceId);\n};"},"function":{"bindings":[{"authLevel":"function","name":"req","type":"httpTrigger","direction":"in","route":"orchestrators/{functionName}","methods":["post","get"]},{"name":"$return","type":"http","direction":"out"},{"name":"starter","type":"orchestrationClient","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsHttpStart","description":"$DurableFunctionsHttpStart_description","name":"Durable Functions HTTP starter","language":"JavaScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsHttpStart-TypeScript","runtime":"2","files":{"index.ts":"import * as df from \"durable-functions\"\nimport { AzureFunction, Context, HttpRequest } from \"@azure/functions\"\n\nconst httpStart: AzureFunction = async function (context: Context, req: HttpRequest): Promise {\n const client = df.getClient(context);\n const instanceId = await client.startNew(req.params.functionName, undefined, req.body);\n\n context.log(`Started orchestration with ID = '${instanceId}'.`);\n\n return client.createCheckStatusResponse(context.bindingData.req, instanceId);\n};\n\nexport default httpStart;\n"},"function":{"bindings":[{"authLevel":"function","name":"req","type":"httpTrigger","direction":"in","route":"orchestrators/{functionName}","methods":["post","get"]},{"name":"$return","type":"http","direction":"out"},{"name":"starter","type":"orchestrationClient","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsHttpStart","description":"$DurableFunctionsHttpStart_description","name":"Durable Functions HTTP starter","language":"TypeScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsOrchestrator-CSharp","runtime":"2","files":{"run.csx":"/*\r\n * This function is not intended to be invoked directly. Instead it will be\r\n * triggered by an HTTP starter function.\r\n * \r\n * Before running this sample, please:\r\n * - create a Durable activity function (default name is \"Hello\")\r\n * - create a Durable HTTP starter function\r\n */\r\n\r\n#r \"Microsoft.Azure.WebJobs.Extensions.DurableTask\"\r\n\r\npublic static async Task> Run(DurableOrchestrationContext context)\r\n{\r\n var outputs = new List();\r\n\r\n // Replace \"Hello\" with the name of your Durable Activity Function.\r\n outputs.Add(await context.CallActivityAsync(\"Hello\", \"Tokyo\"));\r\n outputs.Add(await context.CallActivityAsync(\"Hello\", \"Seattle\"));\r\n outputs.Add(await context.CallActivityAsync(\"Hello\", \"London\"));\r\n\r\n // returns [\"Hello Tokyo!\", \"Hello Seattle!\", \"Hello London!\"]\r\n return outputs;\r\n}"},"function":{"bindings":[{"name":"context","type":"orchestrationTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsOrchestrator","description":"$DurableFunctionsOrchestrator_description","name":"Durable Functions orchestrator","language":"C#","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsOrchestrator-JavaScript","runtime":"2","files":{"index.js":"/*\n * This function is not intended to be invoked directly. Instead it will be\n * triggered by an HTTP starter function.\n * \n * Before running this sample, please:\n * - create a Durable activity function (default name is \"Hello\")\n * - create a Durable HTTP starter function\n * - run 'npm install durable-functions' from the wwwroot folder of your \n * function app in Kudu\n */\n\nconst df = require(\"durable-functions\");\n\nmodule.exports = df.orchestrator(function* (context) {\n const outputs = [];\n\n // Replace \"Hello\" with the name of your Durable Activity Function.\n outputs.push(yield context.df.callActivity(\"Hello\", \"Tokyo\"));\n outputs.push(yield context.df.callActivity(\"Hello\", \"Seattle\"));\n outputs.push(yield context.df.callActivity(\"Hello\", \"London\"));\n\n // returns [\"Hello Tokyo!\", \"Hello Seattle!\", \"Hello London!\"]\n return outputs;\n});"},"function":{"bindings":[{"name":"context","type":"orchestrationTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsOrchestratorJS","description":"$DurableFunctionsOrchestrator_description","name":"Durable Functions orchestrator","language":"JavaScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"DurableFunctionsOrchestrator-TypeScript","runtime":"2","files":{"index.ts":"/*\n * This function is not intended to be invoked directly. Instead it will be\n * triggered by an HTTP starter function.\n * \n * Before running this sample, please:\n * - create a Durable activity function (default name is \"Hello\")\n * - create a Durable HTTP starter function\n * - run 'npm install durable-functions' from the wwwroot folder of your \n * function app in Kudu\n */\n\nimport * as df from \"durable-functions\"\n\nconst orchestrator = df.orchestrator(function* (context) {\n const outputs = [];\n\n // Replace \"Hello\" with the name of your Durable Activity Function.\n outputs.push(yield context.df.callActivity(\"Hello\", \"Tokyo\"));\n outputs.push(yield context.df.callActivity(\"Hello\", \"Seattle\"));\n outputs.push(yield context.df.callActivity(\"Hello\", \"London\"));\n\n // returns [\"Hello Tokyo!\", \"Hello Seattle!\", \"Hello London!\"]\n return outputs;\n});\n\nexport default orchestrator;\n"},"function":{"bindings":[{"name":"context","type":"orchestrationTrigger","direction":"in"}]},"metadata":{"defaultFunctionName":"DurableFunctionsOrchestratorJS","description":"$DurableFunctionsOrchestrator_description","name":"Durable Functions orchestrator","language":"TypeScript","category":["$temp_category_core","$temp_category_durableFunctions"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.DurableTask","version":"1.8.0"}]}},{"id":"EventGridTrigger-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Azure.EventGrid\"\r\nusing Microsoft.Azure.EventGrid.Models;\r\n\r\npublic static void Run(EventGridEvent eventGridEvent, ILogger log)\r\n{\r\n log.LogInformation(eventGridEvent.Data.ToString());\r\n}\r\n","sample.dat":"{\n \"topic\": \"/subscriptions/5b4b650e-28b9-4790-b3ab-ddbd88d727c4/resourcegroups/test/providers/Microsoft.EventHub/namespaces/test\",\n \"subject\": \"eventhubs/test\",\n \"eventType\": \"captureFileCreated\",\n \"eventTime\": \"2017-07-14T23:10:27.7689666Z\",\n \"id\": \"7b11c4ce-1c34-4416-848b-1730e766f126\",\n \"data\": {\n \"fileUrl\": \"https://test.blob.core.windows.net/debugging/testblob.txt\",\n \"fileType\": \"AzureBlockBlob\",\n \"partitionId\": \"1\",\n \"sizeInBytes\": 0,\n \"eventCount\": 0,\n \"firstSequenceNumber\": -1,\n \"lastSequenceNumber\": -1,\n \"firstEnqueueTime\": \"0001-01-01T00:00:00\",\n \"lastEnqueueTime\": \"0001-01-01T00:00:00\"\n },\n \"dataVersion\": \"\",\n \"metadataVersion\": \"1\" \n}\n"},"function":{"bindings":[{"type":"eventGridTrigger","name":"eventGridEvent","direction":"in"}]},"metadata":{"defaultFunctionName":"EventGridTrigger","description":"$EventGridTrigger_description","name":"Azure Event Grid trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventGrid","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventGrid","version":"2.0.0"}]}},{"id":"EventGridTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, eventGridEvent) {\r\n context.log(typeof eventGridEvent);\r\n context.log(eventGridEvent);\r\n};","sample.dat":"{\n \"topic\": \"/subscriptions/5b4b650e-28b9-4790-b3ab-ddbd88d727c4/resourcegroups/test/providers/Microsoft.EventHub/namespaces/test\",\n \"subject\": \"eventhubs/test\",\n \"eventType\": \"captureFileCreated\",\n \"eventTime\": \"2017-07-14T23:10:27.7689666Z\",\n \"id\": \"7b11c4ce-1c34-4416-848b-1730e766f126\",\n \"data\": {\n \"fileUrl\": \"https://test.blob.core.windows.net/debugging/testblob.txt\",\n \"fileType\": \"AzureBlockBlob\",\n \"partitionId\": \"1\",\n \"sizeInBytes\": 0,\n \"eventCount\": 0,\n \"firstSequenceNumber\": -1,\n \"lastSequenceNumber\": -1,\n \"firstEnqueueTime\": \"0001-01-01T00:00:00\",\n \"lastEnqueueTime\": \"0001-01-01T00:00:00\"\n },\n \"dataVersion\": \"\", \n \"metadataVersion\": \"1\" \n}\n"},"function":{"bindings":[{"type":"eventGridTrigger","name":"eventGridEvent","direction":"in"}]},"metadata":{"defaultFunctionName":"EventGridTrigger","description":"$EventGridTrigger_description","name":"Azure Event Grid trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventGrid","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventGrid","version":"2.0.0"}]}},{"id":"EventGridTrigger-PowerShell","runtime":"2","files":{"run.ps1":"param($eventGridEvent, $TriggerMetadata)\r\n\r\n# Make sure to pass hashtables to Out-String so they're logged correctly\r\n$eventGridEvent | Out-String | Write-Host\r\n","sample.dat":"{\n \"topic\": \"/subscriptions/5b4b650e-28b9-4790-b3ab-ddbd88d727c4/resourcegroups/test/providers/Microsoft.EventHub/namespaces/test\",\n \"subject\": \"eventhubs/test\",\n \"eventType\": \"captureFileCreated\",\n \"eventTime\": \"2017-07-14T23:10:27.7689666Z\",\n \"id\": \"7b11c4ce-1c34-4416-848b-1730e766f126\",\n \"data\": {\n \"fileUrl\": \"https://test.blob.core.windows.net/debugging/testblob.txt\",\n \"fileType\": \"AzureBlockBlob\",\n \"partitionId\": \"1\",\n \"sizeInBytes\": 0,\n \"eventCount\": 0,\n \"firstSequenceNumber\": -1,\n \"lastSequenceNumber\": -1,\n \"firstEnqueueTime\": \"0001-01-01T00:00:00\",\n \"lastEnqueueTime\": \"0001-01-01T00:00:00\"\n },\n \"dataVersion\": \"\", \n \"metadataVersion\": \"1\" \n}\n"},"function":{"bindings":[{"type":"eventGridTrigger","name":"eventGridEvent","direction":"in"}]},"metadata":{"defaultFunctionName":"EventGridTrigger","description":"$EventGridTrigger_description","name":"Azure Event Grid trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventGrid","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventGrid","version":"2.0.0"}]}},{"id":"EventGridTrigger-Python","runtime":"2","files":{"sample.dat":"{\n 'topic': '/subscriptions/5b4b650e-28b9-4790-b3ab-ddbd88d727c4/resourcegroups/test/providers/Microsoft.EventHub/namespaces/test',\n 'subject': 'eventhubs/test',\n 'eventType': 'captureFileCreated',\n 'eventTime': '2017-07-14T23:10:27.7689666Z',\n 'id': '7b11c4ce-1c34-4416-848b-1730e766f126',\n 'data': {\n 'fileUrl': 'https://test.blob.core.windows.net/debugging/testblob.txt',\n 'fileType': 'AzureBlockBlob',\n 'partitionId': '1',\n 'sizeInBytes': 0,\n 'eventCount': 0,\n 'firstSequenceNumber': -1,\n 'lastSequenceNumber': -1,\n 'firstEnqueueTime': '0001-01-01T00:00:00',\n 'lastEnqueueTime': '0001-01-01T00:00:00'\n },\n \"dataVersion\": \"\", \n \"metadataVersion\": \"1\" \n}\n","__init__.py":"import json\r\nimport logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(event: func.EventGridEvent):\r\n result = json.dumps({\r\n 'id': event.id,\r\n 'data': event.get_json(),\r\n 'topic': event.topic,\r\n 'subject': event.subject,\r\n 'event_type': event.event_type,\r\n })\r\n\r\n logging.info('Python EventGrid trigger processed an event: %s', result)\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"type":"eventGridTrigger","name":"event","direction":"in"}]},"metadata":{"defaultFunctionName":"EventGridTrigger","description":"$EventGridTrigger_description","name":"Azure Event Grid trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventGrid","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventGrid","version":"2.0.0"}]}},{"id":"EventGridTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst eventGridTrigger: AzureFunction = async function (context: Context, eventGridEvent: any): Promise {\r\n context.log(typeof eventGridEvent);\r\n context.log(eventGridEvent);\r\n};\r\n\r\nexport default eventGridTrigger;\r\n","sample.dat":"{\n \"topic\": \"/subscriptions/5b4b650e-28b9-4790-b3ab-ddbd88d727c4/resourcegroups/test/providers/Microsoft.EventHub/namespaces/test\",\n \"subject\": \"eventhubs/test\",\n \"eventType\": \"captureFileCreated\",\n \"eventTime\": \"2017-07-14T23:10:27.7689666Z\",\n \"id\": \"7b11c4ce-1c34-4416-848b-1730e766f126\",\n \"data\": {\n \"fileUrl\": \"https://test.blob.core.windows.net/debugging/testblob.txt\",\n \"fileType\": \"AzureBlockBlob\",\n \"partitionId\": \"1\",\n \"sizeInBytes\": 0,\n \"eventCount\": 0,\n \"firstSequenceNumber\": -1,\n \"lastSequenceNumber\": -1,\n \"firstEnqueueTime\": \"0001-01-01T00:00:00\",\n \"lastEnqueueTime\": \"0001-01-01T00:00:00\"\n },\n \"dataVersion\": \"\", \n \"metadataVersion\": \"1\" \n}\n"},"function":{"bindings":[{"type":"eventGridTrigger","name":"eventGridEvent","direction":"in"}]},"metadata":{"defaultFunctionName":"EventGridTrigger","description":"$EventGridTrigger_description","name":"Azure Event Grid trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventGrid","enabledInTryMode":false,"userPrompt":[],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventGrid","version":"2.0.0"}]}},{"id":"EventHubTrigger-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Azure.EventHubs\"\r\n\r\nusing System;\r\nusing System.Text;\r\nusing Microsoft.Azure.EventHubs;\r\n\r\npublic static async Task Run(EventData[] events, ILogger log)\r\n{\r\n var exceptions = new List();\r\n\r\n foreach (EventData eventData in events)\r\n {\r\n try\r\n {\r\n string messageBody = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count);\r\n\r\n // Replace these two lines with your processing logic.\r\n log.LogInformation($\"C# Event Hub trigger function processed a message: {messageBody}\");\r\n await Task.Yield();\r\n }\r\n catch (Exception e)\r\n {\r\n // We need to keep processing the rest of the batch - capture this exception and continue.\r\n // Also, consider capturing details of the message that failed processing so it can be processed again later.\r\n exceptions.Add(e);\r\n }\r\n }\r\n\r\n // Once processing of the batch is complete, if any messages in the batch failed processing throw an exception so that there is a record of the failure.\r\n\r\n if (exceptions.Count > 1)\r\n throw new AggregateException(exceptions);\r\n\r\n if (exceptions.Count == 1)\r\n throw exceptions.Single();\r\n}\r\n","sample.dat":"Test Message"},"function":{"bindings":[{"type":"eventHubTrigger","name":"events","direction":"in","eventHubName":"samples-workitems","cardinality":"many","connection":"","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"EventHubTrigger","description":"$EventHubTrigger_description","name":"Azure Event Hub trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventHub","enabledInTryMode":false,"userPrompt":["connection","eventHubName","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"EventHubTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, eventHubMessages) {\r\n context.log(`JavaScript eventhub trigger function called for message array ${eventHubMessages}`);\r\n \r\n eventHubMessages.forEach((message, index) => {\r\n context.log(`Processed message ${message}`);\r\n });\r\n};","sample.dat":"\"Test Message\""},"function":{"bindings":[{"type":"eventHubTrigger","name":"eventHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"EventHubTrigger","description":"$EventHubTrigger_description","name":"Azure Event Hub trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventHub","enabledInTryMode":false,"userPrompt":["connection","eventHubName","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"EventHubTrigger-PowerShell","runtime":"2","files":{"run.ps1":"param($eventHubMessages, $TriggerMetadata)\r\n\r\nWrite-Host \"PowerShell eventhub trigger function called for message array: $eventHubMessages\"\r\n\r\n$eventHubMessages | ForEach-Object { Write-Host \"Processed message: $_\" }\r\n","sample.dat":"\"Test Message\""},"function":{"bindings":[{"type":"eventHubTrigger","name":"eventHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"EventHubTrigger","description":"$EventHubTrigger_description","name":"Azure Event Hub trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventHub","enabledInTryMode":false,"userPrompt":["connection","eventHubName","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"EventHubTrigger-Python","runtime":"2","files":{"sample.dat":"Test Message","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(event: func.EventHubEvent):\r\n logging.info('Python EventHub trigger processed an event: %s',\r\n event.get_body().decode('utf-8'))\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"type":"eventHubTrigger","name":"event","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"EventHubTrigger","description":"$EventHubTrigger_description","name":"Azure Event Hub trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventHub","enabledInTryMode":false,"userPrompt":["connection","eventHubName","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"EventHubTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst eventHubTrigger: AzureFunction = async function (context: Context, eventHubMessages: any[]): Promise {\r\n context.log(`Eventhub trigger function called for message array ${eventHubMessages}`);\r\n \r\n eventHubMessages.forEach((message, index) => {\r\n context.log(`Processed message ${message}`);\r\n });\r\n};\r\n\r\nexport default eventHubTrigger;\r\n","sample.dat":"\"Test Message\""},"function":{"bindings":[{"type":"eventHubTrigger","name":"eventHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"EventHubTrigger","description":"$EventHubTrigger_description","name":"Azure Event Hub trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"eventHub","enabledInTryMode":false,"userPrompt":["connection","eventHubName","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"HttpTrigger-CSharp","runtime":"2","files":{"readme.md":"# HttpTrigger - C#\r\n\r\nThe `HttpTrigger` makes it incredibly easy to have your functions executed via an HTTP call to your function.\r\n\r\n## How it works\r\n\r\nWhen you call the function, be sure you checkout which security rules you apply. If you're using an apikey, you'll need to include that in your request.\r\n\r\n## Learn more\r\n\r\n Documentation","run.csx":"#r \"Newtonsoft.Json\"\r\n\r\nusing System.Net;\r\nusing Microsoft.AspNetCore.Mvc;\r\nusing Microsoft.Extensions.Primitives;\r\nusing Newtonsoft.Json;\r\n\r\npublic static async Task Run(HttpRequest req, ILogger log)\r\n{\r\n log.LogInformation(\"C# HTTP trigger function processed a request.\");\r\n\r\n string name = req.Query[\"name\"];\r\n\r\n string requestBody = await new StreamReader(req.Body).ReadToEndAsync();\r\n dynamic data = JsonConvert.DeserializeObject(requestBody);\r\n name = name ?? data?.name;\r\n\r\n return name != null\r\n ? (ActionResult)new OkObjectResult($\"Hello, {name}\")\r\n : new BadRequestObjectResult(\"Please pass a name on the query string or in the request body\");\r\n}\r\n","sample.dat":"{\r\n \"name\": \"Azure\"\r\n}"},"function":{"bindings":[{"authLevel":"function","name":"req","type":"httpTrigger","direction":"in","methods":["get","post"]},{"name":"$return","type":"http","direction":"out"}]},"metadata":{"defaultFunctionName":"HttpTrigger","description":"$HttpTrigger_description","name":"HTTP trigger","language":"C#","category":["$temp_category_core","$temp_category_api"],"categoryStyle":"http","enabledInTryMode":true,"userPrompt":["authLevel"]}},{"id":"HttpTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, req) {\r\n context.log('JavaScript HTTP trigger function processed a request.');\r\n\r\n if (req.query.name || (req.body && req.body.name)) {\r\n context.res = {\r\n // status: 200, /* Defaults to 200 */\r\n body: \"Hello \" + (req.query.name || req.body.name)\r\n };\r\n }\r\n else {\r\n context.res = {\r\n status: 400,\r\n body: \"Please pass a name on the query string or in the request body\"\r\n };\r\n }\r\n};","sample.dat":"{\r\n \"name\": \"Azure\"\r\n}"},"function":{"bindings":[{"authLevel":"function","type":"httpTrigger","direction":"in","name":"req","methods":["get","post"]},{"type":"http","direction":"out","name":"res"}]},"metadata":{"defaultFunctionName":"HttpTrigger","description":"$HttpTrigger_description","name":"HTTP trigger","language":"JavaScript","trigger":"HttpTrigger","category":["$temp_category_core","$temp_category_api"],"categoryStyle":"http","enabledInTryMode":true,"userPrompt":["authLevel"]}},{"id":"HttpTrigger-PowerShell","runtime":"2","files":{"run.ps1":"using namespace System.Net\n\n# Input bindings are passed in via param block.\nparam($Request, $TriggerMetadata)\n\n# Write to the Azure Functions log stream.\nWrite-Host \"PowerShell HTTP trigger function processed a request.\"\n\n# Interact with query parameters or the body of the request.\n$name = $Request.Query.Name\nif (-not $name) {\n $name = $Request.Body.Name\n}\n\nif ($name) {\n $status = [HttpStatusCode]::OK\n $body = \"Hello $name\"\n}\nelse {\n $status = [HttpStatusCode]::BadRequest\n $body = \"Please pass a name on the query string or in the request body.\"\n}\n\n# Associate values to output bindings by calling 'Push-OutputBinding'.\nPush-OutputBinding -Name Response -Value ([HttpResponseContext]@{\n StatusCode = $status\n Body = $body\n})\n","sample.dat":"{\r\n \"name\": \"Azure\"\r\n}\r\n"},"function":{"bindings":[{"authLevel":"function","type":"httpTrigger","direction":"in","name":"Request","methods":["get","post"]},{"type":"http","direction":"out","name":"Response"}]},"metadata":{"defaultFunctionName":"HttpTrigger","description":"$HttpTrigger_description","name":"HTTP trigger","language":"PowerShell","trigger":"HttpTrigger","category":["$temp_category_core","$temp_category_api"],"categoryStyle":"http","enabledInTryMode":true,"userPrompt":["authLevel"]}},{"id":"HttpTrigger-Python","runtime":"2","files":{"sample.dat":"{\r\n \"name\": \"Azure\"\r\n}","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(req: func.HttpRequest) -> func.HttpResponse:\r\n logging.info('Python HTTP trigger function processed a request.')\r\n\r\n name = req.params.get('name')\r\n if not name:\r\n try:\r\n req_body = req.get_json()\r\n except ValueError:\r\n pass\r\n else:\r\n name = req_body.get('name')\r\n\r\n if name:\r\n return func.HttpResponse(f\"Hello {name}!\")\r\n else:\r\n return func.HttpResponse(\r\n \"Please pass a name on the query string or in the request body\",\r\n status_code=400\r\n )\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"authLevel":"function","type":"httpTrigger","direction":"in","name":"req","methods":["get","post"]},{"type":"http","direction":"out","name":"$return"}]},"metadata":{"defaultFunctionName":"HttpTrigger","description":"$HttpTrigger_description","name":"HTTP trigger","language":"Python","trigger":"HttpTrigger","category":["$temp_category_core","$temp_category_api"],"categoryStyle":"http","enabledInTryMode":true,"userPrompt":["authLevel"],"filters":["Python3"]}},{"id":"HttpTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context, HttpRequest } from \"@azure/functions\"\r\n\r\nconst httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {\r\n context.log('HTTP trigger function processed a request.');\r\n const name = (req.query.name || (req.body && req.body.name));\r\n\r\n if (name) {\r\n context.res = {\r\n // status: 200, /* Defaults to 200 */\r\n body: \"Hello \" + (req.query.name || req.body.name)\r\n };\r\n }\r\n else {\r\n context.res = {\r\n status: 400,\r\n body: \"Please pass a name on the query string or in the request body\"\r\n };\r\n }\r\n};\r\n\r\nexport default httpTrigger;\r\n","sample.dat":"{\r\n \"name\": \"Azure\"\r\n}"},"function":{"bindings":[{"authLevel":"function","type":"httpTrigger","direction":"in","name":"req","methods":["get","post"]},{"type":"http","direction":"out","name":"res"}]},"metadata":{"defaultFunctionName":"HttpTrigger","description":"$HttpTrigger_description","name":"HTTP trigger","language":"TypeScript","trigger":"HttpTrigger","category":["$temp_category_core","$temp_category_api"],"categoryStyle":"http","enabledInTryMode":true,"userPrompt":["authLevel"]}},{"id":"IoTHubTrigger-CSharp","runtime":"2","files":{"run.csx":"using System;\n\npublic static void Run(string myIoTHubMessage, ILogger log)\n{\n log.LogInformation($\"C# IoT Hub trigger function processed a message: {myIoTHubMessage}\");\n}","sample.dat":"Test Message"},"function":{"bindings":[{"type":"eventHubTrigger","name":"myIoTHubMessage","direction":"in","eventHubName":"samples-workitems","connection":"","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"IoTHub_EventHub","description":"$IoTHubTrigger_description","name":"IoT Hub (Event Hub)","language":"C#","category":["$temp_category_IoTHub"],"categoryStyle":"iot","enabledInTryMode":false,"userPrompt":["connection","path","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"IoTHubTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = function (context, IoTHubMessages) {\n context.log(`JavaScript eventhub trigger function called for message array: ${IoTHubMessages}`);\n \n IoTHubMessages.forEach(message => {\n context.log(`Processed message: ${message}`);\n });\n\n context.done();\n};","sample.dat":"Test Message"},"function":{"bindings":[{"type":"eventHubTrigger","name":"IoTHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"IoTHub_EventHub","description":"$IoTHubTrigger_description","name":"IoT Hub (Event Hub)","language":"JavaScript","category":["$temp_category_IoTHub"],"categoryStyle":"iot","enabledInTryMode":false,"userPrompt":["connection","path","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"IoTHubTrigger-PowerShell","runtime":"2","files":{"run.ps1":"param($IoTHubMessages, $TriggerMetadata)\n\nWrite-Host \"PowerShell eventhub trigger function called for message array: $IoTHubMessages\"\n\n$IoTHubMessages | ForEach-Object { Write-Host \"Processed message: $_\" }\n","sample.dat":"Test Message"},"function":{"bindings":[{"type":"eventHubTrigger","name":"IoTHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"IoTHub_EventHub","description":"$IoTHubTrigger_description","name":"IoT Hub (Event Hub)","language":"PowerShell","category":["$temp_category_IoTHub"],"categoryStyle":"iot","enabledInTryMode":false,"userPrompt":["connection","path","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"IoTHubTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\n\nconst IoTHubTrigger: AzureFunction = async function (context: Context, IoTHubMessages: any[]): Promise {\n context.log(`Eventhub trigger function called for message array: ${IoTHubMessages}`);\n \n IoTHubMessages.forEach(message => {\n context.log(`Processed message: ${message}`);\n });\n};\n\nexport default IoTHubTrigger;\n","sample.dat":"Test Message"},"function":{"bindings":[{"type":"eventHubTrigger","name":"IoTHubMessages","direction":"in","eventHubName":"samples-workitems","connection":"","cardinality":"many","consumerGroup":"$Default"}]},"metadata":{"defaultFunctionName":"IoTHub_EventHub","description":"$IoTHubTrigger_description","name":"IoT Hub (Event Hub)","language":"TypeScript","category":["$temp_category_IoTHub"],"categoryStyle":"iot","enabledInTryMode":false,"userPrompt":["connection","path","consumerGroup"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.EventHubs","version":"3.0.3"}]}},{"id":"OutlookMessageWebhookCreator-CSharp","runtime":"2","files":{"run.csx":"using System;\r\nusing System.Net;\r\n\r\npublic static HttpResponseMessage run(HttpRequestMessage req, out string clientState, ILogger log)\r\n{\r\n log.LogInformation(\"C# HTTP trigger function processed a request.\");\r\n\tclientState = Guid.NewGuid().ToString();\r\n\treturn new HttpResponseMessage(HttpStatusCode.OK);\r\n}"},"function":{"bindings":[{"name":"req","type":"httpTrigger","direction":"in"},{"type":"graphWebhookSubscription","name":"clientState","direction":"out","action":"create","subscriptionResource":"me/mailFolders('Inbox')/messages","changeTypes":["created"],"identity":"userFromRequest"},{"type":"http","name":"$return","direction":"out"}]},"metadata":{"defaultFunctionName":"OutlookMessageWebhookCreator","description":"$OutlookMessageWebhookCreator_description","name":"Outlook message webhook creator","language":"C#","enabledInTryMode":false,"category":["$temp_category_msgraph"],"categoryStyle":"other","extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph","version":"1.0.0-beta6"}],"AADPermissions":[{"resourceAppId":"00000002-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"311a71cc-e848-46a1-bdf8-97ff7156d8e6"}]},{"resourceAppId":"00000003-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"570282fd-fa5c-430d-a7fd-fc8dc98a9dca"}]}]}},{"id":"OutlookMessageWebhookDeleter-CSharp","runtime":"2","files":{"run.csx":"#r \"Newtonsoft.Json\"\r\n#r \"Microsoft.Graph\"\r\n\r\nusing Microsoft.Graph;\r\nusing System.Net;\r\nusing Microsoft.AspNetCore.Mvc;\r\nusing Microsoft.Extensions.Primitives;\r\nusing Newtonsoft.Json;\r\n\r\npublic static async Task Run(HttpRequest req, Subscription[] existingSubscriptions, IAsyncCollector subscriptionsToDelete, ILogger log)\r\n{\r\n log.LogInformation(\"C# HTTP trigger function processed a request.\");\r\n foreach (var subscription in existingSubscriptions)\r\n {\r\n log.LogInformation($\"Deleting subscription {subscription.Id}\");\r\n await subscriptionsToDelete.AddAsync(subscription.Id);\r\n }\r\n}"},"function":{"bindings":[{"name":"req","type":"httpTrigger","direction":"in"},{"type":"graphWebhookSubscription","name":"existingSubscriptions","direction":"in","filter":"userFromRequest"},{"type":"graphWebhookSubscription","name":"subscriptionsToDelete","direction":"out","action":"delete","identity":"userFromRequest"},{"type":"http","name":"res","direction":"out"}]},"metadata":{"defaultFunctionName":"OutlookMessageWebhookDeleter","description":"$OutlookMessageWebhookDeleter_description","name":"Outlook message webhook deleter","language":"C#","enabledInTryMode":false,"category":["$temp_category_msgraph"],"categoryStyle":"other","extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph","version":"1.0.0-beta6"}],"AADPermissions":[{"resourceAppId":"00000002-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"311a71cc-e848-46a1-bdf8-97ff7156d8e6"}]},{"resourceAppId":"00000003-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"570282fd-fa5c-430d-a7fd-fc8dc98a9dca"}]}]}},{"id":"OutlookMessageWebhookHandler-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Graph\"\r\nusing Microsoft.Graph;\r\nusing System.Net;\r\n\r\npublic static async Task Run(Message msg, ILogger log) \r\n{\r\n\tlog.LogInformation(\"Microsoft Graph webhook trigger function processed a request.\");\r\n\r\n // Testable by sending an email with the Subject \"[DEMO] Azure Functions\" and some text body\r\n if (msg.Subject.Contains(\"[DEMO] Azure Functions\")) {\r\n log.LogInformation($\"Processed email: {msg.BodyPreview}\");\r\n }\r\n}"},"function":{"bindings":[{"name":"msg","type":"graphWebhookTrigger","direction":"in","resourceType":"#Microsoft.Graph.Message"}]},"metadata":{"defaultFunctionName":"OutlookMessageWebhookHandler","description":"$OutlookMessageWebhookHandler_description","name":"Outlook message webhook handler","language":"C#","enabledInTryMode":false,"category":["$temp_category_msgraph"],"categoryStyle":"other","extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph","version":"1.0.0-beta6"}],"AADPermissions":[{"resourceAppId":"00000002-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"311a71cc-e848-46a1-bdf8-97ff7156d8e6"}]},{"resourceAppId":"00000003-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"570282fd-fa5c-430d-a7fd-fc8dc98a9dca"}]}]}},{"id":"OutlookMessageWebhookRefresher-CSharp","runtime":"2","files":{"run.csx":"#r \"Microsoft.Azure.WebJobs.Extensions.Tokens\"\r\n#r \"Microsoft.Azure.WebJobs.Extensions.O365\"\r\n\r\nusing System.Net;\r\nusing Microsoft.AspNetCore.Mvc;\r\nusing Microsoft.Extensions.Primitives;\r\nusing Microsoft.Azure.WebJobs;\r\n\r\npublic static async Task Run(TimerInfo myTimer, UserSubscription[] existingSubscriptions, IBinder binder, ILogger log)\r\n{\r\n log.LogInformation($\"C# Timer trigger function executed at: {DateTime.Now}\");\r\n foreach (var subscription in existingSubscriptions)\r\n {\r\n // binding in code to allow dynamic identity\r\n var subscriptionsToRefresh = await binder.BindAsync>(\r\n new GraphWebhookSubscriptionAttribute()\r\n {\r\n Action = GraphWebhookSubscriptionAction.Refresh,\r\n Identity = TokenIdentityMode.UserFromId,\r\n UserId = subscription.UserId\r\n }\r\n );\r\n {\r\n log.LogInformation($\"Refreshing subscription {subscription.Id}\");\r\n await subscriptionsToRefresh.AddAsync(subscription.Id);\r\n }\r\n }\r\n}\r\n\r\npublic class UserSubscription {\r\n public string UserId {get; set;}\r\n public string Id {get; set;}\r\n}"},"function":{"bindings":[{"name":"myTimer","type":"timerTrigger","direction":"in","schedule":"0 * * */2 * *"},{"type":"graphWebhookSubscription","name":"existingSubscriptions","direction":"in"}]},"metadata":{"defaultFunctionName":"OutlookMessageWebhookRefresher","description":"$OutlookMessageWebhookRefresher_description","name":"Outlook message webhook refresher","language":"C#","enabledInTryMode":false,"category":["$temp_category_msgraph"],"categoryStyle":"other","extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph","version":"1.0.0-beta6"}],"AADPermissions":[{"resourceAppId":"00000002-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"311a71cc-e848-46a1-bdf8-97ff7156d8e6"}]},{"resourceAppId":"00000003-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"570282fd-fa5c-430d-a7fd-fc8dc98a9dca"}]}]}},{"id":"ProfilePhotoAPI-CSharp","runtime":"2","files":{"run.csx":"using System.Net; \r\nusing System.Net.Http; \r\nusing System.Net.Http.Headers; \r\n\r\npublic static async Task Run(HttpRequestMessage req, string graphToken, ILogger log)\r\n{\r\n log.LogInformation(\"C# HTTP trigger function processed a request.\"); \r\n HttpClient client = new HttpClient();\r\n client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", graphToken);\r\n return await client.GetAsync(\"https://graph.microsoft.com/v1.0/me/photo/$value\");\r\n}"},"function":{"bindings":[{"name":"req","type":"httpTrigger","direction":"in","authLevel":"anonymous","methods":["get"]},{"name":"graphToken","type":"token","direction":"in","resource":"https://graph.microsoft.com","identity":"userFromRequest"},{"name":"$return","type":"http","direction":"out"}]},"metadata":{"defaultFunctionName":"ProfilePhotoAPI","description":"$ProfilePhotoAPI_description","name":"Microsoft Graph profile photo API","trigger":"HttpTrigger","language":"C#","enabledInTryMode":false,"category":["$temp_category_msgraph"],"categoryStyle":"other","extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.AuthTokens","version":"1.0.0-beta6"}],"AADPermissions":[{"resourceAppId":"00000002-0000-0000-c000-000000000000","resourceAccess":[{"type":"Scope","id":"311a71cc-e848-46a1-bdf8-97ff7156d8e6"}]}]}},{"id":"QueueTrigger-CSharp","runtime":"2","files":{"readme.md":"# QueueTrigger - C#\r\n\r\nThe `QueueTrigger` makes it incredibly easy to react to new Queues inside of Azure Queue Storage. This sample demonstrates a simple use case of processing data from a given Queue using C#.\r\n\r\n## How it works\r\n\r\nFor a `QueueTrigger` to work, you must provide a queue name that defines the queue messages will be read from.\r\n\r\n## Learn more\r\n\r\n Documentation","run.csx":"using System;\r\n\r\npublic static void Run(string myQueueItem, ILogger log)\r\n{\r\n log.LogInformation($\"C# Queue trigger function processed: {myQueueItem}\");\r\n}\r\n","sample.dat":"sample queue data"},"function":{"bindings":[{"name":"myQueueItem","type":"queueTrigger","direction":"in","queueName":"myqueue-items","connection":""}]},"metadata":{"defaultFunctionName":"QueueTrigger","description":"$QueueTrigger_description","name":"Azure Queue Storage trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"queue","enabledInTryMode":true,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"QueueTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, myQueueItem) {\r\n context.log('JavaScript queue trigger function processed work item', myQueueItem);\r\n};","readme.md":"# QueueTrigger - JavaScript\r\n\r\nThe `QueueTrigger` makes it incredibly easy to react to new Queues inside of Azure Queue Storage. This sample demonstrates a simple use case of processing data from a given Queue using C#.\r\n\r\n## How it works\r\n\r\nFor a `QueueTrigger` to work, you provide a path which dictates where the queue messages are located inside your container.\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":"sample queue data"},"function":{"bindings":[{"name":"myQueueItem","type":"queueTrigger","direction":"in","queueName":"js-queue-items","connection":""}]},"metadata":{"defaultFunctionName":"QueueTrigger","description":"$QueueTrigger_description","name":"Azure Queue Storage trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"queue","enabledInTryMode":true,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"QueueTrigger-PowerShell","runtime":"2","files":{"readme.md":"# QueueTrigger - PowerShell\r\n\r\nThe `QueueTrigger` makes it incredibly easy to react to new Queues inside of [Azure Queue Storage](https://azure.microsoft.com/en-us/services/storage/queues/).\r\nThis sample demonstrates a simple use case of processing data from a given Queue using PowerShell.\r\n\r\n## How it works\r\n\r\nFor a `QueueTrigger` to work, you provide a path which dictates where the queue messages are located inside your container.\r\n\r\n## Learn more\r\n\r\n Documentation\r\n","run.ps1":"# Input bindings are passed in via param block.\r\nparam([string] $QueueItem, $TriggerMetadata)\r\n\r\n# Write out the queue message and insertion time to the information log.\r\nWrite-Host \"PowerShell queue trigger function processed work item: $QueueItem\"\r\nWrite-Host \"Queue item insertion time: $($TriggerMetadata.InsertionTime)\"\r\n","sample.dat":"sample queue data"},"function":{"bindings":[{"name":"QueueItem","type":"queueTrigger","direction":"in","queueName":"ps-queue-items","connection":""}]},"metadata":{"defaultFunctionName":"QueueTrigger","description":"$QueueTrigger_description","name":"Azure Queue Storage trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"queue","enabledInTryMode":true,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"QueueTrigger-Python","runtime":"2","files":{"readme.md":"# QueueTrigger - Python\r\n\r\nThe `QueueTrigger` makes it incredibly easy to react to new Queues inside of Azure Queue Storage. This sample demonstrates a simple use case of processing data from a given Queue.\r\n\r\n## How it works\r\n\r\nFor a `QueueTrigger` to work, you provide a path which dictates where the queue messages are located inside your container.\r\n\r\n## Learn more\r\n\r\n Documentation\r\n","sample.dat":"sample queue data","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(msg: func.QueueMessage) -> None:\r\n logging.info('Python queue trigger function processed a queue item: %s',\r\n msg.get_body().decode('utf-8'))\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"name":"msg","type":"queueTrigger","direction":"in","queueName":"python-queue-items","connection":""}]},"metadata":{"defaultFunctionName":"QueueTrigger","description":"$QueueTrigger_description","name":"Azure Queue Storage trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"queue","enabledInTryMode":true,"userPrompt":["connection","queueName"],"filters":["Python3"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"QueueTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst queueTrigger: AzureFunction = async function (context: Context, myQueueItem: string): Promise {\r\n context.log('Queue trigger function processed work item', myQueueItem);\r\n};\r\n\r\nexport default queueTrigger;\r\n","readme.md":"# QueueTrigger - TypeScript\r\n\r\nThe `QueueTrigger` makes it incredibly easy to react to new Queues inside of Azure Queue Storage. This sample demonstrates a simple use case of processing data from a given Queue using C#.\r\n\r\n## How it works\r\n\r\nFor a `QueueTrigger` to work, you provide a path which dictates where the queue messages are located inside your container.\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":"sample queue data"},"function":{"bindings":[{"name":"myQueueItem","type":"queueTrigger","direction":"in","queueName":"js-queue-items","connection":""}]},"metadata":{"defaultFunctionName":"QueueTrigger","description":"$QueueTrigger_description","name":"Azure Queue Storage trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"queue","enabledInTryMode":true,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"SendGrid-CSharp","runtime":"2","files":{"run.csx":"// The 'From' and 'To' fields are automatically populated with the values specified by the binding settings.\r\n//\r\n// You can also optionally configure the default From/To addresses globally via host.config, e.g.:\r\n//\r\n// {\r\n// \"sendGrid\": {\r\n// \"to\": \"user@host.com\",\r\n// \"from\": \"Azure Functions \"\r\n// }\r\n// }\r\n#r \"SendGrid\"\r\n\r\nusing System;\r\nusing SendGrid.Helpers.Mail;\r\nusing Microsoft.Azure.WebJobs.Host;\r\n\r\npublic static SendGridMessage Run(Order order, ILogger log)\r\n{\r\n log.LogInformation($\"C# Queue trigger function processed order: {order.OrderId}\");\r\n\r\n SendGridMessage message = new SendGridMessage()\r\n {\r\n Subject = $\"Thanks for your order (#{order.OrderId})!\"\r\n };\r\n\r\n message.AddContent(\"text/plain\", $\"{order.CustomerName}, your order ({order.OrderId}) is being processed!\");\r\n return message;\r\n}\r\npublic class Order\r\n{\r\n public string OrderId { get; set; }\r\n public string CustomerName { get; set; }\r\n public string CustomerEmail { get; set; }\r\n}\r\n","sample.dat":"{ \"OrderId\": 12345, \"CustomerName\": \"Joe Schmoe\", \"CustomerEmail\": \"joeschmoe@foo.com\" }"},"function":{"bindings":[{"type":"queueTrigger","name":"order","direction":"in","queueName":"samples-orders"},{"type":"sendGrid","name":"$return","direction":"out","apiKey":"SendGridApiKey","from":"Azure Functions ","to":"{CustomerEmail}"}]},"metadata":{"defaultFunctionName":"SendGrid","description":"$SendGrid_description","name":"SendGrid","language":"C#","category":["$temp_category_samples","$temp_category_dataProcessing"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":["to","from","apiKey"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.SendGrid","version":"3.0.0"},{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"SendGrid-JavaScript","runtime":"2","files":{"index.js":"var util = require('util');\r\n\r\n// The 'From' and 'To' fields are automatically populated with the values specified by the binding settings.\r\n//\r\n// You can also optionally configure the default From/To addresses globally via host.config, e.g.:\r\n//\r\n// {\r\n// \"sendGrid\": {\r\n// \"to\": \"user@host.com\",\r\n// \"from\": \"Azure Functions \"\r\n// }\r\n// }\r\nmodule.exports = async function (context, order) {\r\n context.log('JavaScript queue trigger function processed order', order.orderId);\r\n\r\n context.bindings.message = {\r\n subject: util.format('Thanks for your order (#%d)!', order.orderId),\r\n content: [{\r\n type: 'text/plain',\r\n value: util.format(\"%s, your order (%d) is being processed!\", order.customerName, order.orderId)\r\n }]\r\n };\r\n}","sample.dat":"{ \"orderId\": 12345, \"customerName\": \"Joe Schmoe\", \"customerEmail\": \"joeschmoe@foo.com\" }"},"function":{"bindings":[{"type":"queueTrigger","name":"order","direction":"in","queueName":"samples-orders"},{"type":"sendGrid","name":"message","direction":"out","apiKey":"SendGridApiKey","from":"Azure Functions ","to":"{customerEmail}","subject":"","text":""}]},"metadata":{"defaultFunctionName":"SendGrid","description":"$SendGrid_description","name":"SendGrid","language":"JavaScript","category":["$temp_category_samples","$temp_category_dataProcessing"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":["to","from","subject","text","apiKey"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.SendGrid","version":"3.0.0"},{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"SendGrid-PowerShell","runtime":"2","files":{"run.ps1":"# The 'From' and 'To' fields are automatically populated with the values specified by the binding settings.\r\n#\r\n# You can also optionally configure the default From/To addresses globally via host.config, e.g.:\r\n#\r\n# {\r\n# \"sendGrid\": {\r\n# \"to\": \"user@host.com\",\r\n# \"from\": \"Azure Functions \"\r\n# }\r\n# }\r\nparam($order, $TriggerMetadata)\r\n\r\nWrite-Host \"JavaScript queue trigger function processed order: $($order.orderId)\"\r\n\r\nPush-OutputBinding -Name Response -Value (@{\r\n subject = \"Thanks for your order (#$($order.orderId))!\"\r\n content = @(@{\r\n type = 'text/plain'\r\n value = \"$($order.customerName), your order ($($order.orderId)) is being processed!\"\r\n })\r\n})\r\n","sample.dat":"{ \"orderId\": 12345, \"customerName\": \"Joe Schmoe\", \"customerEmail\": \"joeschmoe@foo.com\" }"},"function":{"bindings":[{"type":"queueTrigger","name":"order","direction":"in","queueName":"samples-orders"},{"type":"sendGrid","name":"message","direction":"out","apiKey":"SendGridApiKey","from":"Azure Functions ","to":"{customerEmail}","subject":"","text":""}]},"metadata":{"defaultFunctionName":"SendGrid","description":"$SendGrid_description","name":"SendGrid","language":"PowerShell","category":["$temp_category_samples","$temp_category_dataProcessing"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":["to","from","subject","text","apiKey"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.SendGrid","version":"3.0.0"},{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"SendGrid-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\n// The 'From' and 'To' fields are automatically populated with the values specified by the binding settings.\r\n//\r\n// You can also optionally configure the default From/To addresses globally via host.config, e.g.:\r\n//\r\n// {\r\n// \"sendGrid\": {\r\n// \"to\": \"user@host.com\",\r\n// \"from\": \"Azure Functions \"\r\n// }\r\n// }\r\n\r\nconst sendGrid: AzureFunction = async function (context: Context, order: any): Promise {\r\n context.log('Queue trigger function processed order', order.orderId);\r\n\r\n context.bindings.message = {\r\n subject: `Thanks for your order (#${order.orderId})!`,\r\n content: [{\r\n type: 'text/plain',\r\n value: `${order.customerName}, your order (${order.orderId}) is being processed!`\r\n }]\r\n };\r\n}\r\n\r\nexport default sendGrid;\r\n","sample.dat":"{ \"orderId\": 12345, \"customerName\": \"Joe Schmoe\", \"customerEmail\": \"joeschmoe@foo.com\" }"},"function":{"bindings":[{"type":"queueTrigger","name":"order","direction":"in","queueName":"samples-orders"},{"type":"sendGrid","name":"message","direction":"out","apiKey":"SendGridApiKey","from":"Azure Functions ","to":"{customerEmail}","subject":"","text":""}]},"metadata":{"defaultFunctionName":"SendGrid","description":"$SendGrid_description","name":"SendGrid","language":"TypeScript","category":["$temp_category_samples","$temp_category_dataProcessing"],"categoryStyle":"other","enabledInTryMode":false,"userPrompt":["to","from","subject","text","apiKey"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.SendGrid","version":"3.0.0"},{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.4"}]}},{"id":"ServiceBusQueueTrigger-CSharp","runtime":"2","files":{"run.csx":"using System;\r\nusing System.Threading.Tasks;\r\n\r\npublic static void Run(string myQueueItem, ILogger log)\r\n{\r\n log.LogInformation($\"C# ServiceBus queue trigger function processed message: {myQueueItem}\");\r\n}\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"myQueueItem","type":"serviceBusTrigger","direction":"in","queueName":"myqueue","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusQueueTrigger","description":"$ServiceBusQueueTrigger_description","name":"Azure Service Bus Queue trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusQueueTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function(context, mySbMsg) {\r\n context.log('JavaScript ServiceBus queue trigger function processed message', mySbMsg);\r\n};","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","queueName":"myinputqueue","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusQueueTrigger","description":"$ServiceBusQueueTrigger_description","name":"Azure Service Bus Queue trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusQueueTrigger-PowerShell","runtime":"2","files":{"run.ps1":"param([string] $mySbMsg, $TriggerMetadata)\r\n\r\nWrite-Host \"PowerShell ServiceBus queue trigger function processed message: $mySbMsg\"\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","queueName":"myinputqueue","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusQueueTrigger","description":"$ServiceBusQueueTrigger_description","name":"Azure Service Bus Queue trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusQueueTrigger-Python","runtime":"2","files":{"sample.dat":"Service Bus Message","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(msg: func.ServiceBusMessage):\r\n logging.info('Python ServiceBus queue trigger processed message: %s',\r\n msg.get_body().decode('utf-8'))\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"name":"msg","type":"serviceBusTrigger","direction":"in","queueName":"myinputqueue","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusQueueTrigger","description":"$ServiceBusQueueTrigger_description","name":"Azure Service Bus Queue trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusQueueTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst serviceBusQueueTrigger: AzureFunction = async function(context: Context, mySbMsg: any): Promise {\r\n context.log('ServiceBus queue trigger function processed message', mySbMsg);\r\n};\r\n\r\nexport default serviceBusQueueTrigger;\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","queueName":"myinputqueue","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusQueueTrigger","description":"$ServiceBusQueueTrigger_description","name":"Azure Service Bus Queue trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","queueName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusTopicTrigger-CSharp","runtime":"2","files":{"run.csx":"using System;\r\nusing System.Threading.Tasks;\r\n\r\npublic static void Run(string mySbMsg, ILogger log)\r\n{\r\n log.LogInformation($\"C# ServiceBus topic trigger function processed message: {mySbMsg}\");\r\n}\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","topicName":"mytopic","subscriptionName":"mysubscription","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusTopicTrigger","description":"$ServiceBusTopicTrigger_description","name":"Azure Service Bus Topic trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","topicName","subscriptionName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusTopicTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function(context, mySbMsg) {\r\n context.log('JavaScript ServiceBus topic trigger function processed message', mySbMsg);\r\n};","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","topicName":"mytopic","subscriptionName":"mysubscription","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusTopicTrigger","description":"$ServiceBusTopicTrigger_description","name":"Azure Service Bus Topic trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","topicName","subscriptionName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusTopicTrigger-PowerShell","runtime":"2","files":{"run.ps1":"param([string] $mySbMsg, $TriggerMetadata)\r\n\r\nWrite-Host \"PowerShell ServiceBus topic trigger function processed message: $mySbMsg\"\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","topicName":"mytopic","subscriptionName":"mysubscription","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusTopicTrigger","description":"$ServiceBusTopicTrigger_description","name":"Azure Service Bus Topic trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","topicName","subscriptionName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusTopicTrigger-Python","runtime":"2","files":{"sample.dat":"Service Bus Message","__init__.py":"import logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(msg: func.ServiceBusMessage):\r\n logging.info('Python ServiceBus topic trigger processed message: %s',\r\n msg.get_body().decode('utf-8'))\r\n"},"function":{"bindings":[{"name":"msg","type":"serviceBusTrigger","direction":"in","topicName":"mytopic","subscriptionName":"mysubscription","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusTopicTrigger","description":"$ServiceBusTopicTrigger_description","name":"Azure Service Bus Topic trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","topicName","subscriptionName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"ServiceBusTopicTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst serviceBusTopicTrigger: AzureFunction = async function(context: Context, mySbMsg: any): Promise {\r\n context.log('ServiceBus topic trigger function processed message', mySbMsg);\r\n};\r\n\r\nexport default serviceBusTopicTrigger;\r\n","sample.dat":"Service Bus Message"},"function":{"bindings":[{"name":"mySbMsg","type":"serviceBusTrigger","direction":"in","topicName":"mytopic","subscriptionName":"mysubscription","connection":""}]},"metadata":{"defaultFunctionName":"ServiceBusTopicTrigger","description":"$ServiceBusTopicTrigger_description","name":"Azure Service Bus Topic trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"serviceBus","enabledInTryMode":false,"userPrompt":["connection","topicName","subscriptionName"],"extensions":[{"id":"Microsoft.Azure.WebJobs.Extensions.ServiceBus","version":"3.0.3"}]}},{"id":"TimerTrigger-CSharp","runtime":"2","files":{"readme.md":"# TimerTrigger - C#\r\n\r\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\r\n\r\n## How it works\r\n\r\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\r\n\r\n## Learn more\r\n\r\n Documentation","run.csx":"using System;\r\n\r\npublic static void Run(TimerInfo myTimer, ILogger log)\r\n{\r\n log.LogInformation($\"C# Timer trigger function executed at: {DateTime.Now}\");\r\n}\r\n","sample.dat":""},"function":{"bindings":[{"name":"myTimer","type":"timerTrigger","direction":"in","schedule":"0 */5 * * * *"}]},"metadata":{"defaultFunctionName":"TimerTrigger","description":"$TimerTrigger_description","name":"Timer trigger","language":"C#","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"timer","enabledInTryMode":true,"userPrompt":["schedule"]}},{"id":"TimerTrigger-JavaScript","runtime":"2","files":{"index.js":"module.exports = async function (context, myTimer) {\r\n var timeStamp = new Date().toISOString();\r\n \r\n if (myTimer.IsPastDue)\r\n {\r\n context.log('JavaScript is running late!');\r\n }\r\n context.log('JavaScript timer trigger function ran!', timeStamp); \r\n};","readme.md":"# TimerTrigger - JavaScript\r\n\r\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\r\n\r\n## How it works\r\n\r\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\r\n\r\n## Learn more\r\n\r\n Documentation","sample.dat":""},"function":{"bindings":[{"name":"myTimer","type":"timerTrigger","direction":"in","schedule":"0 */5 * * * *"}]},"metadata":{"defaultFunctionName":"TimerTrigger","description":"$TimerTrigger_description","name":"Timer trigger","language":"JavaScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"timer","enabledInTryMode":true,"userPrompt":["schedule"]}},{"id":"TimerTrigger-PowerShell","runtime":"2","files":{"readme.md":"# TimerTrigger - PowerShell\r\n\r\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\r\n\r\n## How it works\r\n\r\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\r\n\r\n## Learn more\r\n\r\n Documentation\r\n","run.ps1":"# Input bindings are passed in via param block.\nparam($Timer)\n\n# Get the current universal time in the default string format\n$currentUTCtime = (Get-Date).ToUniversalTime()\n\n# The 'IsPastDue' porperty is 'true' when the current function invocation is later than scheduled.\nif ($Timer.IsPastDue) {\n Write-Host \"PowerShell timer is running late!\"\n}\n\n# Write an information log with the current time.\nWrite-Host \"PowerShell timer trigger function ran! TIME: $currentUTCtime\"\n","sample.dat":""},"function":{"bindings":[{"name":"Timer","type":"timerTrigger","direction":"in","schedule":"0 */5 * * * *"}]},"metadata":{"defaultFunctionName":"TimerTrigger","description":"$TimerTrigger_description","name":"Timer trigger","language":"PowerShell","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"timer","enabledInTryMode":true,"userPrompt":["schedule"]}},{"id":"TimerTrigger-Python","runtime":"2","files":{"readme.md":"# TimerTrigger - Python\r\n\r\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\r\n\r\n## How it works\r\n\r\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\r\n\r\n## Learn more\r\n\r\n Documentation\r\n","sample.dat":"","__init__.py":"import datetime\r\nimport logging\r\n\r\nimport azure.functions as func\r\n\r\n\r\ndef main(mytimer: func.TimerRequest) -> None:\r\n utc_timestamp = datetime.datetime.utcnow().replace(\r\n tzinfo=datetime.timezone.utc).isoformat()\r\n\r\n if mytimer.past_due:\r\n logging.info('The timer is past due!')\r\n\r\n logging.info('Python timer trigger function ran at %s', utc_timestamp)\r\n"},"function":{"scriptFile":"__init__.py","bindings":[{"name":"mytimer","type":"timerTrigger","direction":"in","schedule":"0 */5 * * * *"}]},"metadata":{"defaultFunctionName":"TimerTrigger","description":"$TimerTrigger_description","name":"Timer trigger","language":"Python","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"timer","enabledInTryMode":true,"userPrompt":["schedule"],"filters":["Python3"]}},{"id":"TimerTrigger-TypeScript","runtime":"2","files":{"index.ts":"import { AzureFunction, Context } from \"@azure/functions\"\r\n\r\nconst timerTrigger: AzureFunction = async function (context: Context, myTimer: any): Promise {\r\n var timeStamp = new Date().toISOString();\r\n \r\n if (myTimer.IsPastDue)\r\n {\r\n context.log('Timer function is running late!');\r\n }\r\n context.log('Timer trigger function ran!', timeStamp); \r\n};\r\n\r\nexport default timerTrigger;\r\n","readme.md":"# TimerTrigger - TypeScript\r\n\r\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\r\n\r\n## How it works\r\n\r\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\r\n\r\n## Learn more\r\n\r\n Documentation"},"function":{"bindings":[{"name":"myTimer","type":"timerTrigger","direction":"in","schedule":"0 */5 * * * *"}]},"metadata":{"defaultFunctionName":"TimerTrigger","description":"$TimerTrigger_description","name":"Timer trigger","language":"TypeScript","category":["$temp_category_core","$temp_category_dataProcessing"],"categoryStyle":"timer","enabledInTryMode":true,"userPrompt":["schedule"]}}] \ No newline at end of file From baafde27fef4d4e158a1c6aa00b34873ed1a580e Mon Sep 17 00:00:00 2001 From: Ron Brogan Date: Thu, 6 Feb 2020 21:34:34 -0800 Subject: [PATCH 004/127] Update unzipper npm package to resolve unzip issue during installation --- src/Azure.Functions.Cli/npm/npm-shrinkwrap.json | 12 ++++++------ src/Azure.Functions.Cli/npm/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json index c3d9f9939..3c59c82d0 100644 --- a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json +++ b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json @@ -275,9 +275,9 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -346,9 +346,9 @@ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" }, "unzipper": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.5.tgz", - "integrity": "sha512-i5ufkXNjWZYxU/0nKKf6LkvW8kn9YzRvfwuPWjXP+JTFce/8bqeR0gEfbiN2IDdJa6ZU6/2IzFRLK0z1v0uptw==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.8.tgz", + "integrity": "sha512-CZRd20MbyAfWL1Xc+lqgNFYDj5e/HaH9pLEWZgseQWCvEtNfX0wOjnhaJ6PcGpNnDvifSW1ZNSCHX6GoNliR0A==", "requires": { "big-integer": "^1.6.17", "binary": "~0.3.0", diff --git a/src/Azure.Functions.Cli/npm/package.json b/src/Azure.Functions.Cli/npm/package.json index b51ecff47..b0a610c92 100644 --- a/src/Azure.Functions.Cli/npm/package.json +++ b/src/Azure.Functions.Cli/npm/package.json @@ -34,6 +34,6 @@ "progress": "2.0.3", "rimraf": "3.0.0", "tmp": "0.1.0", - "unzipper": "0.10.5" + "unzipper": "0.10.8" } } From c9fd5f40927e43455915b716f7d52cb6ed7d8d84 Mon Sep 17 00:00:00 2001 From: "Hanzhang Zeng (Roger)" <48038149+Hazhzeng@users.noreply.github.com> Date: Mon, 10 Feb 2020 10:45:01 -0800 Subject: [PATCH 005/127] Update Python Worker to 1.0.3 (#1788) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index d722afcc2..00b5d0951 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -117,7 +117,7 @@ - + From 8d09fe43b5300da98f90107c8896d99ab69c5dd7 Mon Sep 17 00:00:00 2001 From: SatishRanjan Date: Tue, 11 Feb 2020 16:21:45 -0800 Subject: [PATCH 006/127] Initial PR for the changes to manages function keys in Kubernetes (#1715) * Function keys in Kubernetes --- .../KubernetesDeployAction.cs | 25 +- .../Extensions/HttpExtension.cs | 64 ++++ .../Kubernetes/FuncKeys/FuncAppKeysHelper.cs | 110 ++++++ .../FuncKeys/KeyBasedDictionaryComparer.cs | 35 ++ .../Kubernetes/KubernetesHelpers.cs | 344 +++++++++++++++++- .../Models/Kubernetes/ContainerV1.cs | 24 ++ .../Kubernetes/Models/Kubernetes/PodV1.cs | 26 ++ .../Models/Kubernetes/RoleBindingV1.cs | 28 ++ .../Kubernetes/Models/Kubernetes/RoleV1.cs | 28 ++ .../Models/Kubernetes/ServiceAccountV1.cs | 1 - .../Kubernetes/Models/Kubernetes/ServiceV1.cs | 23 +- .../KubernetesHelperUnitTests.cs | 3 - 12 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 src/Azure.Functions.Cli/Extensions/HttpExtension.cs create mode 100644 src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs create mode 100644 src/Azure.Functions.Cli/Kubernetes/FuncKeys/KeyBasedDictionaryComparer.cs create mode 100644 src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleBindingV1.cs create mode 100644 src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleV1.cs diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs index 73158c1f8..20fa42eb8 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs @@ -2,14 +2,18 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; using Azure.Functions.Cli.Kubernetes; +using Azure.Functions.Cli.Kubernetes.FuncKeys; using Azure.Functions.Cli.Kubernetes.Models; +using Azure.Functions.Cli.Kubernetes.Models.Kubernetes; using Colors.Net; using Fclp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -30,6 +34,8 @@ class KubernetesDeployAction : BaseAction public string ImageName { get; private set; } public string ConfigMapName { get; private set; } public string SecretsCollectionName { get; private set; } + public string KeysSecretCollectionName { get; private set; } + public bool MountFuncKeysAsContainerVolume { get; private set; } public int? PollingInterval { get; private set; } public int? CooldownPeriod { get; private set; } public string ServiceType { get; set; } = "LoadBalancer"; @@ -58,8 +64,10 @@ public override ICommandLineParserResult ParseArgs(string[] args) SetFlag("cooldown-period", "The cooldown period for the deployment before scaling back to 0 after all triggers are no longer active. Default: 300 (seconds)", p => CooldownPeriod = p); SetFlag("min-replicas", "Minimum replica count", m => MinReplicaCount = m); SetFlag("max-replicas", "Maximum replica count to scale to by HPA", m => MaxReplicaCount = m); - SetFlag("secret-name", "The name of a kubernetes secret collection to use in the deployment instead of generating one based on local.settings.json", sn => SecretsCollectionName = sn); - SetFlag("config-map-name", "The name of a config map to use in the deployment", cm => ConfigMapName = cm); + SetFlag("keys-secret-name", "The name of a kubernetes secret collection to use for the function app keys (host keys, function keys etc.)", ksn => KeysSecretCollectionName = ksn); + SetFlag("mount-funckeys-as-containervolume", "The flag indicating to mount the func app keys as container volume", kmv => MountFuncKeysAsContainerVolume = kmv); + SetFlag("secret-name", "The name of an existing kubernetes secret collection, containing func app settings, to use in the deployment instead of creating new a new one based upon local.settings.json", sn => SecretsCollectionName = sn); + SetFlag("config-map-name", "The name of an existing config map with func app settings to use in the deployment", cm => ConfigMapName = cm); SetFlag("service-type", "Kubernetes Service Type. Default LoadBalancer Valid options: " + string.Join(",", ServiceTypes), s => { if (!string.IsNullOrEmpty(s) && !ServiceTypes.Contains(s)) @@ -69,7 +77,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) ServiceType = s; }); SetFlag("no-docker", "With --image-name, the core-tools will inspect the functions inside the image. This will require mounting the image filesystem. Passing --no-docker uses current directory for functions.", nd => NoDocker = nd); - SetFlag("use-config-map", "Use a ConfigMap/V1 instead of a Secret/V1 object.", c => UseConfigMap = c); + SetFlag("use-config-map", "Use a ConfigMap/V1 instead of a Secret/V1 object for function app settings configurations", c => UseConfigMap = c); SetFlag("dry-run", "Show the deployment template", f => DryRun = f); SetFlag("ignore-errors", "Proceed with the deployment if a resource returns an error. Default: false", f => IgnoreErrors = f); return base.ParseArgs(args); @@ -101,8 +109,8 @@ public override async Task RunAsync() } triggers = await DockerHelpers.GetTriggersFromDockerImage(resolvedImageName); } - - var resources = KubernetesHelper.GetFunctionsDeploymentResources( + + (var resources, var funcKeys) = await KubernetesHelper.GetFunctionsDeploymentResources( Name, resolvedImageName, Namespace, @@ -116,7 +124,9 @@ public override async Task RunAsync() CooldownPeriod, ServiceType, MinReplicaCount, - MaxReplicaCount); + MaxReplicaCount, + KeysSecretCollectionName, + MountFuncKeysAsContainerVolume); if (DryRun) { @@ -138,6 +148,9 @@ public override async Task RunAsync() { await KubectlHelper.KubectlApply(resource, showOutput: true, ignoreError: IgnoreErrors, @namespace: Namespace); } + + //Print the function keys message to the console + await KubernetesHelper.PrintFunctionsInfo($"{Name}-http", Namespace, funcKeys, triggers); } } diff --git a/src/Azure.Functions.Cli/Extensions/HttpExtension.cs b/src/Azure.Functions.Cli/Extensions/HttpExtension.cs new file mode 100644 index 000000000..49f7bd0e6 --- /dev/null +++ b/src/Azure.Functions.Cli/Extensions/HttpExtension.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; + +namespace Azure.Functions.Cli.Extensions +{ + public static class HttpExtension + { + /// + /// Clones HttpRequestMessage + /// + /// The HttpRequestMessage to be cloned + /// + public static HttpRequestMessage Clone(this HttpRequestMessage request) + { + if (request == null) + { + return null; + } + + var clone = new HttpRequestMessage(request.Method, request.RequestUri) + { + Content = request.Content.Clone(), + Version = request.Version + }; + foreach (KeyValuePair prop in request.Properties) + { + clone.Properties.Add(prop); + } + foreach (KeyValuePair> header in request.Headers) + { + clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } + + /// + /// Clones HttpContent object + /// + /// HttpContent to be cloned + /// The cloned HttpContent object + public static HttpContent Clone(this HttpContent content) + { + if (content == null) + { + return null; + } + + var ms = new MemoryStream(); + content.CopyToAsync(ms).Wait(); + ms.Position = 0; + + var clone = new StreamContent(ms); + foreach (KeyValuePair> header in content.Headers) + { + clone.Headers.Add(header.Key, header.Value); + } + return clone; + } + } +} diff --git a/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs new file mode 100644 index 000000000..b9fedf88b --- /dev/null +++ b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs @@ -0,0 +1,110 @@ +using Azure.Functions.Cli.Kubernetes.Models.Kubernetes; +using Colors.Net; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Azure.Functions.Cli.Kubernetes.FuncKeys +{ + public class FuncAppKeysHelper + { + private const string FuncAppKeysVolumeName = "functions-keys-volume"; + private const string KubernetesSecretsMountPath = "/run/secrets/functions-keys"; + private const string AzureWebJobsSecretStorageTypeEnvVariableName = "AzureWebJobsSecretStorageType"; + private const string AzureWebJobsKubernetesSecretNameEnvVariableName = "AzureWebJobsKubernetesSecretName"; + private const string MasterKey = "host.master"; + private const string HostFunctionKey = "host.function.default"; + private const string HostSystemKey = "host.systemKey.default"; + private const string FunctionKeyPrefix = "functions."; + private const string FunctionDefaultKeyName = "default"; + /// + /// Implementation of this method creates the Host and Function Keys + /// + /// The of function names + /// The of function app's host and function keys + public static IDictionary CreateKeys(IEnumerable functionNames) + { + var funcAppKeys = new Dictionary + { + { MasterKey, GenerateKey() }, + { HostFunctionKey, GenerateKey() }, + { HostSystemKey, GenerateKey() } + }; + + if (functionNames?.Any() == true) + { + foreach (var funcName in functionNames) + { + funcAppKeys[$"{FunctionKeyPrefix}{funcName.ToLower()}.{FunctionDefaultKeyName}"] = GenerateKey(); + } + } + + return funcAppKeys; + } + + public static IDictionary FuncKeysKubernetesEnvironVariables(string keysSecretCollectionName, bool mountKeysAsContainerVolume) + { + var funcKeysKubernetesEnvironVariables = new Dictionary + { + { AzureWebJobsSecretStorageTypeEnvVariableName, "kubernetes" } + }; + + //if keys needs are not to be mounted as container volume then add "AzureWebJobsKubernetesSecretName" enviornment varibale to the container + if (!mountKeysAsContainerVolume) + { + funcKeysKubernetesEnvironVariables.Add(AzureWebJobsKubernetesSecretNameEnvVariableName, $"secrets/{keysSecretCollectionName}"); + } + + return funcKeysKubernetesEnvironVariables; + } + + public static void CreateFuncAppKeysVolumeMountDeploymentResource(IEnumerable deployments, string funcAppKeysSecretsCollectionName) + { + if (deployments?.Any() == false) + { + return; + } + + var volume = new VolumeV1 + { + Name = FuncAppKeysVolumeName + }; + + if (!string.IsNullOrWhiteSpace(funcAppKeysSecretsCollectionName)) + { + volume.VolumeSecret = new VolumeSecretV1 { SecretName = funcAppKeysSecretsCollectionName }; + } + + //Mount the app keys as volume mount to the container at the path "/run/secrets/functions-keys" + foreach (var deployment in deployments) + { + deployment.Spec.Template.Spec.Volumes = new VolumeV1[] { volume }; + deployment.Spec.Template.Spec.Containers.First().VolumeMounts = new ContainerVolumeMountV1[] + { + new ContainerVolumeMountV1 + { + Name = FuncAppKeysVolumeName, + MountPath = KubernetesSecretsMountPath + } + }; + } + } + + private static string GenerateKey() + { + using (var rng = RandomNumberGenerator.Create()) + { + byte[] data = new byte[40]; + rng.GetBytes(data); + string secret = Convert.ToBase64String(data); + + // Replace pluses as they are problematic as URL values + return secret.Replace('+', 'a'); + } + } + } +} diff --git a/src/Azure.Functions.Cli/Kubernetes/FuncKeys/KeyBasedDictionaryComparer.cs b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/KeyBasedDictionaryComparer.cs new file mode 100644 index 000000000..b3974f75f --- /dev/null +++ b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/KeyBasedDictionaryComparer.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Azure.Functions.Cli.Kubernetes.FuncKeys +{ + public class KeyBasedDictionaryComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) + { + //If the compared objects reference the same data. + if (ReferenceEquals(x, y)) + { + return true; + } + + //If any of the compared objects is null. + if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) + { + return false; + } + + return x.Key == y.Key; + } + + public int GetHashCode(KeyValuePair keyValPair) + { + //If the keyValPair is null + if (ReferenceEquals(keyValPair, null)) return 0; + + //Get hash code for the Key field. + int hashKey = keyValPair.Key == null ? 0 : keyValPair.Key.GetHashCode(); + + return hashKey; + } + } +} diff --git a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs index 12f49cff6..c211f2e7a 100644 --- a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs +++ b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs @@ -2,17 +2,24 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Azure.Functions.Cli.Arm.Models; using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; +using Azure.Functions.Cli.Kubernetes.FuncKeys; using Azure.Functions.Cli.Kubernetes.Models; using Azure.Functions.Cli.Kubernetes.Models.Kubernetes; using Colors.Net; +using Dynamitey.DynamicObjects; +using Microsoft.IdentityModel.Xml; +using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Identity; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YamlDotNet.Serialization; @@ -120,6 +127,18 @@ internal static async Task NamespaceExists(string @namespace) (_, _, var exitCode) = await KubectlHelper.RunKubectl($"get namespace {@namespace}", ignoreError: true, showOutput: false); return exitCode == 0; } + + internal static async Task<(string, bool)> ResourceExists(string resourceTypeName, string resourceName, string @namespace, bool returnJsonOutput = false) + { + var cmd = $"get {resourceTypeName} {resourceName} --namespace {@namespace}"; + if (returnJsonOutput) + { + cmd = string.Concat(cmd, " -o json"); + } + + (string output, _, var exitCode) = await KubectlHelper.RunKubectl(cmd, ignoreError: true, showOutput: false); + return (output, exitCode == 0); + } internal static Task CreateNamespace(string @namespace) => KubectlHelper.RunKubectl($"create namespace {@namespace}", ignoreError: false, showOutput: true); @@ -132,7 +151,7 @@ internal static string GetKedaResources(string @namespace) .Replace("KEDA_NAMESPACE", @namespace); } - internal static IEnumerable GetFunctionsDeploymentResources( + internal async static Task<(IEnumerable, IDictionary)> GetFunctionsDeploymentResources( string name, string imageName, string @namespace, @@ -146,7 +165,9 @@ internal static IEnumerable GetFunctionsDeploymentResources int? cooldownPeriod = null, string serviceType = "LoadBalancer", int? minReplicas = null, - int? maxReplicas = null) + int? maxReplicas = null, + string keysSecretCollectionName = null, + bool mountKeysAsContainerVolume = false) { ScaledObjectV1Alpha1 scaledobject = null; var result = new List(); @@ -154,11 +175,17 @@ internal static IEnumerable GetFunctionsDeploymentResources var httpFunctions = triggers.FunctionsJson .Where(b => b.Value["bindings"]?.Any(e => e?["type"].ToString().IndexOf("httpTrigger", StringComparison.OrdinalIgnoreCase) != -1) == true); var nonHttpFunctions = triggers.FunctionsJson.Where(f => httpFunctions.All(h => h.Key != f.Key)); + keysSecretCollectionName = string.IsNullOrEmpty(keysSecretCollectionName) + ? $"func-keys-kube-secret-{name}" + : keysSecretCollectionName; if (httpFunctions.Any()) { int position = 0; var enabledFunctions = httpFunctions.ToDictionary(k => $"AzureFunctionsJobHost__functions__{position++}", v => v.Key); - var deployment = GetDeployment(name + "-http", @namespace, imageName, pullSecret, 1, enabledFunctions, new Dictionary + //Environment variables for the func app keys kubernetes secret + var kubernetesSecretEnvironmentVariable = FuncAppKeysHelper.FuncKeysKubernetesEnvironVariables(keysSecretCollectionName, mountKeysAsContainerVolume); + var additionalEnvVars = enabledFunctions.Concat(kubernetesSecretEnvironmentVariable).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var deployment = GetDeployment(name + "-http", @namespace, imageName, pullSecret, 1, additionalEnvVars, new Dictionary { { "osiris.deislabs.io/enabled", "true" }, { "osiris.deislabs.io/minReplicas", "1" } @@ -186,11 +213,13 @@ internal static IEnumerable GetFunctionsDeploymentResources { secrets[Constants.FunctionsWorkerRuntime] = GlobalCoreToolsSettings.CurrentWorkerRuntime.ToString(); } - + + int resourceIndex = 0; if (useConfigMap) { var configMap = GetConfigMap(name, @namespace, secrets); - result.Insert(0, configMap); + result.Insert(resourceIndex, configMap); + resourceIndex++; foreach (var deployment in deployments) { deployment.Spec.Template.Spec.Containers.First().EnvFrom = new ContainerEnvironmentFromV1[] @@ -240,7 +269,8 @@ internal static IEnumerable GetFunctionsDeploymentResources else { var secret = GetSecret(name, @namespace, secrets); - result.Insert(0, secret); + result.Insert(resourceIndex, secret); + resourceIndex++; foreach (var deployment in deployments) { deployment.Spec.Template.Spec.Containers.First().EnvFrom = new ContainerEnvironmentFromV1[] @@ -256,11 +286,49 @@ internal static IEnumerable GetFunctionsDeploymentResources } } - result = result.Concat(deployments).ToList(); + IDictionary resultantFunctionKeys = new Dictionary(); + if (httpFunctions.Any()) + { + var currentImageFuncKeys = FuncAppKeysHelper.CreateKeys(httpFunctions.Select(f => f.Key)); + resultantFunctionKeys = GetFunctionKeys(currentImageFuncKeys, await GetExistingFunctionKeys(keysSecretCollectionName, @namespace)); + if (resultantFunctionKeys?.Any() == true) + { + result.Insert(resourceIndex, GetSecret(keysSecretCollectionName, @namespace, resultantFunctionKeys)); + resourceIndex++; + } + + //if function keys Secrets needs to be mounted as volume in the function runtime container + if (mountKeysAsContainerVolume) + { + FuncAppKeysHelper.CreateFuncAppKeysVolumeMountDeploymentResource(deployments, keysSecretCollectionName); + } + //Create the Pod identity with the role to modify the function kubernetes secret + else + { + var svcActName = $"{name}-function-keys-identity-svc-act"; + var svcActDeploymentResource = GetServiceAccount(svcActName, @namespace); + result.Insert(resourceIndex, svcActDeploymentResource); + resourceIndex++; + + var funcKeysManagerRoleName = "functions-keys-manager-role"; + var secretManagerRole = GetSecretManagerRole(funcKeysManagerRoleName, @namespace); + result.Insert(resourceIndex, secretManagerRole); + resourceIndex++; + var roleBindingName = $"{svcActName}-functions-keys-manager-rolebinding"; + var funcKeysRoleBindingDeploymentResource = GetRoleBinding(roleBindingName, @namespace, funcKeysManagerRoleName, svcActName); + result.Insert(resourceIndex, funcKeysRoleBindingDeploymentResource); + resourceIndex++; + + //add service account identity to the pod + foreach (var deployment in deployments) + { + deployment.Spec.Template.Spec.ServiceAccountName = svcActName; + } + } + } - return scaledobject != null - ? result.Append(scaledobject) - : result; + result = result.Concat(deployments).ToList(); + return (scaledobject != null ? result.Append(scaledobject) : result, resultantFunctionKeys); } internal static async Task RemoveKeda(string @namespace) @@ -271,6 +339,185 @@ internal static async Task RemoveKeda(string @namespace) } } + private async static Task> GetExistingFunctionKeys(string keysSecretCollectionName, string @namespace) + { + if (string.IsNullOrWhiteSpace(keysSecretCollectionName) + || string.IsNullOrWhiteSpace(@namespace)) + { + return new Dictionary(); + } + + (string output, bool keysSecretExist) = await ResourceExists("secret", keysSecretCollectionName, @namespace, true); + if (keysSecretExist) + { + var allExistingFuncKeys = TryParse(output); + if (allExistingFuncKeys?.Data?.Any() == true) + { + return allExistingFuncKeys.Data.ToDictionary(k => k.Key, v => Encoding.UTF8.GetString(Convert.FromBase64String(v.Value))); + } + } + + return new Dictionary(); + } + + internal async static Task PrintFunctionsInfo(string serviceName, string @namespace, IDictionary funcKeys, TriggersPayload triggers) + { + if (string.IsNullOrWhiteSpace(serviceName) + || string.IsNullOrWhiteSpace(@namespace) + || funcKeys?.Any() == false + || triggers == null) + { + return; + } + + var httpFunctions = triggers.FunctionsJson + .Where(b => b.Value["bindings"]?.Any(e => e?["type"].ToString().IndexOf("httpTrigger", StringComparison.OrdinalIgnoreCase) != -1) == true) + .Select(item => item.Key); + + var loadBalancerIp = await GetLoadBalancerIp(serviceName, @namespace, 24); + if (string.IsNullOrEmpty(loadBalancerIp)) + { + ColoredConsole.WriteLine(WarningColor($"The service: {serviceName} is not yet ready, please re-run the deployment to get the function keys.")); + return; + } + + var masterKey = funcKeys["host.master"]; + if (httpFunctions?.Any() == true) + { + foreach (var functionName in httpFunctions) + { + var getFunctionAdminUri = $"http://{loadBalancerIp}/admin/functions/{functionName}?code={masterKey}"; + var httpResponseMessage = await GetHttpResponse(new HttpRequestMessage(HttpMethod.Get, getFunctionAdminUri), 20); + + if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.NotFound) + { + ColoredConsole.WriteLine(WarningColor($"The service: {functionName} is not yet ready in the runtime yet, please re-run the deployment to get the function keys.")); + return; + } + + if (httpResponseMessage.IsSuccessStatusCode) + { + var responseContent = await httpResponseMessage.Content.ReadAsStringAsync(); + var functionsInfo = JsonConvert.DeserializeObject(responseContent); + + var trigger = functionsInfo + .Config?["bindings"] + ?.FirstOrDefault(o => o["type"]?.ToString().IndexOf("Trigger", StringComparison.OrdinalIgnoreCase) != -1) + ?["type"]; + + trigger = trigger ?? "No Trigger Found"; + var showFunctionKey = true; + + var authLevel = functionsInfo + .Config?["bindings"] + ?.FirstOrDefault(o => !string.IsNullOrEmpty(o["authLevel"]?.ToString())) + ?["authLevel"]; + + if (authLevel != null && authLevel.ToString().Equals("anonymous", StringComparison.OrdinalIgnoreCase)) + { + showFunctionKey = false; + } + + ColoredConsole.WriteLine($"\t{functionName} - [{VerboseColor(trigger.ToString())}]"); + if (!string.IsNullOrEmpty(functionsInfo.InvokeUrlTemplate)) + { + if (showFunctionKey) + { + ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor($"{functionsInfo.InvokeUrlTemplate}?code={funcKeys[$"functions.{functionName.ToLower()}.default"]}")}"); + } + else + { + ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor(functionsInfo.InvokeUrlTemplate)}"); + } + } + ColoredConsole.WriteLine(); + + } + } + } + + //Print the master key as well for the user + ColoredConsole.WriteLine($"\tMaster key: {VerboseColor($"{funcKeys[$"host.master"]}")}"); + } + + private async static Task GetHttpResponse(HttpRequestMessage httpRequestMessage, int retryCount = 5) + { + HttpResponseMessage httpResponseMsg = new HttpResponseMessage(); + if (httpRequestMessage == null) + { + return httpResponseMsg; + } + + int currentRetry = 0; + using (var httpClient = new HttpClient(new HttpClientHandler())) + { + while (currentRetry++ < retryCount) + { + httpResponseMsg = await httpClient.SendAsync(httpRequestMessage.Clone()); + if (httpResponseMsg.IsSuccessStatusCode || + (httpResponseMsg.StatusCode != System.Net.HttpStatusCode.BadGateway + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.RequestTimeout + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.GatewayTimeout + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.NotFound)) + { + return httpResponseMsg; + } + + await Task.Delay(new Random().Next(500, 2000)); + } + } + + return httpResponseMsg; + } + + private async static Task GetLoadBalancerIp(string serviceName, string @namespace, int retryCount = 12) + { + if (string.IsNullOrWhiteSpace(serviceName) + || string.IsNullOrWhiteSpace(@namespace)) + { + return string.Empty; + } + + ColoredConsole.WriteLine(AdditionalInfoColor($"Getting loadbalancer ip for the service: {serviceName}")); + int currentRetry = 0; + while (currentRetry++ < retryCount) + { + (string output, bool serviceExists) = await ResourceExists("service", serviceName, @namespace, true); + if (serviceExists) + { + var service = TryParse(output); + if (service?.Status?.LoadBalancer?.Ingress?.Any() == true) + { + return service?.Status?.LoadBalancer?.Ingress.First().Ip; + } + } + + await Task.Delay(5000); + ColoredConsole.WriteLine(AdditionalInfoColor($"Waiting for the service to be ready: {serviceName}")); + } + + return string.Empty; + } + + private static IDictionary GetFunctionKeys(IDictionary currentImageFuncKeys, IDictionary existingFuncKeys) + { + if ((currentImageFuncKeys == null || !currentImageFuncKeys.Any()) + || (existingFuncKeys == null || !existingFuncKeys.Any())) + { + return currentImageFuncKeys; + } + + //The function keys that doesn't exist in Kubernetes yet + IDictionary funcKeys = currentImageFuncKeys.Except(existingFuncKeys, new KeyBasedDictionaryComparer()).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + //Merge the new keys with the keys that already exist in kubernetes + foreach (var commonKey in existingFuncKeys.Intersect(currentImageFuncKeys, new KeyBasedDictionaryComparer())) + { + funcKeys.Add(commonKey); + } + + return funcKeys; + } + internal static string SerializeResources(IEnumerable resources, OutputSerializationOptions outputFormat) { var sb = new StringBuilder(); @@ -534,5 +781,82 @@ private static ConfigMapV1 GetConfigMap(string name, string @namespace, IDiction Data = secrets }; } + + public static ServiceAccountV1 GetServiceAccount(string name, string @namespace) + { + return new ServiceAccountV1 + { + ApiVersion = "v1", + Kind = "ServiceAccount", + Metadata = new ObjectMetadataV1 + { + Name = name, + Namespace = @namespace + } + }; + } + + public static RoleV1 GetSecretManagerRole(string name, string @namespace) + { + return new RoleV1 + { + ApiVersion = "rbac.authorization.k8s.io/v1", + Kind = "Role", + Metadata = new ObjectMetadataV1 + { + Name = name, + Namespace = @namespace + }, + Rules = new RuleV1[] + { + new RuleV1 + { + ApiGroups = new string[]{""}, + Resources = new string[]{"secrets", "configMaps"}, + Verbs = new string[]{ "get", "list", "watch", "create", "update", "patch", "delete" } + } + } + }; + } + + public static RoleBindingV1 GetRoleBinding(string name, string @namespace, string refRoleName, string subjectName) + { + return new RoleBindingV1 + { + ApiVersion = "rbac.authorization.k8s.io/v1", + Kind = "RoleBinding", + Metadata = new ObjectMetadataV1 + { + Name = name, + Namespace = @namespace + }, + RoleRef = new RoleSubjectV1 + { + ApiGroup = "rbac.authorization.k8s.io", + Kind = "Role", + Name = refRoleName + }, + Subjects = new RoleSubjectV1[] + { + new RoleSubjectV1 + { + Kind = "ServiceAccount", + Name = subjectName + } + } + }; + } + + private static T TryParse(string jsonData) + { + try + { + return JsonConvert.DeserializeObject(jsonData); + } + catch + { + return default; + } + } } } diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs index 4bf569605..50a9bacc5 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs @@ -26,6 +26,18 @@ public class ContainerV1 [JsonProperty("imagePullPolicy")] public string ImagePullPolicy { get; internal set; } + + [JsonProperty("volumeMounts")] + public IEnumerable VolumeMounts { get; internal set; } + } + + public class ContainerVolumeMountV1 + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("mountPath")] + public string MountPath { get; set; } } public class ContainerResourceRequestsV1 @@ -94,4 +106,16 @@ public class NamedObjectV1 [JsonProperty("name")] public string Name { get; set; } } + + public class VolumeMountV1 + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("mountPath")] + public string MountPath { get; set; } + + [JsonProperty("readOnly")] + public bool ReadOnly { get; set; } + } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/PodV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/PodV1.cs index 392147e51..8fe04711e 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/PodV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/PodV1.cs @@ -25,6 +25,32 @@ public class PodTemplateSpecV1 : IKubernetesSpec [JsonProperty("serviceAccountName")] public string ServiceAccountName { get; internal set; } + + [JsonProperty("volumes")] + public IEnumerable Volumes { get; set; } + } + public class VolumeV1 + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("configMap")] + public VolumeConfigMapV1 VolumeConfigMap { get; set; } + + [JsonProperty("secret")] + public VolumeSecretV1 VolumeSecret { get; set; } + } + + public class VolumeConfigMapV1 + { + [JsonProperty("name")] + public string Name { get; set; } + } + + public class VolumeSecretV1 + { + [JsonProperty("secretName")] + public string SecretName { get; set; } } public class PodTolerationV1 diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleBindingV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleBindingV1.cs new file mode 100644 index 000000000..52b4ce754 --- /dev/null +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleBindingV1.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Functions.Cli.Kubernetes.Models.Kubernetes +{ + public class RoleBindingV1 : BaseKubernetesResource + { + [JsonProperty("roleRef")] + public RoleSubjectV1 RoleRef { get; set; } + + [JsonProperty("subjects")] + public IEnumerable Subjects { get; set; } + } + + public class RoleSubjectV1 + { + [JsonProperty("apiGroup")] + public string ApiGroup { get; set; } + + [JsonProperty("kind")] + public string Kind { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleV1.cs new file mode 100644 index 000000000..984838a3f --- /dev/null +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/RoleV1.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Functions.Cli.Kubernetes.Models.Kubernetes +{ + public class RoleV1 : BaseKubernetesResource + { + [JsonProperty("rules")] + public IEnumerable Rules { get; set; } + } + + public class RuleV1 + { + [JsonProperty("apiGroups")] + public IEnumerable ApiGroups { get; set; } + + [JsonProperty("resources")] + public IEnumerable Resources { get; set; } + + [JsonProperty("resourceNames")] + public IEnumerable ResourceNames { get; set; } + + [JsonProperty("verbs")] + public IEnumerable Verbs { get; set; } + } +} diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceAccountV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceAccountV1.cs index e61f51e31..889fa7a4d 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceAccountV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceAccountV1.cs @@ -2,6 +2,5 @@ namespace Azure.Functions.Cli.Kubernetes.Models.Kubernetes { public class ServiceAccountV1 : BaseKubernetesResource { - } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs index a1256cd96..2e05d9c79 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs @@ -4,7 +4,10 @@ namespace Azure.Functions.Cli.Kubernetes.Models.Kubernetes { public class ServiceV1 : BaseKubernetesResource - { } + { + [JsonProperty("status")] + public ServiceStatus Status { get; set; } + } public class ServiceSpecV1 : IKubernetesSpec { @@ -32,4 +35,22 @@ public class ServicePortV1 [JsonProperty("targetPort")] public int TargetPort { get; set; } } + + public class ServiceStatus + { + [JsonProperty("loadBalancer")] + public ServiceLoadBalancer LoadBalancer { get; set; } + } + + public class ServiceLoadBalancer + { + [JsonProperty("ingress")] + public IEnumerable Ingress { get; set; } + } + + public class ServiceIp + { + [JsonProperty("ip")] + public string Ip { get; set; } + } } \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/KubernetesHelperUnitTests.cs b/test/Azure.Functions.Cli.Tests/KubernetesHelperUnitTests.cs index f370bcdfa..6faefce1a 100644 --- a/test/Azure.Functions.Cli.Tests/KubernetesHelperUnitTests.cs +++ b/test/Azure.Functions.Cli.Tests/KubernetesHelperUnitTests.cs @@ -1,8 +1,5 @@ -using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Kubernetes; -using FluentAssertions; using Newtonsoft.Json.Linq; -using System; using System.Collections.Generic; using Xunit; From bb3b1e1609e158388966b494225e7130a30e0608 Mon Sep 17 00:00:00 2001 From: Satish Ranjan Date: Tue, 11 Feb 2020 16:37:00 -0800 Subject: [PATCH 007/127] Removing tab chars minged prepended to new line --- .../Actions/KubernetesActions/KubernetesDeployAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs index 20fa42eb8..04924e0d5 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs @@ -109,7 +109,7 @@ public override async Task RunAsync() } triggers = await DockerHelpers.GetTriggersFromDockerImage(resolvedImageName); } - + (var resources, var funcKeys) = await KubernetesHelper.GetFunctionsDeploymentResources( Name, resolvedImageName, From 65892aa2b44d017e2f5a58148957720eb9c12746 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Tue, 11 Feb 2020 17:12:21 -0800 Subject: [PATCH 008/127] Update npm version --- src/Azure.Functions.Cli/npm/npm-shrinkwrap.json | 2 +- src/Azure.Functions.Cli/npm/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json index 3c59c82d0..a0248e091 100644 --- a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json +++ b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "azure-functions-core-tools", - "version": "2.4.290", + "version": "2.7.2184", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/Azure.Functions.Cli/npm/package.json b/src/Azure.Functions.Cli/npm/package.json index b0a610c92..bf59f53c7 100644 --- a/src/Azure.Functions.Cli/npm/package.json +++ b/src/Azure.Functions.Cli/npm/package.json @@ -1,6 +1,6 @@ { "name": "azure-functions-core-tools", - "version": "2.4.290", + "version": "2.7.2184", "description": "Azure Functions Core Tools", "scripts": { "postinstall": "node lib/install.js", From 851b7ba360f664518ebaeb17d8639143defdf226 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Thu, 12 Dec 2019 00:05:47 -0800 Subject: [PATCH 009/127] Fix sigcheck test for signed builds --- build/BuildSteps.cs | 12 ++++++++++++ build/Settings.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 6ea9f6049..1a4ef50d2 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -360,6 +360,12 @@ public static void TestSignedArtifacts() client.DownloadFile(Settings.SignInfo.SigcheckDownloadURL, sigcheckPath); } + // https://peter.hahndorf.eu/blog/post/2010/03/07/WorkAroundSysinternalsLicensePopups + // Can't use sigcheck without signing the License Agreement + Console.WriteLine("Signing EULA"); + Console.WriteLine(Shell.GetOutput("reg.exe", "ADD HKCU\\Software\\Sysinternals /v EulaAccepted /t REG_DWORD /d 1 /f")); + Console.WriteLine(Shell.GetOutput("reg.exe", "ADD HKU\\.DEFAULT\\Software\\Sysinternals /v EulaAccepted /t REG_DWORD /d 1 /f")); + foreach (var supportedRuntime in Settings.SignInfo.RuntimesToSign) { var targetDir = Path.Combine(Settings.OutputDir, supportedRuntime); @@ -381,6 +387,12 @@ public static void TestSignedArtifacts() unSignedPackages.Add(fileName); } } + + if (unSignedPackages.Count() < 1) + { + throw new Exception("Something went wrong while testing for signed packages. There must be a few unsigned allowed binaries"); + } + // The first element is simply the column heading unSignedPackages = unSignedPackages.Skip(1).ToList(); diff --git a/build/Settings.cs b/build/Settings.cs index 2291c9557..5c76d7055 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -103,7 +103,7 @@ public class SignInfo public static readonly string Authenticode = "SignAuthenticode"; public static readonly string ThirdParty = "Sign3rdParty"; public static readonly string[] RuntimesToSign = new[] { "min.win-x86", "min.win-x64" }; - public static readonly string[] FilterExtenstionsSign = new[] { ".json", ".spec", ".cfg", ".pdb", ".config", ".nupkg", ".py" }; + public static readonly string[] FilterExtenstionsSign = new[] { ".json", ".spec", ".cfg", ".pdb", ".config", ".nupkg", ".py", ".md" }; public static readonly string SigcheckDownloadURL = "https://functionsbay.blob.core.windows.net/public/tools/sigcheck64.exe"; public static readonly string[] SkipSigcheckTest = new[] { From 88001c9f7e70ad30cbdb74f7c001e661fff5d125 Mon Sep 17 00:00:00 2001 From: SatishRanjan Date: Wed, 12 Feb 2020 11:33:36 -0800 Subject: [PATCH 010/127] Updating to Keda version 1.2.0 (#1811) Keda version 1.2.0 --- .../StaticResources/keda.yaml | 97 +++++++++++++------ 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/keda.yaml b/src/Azure.Functions.Cli/StaticResources/keda.yaml index b31d0974a..bfd1aafec 100644 --- a/src/Azure.Functions.Cli/StaticResources/keda.yaml +++ b/src/Azure.Functions.Cli/StaticResources/keda.yaml @@ -4614,14 +4614,14 @@ spec: served: true storage: true --- -# Source: keda/templates/service-account.yaml +# Source: keda/templates/01-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: keda namespace: KEDA_NAMESPACE --- -# Source: keda/templates/role.yaml +# Source: keda/templates/10-keda-clusterrole.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -4676,7 +4676,7 @@ rules: verbs: - '*' --- -# Source: keda/templates/role.yaml +# Source: keda/templates/20-metrics-clusterrole.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -4690,21 +4690,21 @@ rules: verbs: - list --- -# Source: keda/templates/role_binding.yaml +# Source: keda/templates/11-keda-clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: keda:system:auth-delegator + name: keda roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: system:auth-delegator + name: keda subjects: - kind: ServiceAccount name: keda namespace: KEDA_NAMESPACE --- -# Source: keda/templates/role_binding.yaml +# Source: keda/templates/21-metrics-clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -4718,21 +4718,21 @@ subjects: name: horizontal-pod-autoscaler namespace: kube-system --- -# Source: keda/templates/role_binding.yaml +# Source: keda/templates/21-metrics-clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: keda + name: keda:system:auth-delegator roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: keda + name: system:auth-delegator subjects: - kind: ServiceAccount name: keda namespace: KEDA_NAMESPACE --- -# Source: keda/templates/role_binding.yaml +# Source: keda/templates/21-metrics-clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -4747,31 +4747,33 @@ subjects: name: keda namespace: KEDA_NAMESPACE --- -# Source: keda/templates/service.yaml +# Source: keda/templates/23-metrics-service.yaml apiVersion: v1 kind: Service metadata: - name: keda + name: keda-metrics-apiserver namespace: KEDA_NAMESPACE spec: ports: - name: https port: 443 targetPort: 6443 + protocol: TCP - name: http port: 80 targetPort: 8080 + protocol: TCP selector: - app: keda + app: keda-metrics-apiserver --- -# Source: keda/templates/operator.yaml +# Source: keda/templates/12-keda-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: - labels: - app: keda name: keda namespace: KEDA_NAMESPACE + labels: + app: keda spec: replicas: 1 selector: @@ -4783,13 +4785,17 @@ spec: app: keda spec: serviceAccountName: keda + securityContext: + {} containers: - name: keda - image: "docker.io/kedacore/keda:1.1.0" + securityContext: + {} + image: "docker.io/kedacore/keda:1.2.0" command: - keda args: - - '--zap-level=info' + - "--zap-level=info" imagePullPolicy: Always env: - name: WATCH_NAMESPACE @@ -4800,31 +4806,62 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: keda + resources: + {} +--- +# Source: keda/templates/22-metrics-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keda-metrics-apiserver + namespace: KEDA_NAMESPACE + labels: + app: keda-metrics-apiserver +spec: + replicas: 1 + selector: + matchLabels: + app: keda-metrics-apiserver + template: + metadata: + labels: + app: keda-metrics-apiserver + spec: + serviceAccountName: keda + securityContext: + {} + containers: - name: keda-metrics-apiserver - image: docker.io/kedacore/keda-metrics-adapter:1.1.0 + securityContext: + {} + image: "docker.io/kedacore/keda-metrics-adapter:1.2.0" imagePullPolicy: Always env: - name: WATCH_NAMESPACE value: "" args: - - /usr/local/bin/keda-adapter - - --secure-port=6443 - - --logtostderr=true - - --v=0 + - /usr/local/bin/keda-adapter + - --secure-port=6443 + - --logtostderr=true + - --v=0 ports: - - containerPort: 6443 - name: https - - containerPort: 8080 - name: http + - containerPort: 6443 + name: https + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + resources: + {} --- -# Source: keda/templates/api_service.yaml +# Source: keda/templates/24-metrics-apiservice.yaml apiVersion: apiregistration.k8s.io/v1beta1 kind: APIService metadata: name: v1beta1.external.metrics.k8s.io spec: service: - name: keda + name: keda-metrics-apiserver namespace: KEDA_NAMESPACE group: external.metrics.k8s.io version: v1beta1 From 6d7d09c036c088eb694b175c7c7c1fd0b0d7ef58 Mon Sep 17 00:00:00 2001 From: Brett Samblanet Date: Fri, 14 Feb 2020 06:58:34 -0800 Subject: [PATCH 011/127] updating to WebHost 2.0.12998 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 00b5d0951..ef3cc8462 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -116,7 +116,7 @@ - + From 03232bdbbe771d45bdd918c9c13055287cb046e1 Mon Sep 17 00:00:00 2001 From: Julius Liu Date: Fri, 14 Feb 2020 16:29:12 -0800 Subject: [PATCH 012/127] Add flag to skip AzureWebJobsStorage check. (#1820) * Add flag to skip AzureWebJobsStorage check. * Fix formatting, unit-test with blob/timer trigger Co-authored-by: Ahmed ElSayed --- .../Actions/HostActions/StartHostAction.cs | 15 ++++++++---- .../ActionsTests/StartHostActionTests.cs | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 1488b9ae9..692507017 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -65,6 +65,7 @@ internal class StartHostAction : BaseAction public bool EnableAuth { get; set; } public List EnabledFunctions { get; private set; } + public bool SkipAzureStorageCheck { get; private set; } public StartHostAction(ISecretsManager secretsManager) { @@ -122,7 +123,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) Parser .Setup("no-build") - .WithDescription("Do no build current project before running. For dotnet projects only. Default is set to false.") + .WithDescription("Do not build current project before running. For dotnet projects only. Default is set to false.") .SetDefault(false) .Callback(b => NoBuild = b); @@ -137,6 +138,12 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription("A space seperated list of functions to load.") .Callback(f => EnabledFunctions = f); + Parser + .Setup("skip-azure-storage-check") + .WithDescription("Skip the check for AzureWebJobsStorage being set. WARNING: Proceed with caution, only set this flag if you are sure you know what you're doing.") + .SetDefault(false) + .Callback(skip => SkipAzureStorageCheck = skip); + return base.ParseArgs(args); } @@ -191,7 +198,7 @@ private async Task> GetConfigurationSettings(string .GetEnvironmentVariables() .Cast() .ToDictionary(k => k.Key.ToString(), v => v.Value.ToString()); - await CheckNonOptionalSettings(settings.Union(environment), scriptPath); + await CheckNonOptionalSettings(settings.Union(environment), scriptPath, SkipAzureStorageCheck); // when running locally in CLI we want the host to run in debug mode // which optimizes host responsiveness @@ -349,7 +356,7 @@ private string CleanAndFormatHttpMethods(string httpMethods) .Replace("\"", string.Empty).ToUpperInvariant(); } - internal static async Task CheckNonOptionalSettings(IEnumerable> secrets, string scriptPath) + internal static async Task CheckNonOptionalSettings(IEnumerable> secrets, string scriptPath, bool skipAzureWebJobsStorageCheck = false) { try { @@ -374,7 +381,7 @@ internal static async Task CheckNonOptionalSettings(IEnumerable b.IndexOf("Trigger", StringComparison.OrdinalIgnoreCase) != -1) .All(t => Constants.TriggersWithoutStorage.Any(tws => tws.Equals(t, StringComparison.OrdinalIgnoreCase))); - if (string.IsNullOrWhiteSpace(azureWebJobsStorage) && !allNonStorageTriggers) + if (!skipAzureWebJobsStorageCheck && string.IsNullOrWhiteSpace(azureWebJobsStorage) && !allNonStorageTriggers) { throw new CliException($"Missing value for AzureWebJobsStorage in {SecretsManager.AppSettingsFileName}. " + $"This is required for all triggers other than {string.Join(", ", Constants.TriggersWithoutStorage)}. " diff --git a/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs b/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs index 8c55f4a76..869a5b852 100644 --- a/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs +++ b/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs @@ -72,6 +72,29 @@ public async Task CheckNonOptionalSettingsDoesntThrowOnMissingAzureWebJobsStorag exception.Should().BeNull(); } + [Fact] + public async Task CheckNonOptionalSettingsDoesntThrowOnMissingAzureWebJobsStorageWhenFlagIsSet() + { + var fileSystem = GetFakeFileSystem(new[] + { + ("x:\\folder1", "{'bindings': [{'type': 'blobTrigger'}]}"), + ("x:\\folder2", "{'bindings': [{'type': 'timerTrigger'}]}") + }); + + FileSystemHelpers.Instance = fileSystem; + Exception exception = null; + try + { + await StartHostAction.CheckNonOptionalSettings(new Dictionary(), "x:\\", true); + } + catch (Exception e) + { + exception = e; + } + + exception.Should().BeNull(); + } + [SkippableFact] public async Task CheckNonOptionalSettingsPrintsWarningForMissingSettings() { From c38225f901f556927e45848fe7c82d7bc8c1a028 Mon Sep 17 00:00:00 2001 From: Troy Witthoeft Date: Fri, 14 Feb 2020 20:21:10 -0500 Subject: [PATCH 013/127] Update StartHost.cs (#1735) Fixing issue #1732 Updating the else if block to skip attempting to set the environmental variable when both the var and it's value are blank or null. Co-authored-by: Ahmed ElSayed --- src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 692507017..c3d97da23 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -218,7 +218,7 @@ private void UpdateEnvironmentVariables(IDictionary secrets) { Environment.SetEnvironmentVariable(secret.Key, secret.Value, EnvironmentVariableTarget.Process); } - else if (secret.Value == string.Empty) + else if (!string.IsNullOrEmpty(secret.Key) && secret.Value == string.Empty) { EnvironmentNativeMethods.SetEnvironmentVariable(secret.Key, secret.Value); } From 7fa44e5ab41a10a20bd696cfa6a3cf281820a7dc Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Mon, 17 Feb 2020 15:04:48 -0800 Subject: [PATCH 014/127] Fix packapp issue when pip not in path (#1760) --- tools/python/packapp/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/python/packapp/__main__.py b/tools/python/packapp/__main__.py index 7e29a83a0..784d8d2b6 100644 --- a/tools/python/packapp/__main__.py +++ b/tools/python/packapp/__main__.py @@ -158,7 +158,7 @@ def find_and_build_deps(args): def ensure_wheel(name, version, args, dest): cmd = [ - 'pip', 'download', '--no-deps', '--only-binary', ':all:', + sys.executable, '-m', 'pip', 'download', '--no-deps', '--only-binary', ':all:', '--platform', _platform_map.get(args.platform), '--python-version', args.python_version, '--implementation', 'cp', @@ -177,7 +177,7 @@ def ensure_wheel(name, version, args, dest): def build_independent_wheel(name, version, args, dest): with tempfile.TemporaryDirectory(prefix='azureworker') as td: cmd = [ - 'pip', 'wheel', '--no-deps', '--no-binary', ':all:', + sys.executable, '-m', 'pip', 'wheel', '--no-deps', '--no-binary', ':all:', '--wheel-dir', td, f'{name}=={version}' ] From ed3b7092a66b8c35c2078a7de702cac9617d3113 Mon Sep 17 00:00:00 2001 From: soninaren Date: Thu, 20 Feb 2020 22:49:07 -0800 Subject: [PATCH 015/127] updating the templates to 2.1.1517 and updating webHost to 2.0.13017 --- build/Settings.cs | 6 +++--- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/Settings.cs b/build/Settings.cs index 5c76d7055..7ad8840ae 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -20,9 +20,9 @@ private static string config(string @default = null, [CallerMemberName] string k : value; } - public const string ItemTemplatesVersion = "2.0.10369"; - public const string ProjectTemplatesVersion = "2.0.10369"; - public const string TemplateJsonVersion = "2.0.10344"; + public const string ItemTemplatesVersion = "2.1.1517"; + public const string ProjectTemplatesVersion = "2.1.1517"; + public const string TemplateJsonVersion = "2.1.1517"; public static readonly string SrcProjectPath = Path.GetFullPath("../src/Azure.Functions.Cli/"); diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index ef3cc8462..bec96a6e7 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -116,7 +116,7 @@ - + diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 2c7cedbc7..e99484685 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -96,7 +96,7 @@ await CliTester.Run(new RunConfiguration var result = await response.Content.ReadAsStringAsync(); p.Kill(); await Task.Delay(TimeSpan.FromSeconds(2)); - result.Should().Be("Hello, Test", because: "response from default function should be 'Hello, {name}'"); + result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); } }, }, _output); From ae06bd1a6012aa6a0859b5f30d3077947267d2b6 Mon Sep 17 00:00:00 2001 From: soninaren Date: Fri, 21 Feb 2020 13:08:39 -0800 Subject: [PATCH 016/127] Signing new binaries --- build/Settings.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build/Settings.cs b/build/Settings.cs index 7ad8840ae..777e9cb17 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -122,8 +122,8 @@ public class SignInfo "Microsoft.Azure.AppService.*", "Microsoft.Azure.WebJobs.*", "Microsoft.Azure.WebSites.DataProtection.dll", - Path.Combine("templates", "itemTemplates.2.0.10328.nupkg"), - Path.Combine("templates", "projectTemplates.2.0.10328.nupkg"), + Path.Combine("templates", "itemTemplates.2.1.1517.nupkg"), + Path.Combine("templates", "projectTemplates.2.1.1517.nupkg"), Path.Combine("tools", "python", "packapp", "__main__.py"), Path.Combine("workers", "python") }; @@ -150,6 +150,9 @@ public class SignInfo "Newtonsoft.Json.Bson.dll", "Newtonsoft.Json.dll", "protobuf-net.dll", + "protobuf-net.Core.dll", + "DotNetTI.BreakingChangeAnalysis.dll", + "Marklio.Metadata.dll", "Remotion.Linq.dll", "System.IO.Abstractions.dll", "YamlDotNet.dll", From f5bd66f1a00b1122251186fe3ee7b97ec5ba98ba Mon Sep 17 00:00:00 2001 From: Denis Balan <33955091+DenisBalan@users.noreply.github.com> Date: Sun, 23 Feb 2020 11:13:12 +0200 Subject: [PATCH 017/127] Add x64 notice installing tools for debugging (#1813) * Add x64 notice installing tools for debugging For debugging under vscode, x64 installer is required * Update README.md to use '/x64' flag only Co-authored-by: Ankit Kumar --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce4a058a3..595c4b4b4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,10 @@ To install with chocolatey: ```bash choco install azure-functions-core-tools ``` - +#### Notice: To debug functions under vscode, x64 bitness is required +```bash +choco install azure-functions-core-tools --params "'/x64'" +``` ### Mac **Homebrew**: From 5ac28aff6d50ae8613c07f91e484021d1fb9d8af Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Mon, 24 Feb 2020 12:17:17 -0800 Subject: [PATCH 018/127] Update PowerShellWorker version to 2.0.240 (#1840) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index bec96a6e7..631beaaac 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -115,7 +115,7 @@ - + From 4a0110d0b673038907c1ea75e7254055ca05ab34 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Mon, 24 Feb 2020 14:32:26 -0800 Subject: [PATCH 019/127] =?UTF-8?q?Display=20AZURE=5FFUNCTIONS=5FENVIRONME?= =?UTF-8?q?NT=20and=20set=20ASPNETCORE=5FENVIRONM=E2=80=A6=20(#1835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1834 Closes #1823 --- .../Actions/HostActions/StartHostAction.cs | 14 ++++++++++---- src/Azure.Functions.Cli/Common/Constants.cs | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index c3d97da23..897621d46 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -187,12 +187,12 @@ private async Task BuildWebHost(ScriptApplicationHostOptions hostOptio private async Task> GetConfigurationSettings(string scriptPath, Uri uri) { var settings = _secretsManager.GetSecrets(); - settings.Add(Constants.WebsiteHostname, uri.Authority); + settings.TryAdd(Constants.WebsiteHostname, uri.Authority); // Add our connection strings var connectionStrings = _secretsManager.GetConnectionStrings(); settings.AddRange(connectionStrings.ToDictionary(c => $"ConnectionStrings:{c.Name}", c => c.Value)); - settings.Add(EnvironmentSettingNames.AzureWebJobsScriptRoot, scriptPath); + settings.TryAdd(EnvironmentSettingNames.AzureWebJobsScriptRoot, scriptPath); var environment = Environment .GetEnvironmentVariables() @@ -202,7 +202,8 @@ private async Task> GetConfigurationSettings(string // when running locally in CLI we want the host to run in debug mode // which optimizes host responsiveness - settings.Add("AZURE_FUNCTIONS_ENVIRONMENT", "Development"); + settings.TryAdd(Constants.AzureFunctionsEnvorinmentEnvironmentVariable, "Development"); + settings.TryAdd(Constants.AspNetCoreEnvironmentEnvironmentVariable, "Development"); return settings; } @@ -210,7 +211,12 @@ private void UpdateEnvironmentVariables(IDictionary secrets) { foreach (var secret in secrets) { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(secret.Key))) + if (string.Equals(secret.Key, Constants.AzureFunctionsEnvorinmentEnvironmentVariable, StringComparison.OrdinalIgnoreCase)) + { + ColoredConsole.WriteLine($"{Constants.AzureFunctionsEnvorinmentEnvironmentVariable}: {secret.Value}"); + Environment.SetEnvironmentVariable(secret.Key, secret.Value, EnvironmentVariableTarget.Process); + } + else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(secret.Key))) { ColoredConsole.WriteLine(WarningColor($"Skipping '{secret.Key}' from local settings as it's already defined in current environment variables.")); } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 20785c425..6db0f9afa 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -44,6 +44,8 @@ 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 AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; + public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); From 3eb1c7240b0f31f419c3d223f9e2816ff6209770 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Fri, 28 Feb 2020 16:28:44 -0800 Subject: [PATCH 020/127] Update README.md (#1851) Add info to make gozip executable --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 595c4b4b4..e9aa2f2f0 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,12 @@ unzip -d azure-functions-cli Azure.Functions.Cli.linux-x64.*.zip 3. Make the `func` command executable -Zip files do not maintain the executable bit on binaries. So, you'll need to make the `func` binary executable. Assuming you used the instructions above to unzip: +Zip files do not maintain the executable bit on binaries. So, you'll need to make the `func` binary, as well as `gozip` (used by func during packaging) executables. Assuming you used the instructions above to unzip: ```bash cd azure-functions-cli chmod +x func +chmod +x gozip ./func ``` From 8cbebb5264a2b21599031325c684fde0dabf3c7c Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Mon, 2 Mar 2020 15:45:18 -0800 Subject: [PATCH 021/127] Remove Osiris from kubernetes commands (#1833) Closes #1826 --- README.md | 13 +- .../KubernetesActions/InstallKedaAction.cs | 18 +- .../KubernetesActions/RemoveKedaAction.cs | 2 +- .../Azure.Functions.Cli.csproj | 3 - .../Kubernetes/KubernetesHelpers.cs | 60 +-- .../StaticResources/StaticResources.cs | 1 - .../StaticResources/osiris.yaml | 496 ------------------ 7 files changed, 13 insertions(+), 580 deletions(-) delete mode 100644 src/Azure.Functions.Cli/StaticResources/osiris.yaml diff --git a/README.md b/README.md index e9aa2f2f0..a7b7e3a2e 100644 --- a/README.md +++ b/README.md @@ -170,18 +170,17 @@ Using the Core Tools, you can easily configure a Kubernetes cluster and run Azur ### Installing Kubernetes scalers -This deploys [KEDA](https://github.com/kedacore/keda) and [Osiris](https://github.com/deislabs/osiris) to your cluster which allows you to deploy your functions in a scale-to-zero by default. +This deploys [KEDA](https://github.com/kedacore/keda) to your cluster which allows you to deploy your functions in a scale-to-zero by default for non-http scenarios only. ```bash func kubernetes install --namespace {namespace} ``` **KEDA:** Handles monitoring polling event sources currently QueueTrigger and ServiceBusTrigger. -**Osiris:**: Handles Http traffic monitoring and on demand scale your deployment to and from 0 ### Deploy to Kubernetes -**First make sure you have Dockerfile for your project.** You can generate one using +**First make sure you have Dockerfile for your project.** You can generate one using ```bash func init --docker # or --docker-only (for existing projects) ``` @@ -242,16 +241,16 @@ aks-agentpool-20257154-2 Ready agent 1d v1.11.5 An ACR instance can be created using the Azure Portal or the [Azure CLI](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-azure-cli#create-a-container-registry) #### Login to the ACR Registry -Before pushing and pulling container images, you must log in to the ACR instance. +Before pushing and pulling container images, you must log in to the ACR instance. ```azurecli az acr login --name ``` #### Give the AKS cluster access to the ACR Registry -The AKS cluster needs access to the ACR Registry to pull the container. Azure creates a service principal to support cluster operability with other Azure resources. This can be used for authentication with an ACR registry. See here for how to grant the right access here: [Authenticate with Azure Container Registry from Azure Kubernetes Service](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-aks) +The AKS cluster needs access to the ACR Registry to pull the container. Azure creates a service principal to support cluster operability with other Azure resources. This can be used for authentication with an ACR registry. See here for how to grant the right access here: [Authenticate with Azure Container Registry from Azure Kubernetes Service](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-aks) -#### Run the deployment +#### Run the deployment The deployment will build the docker container and upload the container image to your referenced ACR instance (Note: Specify the ACR Login Server in the --registry parameter this is usually of the form .azurecr.io) and then your AKS cluster will use that as a source to obtain the container and deploy it. ```bash @@ -263,7 +262,7 @@ If the deployment is successful, you should see this: Function deployed successfully! Function IP: 40.121.21.192 -#### Verifying your deployment +#### Verifying your deployment You can verify your deployment by using the Kubernetes web dashboard. To start the Kubernetes dashboard, use the [az aks browse](https://docs.microsoft.com/en-us/cli/azure/aks?view=azure-cli-latest#az-aks-browse) command. ```azurecli diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/InstallKedaAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/InstallKedaAction.cs index b35fa2e4c..ca8e946da 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/InstallKedaAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/InstallKedaAction.cs @@ -14,18 +14,15 @@ namespace Azure.Functions.Cli.Actions.KubernetesActions { - [Action(Name = "install", Context = Context.Kubernetes, HelpText = "Install Keda (non-http scale to zero) and Osiris (http scale to zero) in the kubernetes cluster from kubectl config")] + [Action(Name = "install", Context = Context.Kubernetes, HelpText = "Install Keda (non-http scale to zero) in the kubernetes cluster from kubectl config")] internal class DeployKedaAction : BaseAction { public string Namespace { get; private set; } = "default"; - public bool KedaOnly { get; private set; } public bool DryRun { get; private set; } public override ICommandLineParserResult ParseArgs(string[] args) { SetFlag("namespace", "Kubernetes namespace to deploy to. Default: default", s => Namespace = s); - SetFlag("keda", "Install Keda only. By default both keda (non-http scale to zero) and osiris (http scale to zero) are installed", f => KedaOnly = f); - SetFlag("keda-only", string.Empty, f => KedaOnly = f); SetFlag("dry-run", "Show the deployment template", f => DryRun = f); return base.ParseArgs(args); } @@ -35,26 +32,15 @@ public async override Task RunAsync() if (DryRun) { ColoredConsole.WriteLine(KubernetesHelper.GetKedaResources(Namespace)); - if (!KedaOnly) - { - ColoredConsole.WriteLine(KubernetesHelper.GetOsirisResources(Namespace)); - } } else { - var sb = new StringBuilder(); - sb.AppendLine(KubernetesHelper.GetKedaResources(Namespace)); - if (!KedaOnly) - { - sb.AppendLine(KubernetesHelper.GetOsirisResources(Namespace)); - } - if (!await KubernetesHelper.NamespaceExists(Namespace)) { await KubernetesHelper.CreateNamespace(Namespace); } - await KubectlHelper.KubectlApply(sb.ToString(), showOutput: true); + await KubectlHelper.KubectlApply(KubernetesHelper.GetKedaResources(Namespace), showOutput: true); } } } diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/RemoveKedaAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/RemoveKedaAction.cs index f477209f9..8261ddf53 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/RemoveKedaAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/RemoveKedaAction.cs @@ -5,7 +5,7 @@ namespace Azure.Functions.Cli.Actions.KubernetesActions { - [Action(Name = "remove", Context = Context.Kubernetes, HelpText = "Remove Keda (non-http scale to zero) and Osiris (http scale to zero) from the kubernetes")] + [Action(Name = "remove", Context = Context.Kubernetes, HelpText = "Remove Keda (non-http scale to zero) from the kubernetes")] internal class RemoveKedaAction : BaseAction { public string Namespace { get; private set; } = "default"; diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 631beaaac..164db3b56 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -90,9 +90,6 @@ $(AssemblyName).print-functions.sh - - $(AssemblyName).osiris.yaml - $(AssemblyName).keda.yaml diff --git a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs index c211f2e7a..566d494fb 100644 --- a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs +++ b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs @@ -78,56 +78,12 @@ public static void ValidateKubernetesName(string name) } } - internal static string GetOsirisResources(string @namespace) - { - var injectorCaCert = SecurityHelpers.CreateCACertificate("osiris-proxy-injector-ca"); - var hijackerCaCert = SecurityHelpers.CreateCACertificate("osiris-endpoints-hijacker-ca"); - - var injectorAltNames = new[] - { - $"osiris-osiris-edge-proxy-injector.{@namespace}", - $"osiris-osiris-edge-proxy-injector.{@namespace}.svc", - $"osiris-osiris-edge-proxy-injector.{@namespace}.svc.cluster", - $"osiris-osiris-edge-proxy-injector.{@namespace}.svc.cluster.local", - }; - - var hijackerAltnames = new[] - { - $"osiris-osiris-edge-endpoints-hijacker.{@namespace}", - $"osiris-osiris-edge-endpoints-hijacker.{@namespace}.svc", - $"osiris-osiris-edge-endpoints-hijacker.{@namespace}.svc.cluster", - $"osiris-osiris-edge-endpoints-hijacker.{@namespace}.svc.cluster.local", - }; - - var injectorTlsCert = SecurityHelpers.CreateCertificateFromCA(injectorCaCert, "osiris-osiris-edge-proxy-injector", injectorAltNames); - var hijackerTlsCert = SecurityHelpers.CreateCertificateFromCA(hijackerCaCert, "osiris-osiris-edge-endpoints-hijacker", hijackerAltnames); - - var caCertForEdgePointHijacker = SecurityHelpers.GetPemCert(hijackerCaCert); - var tlsCertForEdgeEndpointHijacker = SecurityHelpers.GetPemCert(hijackerTlsCert); - var tlsKeyForEdgeEndpointHijacker = SecurityHelpers.GetPemRsaKey(hijackerTlsCert); - - var caCertForProxyInjector = SecurityHelpers.GetPemCert(injectorCaCert); - var tlsCertForProxyInjector = SecurityHelpers.GetPemCert(injectorTlsCert); - var tlsKeyForProxyInjector = SecurityHelpers.GetPemRsaKey(injectorTlsCert); - - return StaticResources - .OsirisTemplate - .Result - .Replace("OSIRIS_NAMESPACE_PLACEHOLDER", @namespace) - .Replace("TLS_CERT_FOR_EDGE_ENDPOINTS_HIJACKER", tlsCertForEdgeEndpointHijacker) - .Replace("TLS_KEY_FOR_EDGE_ENDPOINTS_HIJACKER", tlsKeyForEdgeEndpointHijacker) - .Replace("CA_CERT_FOR_EDGE_ENDPOINT_HIJACKER", caCertForEdgePointHijacker) - .Replace("TLS_CERT_FOR_EDGE_PROXY_INJECTOR", tlsCertForProxyInjector) - .Replace("TLS_KEY_FOR_EDGE_PROXY_INJECTOR", tlsKeyForProxyInjector) - .Replace("CA_CERT_FOR_EDGE_PROXY_INJECTOR", caCertForProxyInjector); - } - internal static async Task NamespaceExists(string @namespace) { (_, _, var exitCode) = await KubectlHelper.RunKubectl($"get namespace {@namespace}", ignoreError: true, showOutput: false); return exitCode == 0; } - + internal static async Task<(string, bool)> ResourceExists(string resourceTypeName, string resourceName, string @namespace, bool returnJsonOutput = false) { var cmd = $"get {resourceTypeName} {resourceName} --namespace {@namespace}"; @@ -185,17 +141,9 @@ internal static string GetKedaResources(string @namespace) //Environment variables for the func app keys kubernetes secret var kubernetesSecretEnvironmentVariable = FuncAppKeysHelper.FuncKeysKubernetesEnvironVariables(keysSecretCollectionName, mountKeysAsContainerVolume); var additionalEnvVars = enabledFunctions.Concat(kubernetesSecretEnvironmentVariable).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var deployment = GetDeployment(name + "-http", @namespace, imageName, pullSecret, 1, additionalEnvVars, new Dictionary - { - { "osiris.deislabs.io/enabled", "true" }, - { "osiris.deislabs.io/minReplicas", "1" } - }, port: 80); + var deployment = GetDeployment(name + "-http", @namespace, imageName, pullSecret, 1, additionalEnvVars, port: 80); deployments.Add(deployment); - var service = GetService(name + "-http", @namespace, deployment, serviceType, new Dictionary - { - { "osiris.deislabs.io/enabled", "true" }, - { "osiris.deislabs.io/deployment", deployment.Metadata.Name } - }); + var service = GetService(name + "-http", @namespace, deployment, serviceType); result.Add(service); } @@ -213,7 +161,7 @@ internal static string GetKedaResources(string @namespace) { secrets[Constants.FunctionsWorkerRuntime] = GlobalCoreToolsSettings.CurrentWorkerRuntime.ToString(); } - + int resourceIndex = 0; if (useConfigMap) { diff --git a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs index d4bfe655b..a5e6c7840 100644 --- a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs +++ b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs @@ -69,7 +69,6 @@ private static async Task GetValue(string name) public static Task PrintFunctionJson => GetValue("print-functions.sh"); - public static Task OsirisTemplate => GetValue("osiris.yaml"); public static Task KedaTemplate => GetValue("keda.yaml"); diff --git a/src/Azure.Functions.Cli/StaticResources/osiris.yaml b/src/Azure.Functions.Cli/StaticResources/osiris.yaml deleted file mode 100644 index e500c553d..000000000 --- a/src/Azure.Functions.Cli/StaticResources/osiris.yaml +++ /dev/null @@ -1,496 +0,0 @@ -# Source: osiris-edge/templates/endpoints-hijacker-webhook-config.yaml -apiVersion: v1 -kind: Secret -metadata: - name: osiris-osiris-edge-endpoints-hijacker-cert - labels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris -data: - tls.crt: TLS_CERT_FOR_EDGE_ENDPOINTS_HIJACKER - tls.key: TLS_KEY_FOR_EDGE_ENDPOINTS_HIJACKER ---- -# Source: osiris-edge/templates/image-pull-secret.yaml -apiVersion: v1 -kind: Secret -metadata: - name: osiris-osiris-edge -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: eyJhdXRocyI6eyJvc2lyaXMuYXp1cmVjci5pbyI6eyJ1c2VybmFtZSI6ImVhZTk3NDlhLWZjY2YtNGEyNC1hYzBkLTY1MDZmZTJhNmFiMyIsInBhc3N3b3JkIjoiMmZjNmE3MjEtODVlNC00MWNhLTkzM2QtMmNhMDJlMTM5NGM0IiwiYXV0aCI6IlpXRmxPVGMwT1dFdFptTmpaaTAwWVRJMExXRmpNR1F0TmpVd05tWmxNbUUyWVdJek9qSm1ZelpoTnpJeExUZzFaVFF0TkRGallTMDVNek5rTFRKallUQXlaVEV6T1RSak5BPT0ifX19 ---- -# Source: osiris-edge/templates/proxy-injector-webhook-config.yaml -apiVersion: v1 -kind: Secret -metadata: - name: osiris-osiris-edge-proxy-injector-cert - labels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris -data: - tls.crt: TLS_CERT_FOR_EDGE_PROXY_INJECTOR - tls.key: TLS_KEY_FOR_EDGE_PROXY_INJECTOR ---- -# Source: osiris-edge/templates/service-account.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: osiris-osiris-edge - labels: - app.kubernetes.io/name: osiris-edge - app.kubernetes.io/instance: osiris ---- -# Source: osiris-edge/templates/cluster-role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: osiris-osiris-edge - labels: - app.kubernetes.io/name: osiris-edge - app.kubernetes.io/instance: osiris -rules: -- apiGroups: - - "" - resources: - - nodes - - pods - - services - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - endpoints - verbs: - - get - - list - - watch - - create - - update -- apiGroups: - - apps - resources: - - deployments - verbs: - - get - - list - - watch - - update - - patch ---- -# Source: osiris-edge/templates/cluster-role-binding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: osiris-osiris-edge - labels: - app.kubernetes.io/name: osiris-edge - app.kubernetes.io/instance: osiris -subjects: -- kind: ServiceAccount - name: osiris-osiris-edge - namespace: OSIRIS_NAMESPACE_PLACEHOLDER -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: osiris-osiris-edge ---- -# Source: osiris-edge/templates/endpoints-hijacker-service.yaml -apiVersion: v1 -kind: Service -metadata: - name: osiris-osiris-edge-endpoints-hijacker - labels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris -spec: - type: ClusterIP - ports: - - port: 443 - targetPort: https - protocol: TCP - name: https - selector: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris ---- -# Source: osiris-edge/templates/proxy-injector-service.yaml -apiVersion: v1 -kind: Service -metadata: - name: osiris-osiris-edge-proxy-injector - labels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris -spec: - type: ClusterIP - ports: - - port: 443 - targetPort: https - protocol: TCP - name: https - selector: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris ---- -# Source: osiris-edge/templates/activator-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: osiris-osiris-edge-activator - labels: - app.kubernetes.io/name: osiris-edge-activator - app.kubernetes.io/instance: osiris -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: osiris-edge-activator - app.kubernetes.io/instance: osiris - template: - metadata: - labels: - app.kubernetes.io/name: osiris-edge-activator - app.kubernetes.io/instance: osiris - spec: - serviceAccountName: osiris-osiris-edge - imagePullSecrets: - - name: osiris-osiris-edge - containers: - - name: activator - image: osiris.azurecr.io/osiris:6b69328 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 1000 - command: - - /osiris/bin/osiris - args: - - --logtostderr=true - - activator - ports: - - name: proxy - containerPort: 5000 - protocol: TCP - - name: healthz - containerPort: 5001 - protocol: TCP - livenessProbe: - httpGet: - port: healthz - path: /healthz - readinessProbe: - httpGet: - port: healthz - path: /healthz - resources: - {} ---- -# Source: osiris-edge/templates/endpoints-controller-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: osiris-osiris-edge-endpoints-controller - labels: - app.kubernetes.io/name: osiris-edge-endpoints-controller - app.kubernetes.io/instance: osiris -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: osiris-edge-endpoints-controller - app.kubernetes.io/instance: osiris - template: - metadata: - labels: - app.kubernetes.io/name: osiris-edge-endpoints-controller - app.kubernetes.io/instance: osiris - spec: - serviceAccountName: osiris-osiris-edge - imagePullSecrets: - - name: osiris-osiris-edge - containers: - - name: endpoints-controller - image: osiris.azurecr.io/osiris:6b69328 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 1000 - command: - - /osiris/bin/osiris - args: - - --logtostderr=true - - endpoints-controller - env: - - name: OSIRIS_NAMESPACE - value: OSIRIS_NAMESPACE_PLACEHOLDER - - name: ACTIVATOR_POD_LABEL_SELECTOR_KEY - value: app.kubernetes.io/name - - name: ACTIVATOR_POD_LABEL_SELECTOR_VALUE - value: osiris-edge-activator - ports: - - name: healthz - containerPort: 5000 - protocol: TCP - livenessProbe: - httpGet: - port: healthz - path: /healthz - readinessProbe: - httpGet: - port: healthz - path: /healthz - resources: - {} ---- -# Source: osiris-edge/templates/endpoints-hijacker-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: osiris-osiris-edge-endpoints-hijacker - labels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris - template: - metadata: - labels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris - releaseRevision: "1" - spec: - serviceAccountName: osiris-osiris-edge - imagePullSecrets: - - name: osiris-osiris-edge - containers: - - name: endpoints-hijacker - image: osiris.azurecr.io/osiris:6b69328 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 1000 - command: - - /osiris/bin/osiris - args: - - --logtostderr=true - - endpoints-hijacker - env: - - name: TLS_CERT_FILE - value: /osiris/cert/tls.crt - - name: TLS_KEY_FILE - value: /osiris/cert/tls.key - ports: - - name: https - containerPort: 5000 - protocol: TCP - livenessProbe: - httpGet: - port: https - scheme: HTTPS - path: /healthz - readinessProbe: - httpGet: - port: https - scheme: HTTPS - path: /healthz - volumeMounts: - - name: cert - mountPath: /osiris/cert - readOnly: true - resources: - {} - - volumes: - - name: cert - secret: - secretName: osiris-osiris-edge-endpoints-hijacker-cert ---- -# Source: osiris-edge/templates/proxy-injector-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: osiris-osiris-edge-proxy-injector - labels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris - template: - metadata: - labels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris - releaseRevision: "1" - spec: - serviceAccountName: osiris-osiris-edge - imagePullSecrets: - - name: osiris-osiris-edge - containers: - - name: proxy-injector - image: osiris.azurecr.io/osiris:6b69328 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 1000 - command: - - /osiris/bin/osiris - args: - - --logtostderr=true - - proxy-injector - env: - - name: TLS_CERT_FILE - value: /osiris/cert/tls.crt - - name: TLS_KEY_FILE - value: /osiris/cert/tls.key - - name: PROXY_IMAGE - value: osiris.azurecr.io/osiris:6b69328 - - name: PROXY_IMAGE_PULL_POLICY - value: IfNotPresent - ports: - - name: https - containerPort: 5000 - protocol: TCP - livenessProbe: - httpGet: - port: https - scheme: HTTPS - path: /healthz - readinessProbe: - httpGet: - port: https - scheme: HTTPS - path: /healthz - volumeMounts: - - name: cert - mountPath: /osiris/cert - readOnly: true - resources: - {} - - volumes: - - name: cert - secret: - secretName: osiris-osiris-edge-proxy-injector-cert ---- -# Source: osiris-edge/templates/zeroscaler-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: osiris-osiris-edge-zeroscaler - labels: - app.kubernetes.io/name: osiris-edge-zeroscaler - app.kubernetes.io/instance: osiris -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: osiris-edge-zeroscaler - app.kubernetes.io/instance: osiris - template: - metadata: - labels: - app.kubernetes.io/name: osiris-edge-zeroscaler - app.kubernetes.io/instance: osiris - spec: - serviceAccountName: osiris-osiris-edge - imagePullSecrets: - - name: osiris-osiris-edge - containers: - - name: zeroscaler - image: osiris.azurecr.io/osiris:6b69328 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 1000 - command: - - /osiris/bin/osiris - args: - - --logtostderr=true - - zeroscaler - env: - - name: METRICS_CHECK_INTERVAL - value: "150" - ports: - - name: healthz - containerPort: 5000 - protocol: TCP - livenessProbe: - httpGet: - port: healthz - path: /healthz - readinessProbe: - httpGet: - port: healthz - path: /healthz - resources: - {} ---- -# Source: osiris-edge/templates/endpoints-hijacker-webhook-config.yaml -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: MutatingWebhookConfiguration -metadata: - name: osiris-osiris-edge-endpoints-hijacker - labels: - app.kubernetes.io/name: osiris-edge-endpoints-hijacker - app.kubernetes.io/instance: osiris -webhooks: -- name: endpoints-hijacker.osiris.deislabs.io - clientConfig: - service: - namespace: OSIRIS_NAMESPACE_PLACEHOLDER - name: osiris-osiris-edge-endpoints-hijacker - path: "/mutate" - caBundle: CA_CERT_FOR_EDGE_ENDPOINT_HIJACKER - rules: - - apiGroups: - - "" - apiVersions: - - v1 - resources: - - services - operations: - - CREATE - - UPDATE - failurePolicy: Ignore ---- -# Source: osiris-edge/templates/proxy-injector-webhook-config.yaml -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: MutatingWebhookConfiguration -metadata: - name: osiris-osiris-edge-proxy-injector - labels: - app.kubernetes.io/name: osiris-edge-proxy-injector - app.kubernetes.io/instance: osiris -webhooks: -- name: proxy-injector.osiris.deislabs.io - clientConfig: - service: - namespace: OSIRIS_NAMESPACE_PLACEHOLDER - name: osiris-osiris-edge-proxy-injector - path: "/mutate" - caBundle: CA_CERT_FOR_EDGE_PROXY_INJECTOR - rules: - - apiGroups: - - apps - apiVersions: - - v1 - resources: - - deployments - operations: - - CREATE - - UPDATE - - apiGroups: - - "" - apiVersions: - - v1 - resources: - - pods - operations: - - CREATE - failurePolicy: Ignore From d2f17f142b757a7435cd63faaef7fdb63c4eafd9 Mon Sep 17 00:00:00 2001 From: Katy Shimizu Date: Thu, 6 Feb 2020 11:34:32 -0800 Subject: [PATCH 022/127] Added MSI creation assets and CI/CD integration scripts. --- azure-pipelines.yml | 7 ++-- build.ps1 | 54 +++++++++++++++++++++++++ build/BuildSteps.cs | 17 ++++---- build/funcinstall.wxs | 86 ++++++++++++++++++++++++++++++++++++++++ build/icon.ico | Bin 0 -> 32726 bytes build/installbanner.bmp | Bin 0 -> 85894 bytes build/installdialog.bmp | Bin 0 -> 461814 bytes build/license.rtf | Bin 0 -> 1294 bytes 8 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 build/funcinstall.wxs create mode 100644 build/icon.ico create mode 100644 build/installbanner.bmp create mode 100644 build/installdialog.bmp create mode 100644 build/license.rtf diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fc782ca5f..0916ac4da 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,6 @@ variables: devops_buildNumber: $[counter(format(''), 1500)] APPVEYOR_REPO_BRANCH: $[coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranchName'])] APPVEYOR_REPO_COMMIT: $(Build.SourceVersion) - steps: - pwsh: | Write-Host "Target branch: '$(APPVEYOR_REPO_BRANCH)'" @@ -40,7 +39,7 @@ steps: $accessToken = (az account get-access-token --query "accessToken" | % { $_.Trim('"') }) echo "##vso[task.setvariable variable=azure_management_access_token]$accessToken" - pwsh: | - .\build.ps1 + .\build.ps1 -MsiGenBranches master,v3.x env: AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) BuildArtifactsStorage: $(BuildArtifactsStorage) @@ -56,7 +55,9 @@ steps: - task: CopyFiles@2 inputs: SourceFolder: '$(Build.Repository.LocalPath)\artifacts' - Contents: 'Azure.Functions.Cli.*' + Contents: | + Azure.Functions.Cli.* + func-cli*.msi TargetFolder: '$(Build.ArtifactStagingDirectory)' CleanTargetFolder: true - task: PublishBuildArtifacts@1 diff --git a/build.ps1 b/build.ps1 index 59ab4ba42..9285622e9 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,3 +1,6 @@ +param([String[]] $MsiGenBranches) + +$baseDir = Get-Location if ($env:APPVEYOR_REPO_BRANCH -eq "disabled") { Set-Location ".\src\Azure.Functions.Cli" @@ -34,3 +37,54 @@ else { Invoke-Expression -Command "dotnet run" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } } + +if ($MsiGenBranches.Contains($env:APPVEYOR_REPO_BRANCH)) { + Write-Host "Generating MSI files" + + # Add WiX to PATH + if (-not (@($env:Path -split ";") -contains $env:WIX)) + { + # Check if the Wix path points to the bin folder + if ((Split-Path $env:WIX -Leaf) -ne "bin") + { + $env:Path += ";$env:WIX\bin" + } + else + { + $env:Path += ";$env:WIX" + } + } + + # Get runtime version + $artifactsPath = "$baseDir\artifacts" + $buildDir = "$baseDir\build" + $cli = Get-ChildItem -Path $artifactsPath -Include func.dll -Recurse | Select-Object -First 1 + $cliVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($cli).FileVersion + + # Generate MSI installers for Windows + @('x64', 'x86') | ForEach-Object { + $platform = $_ + $targetDir = "$artifactsPath\win-$platform" + + Copy-Item "$buildDir\icon.ico" -Destination $artifactsPath\win-$platform + Copy-Item "$buildDir\license.rtf" -Destination $artifactsPath\win-$platform + Copy-Item "$buildDir\installbanner.bmp" -Destination $artifactsPath\win-$platform + Copy-Item "$buildDir\installdialog.bmp" -Destination $artifactsPath\win-$platform + Set-Location $targetDir + + $masterWxsName = "funcinstall" + $fragmentName = "$platform-frag" + $msiName = "func-cli-$cliVersion-$platform" + + $masterWxsPath = "$buildDir\$masterWxsName.wxs" + $fragmentPath = "$buildDir\$fragmentName.wxs" + $msiPath = "$artifactsPath\$msiName.msi" + + Invoke-Expression "heat dir '.' -cg FuncHost -dr INSTALLDIR -gg -ke -out $fragmentPath -srd -sreg -template fragment -var var.Source" + Invoke-Expression "candle -arch $platform -dPlatform='$platform' -dSource='.' -dProductVersion='$cliVersion' $masterWxsPath $fragmentPath" + Invoke-Expression "light -ext WixUIExtension -out $msiPath -sice:ICE61 $masterWxsName.wixobj $fragmentName.wixobj" + + Set-Location $baseDir + Get-ChildItem -Path $targetDir -Recurse | Remove-Item -Force -Recurse -ea SilentlyContinue + } +} diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 1a4ef50d2..247711a1f 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -425,13 +425,16 @@ public static void Zip() ColoredConsole.WriteLine($"Creating {shaPath}"); File.WriteAllText(shaPath, ComputeSha256(zipPath)); - try - { - Directory.Delete(path, recursive: true); - } - catch - { - ColoredConsole.Error.WriteLine($"Error deleting {path}"); + if (!runtime.StartsWith("win")) { + try + { + Directory.Delete(path, recursive: true); + } + catch + { + ColoredConsole.Error.WriteLine($"Error deleting {path}"); + } + } ColoredConsole.WriteLine(); diff --git a/build/funcinstall.wxs b/build/funcinstall.wxs new file mode 100644 index 000000000..d08595336 --- /dev/null +++ b/build/funcinstall.wxs @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fecd5903168febf542f4d212045417ea4f2667a9 GIT binary patch literal 32726 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�F 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 literal 0 HcmV?d00001 diff --git a/build/installbanner.bmp b/build/installbanner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ed06210300496a7425ac07ab2e18fe64e4ebffb5 GIT binary patch literal 85894 zcmeI5`)^xk8OO6fAatQXpKW6k~2+o~EA#dTXP1d2-F+qYSTi#JUc$xsd*FY4%`GY|lQtw7-Tf?^p~Khdje1#ezDZlHiPi-Whd+2yRlu!l) zKmY{N2`Egu+5LvI!AAP)J^!6}WHznD*$3Pb;1-W;?v^Xsa%}RHmgfT(zfZIB!8$Uq zG^90U{1!TH%m=w300JP8fq=r|;~l>vbBd#ZMEvF0($vO6Gm`pgzijpK9S*6yjX%@N zf>9$wd3-q(xb%vU^7`t#&bo~aW=nyNXCs4j5C8!X$V@-c!6{|cHJxG9Ul~Tb+QUEkcqlg@kqJ^tYB5 zZsbYiy+v;ID^?E^Q}TYPyqTFg)ZdQhu&z+!!Wl6!E4Kv)-%{1AQVt!@b1YB{1VCWx z5|Cqy!xza(>tz20{1?Ssqn;~5GWCjERuYzVTtWm@nQ+k$9H z-l=}RpM~Ov)wE1)*|UcJ387`rI|E16)%Y!`HKF6FG;j|BAOHgC1a9~Jl|-Cm)OliG zcy1&;d-eTTlslKXW0~8R`Cx?iMR;$R_k{V?F#p4hB1|hn$C+MA(-mee2NPxxp(Q!Ht&yGYH~r-&(e zkFc(o+tsIgl$*;rq&yy;^`3i~tUB}S1bA>jL^LENb)Edi z;jHtKYN{srs6ujZ*kh4nwPx)(`<4*Q|LH(r;wvzMW4LAPu72@ z+(_nj(s9MyBHR$)3}zPc*wUo;{9)RrPhH;`_*+I1r{1CC$sxcE2!H?x7!e=?%V3>w zJ){NCj8g84@+bYw>P?tdL|Bzhsr(8fy-r*$3Yo>%u2#j|%FM0Gu5QJ-m~LH(KD_HY zcbK*U4N@>$$3L=(I6X0R+-TU42Ld1f0;vQjIJhq5uH(;bw3W+T-Ft&shm)q2RWHL9 z((8m|o7;GXt4rNuR~F^9u40Rm-gAe^PezEiv%x^bsSb2}4IJ)300clFCxOXpO@npS z!oYGOgOtBDmu^}S8-K_SE_7Vk*2*e76wAh|U8(5iuifDKoP6cM9q-vg;#i5g>d8C~ zdxMVW95oaG0T2Lzbp)n7Cn!WXO_s?eF+3MY&AS=nANP@IMeN=bCUTE3k&|h~tWC*x zv?~phd^n=|!|9hVtWr9?=(tYW_pco&VIz0L)W);jL&vj`K{^P400?9yFy%XAZ>T0W z64%N7OSAseaxdJ{17nF@Kjf?;TvI4KmbRp6?ohwxRHSrE_*!k+@$Wt(MBLu+#cAJ} zwG6{M=(r&sWP$()fIt=kGl6sVhA$@!EGbfc>OQxK0EfDc3n|}55o^h`LQ`^D(3{a2+0w4ea`4E^HzCcn=%6skP{%~kiuQDz1 z-2p~1$92;R>G%#OMIIL$ICbxok9H~U4I#)i5kE*xQG?Tg^ZE3iECU@+qQDIZfB*<= zn!xPXWk=&4P0A01XA`#aBieCp$tk&TKCi-k&b@%{vJpXYmMCr0OK%)YQ7B^Lqmw&yN#IfdB}AKz0K2H<~?f?;E}HDh0Wv zQuDI<*?>6CEpZB=o!U56T&Og@8Mj~Rxld6nf`Gx1yzR-Ac_8yFKpyNh#kOu-F z00Maupy;xS98DJfDa9OLzuLKdVBM75+$AEb(AFV(DRL~nvXHF1*&FEiW{IIZ2!H?x z zuIN(UrM~E4DXuGpbOaqQgc|CA00@9UP6Bkg+b(gkMABUFdb7 z;e7& literal 0 HcmV?d00001 diff --git a/build/installdialog.bmp b/build/installdialog.bmp new file mode 100644 index 0000000000000000000000000000000000000000..47a8fadfadafa17310af912650c65473d495e3f8 GIT binary patch literal 461814 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 Gw}nd_y9&Dc=wsO{yg## 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 literal 0 HcmV?d00001 diff --git a/build/license.rtf b/build/license.rtf new file mode 100644 index 0000000000000000000000000000000000000000..e2f610283407fe9b925b9bf607da235b296773e4 GIT binary patch literal 1294 zcmZ8hUvJws5Z`lve1}7xwn6Y@XKUB?NwMjes4N+j+%$k*D2cMT$)rJ2F@hl9eMeeJ z(E)~R9q)esj_^xmTsN<{vNAlH(_y~&Yf-hL>*7l3Q8xN`;?5Tat`76~_V%Ufv~sQ< z@JC#AN)A=m^xRnCVp#G))<%ALeZ9VVseZ2xLJ7m2Ho#=li*^yiJf6j~`QNu67O&Nl z6z6}XYX7XpW)9a^)rp(7vV0N8l^qrzKj8B?4sIT(>|#-Q6`x>BOGsrSlofvD`f909 z&qf}42eXWnu-2n$xszJWAQ=V-M;45*!aRv~b{X0UbClL%7f1`eFrt2jgW<}FHiFIw z0lEY4?I0p>8n}9flQ0$sbnUoQQXPOpgXF%`^$25i=g!;+Y;J*Ds~gD?4_e)fN16Plg=hlaj^Hd^uSe${I3pYHEW)N{7+bI5?Rbz!IWhS0 z$u=qt;Wa#<3FNtzT%a4dKmQCTWhpxrT1$;U$$1_ zF|YNL2!1Y0SK6W65P4Wa5OUe_BY4>=C}wY!w0dzGI{) zAZI|gyOa{F(QKLSS2VkaJDkgMyx|n@If9lsc)|&a5}aGZmaydpk&`=`((*BaH7zrb zxkk_gb_px#a-Sv)c6+wV3xf13gw1HSX2?ReBr9jgiZvkL@CAxZlBR(w*`s1Y3Mydo=y7Wkr${ Rv2Z=vPtDUt@&D?V{{S}6r5^wQ literal 0 HcmV?d00001 From 3f2771101d72615dcb46bbd1edc529489b1f2a0c Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Mon, 2 Mar 2020 15:05:58 -0800 Subject: [PATCH 023/127] Update README with v3 apt --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index a7b7e3a2e..f161d7eb9 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,18 @@ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list 2. Install +##### v2 ```bash sudo apt-get update sudo apt-get install azure-functions-core-tools ``` +##### v3 +```bash +sudo apt-get update +sudo apt-get install azure-functions-core-tools-3 +``` + #### Other Linux Distributions 1. Download latest release From 4a901a620ac4243b1e0c47f6fbab9b67bd222494 Mon Sep 17 00:00:00 2001 From: Katy Shimizu Date: Tue, 3 Mar 2020 15:57:40 -0800 Subject: [PATCH 024/127] Revert "Added MSI creation assets and CI/CD integration scripts." This reverts commit d2f17f142b757a7435cd63faaef7fdb63c4eafd9. --- azure-pipelines.yml | 7 ++-- build.ps1 | 54 ------------------------- build/BuildSteps.cs | 17 ++++---- build/funcinstall.wxs | 86 ---------------------------------------- build/icon.ico | Bin 32726 -> 0 bytes build/installbanner.bmp | Bin 85894 -> 0 bytes build/installdialog.bmp | Bin 461814 -> 0 bytes build/license.rtf | Bin 1294 -> 0 bytes 8 files changed, 10 insertions(+), 154 deletions(-) delete mode 100644 build/funcinstall.wxs delete mode 100644 build/icon.ico delete mode 100644 build/installbanner.bmp delete mode 100644 build/installdialog.bmp delete mode 100644 build/license.rtf diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0916ac4da..fc782ca5f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,6 +19,7 @@ variables: devops_buildNumber: $[counter(format(''), 1500)] APPVEYOR_REPO_BRANCH: $[coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranchName'])] APPVEYOR_REPO_COMMIT: $(Build.SourceVersion) + steps: - pwsh: | Write-Host "Target branch: '$(APPVEYOR_REPO_BRANCH)'" @@ -39,7 +40,7 @@ steps: $accessToken = (az account get-access-token --query "accessToken" | % { $_.Trim('"') }) echo "##vso[task.setvariable variable=azure_management_access_token]$accessToken" - pwsh: | - .\build.ps1 -MsiGenBranches master,v3.x + .\build.ps1 env: AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) BuildArtifactsStorage: $(BuildArtifactsStorage) @@ -55,9 +56,7 @@ steps: - task: CopyFiles@2 inputs: SourceFolder: '$(Build.Repository.LocalPath)\artifacts' - Contents: | - Azure.Functions.Cli.* - func-cli*.msi + Contents: 'Azure.Functions.Cli.*' TargetFolder: '$(Build.ArtifactStagingDirectory)' CleanTargetFolder: true - task: PublishBuildArtifacts@1 diff --git a/build.ps1 b/build.ps1 index 9285622e9..59ab4ba42 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,3 @@ -param([String[]] $MsiGenBranches) - -$baseDir = Get-Location if ($env:APPVEYOR_REPO_BRANCH -eq "disabled") { Set-Location ".\src\Azure.Functions.Cli" @@ -37,54 +34,3 @@ else { Invoke-Expression -Command "dotnet run" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } } - -if ($MsiGenBranches.Contains($env:APPVEYOR_REPO_BRANCH)) { - Write-Host "Generating MSI files" - - # Add WiX to PATH - if (-not (@($env:Path -split ";") -contains $env:WIX)) - { - # Check if the Wix path points to the bin folder - if ((Split-Path $env:WIX -Leaf) -ne "bin") - { - $env:Path += ";$env:WIX\bin" - } - else - { - $env:Path += ";$env:WIX" - } - } - - # Get runtime version - $artifactsPath = "$baseDir\artifacts" - $buildDir = "$baseDir\build" - $cli = Get-ChildItem -Path $artifactsPath -Include func.dll -Recurse | Select-Object -First 1 - $cliVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($cli).FileVersion - - # Generate MSI installers for Windows - @('x64', 'x86') | ForEach-Object { - $platform = $_ - $targetDir = "$artifactsPath\win-$platform" - - Copy-Item "$buildDir\icon.ico" -Destination $artifactsPath\win-$platform - Copy-Item "$buildDir\license.rtf" -Destination $artifactsPath\win-$platform - Copy-Item "$buildDir\installbanner.bmp" -Destination $artifactsPath\win-$platform - Copy-Item "$buildDir\installdialog.bmp" -Destination $artifactsPath\win-$platform - Set-Location $targetDir - - $masterWxsName = "funcinstall" - $fragmentName = "$platform-frag" - $msiName = "func-cli-$cliVersion-$platform" - - $masterWxsPath = "$buildDir\$masterWxsName.wxs" - $fragmentPath = "$buildDir\$fragmentName.wxs" - $msiPath = "$artifactsPath\$msiName.msi" - - Invoke-Expression "heat dir '.' -cg FuncHost -dr INSTALLDIR -gg -ke -out $fragmentPath -srd -sreg -template fragment -var var.Source" - Invoke-Expression "candle -arch $platform -dPlatform='$platform' -dSource='.' -dProductVersion='$cliVersion' $masterWxsPath $fragmentPath" - Invoke-Expression "light -ext WixUIExtension -out $msiPath -sice:ICE61 $masterWxsName.wixobj $fragmentName.wixobj" - - Set-Location $baseDir - Get-ChildItem -Path $targetDir -Recurse | Remove-Item -Force -Recurse -ea SilentlyContinue - } -} diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 247711a1f..1a4ef50d2 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -425,16 +425,13 @@ public static void Zip() ColoredConsole.WriteLine($"Creating {shaPath}"); File.WriteAllText(shaPath, ComputeSha256(zipPath)); - if (!runtime.StartsWith("win")) { - try - { - Directory.Delete(path, recursive: true); - } - catch - { - ColoredConsole.Error.WriteLine($"Error deleting {path}"); - } - + try + { + Directory.Delete(path, recursive: true); + } + catch + { + ColoredConsole.Error.WriteLine($"Error deleting {path}"); } ColoredConsole.WriteLine(); diff --git a/build/funcinstall.wxs b/build/funcinstall.wxs deleted file mode 100644 index d08595336..000000000 --- a/build/funcinstall.wxs +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build/icon.ico b/build/icon.ico deleted file mode 100644 index fecd5903168febf542f4d212045417ea4f2667a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32726 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�F 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 diff --git a/build/installbanner.bmp b/build/installbanner.bmp deleted file mode 100644 index ed06210300496a7425ac07ab2e18fe64e4ebffb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85894 zcmeI5`)^xk8OO6fAatQXpKW6k~2+o~EA#dTXP1d2-F+qYSTi#JUc$xsd*FY4%`GY|lQtw7-Tf?^p~Khdje1#ezDZlHiPi-Whd+2yRlu!l) zKmY{N2`Egu+5LvI!AAP)J^!6}WHznD*$3Pb;1-W;?v^Xsa%}RHmgfT(zfZIB!8$Uq zG^90U{1!TH%m=w300JP8fq=r|;~l>vbBd#ZMEvF0($vO6Gm`pgzijpK9S*6yjX%@N zf>9$wd3-q(xb%vU^7`t#&bo~aW=nyNXCs4j5C8!X$V@-c!6{|cHJxG9Ul~Tb+QUEkcqlg@kqJ^tYB5 zZsbYiy+v;ID^?E^Q}TYPyqTFg)ZdQhu&z+!!Wl6!E4Kv)-%{1AQVt!@b1YB{1VCWx z5|Cqy!xza(>tz20{1?Ssqn;~5GWCjERuYzVTtWm@nQ+k$9H z-l=}RpM~Ov)wE1)*|UcJ387`rI|E16)%Y!`HKF6FG;j|BAOHgC1a9~Jl|-Cm)OliG zcy1&;d-eTTlslKXW0~8R`Cx?iMR;$R_k{V?F#p4hB1|hn$C+MA(-mee2NPxxp(Q!Ht&yGYH~r-&(e zkFc(o+tsIgl$*;rq&yy;^`3i~tUB}S1bA>jL^LENb)Edi z;jHtKYN{srs6ujZ*kh4nwPx)(`<4*Q|LH(r;wvzMW4LAPu72@ z+(_nj(s9MyBHR$)3}zPc*wUo;{9)RrPhH;`_*+I1r{1CC$sxcE2!H?x7!e=?%V3>w zJ){NCj8g84@+bYw>P?tdL|Bzhsr(8fy-r*$3Yo>%u2#j|%FM0Gu5QJ-m~LH(KD_HY zcbK*U4N@>$$3L=(I6X0R+-TU42Ld1f0;vQjIJhq5uH(;bw3W+T-Ft&shm)q2RWHL9 z((8m|o7;GXt4rNuR~F^9u40Rm-gAe^PezEiv%x^bsSb2}4IJ)300clFCxOXpO@npS z!oYGOgOtBDmu^}S8-K_SE_7Vk*2*e76wAh|U8(5iuifDKoP6cM9q-vg;#i5g>d8C~ zdxMVW95oaG0T2Lzbp)n7Cn!WXO_s?eF+3MY&AS=nANP@IMeN=bCUTE3k&|h~tWC*x zv?~phd^n=|!|9hVtWr9?=(tYW_pco&VIz0L)W);jL&vj`K{^P400?9yFy%XAZ>T0W z64%N7OSAseaxdJ{17nF@Kjf?;TvI4KmbRp6?ohwxRHSrE_*!k+@$Wt(MBLu+#cAJ} zwG6{M=(r&sWP$()fIt=kGl6sVhA$@!EGbfc>OQxK0EfDc3n|}55o^h`LQ`^D(3{a2+0w4ea`4E^HzCcn=%6skP{%~kiuQDz1 z-2p~1$92;R>G%#OMIIL$ICbxok9H~U4I#)i5kE*xQG?Tg^ZE3iECU@+qQDIZfB*<= zn!xPXWk=&4P0A01XA`#aBieCp$tk&TKCi-k&b@%{vJpXYmMCr0OK%)YQ7B^Lqmw&yN#IfdB}AKz0K2H<~?f?;E}HDh0Wv zQuDI<*?>6CEpZB=o!U56T&Og@8Mj~Rxld6nf`Gx1yzR-Ac_8yFKpyNh#kOu-F z00Maupy;xS98DJfDa9OLzuLKdVBM75+$AEb(AFV(DRL~nvXHF1*&FEiW{IIZ2!H?x z zuIN(UrM~E4DXuGpbOaqQgc|CA00@9UP6Bkg+b(gkMABUFdb7 z;e7& diff --git a/build/installdialog.bmp b/build/installdialog.bmp deleted file mode 100644 index 47a8fadfadafa17310af912650c65473d495e3f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 461814 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 Gw}nd_y9&Dc=wsO{yg## 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 diff --git a/build/license.rtf b/build/license.rtf deleted file mode 100644 index e2f610283407fe9b925b9bf607da235b296773e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1294 zcmZ8hUvJws5Z`lve1}7xwn6Y@XKUB?NwMjes4N+j+%$k*D2cMT$)rJ2F@hl9eMeeJ z(E)~R9q)esj_^xmTsN<{vNAlH(_y~&Yf-hL>*7l3Q8xN`;?5Tat`76~_V%Ufv~sQ< z@JC#AN)A=m^xRnCVp#G))<%ALeZ9VVseZ2xLJ7m2Ho#=li*^yiJf6j~`QNu67O&Nl z6z6}XYX7XpW)9a^)rp(7vV0N8l^qrzKj8B?4sIT(>|#-Q6`x>BOGsrSlofvD`f909 z&qf}42eXWnu-2n$xszJWAQ=V-M;45*!aRv~b{X0UbClL%7f1`eFrt2jgW<}FHiFIw z0lEY4?I0p>8n}9flQ0$sbnUoQQXPOpgXF%`^$25i=g!;+Y;J*Ds~gD?4_e)fN16Plg=hlaj^Hd^uSe${I3pYHEW)N{7+bI5?Rbz!IWhS0 z$u=qt;Wa#<3FNtzT%a4dKmQCTWhpxrT1$;U$$1_ zF|YNL2!1Y0SK6W65P4Wa5OUe_BY4>=C}wY!w0dzGI{) zAZI|gyOa{F(QKLSS2VkaJDkgMyx|n@If9lsc)|&a5}aGZmaydpk&`=`((*BaH7zrb zxkk_gb_px#a-Sv)c6+wV3xf13gw1HSX2?ReBr9jgiZvkL@CAxZlBR(w*`s1Y3Mydo=y7Wkr${ Rv2Z=vPtDUt@&D?V{{S}6r5^wQ From f6318b76d4eb0de6647902a7781902ef387f36b4 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 4 Mar 2020 19:21:19 -0800 Subject: [PATCH 025/127] Update functions host references for 2.0.13036 (#1876) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 164db3b56..c5b796df1 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -113,8 +113,8 @@ - - + + From a4e25788c0cc035f22d5f370c4f074319dc79c27 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Thu, 5 Mar 2020 10:18:22 -0800 Subject: [PATCH 026/127] disabling sampling of requests in default host.json (#1807) --- .../StaticResources/host.json | 10 ++++++- .../E2E/InitTests.cs | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/StaticResources/host.json b/src/Azure.Functions.Cli/StaticResources/host.json index 83a921673..a85a33a30 100644 --- a/src/Azure.Functions.Cli/StaticResources/host.json +++ b/src/Azure.Functions.Cli/StaticResources/host.json @@ -1,3 +1,11 @@ { - "version": "2.0" + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingExcludedTypes": "Request", + "samplingSettings": { + "isEnabled": true + } + } + } } \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index 80b0bf0a6..05e872e0e 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -416,6 +416,33 @@ public Task init_function_app_powershell_supports_managed_dependencies() }, _output); } + [Fact] + public Task init_function_app_contains_logging_config() + { + return CliTester.Run(new RunConfiguration + { + Commands = new[] { "init . --worker-runtime node" }, + CheckFiles = new FileResult[] + { + new FileResult + { + Name = "host.json", + ContentContains = new [] + { + "applicationInsights", + "samplingExcludedTypes", + "Request", + "logging" + } + } + }, + OutputContains = new[] + { + "Writing host.json" + } + }, _output); + } + [Fact] public Task init_managed_dependencies_is_only_supported_in_powershell() { From d8d237f44132541b857ab16314dff1e843025c11 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Sat, 7 Mar 2020 22:29:55 -0800 Subject: [PATCH 027/127] Check if bundle config exists, hostJson exists (#1882) --- .../Actions/HostActions/StartHostAction.cs | 44 ++++++++++++ src/Azure.Functions.Cli/Common/Constants.cs | 2 + .../E2E/StartTests.cs | 70 +++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 897621d46..6ac050fb1 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 { @@ -248,6 +249,7 @@ public override async Task RunAsync() await PreRunConditions(); Utilities.PrintLogo(); Utilities.PrintVersion(); + ValidateHostJsonConfiguration(); var settings = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); @@ -268,6 +270,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) @@ -301,6 +318,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 6db0f9afa..ee67682a1 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"; @@ -45,6 +46,7 @@ internal static class Constants public const string DefaultManagementURL = "https://management.azure.com/"; public const string AzureManagementAccessToken = "AZURE_MANAGEMENT_ACCESS_TOKEN"; public const string AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; + public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; 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() { From ea490c537f685ede39a5ba00d598beae747a2c1c Mon Sep 17 00:00:00 2001 From: "Hanzhang Zeng (Roger)" <48038149+Hazhzeng@users.noreply.github.com> Date: Thu, 12 Mar 2020 11:29:25 -0700 Subject: [PATCH 028/127] Change csproj to point to the new version 1.1.2020022812 (#1861) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index c5b796df1..1cac93039 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -114,7 +114,7 @@ - + From 6478ce04fd380c113cff659d0e36eea5a42aff3b Mon Sep 17 00:00:00 2001 From: "Hanzhang Zeng (Roger)" <48038149+Hazhzeng@users.noreply.github.com> Date: Thu, 12 Mar 2020 11:59:12 -0700 Subject: [PATCH 029/127] Improve dedicated plan remote build experience (#1856) * Makes dedicated plan stream log * Prioritize py launcher for Python version detection * Prioritize python3 over python * Lossen runtime image check * Handle exception when deployment id missing case --- .../AzureActions/PublishFunctionAppAction.cs | 15 ++-- .../Common/DeploymentStatus.cs | 1 + .../Helpers/KuduLiteDeploymentHelpers.cs | 79 +++++++++---------- .../Helpers/PublishHelper.cs | 38 +++++++++ .../Helpers/PythonHelpers.cs | 9 ++- .../PublishHelperTests.cs | 34 ++++++++ 6 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 test/Azure.Functions.Cli.Tests/PublishHelperTests.cs diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 4d8594017..a63e45ec7 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -240,14 +240,13 @@ private async Task> ValidateFunctionAppPublish(Site if (functionApp.IsLinux && !functionApp.IsDynamic && !string.IsNullOrEmpty(functionApp.LinuxFxVersion)) { - var allImages = Constants.WorkerRuntimeImages.Values.SelectMany(image => image).ToList(); - if (!allImages.Any(image => functionApp.LinuxFxVersion.IndexOf(image, StringComparison.OrdinalIgnoreCase) != -1)) + // If linuxFxVersion does not match any of our images + if (PublishHelper.IsLinuxFxVersionUsingCustomImage(functionApp.LinuxFxVersion)) { ColoredConsole.WriteLine($"Your functionapp is using a custom image {functionApp.LinuxFxVersion}.\nAssuming that the image contains the correct framework.\n"); } // If there the functionapp is our image but does not match the worker runtime image, we either fail or force update - else if (Constants.WorkerRuntimeImages.TryGetValue(workerRuntime, out IEnumerable linuxFxImages) && - !linuxFxImages.Any(image => functionApp.LinuxFxVersion.IndexOf(image, StringComparison.OrdinalIgnoreCase) != -1)) + else if (!PublishHelper.IsLinuxFxVersionRuntimeMatched(functionApp.LinuxFxVersion, workerRuntime)) { if (Force) { @@ -439,7 +438,7 @@ await WaitForAppSettingUpdateSCM(functionApp, shouldHaveSettings: functionApp.Az shouldNotHaveSettings: new Dictionary { { "WEBSITE_RUN_FROM_PACKAGE", "1" } }, timeOutSeconds: 300); } - Task pollDedicatedBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForDedicatedBuildToComplete(client, functionApp); + Task pollDedicatedBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp); await PerformServerSideBuild(functionApp, zipStreamFactory, pollDedicatedBuild); } @@ -457,7 +456,7 @@ private async Task HandleLinuxConsumptionPublish(Site functionApp, Func pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForConsumptionServerSideBuild(client, functionApp, AccessToken, ManagementURL); + Task pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp); var deployStatus = await PerformServerSideBuild(functionApp, zipFileFactory, pollConsumptionBuild); return deployStatus == DeployStatus.Success; } @@ -726,6 +725,10 @@ public async Task PerformServerSideBuild(Site functionApp, Func> GetAppSettings(HttpClient c return await InvokeRequest>(client, HttpMethod.Get, "/api/settings"); } - public static async Task WaitForConsumptionServerSideBuild(HttpClient client, Site functionApp, string accessToken, string managementUrl) + public static async Task WaitForRemoteBuild(HttpClient client, Site functionApp) { ColoredConsole.WriteLine("Remote build in progress, please wait..."); DeployStatus statusCode = DeployStatus.Pending; @@ -30,7 +32,7 @@ public static async Task WaitForConsumptionServerSideBuild(HttpCli await Task.Delay(TimeSpan.FromSeconds(Constants.KuduLiteDeploymentConstants.StatusRefreshSeconds)); } - while (statusCode != DeployStatus.Success && statusCode != DeployStatus.Failed) + while (statusCode != DeployStatus.Success && statusCode != DeployStatus.Failed && statusCode != DeployStatus.Unknown) { statusCode = await GetDeploymentStatusById(client, functionApp, id); logLastUpdate = await DisplayDeploymentLog(client, functionApp, id, logLastUpdate); @@ -40,35 +42,6 @@ public static async Task WaitForConsumptionServerSideBuild(HttpCli return statusCode; } - public static async Task WaitForDedicatedBuildToComplete(HttpClient client, Site functionApp) - { - // There is a tracked Locking issue in kudulite causing Race conditions, so we have to use this API - // to gather deployment progress. - ColoredConsole.Write("Remote build in progress, please wait"); - while (true) - { - var json = await InvokeRequest>(client, HttpMethod.Get, "/api/isdeploying"); - - if (bool.TryParse(json["value"], out bool isDeploying)) - { - if (!isDeploying) - { - string deploymentId = await GetLatestDeploymentId(client, functionApp); - DeployStatus status = await GetDeploymentStatusById(client, functionApp, id: deploymentId); - ColoredConsole.Write($"done{Environment.NewLine}"); - return status; - } - } - else - { - throw new CliException($"Expected \"value\" from /api/isdeploying endpoing to be a boolean. Actual: {json["value"]}"); - } - - ColoredConsole.Write("."); - await Task.Delay(5000); - } - } - private static async Task GetLatestDeploymentId(HttpClient client, Site functionApp) { var json = await InvokeRequest>>(client, HttpMethod.Get, "/deployments"); @@ -78,7 +51,8 @@ private static async Task GetLatestDeploymentId(HttpClient client, Site if (latestDeployment.TryGetValue("status", out string statusString)) { DeployStatus status = ConvertToDeployementStatus(statusString); - if (status != DeployStatus.Pending) + if (status == DeployStatus.Building || status == DeployStatus.Deploying + || status == DeployStatus.Success || status == DeployStatus.Failed) { return latestDeployment["id"]; } @@ -88,36 +62,44 @@ private static async Task GetLatestDeploymentId(HttpClient client, Site private static async Task GetDeploymentStatusById(HttpClient client, Site functionApp, string id) { - var json = await InvokeRequest>(client, - HttpMethod.Get, $"/deployments/{id}"); + Dictionary json; + try + { + json = await InvokeRequest>(client, HttpMethod.Get, $"/deployments/{id}"); + } catch (HttpRequestException) + { + return DeployStatus.Unknown; + } - if (json.TryGetValue("status", out string statusString)) + if (!json.TryGetValue("status", out string statusString)) { - return ConvertToDeployementStatus(json["status"]); + return DeployStatus.Unknown; } - return DeployStatus.Failed; + + return ConvertToDeployementStatus(statusString); } - private static async Task DisplayDeploymentLog(HttpClient client, Site functionApp, string id, DateTime lastUpdate, Uri innerUrl = null) + private static async Task DisplayDeploymentLog(HttpClient client, Site functionApp, string id, DateTime lastUpdate, Uri innerUrl = null, StringBuilder innerLogger = null) { string logUrl = innerUrl != null ? innerUrl.ToString() : $"/deployments/{id}/log"; - var json = await InvokeRequest>>(client, - HttpMethod.Get, logUrl); + StringBuilder sbLogger = innerLogger != null ? innerLogger : new StringBuilder(); + var json = await InvokeRequest>>(client, HttpMethod.Get, logUrl); var logs = json.Where(dict => DateTime.Parse(dict["log_time"]) > lastUpdate || dict["details_url"] != null); DateTime currentLogDatetime = lastUpdate; + foreach (var log in logs) { // Filter out details_url log if (DateTime.Parse(log["log_time"]) > lastUpdate) { - ColoredConsole.WriteLine(log["message"]); + sbLogger.AppendLine(log["message"]); } // Recursively log details_url from scm/api/deployments/xxx/log endpoint if (log["details_url"] != null && Uri.TryCreate(log["details_url"], UriKind.Absolute, out Uri detailsUrl)) { - DateTime innerLogDatetime = await DisplayDeploymentLog(client, functionApp, id, currentLogDatetime, detailsUrl); + DateTime innerLogDatetime = await DisplayDeploymentLog(client, functionApp, id, currentLogDatetime, detailsUrl, sbLogger); currentLogDatetime = innerLogDatetime > currentLogDatetime ? innerLogDatetime : currentLogDatetime; } } @@ -127,6 +109,13 @@ private static async Task DisplayDeploymentLog(HttpClient client, Site DateTime lastLogDatetime = DateTime.Parse(logs.Last()["log_time"]); currentLogDatetime = lastLogDatetime > currentLogDatetime ? lastLogDatetime : currentLogDatetime; } + + // Report build status on the root level parser + if (innerUrl == null && sbLogger.Length > 0) + { + ColoredConsole.Write(sbLogger.ToString()); + } + return currentLogDatetime; } @@ -154,7 +143,11 @@ await RetryHelper.Retry(async () => private static DeployStatus ConvertToDeployementStatus(string statusString) { - return Enum.Parse(statusString); + if (Enum.TryParse(statusString, out DeployStatus result)) + { + return result; + } + return DeployStatus.Unknown; } } } diff --git a/src/Azure.Functions.Cli/Helpers/PublishHelper.cs b/src/Azure.Functions.Cli/Helpers/PublishHelper.cs index 9a6ac90db..b73b26938 100644 --- a/src/Azure.Functions.Cli/Helpers/PublishHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/PublishHelper.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Net.Http.Handlers; using System.Threading.Tasks; @@ -96,5 +98,41 @@ public static async Task CheckResponseStatusAsync(HttpResponseMessage response, throw new CliException(errorMessage); } } + + public static bool IsLinuxFxVersionUsingCustomImage(string linuxFxVersion) + { + if (string.IsNullOrEmpty(linuxFxVersion)) + { + return false; + } + + bool isStartingWithDocker = linuxFxVersion.StartsWith("docker|", StringComparison.OrdinalIgnoreCase); + bool isLegacyImageMatched = Constants.WorkerRuntimeImages.Values + .SelectMany(image => image) + .Any(image => linuxFxVersion.Contains(image, StringComparison.OrdinalIgnoreCase)); + + return isStartingWithDocker && !isLegacyImageMatched; + } + + public static bool IsLinuxFxVersionRuntimeMatched(string linuxFxVersion, WorkerRuntime runtime) { + if (string.IsNullOrEmpty(linuxFxVersion)) + { + // Suppress the check since when LinuxFxVersion == "", runtime image will depends on FUNCTIONS_WORKER_RUNTIME setting + return true; + } + + // Test if linux fx version matches any legacy runtime image (e.g. DOCKER|mcr.microsoft.com/azure-functions/dotnet) + bool isStartingWithDocker = linuxFxVersion.StartsWith("docker|", StringComparison.OrdinalIgnoreCase); + bool isLegacyImageMatched = false; + if (Constants.WorkerRuntimeImages.TryGetValue(runtime, out IEnumerable legacyImages)) { + isLegacyImageMatched = legacyImages + .Any(image => linuxFxVersion.Contains(image, StringComparison.OrdinalIgnoreCase)); + } + + // Test if linux fx version matches any official runtime image (e.g. DOTNET, DOTNET|2) + bool isOfficialImageMatched = linuxFxVersion.StartsWith(runtime.ToString(), StringComparison.OrdinalIgnoreCase); + + return isOfficialImageMatched || (isStartingWithDocker && isLegacyImageMatched); + } } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs index 1d0520b6e..b6250133a 100644 --- a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs @@ -125,15 +125,20 @@ public static async Task GetEnvironmentPythonVersion( return await GetVersion(pythonDefaultExecutablePath); } - var pythonGetVersionTask = GetVersion("python"); + // Windows Python Launcher (https://www.python.org/dev/peps/pep-0486/) + var pyGetVersionTask = PlatformHelper.IsWindows ? GetVersion("py") : Task.FromResult(null); + + // Linux / OSX / Venv Interpreter Entrypoints var python3GetVersionTask = GetVersion("python3"); + var pythonGetVersionTask = GetVersion("python"); var python36GetVersionTask = GetVersion("python3.6"); var python37GetVersionTask = GetVersion("python3.7"); var versions = new List { - await pythonGetVersionTask, + await pyGetVersionTask, await python3GetVersionTask, + await pythonGetVersionTask, await python36GetVersionTask, await python37GetVersionTask }; diff --git a/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs b/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs new file mode 100644 index 000000000..44a16e0fe --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs @@ -0,0 +1,34 @@ +using Azure.Functions.Cli.Helpers; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Azure.Functions.Cli.PublishHelperTests +{ + public class PublishHelperTests + { + [Theory] + [InlineData("DOCKER|mcr.microsoft.com/azure-functions/node", false)] + [InlineData("DOCKER|customimage", true)] + [InlineData("PYTHON|3.6", false)] + [InlineData("DOTNET", false)] + [InlineData("", false)] + public void IsLinuxFxVersionUsingCustomImageTest(string linuxFxVersion, bool expected) + { + Assert.Equal(expected, PublishHelper.IsLinuxFxVersionUsingCustomImage(linuxFxVersion)); + } + + [Theory] + [InlineData("DOCKER|mcr.microsoft.com/azure-functions/dotnet", WorkerRuntime.dotnet, true)] + [InlineData("DOCKER|mcr.microsoft.com/azure-functions/node", WorkerRuntime.dotnet, false)] + [InlineData("DOCKER|customimage", WorkerRuntime.dotnet, false)] + [InlineData("PYTHON|3.7", WorkerRuntime.python, true)] + [InlineData("PYTHON|3.7", WorkerRuntime.node, false)] + [InlineData("", WorkerRuntime.dotnet, true)] + public void IsLinuxFxVersionRuntimeMatchedTest(string linuxFxVersion, WorkerRuntime runtime, bool expected) + { + Assert.Equal(expected, PublishHelper.IsLinuxFxVersionRuntimeMatched(linuxFxVersion, runtime)); + } + } +} From a626d8c88ec4eafb4a568c8712076ce1c651e6b0 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Thu, 12 Mar 2020 14:31:18 -0700 Subject: [PATCH 030/127] Remove DotNetZip --- .../Azure.Functions.Cli.csproj | 1 - src/Azure.Functions.Cli/Helpers/ZipHelper.cs | 23 +------------------ 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 1cac93039..9c7d14ebb 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -107,7 +107,6 @@ - diff --git a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs index 4001fd509..9e7cd5d6b 100644 --- a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Azure.Functions.Cli.Common; using Colors.Net; -using Ionic.Zip; using static Colors.Net.StringStaticMethods; namespace Azure.Functions.Cli.Helpers @@ -59,10 +58,7 @@ public static async Task CreateZip(IEnumerable files, string roo return await CreateGoZip(files, rootPath, zipFilePath, goZipLocation); } - ColoredConsole.WriteLine(Yellow("Could not find gozip for packaging. Using DotNetZip to package. " + - "This may cause problems preserving file permissions when using in a Linux based environment.")); - - return CreateDotNetZip(files, rootPath, zipFilePath); + throw new CliException("Could not find gozip for packaging."); } public static bool GoZipExists(out string fileLocation) @@ -80,23 +76,6 @@ public static bool GoZipExists(out string fileLocation) return false; } - public static Stream CreateDotNetZip(IEnumerable files, string rootPath, string zipFilePath) - { - const int defaultBufferSize = 4096; - var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite, defaultBufferSize, FileOptions.DeleteOnClose); - using (ZipFile zip = new ZipFile()) - { - zip.CompressionLevel = Ionic.Zlib.CompressionLevel.BestSpeed; - foreach (var file in files) - { - zip.AddFile(file.FixFileNameForZip(rootPath)); - } - zip.Save(fileStream); - } - fileStream.Seek(0, SeekOrigin.Begin); - return fileStream; - } - public static async Task CreateGoZip(IEnumerable files, string rootPath, string zipFilePath, string goZipLocation) { var contentsFile = Path.GetTempFileName(); From ab92851845638055c9a58487dc311e564e8ef51a Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Fri, 13 Mar 2020 14:55:48 -0700 Subject: [PATCH 031/127] Respect --no-docker flag on func kubernetes deploy (#1832) Closes #1829 --- .../Actions/KubernetesActions/KubernetesDeployAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs index 04924e0d5..b27f54acf 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs @@ -191,7 +191,7 @@ private async Task GetTriggersLocalFiles() { if (!string.IsNullOrEmpty(Registry)) { - return ($"{Registry}/{Name}", true); + return ($"{Registry}/{Name}", true && !NoDocker); } else if (!string.IsNullOrEmpty(ImageName)) { From 0961afe1a08b2f7b35b363193393af6d739489a7 Mon Sep 17 00:00:00 2001 From: SatishRanjan Date: Fri, 13 Mar 2020 17:26:31 -0700 Subject: [PATCH 032/127] KEDA release 1.3.0 (#1894) --- src/Azure.Functions.Cli/StaticResources/keda.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/keda.yaml b/src/Azure.Functions.Cli/StaticResources/keda.yaml index bfd1aafec..190f13465 100644 --- a/src/Azure.Functions.Cli/StaticResources/keda.yaml +++ b/src/Azure.Functions.Cli/StaticResources/keda.yaml @@ -4791,7 +4791,7 @@ spec: - name: keda securityContext: {} - image: "docker.io/kedacore/keda:1.2.0" + image: "docker.io/kedacore/keda:1.3.0" command: - keda args: @@ -4834,7 +4834,7 @@ spec: - name: keda-metrics-apiserver securityContext: {} - image: "docker.io/kedacore/keda-metrics-adapter:1.2.0" + image: "docker.io/kedacore/keda-metrics-adapter:1.3.0" imagePullPolicy: Always env: - name: WATCH_NAMESPACE From a63d867ee958d6992af5c30045a537c5a2cb7a31 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Tue, 31 Mar 2020 16:01:45 -0700 Subject: [PATCH 033/127] fixing httpTrigger template test (#1914) --- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index eb2ee14c1..1fdde3f62 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -44,7 +44,7 @@ await CliTester.Run(new RunConfiguration var response = await client.GetAsync("/api/HttpTrigger?name=Test"); var result = await response.Content.ReadAsStringAsync(); p.Kill(); - result.Should().Be("Hello Test", because: "response from default function should be 'Hello {name}'"); + result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); } }, }, _output); @@ -180,7 +180,7 @@ await CliTester.Run(new RunConfiguration[] }, _output, startHost: true); } - + [Fact] public async Task start_displays_error_on_missing_host_json() { @@ -319,7 +319,7 @@ await CliTester.Run(new RunConfiguration var response = await client.GetAsync("/api/HttpTrigger?name=Test"); var result = await response.Content.ReadAsStringAsync(); p.Kill(); - result.Should().Be("Hello Test", because: "response from default function should be 'Hello {name}'"); + result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); } }, }, _output); From 203fc9868aaa273d07a3b9517d9f341e4cc4b8df Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 1 Apr 2020 13:00:24 -0700 Subject: [PATCH 034/127] Update npm dependencies --- .../npm/npm-shrinkwrap.json | 63 ++++++++----------- src/Azure.Functions.Cli/npm/package.json | 6 +- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json index a0248e091..eeacf7778 100644 --- a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json +++ b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json @@ -10,11 +10,11 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", "requires": { - "es6-promisify": "^5.0.0" + "debug": "4" } }, "ansi-styles": { @@ -115,9 +115,9 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -130,19 +130,6 @@ "readable-stream": "^2.0.2" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -193,12 +180,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "agent-base": "6", + "debug": "4" } }, "inflight": { @@ -234,16 +221,16 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "ms": { @@ -289,9 +276,9 @@ } }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" } @@ -346,9 +333,9 @@ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" }, "unzipper": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.8.tgz", - "integrity": "sha512-CZRd20MbyAfWL1Xc+lqgNFYDj5e/HaH9pLEWZgseQWCvEtNfX0wOjnhaJ6PcGpNnDvifSW1ZNSCHX6GoNliR0A==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", + "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", "requires": { "big-integer": "^1.6.17", "binary": "~0.3.0", diff --git a/src/Azure.Functions.Cli/npm/package.json b/src/Azure.Functions.Cli/npm/package.json index bf59f53c7..a7d011c18 100644 --- a/src/Azure.Functions.Cli/npm/package.json +++ b/src/Azure.Functions.Cli/npm/package.json @@ -30,10 +30,10 @@ "chalk": "3.0.0", "command-exists": "1.2.8", "glob": "7.1.6", - "https-proxy-agent": "3.0.1", + "https-proxy-agent": "5.0.0", "progress": "2.0.3", - "rimraf": "3.0.0", + "rimraf": "3.0.2", "tmp": "0.1.0", - "unzipper": "0.10.8" + "unzipper": "0.10.10" } } From 16335598aefbf9b1f391643db49f97c775af0d81 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Thu, 2 Apr 2020 12:34:25 -0700 Subject: [PATCH 035/127] Add publish scripts (#1912) --- .gitignore | 2 + publish-scripts/chocolatey/__init__.py | 0 publish-scripts/chocolatey/buildNUPKG.py | 85 +++++++++++++ publish-scripts/chocolatey/installps_template | 57 +++++++++ publish-scripts/chocolatey/nuspec_template | 20 +++ publish-scripts/driver.py | 46 +++++++ publish-scripts/shared/__init__.py | 0 publish-scripts/shared/constants.py | 20 +++ publish-scripts/shared/helper.py | 118 ++++++++++++++++++ publish-scripts/ubuntu/__init__.py | 0 publish-scripts/ubuntu/bulidDEB.py | 96 ++++++++++++++ publish-scripts/ubuntu/changelog_template | 5 + publish-scripts/ubuntu/control_template | 11 ++ publish-scripts/ubuntu/copyright | 25 ++++ publish-scripts/ubuntu/postinst_template | 10 ++ 15 files changed, 495 insertions(+) create mode 100644 publish-scripts/chocolatey/__init__.py create mode 100644 publish-scripts/chocolatey/buildNUPKG.py create mode 100644 publish-scripts/chocolatey/installps_template create mode 100644 publish-scripts/chocolatey/nuspec_template create mode 100644 publish-scripts/driver.py create mode 100644 publish-scripts/shared/__init__.py create mode 100644 publish-scripts/shared/constants.py create mode 100644 publish-scripts/shared/helper.py create mode 100644 publish-scripts/ubuntu/__init__.py create mode 100644 publish-scripts/ubuntu/bulidDEB.py create mode 100644 publish-scripts/ubuntu/changelog_template create mode 100644 publish-scripts/ubuntu/control_template create mode 100644 publish-scripts/ubuntu/copyright create mode 100644 publish-scripts/ubuntu/postinst_template diff --git a/.gitignore b/.gitignore index e00243847..5b5f7e9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ Publish /tools/NuGet.exe node_modules/ +__pycache__ + *.ncrunchproject *.ncrunchsolution nCrunchTemp* diff --git a/publish-scripts/chocolatey/__init__.py b/publish-scripts/chocolatey/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/publish-scripts/chocolatey/buildNUPKG.py b/publish-scripts/chocolatey/buildNUPKG.py new file mode 100644 index 000000000..6f801c993 --- /dev/null +++ b/publish-scripts/chocolatey/buildNUPKG.py @@ -0,0 +1,85 @@ +#! /usr/bin/env python3.6 + +# depends on chocolaty +import os +import wget +import sys +from string import Template +from shared import constants +from shared.helper import printReturnOutput +from shared.helper import produceHashForfile + +HASH = "SHA512" +def getChocoVersion(version): + # chocolatey do not support semantic versioning2.0.0 yet + # https://github.com/chocolatey/choco/issues/1610 + # look for hypen, and remove any dots after + strlist = version.split('-') + if len(strlist) == 1: + return strlist[0] + elif len(strlist) == 2: + # prerelease + return f"{strlist[0]}-{strlist[1].replace('.','')}" + else: + raise NotImplementedError + +# for windows, there's v1 and v2 versions +# assume buildFolder is clean +# output a deb nupkg +# depends on chocolatey +def preparePackage(): + fileName_x86 = f"Azure.Functions.Cli.win-x86.{constants.VERSION}.zip" + fileName_x64 = f"Azure.Functions.Cli.win-x64.{constants.VERSION}.zip" + url_x86 = f'https://functionscdn.azureedge.net/public/{constants.VERSION}/{fileName_x86}' + url_x64 = f'https://functionscdn.azureedge.net/public/{constants.VERSION}/{fileName_x64}' + + # version used in url is provided from user input + # version used for packaging nuget packages needs a slight modification + chocoVersion = getChocoVersion(constants.VERSION) + + # download the zip + # output to local folder + # -- For 32 bit + if not os.path.exists(fileName_x86): + print(f"downloading from {url_x86}") + wget.download(url_x86) + # -- For 64 bit + if not os.path.exists(fileName_x64): + print(f"downloading from {url_x64}") + wget.download(url_x64) + + # get the checksums + fileHash_x86 = produceHashForfile(fileName_x86, HASH) + fileHash_x64 = produceHashForfile(fileName_x64, HASH) + + tools = os.path.join(constants.BUILDFOLDER, "tools") + os.makedirs(tools) + + # write install powershell script + scriptDir = os.path.abspath(os.path.dirname(__file__)) + with open(os.path.join(scriptDir, "installps_template")) as f: + # TODO stream replace instead of reading the entire string into memory + stringData = f.read() + t = Template(stringData) + with open(os.path.join(tools, "chocolateyinstall.ps1"), "w") as f: + print("writing install powershell script") + f.write(t.safe_substitute(ZIPURL_X86=url_x86, ZIPURL_X64=url_x64, PACKAGENAME=constants.PACKAGENAME, + CHECKSUM_X86=fileHash_x86, CHECKSUM_X64=fileHash_x64, HASHALG=HASH)) + + # write nuspec package metadata + with open(os.path.join(scriptDir,"nuspec_template")) as f: + stringData = f.read() + t = Template(stringData) + nuspecFile = os.path.join(constants.BUILDFOLDER, constants.PACKAGENAME+".nuspec") + with open(nuspecFile,'w') as f: + print("writing nuspec") + f.write(t.safe_substitute(PACKAGENAME=constants.PACKAGENAME, CHOCOVERSION=chocoVersion)) + + # run choco pack, stdout is merged into python interpreter stdout + output = printReturnOutput(["choco", "pack", nuspecFile, "--outputdirectory", constants.ARTIFACTFOLDER]) + assert("Successfully created package" in output) + +# FIXME why does this line not work when import module from sibling package +if __name__ == "__main__": + # preparePackage(*sys.argv[1:]) + pass \ No newline at end of file diff --git a/publish-scripts/chocolatey/installps_template b/publish-scripts/chocolatey/installps_template new file mode 100644 index 000000000..eb3cb8b9b --- /dev/null +++ b/publish-scripts/chocolatey/installps_template @@ -0,0 +1,57 @@ +$ErrorActionPreference = 'Stop'; + +$packageName= '$PACKAGENAME' +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +$url_x86 = '$ZIPURL_X86' +$url_x64 = '$ZIPURL_X64' +$checksum_x86 = '$CHECKSUM_X86' +$checksum_x64 = '$CHECKSUM_X64' + +# By default, we want 32-bit installer +$url = $url_x86 +$checksum = $checksum_x86 + +# If specifically asked for 64-bit, we use that +$pp = Get-PackageParameters +if ($pp['x64'] -eq 'true') +{ + $url = $url_x64 + $checksum = $checksum_x64 +} + +$packageArgs = @{ + packageName = $packageName + unzipLocation = $toolsDir + url = $url + checksum = $checksum + checksumType = '$HASHALG' +} + +Install-ChocolateyZipPackage @packageArgs + +# only symlink for func.exe +$files = get-childitem $toolsDir -include *.exe -recurse +foreach ($file in $files) { + if (!$file.Name.Equals("func.exe")) { + #generate an ignore file + New-Item "$file.ignore" -type file -force | Out-Null + } +} + +try { + New-Item -type File -Path $toolsDir -Name "telemetryDefaultOn.sentinel" | Out-Null + + # show telemetry information + "" + "Telemetry" + "---------" + "The Azure Functions Core tools collect usage data in order to help us improve your experience." + "The data is anonymous and doesn't include any user specific or personal information. The data is collected by Microsoft." + "" + "You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell." + "" +} +catch +{ + # That's ok +} diff --git a/publish-scripts/chocolatey/nuspec_template b/publish-scripts/chocolatey/nuspec_template new file mode 100644 index 000000000..22ccfd51c --- /dev/null +++ b/publish-scripts/chocolatey/nuspec_template @@ -0,0 +1,20 @@ + + + + $PACKAGENAME + $CHOCOVERSION + Azure Function Core Tools + Microsoft + nugetazurefunctions + https://github.com/Azure/azure-functions-core-tools + https://raw.githubusercontent.com/Azure/azure-functions-core-tools/master/src/Azure.Functions.Cli/npm/assets/azure-functions-logo-color-raster.png + https://raw.githubusercontent.com/Azure/azure-functions-core-tools/master/LICENSE + false + azure functions azure-function cli core-tools + The Azure Functions Core Tools provide a local development experience for creating, developing, testing, running, and debugging Azure Functions. + The Azure Functions Core Tools provide a local development experience for creating, developing, testing, running, and debugging Azure Functions. + + + + + \ No newline at end of file diff --git a/publish-scripts/driver.py b/publish-scripts/driver.py new file mode 100644 index 000000000..3843479a1 --- /dev/null +++ b/publish-scripts/driver.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python3.6 +import platform +import sys +import os +import shutil +from shared import constants + +def main(*args): + # assume follow semantic versioning 2.0.0 + constants.VERSION = args[1] + constants.DRIVERROOTDIR = os.path.dirname(os.path.abspath(__file__)) + platformSystem = platform.system() + if platformSystem == "Linux": + d, _, __ = platform.linux_distribution() + if d == "Ubuntu": + import ubuntu.bulidDEB as dist + print("Detected Ubuntu, starting to work on a deb package...") + else: + print(f"Does not support distribution {d} yet.") + return + elif platformSystem == "Windows": + import chocolatey.buildNUPKG as dist + print("Detected Windows, starting to work on a nupkg package...") + else: + print(f"Does not support platform {platformSystem} yet.") + return + + # at root + initWorkingDir(constants.BUILDFOLDER, True) + initWorkingDir(constants.ARTIFACTFOLDER) + + # build package + print("Building package...") + dist.preparePackage() + +def initWorkingDir(dirName, clean = False): + if clean: + if os.path.exists(dirName): + print(f"trying to clear {dirName}/ directory") + shutil.rmtree(dirName) + print(f"trying to create {dirName}/ directory") + os.makedirs(dirName, exist_ok=True) + +if __name__ == "__main__": + # input example: 2.0.1-beta.25 + main(*sys.argv) diff --git a/publish-scripts/shared/__init__.py b/publish-scripts/shared/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/publish-scripts/shared/constants.py b/publish-scripts/shared/constants.py new file mode 100644 index 000000000..33ae339f5 --- /dev/null +++ b/publish-scripts/shared/constants.py @@ -0,0 +1,20 @@ +#! /usr/bin/env python3.6 + +# same for all different OSes +PACKAGENAME_BASE = "azure-functions-core-tools" +CMD = "func" +ARTIFACTFOLDER = "artifact" +BUILDFOLDER = "build" +TESTFOLDER = "test" + +# to be set in driver.py +# do not use it as default argument!! +VERSION = NotImplementedError +DRIVERROOTDIR = NotImplementedError + +# linux specific, for now, its ubuntu + fedora +LINUXDEPS = {} + +TELEMETRY_INFO = "\n Telemetry \n --------- \n The Azure Functions Core tools collect usage data in order to help us improve your experience." \ + + "\n The data is anonymous and doesn\'t include any user specific or personal information. The data is collected by Microsoft." \ + + "\n \n You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to \'1\' or \'true\' using your favorite shell." \ No newline at end of file diff --git a/publish-scripts/shared/helper.py b/publish-scripts/shared/helper.py new file mode 100644 index 000000000..7a41f133d --- /dev/null +++ b/publish-scripts/shared/helper.py @@ -0,0 +1,118 @@ +#! /usr/bin/env python3.6 +import os +import sys +import hashlib +import subprocess +from . import constants + +def restoreDirectory(f): + def inner(*args, **kwargs): + currentDir = os.getcwd() + returnV = f(*args, **kwargs) + os.chdir(currentDir) + return returnV + return inner + +# for some commands, returnCode means success +# for others you need to verify the output string yourself +def printReturnOutput(args, shell=False, confirm=False): + begin = '=' * 38 + "Running Subprocess" + "=" * 38 + print(begin) + print(' '.join(args)) + if (confirm): + input("This seems to be a non reversable behavior, do you still want to proceed? (Ctrl-C if you are not sure)\n") + output = '-' * 40 + "Console Output" + "-" * 40 + print(output) + try: + binary = subprocess.check_output(args, shell=shell) + if len(binary) < 1: + string = None + else: + string = binary.decode('ascii') + print(string) + footer = "=" * 94 + print(footer) + return string + except subprocess.CalledProcessError: + # rerun the command, direct output to stdout + subprocess.call(args) + raise + +BUFFERSIZE = 1024 +def produceHashForfile(filePath, hashType, Upper = True): + # hashType is string name iof + hashobj = hashlib.new(hashType) + with open(filePath,'rb') as f: + buf = f.read(BUFFERSIZE) + while len(buf) > 0: + hashobj.update(buf) + buf = f.read(BUFFERSIZE) + if Upper: + return hashobj.hexdigest().upper() + else: + return hashobj.hexdigest().lower() + +@restoreDirectory +def linuxOutput(buildFolder): + os.chdir(constants.DRIVERROOTDIR) + + # ubuntu dropped 64, fedora supports both + fileName = f"Azure.Functions.Cli.linux-x64.{constants.VERSION}.zip" + url = f'https://functionscdn.azureedge.net/public/{constants.VERSION}/{fileName}' + + # download the zip + # output to local folder + import wget + if not os.path.exists(fileName): + print(f"downloading from {url}") + wget.download(url) + + usr = os.path.join(buildFolder, "usr") + usrlib = os.path.join(usr, "lib") + usrlibFunc = os.path.join(usrlib, constants.PACKAGENAME) + os.makedirs(usrlibFunc) + # unzip here + import zipfile + with zipfile.ZipFile(fileName) as f: + print(f"extracting to {usrlibFunc}") + f.extractall(usrlibFunc) + + # create relative symbolic link under bin directory, change mode to executable + usrbin = os.path.join(usr, "bin") + os.makedirs(usrbin) + # cd into usr/bin, create relative symlink + os.chdir(usrbin) + print("create symlink for func") + os.symlink(f"../lib/{constants.PACKAGENAME}/func", "func") + # executable to be returned + exeFullPath = os.path.abspath("func") + + os.chdir(buildFolder) + # strip sharedobjects + import glob + + sharedObjects = glob.glob("**/*.so", recursive=True) + + # obj files inside the workers should not be removed as workers like "python" + # come with objects necessary for the worker to work. + sharedObjects = [obj for obj in sharedObjects if "workers" not in obj] + printReturnOutput(["strip", "--strip-unneeded"] + sharedObjects) + + chmodFolderAndFiles(os.path.join(buildFolder, "usr")) + print(f"change bin/func permission to 755") + # octal + os.chmod(exeFullPath, 0o755) + + print(f"change {usrlibFunc}/gozip permission to 755") + os.chmod(f"{usrlibFunc}/gozip", 0o755) + +def chmodFolderAndFiles(folder): + print(f"change permission of files in {folder}") + os.chmod(folder, 0o755) + for r, ds, fs in os.walk(folder): + for d in ds: + # folder permission to 755 + os.chmod(os.path.join(r, d), 0o755) + for f in fs: + # file permission to 644 + os.chmod(os.path.join(r, f), 0o644) diff --git a/publish-scripts/ubuntu/__init__.py b/publish-scripts/ubuntu/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/publish-scripts/ubuntu/bulidDEB.py b/publish-scripts/ubuntu/bulidDEB.py new file mode 100644 index 000000000..635c786db --- /dev/null +++ b/publish-scripts/ubuntu/bulidDEB.py @@ -0,0 +1,96 @@ +#! /usr/bin/env python3.6 +import os +import wget +import zipfile +import shutil +import datetime +from string import Template +from shared import constants +from shared import helper + +def returnDebVersion(version): + # version used in url is provided from user input + # version used for packaging .deb package needs a slight modification + # for beta, change to tilde, so it will be placed before rtm versions in apt + # https://unix.stackexchange.com/questions/230911/what-is-the-meaning-of-the-tilde-in-some-debian-openjdk-package-version-string/230921 + strlist = version.split('-') + if len(strlist) == 1: + return strlist[0]+"-1" + elif len(strlist) == 2: + return f"{strlist[0]}~{strlist[1]}-1" + else: + raise NotImplementedError + +# output a deb package +# depends on gzip, dpkg-deb, strip +@helper.restoreDirectory +def preparePackage(): + os.chdir(constants.DRIVERROOTDIR) + + debianVersion = returnDebVersion(constants.VERSION) + packageFolder = f"{constants.PACKAGENAME}_{debianVersion}" + buildFolder = os.path.join(os.getcwd(), constants.BUILDFOLDER, packageFolder) + helper.linuxOutput(buildFolder) + + os.chdir(buildFolder) + document = os.path.join("usr", "share", "doc", constants.PACKAGENAME) + os.makedirs(document) + # write copywrite + print("include MIT copyright") + scriptDir = os.path.abspath(os.path.dirname(__file__)) + shutil.copyfile(os.path.join(scriptDir, "copyright"), os.path.join(document, "copyright")) + # write changelog + with open(os.path.join(scriptDir, "changelog_template")) as f: + stringData = f.read() # read until EOF + t = Template(stringData) + # datetime example: Tue, 06 April 2018 16:32:31 + time = datetime.datetime.utcnow().strftime("%a, %d %b %Y %X") + with open(os.path.join(document, "changelog.Debian"), "w") as f: + print(f"writing changelog with date utc: {time}") + f.write(t.safe_substitute(DEBIANVERSION=debianVersion, DATETIME=time, VERSION=constants.VERSION, PACKAGENAME=constants.PACKAGENAME)) + # by default gzip compress file in place + output = helper.printReturnOutput(["gzip", "-9", "-n", os.path.join(document, "changelog.Debian")]) + helper.chmodFolderAndFiles(os.path.join("usr", "share")) + + debian = "DEBIAN" + os.makedirs(debian) + # get all files under usr/ and produce a md5 hash + print("trying to produce md5 hashes") + with open('DEBIAN/md5sums', 'w') as md5file: + # iterate over all files under usr/ + # get their md5sum + for dirpath, _, filenames in os.walk('usr'): + for f in filenames: + filepath = os.path.join(dirpath, f) + if not os.path.islink(filepath): + h = helper.produceHashForfile(filepath, 'md5', Upper=False) + md5file.write(f"{h} {filepath}\n") + + # produce the control file from template + deps = [] + for key, value in constants.LINUXDEPS.items(): + entry = f"{key} ({value})" + deps.append(entry) + deps = ','.join(deps) + with open(os.path.join(scriptDir, "control_template")) as f: + stringData = f.read() + t = Template(stringData) + with open(os.path.join(debian, "control"), "w") as f: + print("trying to write control file") + f.write(t.safe_substitute(DEBIANVERSION=debianVersion, PACKAGENAME=constants.PACKAGENAME, DEPENDENCY=deps)) + helper.chmodFolderAndFiles(debian) + + postinst = '' + with open(os.path.join(scriptDir, "postinst_template")) as f: + postinst = f.read() + with open(os.path.join(debian, "postinst"), "w") as f: + print("trying to write postinst file") + f.write(postinst) + + # postinstall has to be 0755 in order for it to work. + os.chmod(os.path.join(debian, "postinst"), 0o755) + + os.chdir(constants.DRIVERROOTDIR) + output = helper.printReturnOutput(["fakeroot", "dpkg-deb", "--build", + os.path.join(constants.BUILDFOLDER, packageFolder), os.path.join(constants.ARTIFACTFOLDER, packageFolder+".deb")]) + assert(f"building package '{constants.PACKAGENAME}'" in output) diff --git a/publish-scripts/ubuntu/changelog_template b/publish-scripts/ubuntu/changelog_template new file mode 100644 index 000000000..d170a0552 --- /dev/null +++ b/publish-scripts/ubuntu/changelog_template @@ -0,0 +1,5 @@ +$PACKAGENAME ($DEBIANVERSION) unstable; urgency=medium + + * https://github.com/Azure/$PACKAGENAME/releases/tag/$VERSION + + -- Ahmed ElSayed $DATETIME -0800 diff --git a/publish-scripts/ubuntu/control_template b/publish-scripts/ubuntu/control_template new file mode 100644 index 000000000..94b301c1d --- /dev/null +++ b/publish-scripts/ubuntu/control_template @@ -0,0 +1,11 @@ +Maintainer: Ahmed ElSayed +Version: $DEBIANVERSION +Section: devel +Priority: optional +Homepage: https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#run-azure-functions-core-tools +Vcs-Git: https://github.com/Azure/azure-functions-core-tools.git +Package: $PACKAGENAME +Architecture: amd64 +Depends: $DEPENDENCY +Description: Azure Function Core Tools v2 + The Azure Functions Core Tools provide a local development experience for creating, developing, testing, running, and debugging Azure Functions. diff --git a/publish-scripts/ubuntu/copyright b/publish-scripts/ubuntu/copyright new file mode 100644 index 000000000..972fe4575 --- /dev/null +++ b/publish-scripts/ubuntu/copyright @@ -0,0 +1,25 @@ +Comment: Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. + +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: * +Copyright: 2015 Microsoft +License: MIT + +License: MIT + Copyright (c) 2015 Microsoft +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/publish-scripts/ubuntu/postinst_template b/publish-scripts/ubuntu/postinst_template new file mode 100644 index 000000000..73d005473 --- /dev/null +++ b/publish-scripts/ubuntu/postinst_template @@ -0,0 +1,10 @@ +#!/bin/sh + +touch $(dirname $(readlink -f $(which func)))/telemetryDefaultOn.sentinel >/dev/null 2>&1 && echo " +Telemetry +--------- +The Azure Functions Core tools collect usage data in order to help us improve your experience. +The data is anonymous and doesn't include any user specific or personal information. The data is collected by Microsoft. + +You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell. +" \ No newline at end of file From 7988d61cc63db5ba80eb886d17447a29b246f7b7 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Fri, 3 Apr 2020 01:03:12 -0700 Subject: [PATCH 036/127] Add build logic for -2 packages --- publish-scripts/chocolatey/buildNUPKG.py | 8 ++++---- publish-scripts/driver.py | 17 +++++++++++++---- publish-scripts/shared/helper.py | 6 +++--- publish-scripts/ubuntu/bulidDEB.py | 14 +++++++------- publish-scripts/ubuntu/control_template | 2 ++ 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/publish-scripts/chocolatey/buildNUPKG.py b/publish-scripts/chocolatey/buildNUPKG.py index 6f801c993..8c7d07e8c 100644 --- a/publish-scripts/chocolatey/buildNUPKG.py +++ b/publish-scripts/chocolatey/buildNUPKG.py @@ -27,7 +27,7 @@ def getChocoVersion(version): # assume buildFolder is clean # output a deb nupkg # depends on chocolatey -def preparePackage(): +def preparePackage(packageName): fileName_x86 = f"Azure.Functions.Cli.win-x86.{constants.VERSION}.zip" fileName_x64 = f"Azure.Functions.Cli.win-x64.{constants.VERSION}.zip" url_x86 = f'https://functionscdn.azureedge.net/public/{constants.VERSION}/{fileName_x86}' @@ -63,17 +63,17 @@ def preparePackage(): t = Template(stringData) with open(os.path.join(tools, "chocolateyinstall.ps1"), "w") as f: print("writing install powershell script") - f.write(t.safe_substitute(ZIPURL_X86=url_x86, ZIPURL_X64=url_x64, PACKAGENAME=constants.PACKAGENAME, + f.write(t.safe_substitute(ZIPURL_X86=url_x86, ZIPURL_X64=url_x64, PACKAGENAME=packageName, CHECKSUM_X86=fileHash_x86, CHECKSUM_X64=fileHash_x64, HASHALG=HASH)) # write nuspec package metadata with open(os.path.join(scriptDir,"nuspec_template")) as f: stringData = f.read() t = Template(stringData) - nuspecFile = os.path.join(constants.BUILDFOLDER, constants.PACKAGENAME+".nuspec") + nuspecFile = os.path.join(constants.BUILDFOLDER, packageName+".nuspec") with open(nuspecFile,'w') as f: print("writing nuspec") - f.write(t.safe_substitute(PACKAGENAME=constants.PACKAGENAME, CHOCOVERSION=chocoVersion)) + f.write(t.safe_substitute(PACKAGENAME=packageName, CHOCOVERSION=chocoVersion)) # run choco pack, stdout is merged into python interpreter stdout output = printReturnOutput(["choco", "pack", nuspecFile, "--outputdirectory", constants.ARTIFACTFOLDER]) diff --git a/publish-scripts/driver.py b/publish-scripts/driver.py index 3843479a1..f4bd9879c 100644 --- a/publish-scripts/driver.py +++ b/publish-scripts/driver.py @@ -25,13 +25,22 @@ def main(*args): print(f"Does not support platform {platformSystem} yet.") return - # at root + # Build packages + # we create two packages 'azure-functions-core-tools-2' and 'azure-functions-core-tools' + + # Build 'azure-functions-core-tools' + initWorkingDir(constants.BUILDFOLDER, True) + initWorkingDir(constants.ARTIFACTFOLDER) + + print(f"Building package {constants.PACKAGENAME_BASE}...") + dist.preparePackage(constants.PACKAGENAME_BASE) + + # Build 'azure-functions-core-tools-2' initWorkingDir(constants.BUILDFOLDER, True) initWorkingDir(constants.ARTIFACTFOLDER) - # build package - print("Building package...") - dist.preparePackage() + print(f"Building package {constants.PACKAGENAME_BASE}-2...") + dist.preparePackage(f"{constants.PACKAGENAME_BASE}-2") def initWorkingDir(dirName, clean = False): if clean: diff --git a/publish-scripts/shared/helper.py b/publish-scripts/shared/helper.py index 7a41f133d..16fffd0ec 100644 --- a/publish-scripts/shared/helper.py +++ b/publish-scripts/shared/helper.py @@ -53,7 +53,7 @@ def produceHashForfile(filePath, hashType, Upper = True): return hashobj.hexdigest().lower() @restoreDirectory -def linuxOutput(buildFolder): +def linuxOutput(buildFolder, packageName): os.chdir(constants.DRIVERROOTDIR) # ubuntu dropped 64, fedora supports both @@ -69,7 +69,7 @@ def linuxOutput(buildFolder): usr = os.path.join(buildFolder, "usr") usrlib = os.path.join(usr, "lib") - usrlibFunc = os.path.join(usrlib, constants.PACKAGENAME) + usrlibFunc = os.path.join(usrlib, packageName) os.makedirs(usrlibFunc) # unzip here import zipfile @@ -83,7 +83,7 @@ def linuxOutput(buildFolder): # cd into usr/bin, create relative symlink os.chdir(usrbin) print("create symlink for func") - os.symlink(f"../lib/{constants.PACKAGENAME}/func", "func") + os.symlink(f"../lib/{packageName}/func", "func") # executable to be returned exeFullPath = os.path.abspath("func") diff --git a/publish-scripts/ubuntu/bulidDEB.py b/publish-scripts/ubuntu/bulidDEB.py index 635c786db..18906caa8 100644 --- a/publish-scripts/ubuntu/bulidDEB.py +++ b/publish-scripts/ubuntu/bulidDEB.py @@ -24,16 +24,16 @@ def returnDebVersion(version): # output a deb package # depends on gzip, dpkg-deb, strip @helper.restoreDirectory -def preparePackage(): +def preparePackage(packageName): os.chdir(constants.DRIVERROOTDIR) debianVersion = returnDebVersion(constants.VERSION) - packageFolder = f"{constants.PACKAGENAME}_{debianVersion}" + packageFolder = f"{packageName}_{debianVersion}" buildFolder = os.path.join(os.getcwd(), constants.BUILDFOLDER, packageFolder) - helper.linuxOutput(buildFolder) + helper.linuxOutput(buildFolder, packageName) os.chdir(buildFolder) - document = os.path.join("usr", "share", "doc", constants.PACKAGENAME) + document = os.path.join("usr", "share", "doc", packageName) os.makedirs(document) # write copywrite print("include MIT copyright") @@ -47,7 +47,7 @@ def preparePackage(): time = datetime.datetime.utcnow().strftime("%a, %d %b %Y %X") with open(os.path.join(document, "changelog.Debian"), "w") as f: print(f"writing changelog with date utc: {time}") - f.write(t.safe_substitute(DEBIANVERSION=debianVersion, DATETIME=time, VERSION=constants.VERSION, PACKAGENAME=constants.PACKAGENAME)) + f.write(t.safe_substitute(DEBIANVERSION=debianVersion, DATETIME=time, VERSION=constants.VERSION, PACKAGENAME=packageName)) # by default gzip compress file in place output = helper.printReturnOutput(["gzip", "-9", "-n", os.path.join(document, "changelog.Debian")]) helper.chmodFolderAndFiles(os.path.join("usr", "share")) @@ -77,7 +77,7 @@ def preparePackage(): t = Template(stringData) with open(os.path.join(debian, "control"), "w") as f: print("trying to write control file") - f.write(t.safe_substitute(DEBIANVERSION=debianVersion, PACKAGENAME=constants.PACKAGENAME, DEPENDENCY=deps)) + f.write(t.safe_substitute(DEBIANVERSION=debianVersion, PACKAGENAME=packageName, DEPENDENCY=deps)) helper.chmodFolderAndFiles(debian) postinst = '' @@ -93,4 +93,4 @@ def preparePackage(): os.chdir(constants.DRIVERROOTDIR) output = helper.printReturnOutput(["fakeroot", "dpkg-deb", "--build", os.path.join(constants.BUILDFOLDER, packageFolder), os.path.join(constants.ARTIFACTFOLDER, packageFolder+".deb")]) - assert(f"building package '{constants.PACKAGENAME}'" in output) + assert(f"building package '{packageName}'" in output) diff --git a/publish-scripts/ubuntu/control_template b/publish-scripts/ubuntu/control_template index 94b301c1d..ce5c47f99 100644 --- a/publish-scripts/ubuntu/control_template +++ b/publish-scripts/ubuntu/control_template @@ -7,5 +7,7 @@ Vcs-Git: https://github.com/Azure/azure-functions-core-tools.git Package: $PACKAGENAME Architecture: amd64 Depends: $DEPENDENCY +Conflicts: azure-functions-core-tools, azure-functions-core-tools-2 +Replaces: azure-functions-core-tools, azure-functions-core-tools-2 Description: Azure Function Core Tools v2 The Azure Functions Core Tools provide a local development experience for creating, developing, testing, running, and debugging Azure Functions. From 01e783d899f7a8ac1b713272e5deac95ab832847 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Mon, 20 Apr 2020 14:11:40 -0700 Subject: [PATCH 037/127] Using ESRP signing service instead of funkins, sign on for master and release/3.0 (#1937) * fixing httpTrigger template test * skipping durable test * Using ESRP signing service instead of funkins, sign on for master and release/3.0 --- .gitignore | 1 + azure-pipelines.yml | 113 +++++++++++- build.ps1 | 10 +- build/BuildSteps.cs | 165 +++++++++++------- build/FileHelpers.cs | 9 + build/Program.cs | 12 +- build/Settings.cs | 15 +- repackageBinaries.ps1 | 122 +++++++++++++ .../E2E/DurableTests.cs | 1 + .../E2E/StartTests.cs | 2 +- 10 files changed, 373 insertions(+), 77 deletions(-) create mode 100644 repackageBinaries.ps1 diff --git a/.gitignore b/.gitignore index 5b5f7e9c8..a0109d8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ nCrunchTemp* .fake/ artifacts/ /src/Azure.Functions.Cli/Properties/launchSettings.json +launchSettings.json \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fc782ca5f..50a00f242 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,12 +23,14 @@ variables: steps: - pwsh: | Write-Host "Target branch: '$(APPVEYOR_REPO_BRANCH)'" + displayName: Set up environment variables - task: NodeTool@0 inputs: versionSpec: '10.x' - task: NuGetToolInstaller@1 inputs: versionSpec: + displayName: Install Nuget tool - task: AzureCLI@2 displayName: Login via Azure CLI to acquire access token inputs: @@ -47,6 +49,115 @@ steps: DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) displayName: 'Executing build script' +- task: EsrpCodeSigning@1 + displayName: 'Authenticode signing' + inputs: + ConnectedServiceName: 'ESRP Service' + FolderPath: '$(Build.Repository.LocalPath)\artifacts\ToSign\Authenticode\' + Pattern: '*.dll, *.exe' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + SessionTimeout: '60' + MaxConcurrency: '50' + MaxRetryAttempts: '5' + condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) +- task: EsrpCodeSigning@1 + displayName: 'Third party signing' + inputs: + ConnectedServiceName: 'ESRP Service' + FolderPath: '$(Build.Repository.LocalPath)\artifacts\ToSign\ThirdParty\' + Pattern: '*.dll, *.exe' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "KeyCode": "CP-231522", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "Append": "/as", + "FileDigest": "/fd \"SHA256\"", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-231522", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + SessionTimeout: '60' + MaxConcurrency: '50' + MaxRetryAttempts: '5' + condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) +- pwsh: | + .\repackageBinaries.ps1 + displayName: Sign and repackage binaries + env: + AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) + BuildArtifactsStorage: $(BuildArtifactsStorage) + DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) + TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) + condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) +- task: DotNetCoreCLI@2 + inputs: + command: 'run' + workingDirectory: '.\build' + arguments: 'TestSignedArtifacts --signTest' + displayName: 'Verify signed binaries' + condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) +- task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + function GenerateSha([string]$filePath,[string]$artifactsPath, [string]$shaFileName) + { + $sha = (Get-FileHash $filePath).Hash.ToLower() + $shaPath = Join-Path $artifactsPath "$shaFileName.sha" + Out-File -InputObject $sha -Encoding ascii -FilePath $shaPath + } + + Set-Location ".\build" + + $artifactsPath = Resolve-Path "..\artifacts\" + $zipFilesSearchPath = Join-Path $artifactsPath "*.zip" + $zipFiles = Get-ChildItem -File $zipFilesSearchPath + + foreach($zipFile in $zipFiles) + { + $zipFullPath = $zipFile.FullName + $fileName = $zipFile.Name + GenerateSha $zipFullPath $artifactsPath $fileName + } + displayName: 'Generate sha files' - task: PublishTestResults@2 inputs: testResultsFormat: 'VSTest' @@ -63,4 +174,4 @@ steps: inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' - publishLocation: 'Container' + publishLocation: 'Container' \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 59ab4ba42..91891da5c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -26,11 +26,5 @@ else { Set-Location ".\build" } -if ($env:APPVEYOR_REPO_BRANCH -eq "master") { - Invoke-Expression -Command "dotnet run --sign" - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -} -else { - Invoke-Expression -Command "dotnet run" - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -} +Invoke-Expression -Command "dotnet run" +if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 1a4ef50d2..985051ecb 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -219,30 +219,99 @@ public static void Test() Shell.Run("dotnet", $"test {Settings.TestProjectFile} --logger trx"); } - public static void GenerateZipToSign() + public static void CopyBinariesToSign() { + string toSignDirPath = Path.Combine(Settings.OutputDir, Settings.SignInfo.ToSignDir); + string authentiCodeDirectory = Path.Combine(toSignDirPath, Settings.SignInfo.ToAuthenticodeSign); + string thirdPartyDirectory = Path.Combine(toSignDirPath, Settings.SignInfo.ToThirdPartySign); + foreach (var supportedRuntime in Settings.SignInfo.RuntimesToSign) { - var targetDir = Path.Combine(Settings.OutputDir, supportedRuntime); - Directory.CreateDirectory(Path.Combine(targetDir, Settings.SignInfo.ToSignDir)); + var sourceDir = Path.Combine(Settings.OutputDir, supportedRuntime); + var toSignPaths = Settings.SignInfo.authentiCodeBinaries.Select(el => Path.Combine(sourceDir, el)); + // Grab all the files and filter the extensions not to be signed + var toAuthenticodeSignFiles = FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignPaths)) + .Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))).ToList(); - var toSignPaths = Settings.SignInfo.authentiCodeBinaries.Select(el => Path.Combine(targetDir, el)); + string dirName = $"Azure.Functions.Cli.{supportedRuntime}.{CurrentVersion}"; + string targetDirectory = Path.Combine(authentiCodeDirectory, dirName); + toAuthenticodeSignFiles.ForEach(f => FileHelpers.CopyFileRelativeToBase(f, targetDirectory, sourceDir)); + + var toSignThirdPartyPaths = Settings.SignInfo.thirdPartyBinaries.Select(el => Path.Combine(sourceDir, el)); // Grab all the files and filter the extensions not to be signed - var toSignFiles = FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignPaths)).Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))); - FileHelpers.CreateZipFile(toSignFiles, targetDir, Path.Combine(targetDir, Settings.SignInfo.ToSignDir, Settings.SignInfo.ToSignZipName)); + var toSignThirdPartyFiles = FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignThirdPartyPaths)) + .Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))).ToList(); + string targetThirdPartyDirectory = Path.Combine(thirdPartyDirectory, dirName); + toSignThirdPartyFiles.ForEach(f => FileHelpers.CopyFileRelativeToBase(f, targetThirdPartyDirectory, sourceDir)); + } + + // binaries we know are unsigned via sigcheck.exe + var unSignedBinaries = GetUnsignedBinaries(toSignDirPath); + + // binaries to be signed via signed tool + var allFiles = Directory.GetFiles(toSignDirPath, "*.*", new EnumerationOptions() { RecurseSubdirectories = true }).ToList(); + // remove all entries for binaries that are actually unsigned (checked via sigcheck.exe) + unSignedBinaries.ForEach(f => allFiles.RemoveAll(n => n.Equals(f, StringComparison.OrdinalIgnoreCase))); + + // all the files that are remaining are signed files, delete the signed files since they don't need to be signed again + allFiles.ForEach(f => File.Delete(f)); + } + + public static void TestPreSignedArtifacts() + { + foreach (var supportedRuntime in Settings.SignInfo.RuntimesToSign) + { + var sourceDir = Path.Combine(Settings.OutputDir, supportedRuntime); + var targetDir = Path.Combine(Settings.OutputDir, Settings.PreSignTestDir, supportedRuntime); + Directory.CreateDirectory(targetDir); + FileHelpers.RecursiveCopy(sourceDir, targetDir); + + var toSignPaths = Settings.SignInfo.authentiCodeBinaries.Select(el => Path.Combine(targetDir, el)); var toSignThirdPartyPaths = Settings.SignInfo.thirdPartyBinaries.Select(el => Path.Combine(targetDir, el)); - // Grab all the files and filter the extensions not to be signed - var toSignThirdPartyFiles = FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignThirdPartyPaths)).Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))); - FileHelpers.CreateZipFile(toSignThirdPartyFiles, targetDir, Path.Combine(targetDir, Settings.SignInfo.ToSignDir, Settings.SignInfo.ToSignThirdPartyName)); + var unSignedFiles = FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignPaths)) + .Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))).ToList(); + + unSignedFiles.AddRange(FileHelpers.GetAllFilesFromFilesAndDirs(FileHelpers.ExpandFileWildCardEntries(toSignThirdPartyPaths)) + .Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext)))); + + unSignedFiles.ForEach(filePath => File.Delete(filePath)); + + var unSignedPackages = GetUnsignedBinaries(targetDir); + if (unSignedPackages.Count() != 0) + { + var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); + ColoredConsole.Error.WriteLine($"This files are missing valid signatures: {Environment.NewLine}{missingSignature}"); + throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); + } } } - public static void UploadZipToSign() + public static void TestSignedArtifacts() { - UploadZipToSignAsync().Wait(); + string[] zipFiles = Directory.GetFiles(Settings.OutputDir, "*.zip"); + + foreach (string zipFilePath in zipFiles) + { + bool isSignedRuntime = Settings.SignInfo.RuntimesToSign.Any(r => zipFilePath.Contains(r)); + if (isSignedRuntime) + { + string targetDir = Path.Combine(Settings.OutputDir, "PostSignTest", Path.GetFileNameWithoutExtension(zipFilePath)); + Directory.CreateDirectory(targetDir); + ZipFile.ExtractToDirectory(zipFilePath, targetDir); + + var unSignedPackages = GetUnsignedBinaries(targetDir); + if (unSignedPackages.Count() != 0) + { + var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); + ColoredConsole.Error.WriteLine($"This files are missing valid signatures: {Environment.NewLine}{missingSignature}"); + throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); + } + } + } } + public static async Task UploadZipToSignAsync() { var storageConnection = Settings.SignInfo.AzureSigningConnectionString; @@ -351,7 +420,7 @@ public static void ReplaceSignedZipAndCleanup() } } - public static void TestSignedArtifacts() + public static List GetUnsignedBinaries(string targetDir) { // Download sigcheck.exe var sigcheckPath = Path.Combine(Settings.OutputDir, "sigcheck.exe"); @@ -366,48 +435,39 @@ public static void TestSignedArtifacts() Console.WriteLine(Shell.GetOutput("reg.exe", "ADD HKCU\\Software\\Sysinternals /v EulaAccepted /t REG_DWORD /d 1 /f")); Console.WriteLine(Shell.GetOutput("reg.exe", "ADD HKU\\.DEFAULT\\Software\\Sysinternals /v EulaAccepted /t REG_DWORD /d 1 /f")); - foreach (var supportedRuntime in Settings.SignInfo.RuntimesToSign) - { - var targetDir = Path.Combine(Settings.OutputDir, supportedRuntime); - // sigcheck.exe will exit with error codes if unsigned binaries present - var csvOutputLines = Shell.GetOutput(sigcheckPath, $" -s -u -c -q {targetDir}", ignoreExitCode: true).Split(Environment.NewLine); + // sigcheck.exe will exit with error codes if unsigned binaries present + var csvOutputLines = Shell.GetOutput(sigcheckPath, $" -s -u -c -q {targetDir}", ignoreExitCode: true).Split(Environment.NewLine); - // CSV separators can differ between languages and regions. - var csvSep = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator; - var unSignedPackages = new List(); + // CSV separators can differ between languages and regions. + var csvSep = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator; + var unSignedPackages = new List(); - foreach (var line in csvOutputLines) + foreach (var line in csvOutputLines) + { + // Some lines contain sigcheck header info and we filter them out by making sure + // there's at least six commas in each valid line. + if (line.Split(csvSep).Length - 1 > 6) { - // Some lines contain sigcheck header info and we filter them out by making sure - // there's at least six commas in each valid line. - if (line.Split(csvSep).Length - 1 > 6) - { - // Package name is the first element in each line. - var fileName = line.Split(csvSep)[0].Trim('"'); - unSignedPackages.Add(fileName); - } + // Package name is the first element in each line. + var fileName = line.Split(csvSep)[0].Trim('"'); + unSignedPackages.Add(fileName); } + } - if (unSignedPackages.Count() < 1) - { - throw new Exception("Something went wrong while testing for signed packages. There must be a few unsigned allowed binaries"); - } + if (unSignedPackages.Count() < 1) + { + throw new Exception("Something went wrong while testing for signed packages. There must be a few unsigned allowed binaries"); + } - // The first element is simply the column heading - unSignedPackages = unSignedPackages.Skip(1).ToList(); + // The first element is simply the column heading + unSignedPackages = unSignedPackages.Skip(1).ToList(); - // Filter out the extensions we didn't want to sign - unSignedPackages = unSignedPackages.Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))).ToList(); + // Filter out the extensions we didn't want to sign + unSignedPackages = unSignedPackages.Where(file => !Settings.SignInfo.FilterExtenstionsSign.Any(ext => file.EndsWith(ext))).ToList(); - // Filter out files we don't want to verify - unSignedPackages = unSignedPackages.Where(file => !Settings.SignInfo.SkipSigcheckTest.Any(ext => file.EndsWith(ext))).ToList(); - if (unSignedPackages.Count() != 0) - { - var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); - ColoredConsole.Error.WriteLine($"This files are missing valid signatures: {Environment.NewLine}{missingSignature}"); - throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); - } - } + // Filter out files we don't want to verify + unSignedPackages = unSignedPackages.Where(file => !Settings.SignInfo.SkipSigcheckTest.Any(ext => file.EndsWith(ext))).ToList(); + return unSignedPackages; } public static void Zip() @@ -421,10 +481,6 @@ public static void Zip() ColoredConsole.WriteLine($"Creating {zipPath}"); ZipFile.CreateFromDirectory(path, zipPath, CompressionLevel.Optimal, includeBaseDirectory: false); - var shaPath = $"{zipPath}.sha2"; - ColoredConsole.WriteLine($"Creating {shaPath}"); - File.WriteAllText(shaPath, ComputeSha256(zipPath)); - try { Directory.Delete(path, recursive: true); @@ -436,15 +492,6 @@ public static void Zip() ColoredConsole.WriteLine(); } - - string ComputeSha256(string file) - { - using (var fileStream = File.OpenRead(file)) - { - var sha1 = new SHA256Managed(); - return BitConverter.ToString(sha1.ComputeHash(fileStream)).Replace("-", string.Empty).ToLower(); - } - } } private static string _version; diff --git a/build/FileHelpers.cs b/build/FileHelpers.cs index 2d73efcdc..e1c09e415 100644 --- a/build/FileHelpers.cs +++ b/build/FileHelpers.cs @@ -93,6 +93,15 @@ public static IEnumerable GetAllFilesFromFilesAndDirs(IEnumerable files, string baseDir, string zipFilePath) { using (var zipfile = ZipFile.Open(zipFilePath, ZipArchiveMode.Create)) diff --git a/build/Program.cs b/build/Program.cs index c2b91f883..a30baf7a5 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.IO; +using System.Linq; using System.Net; using static Build.BuildSteps; @@ -12,6 +13,7 @@ static void Main(string[] args) Orchestrator .CreateForTarget(args) + .Then(TestSignedArtifacts, skip: !args.Contains("--signTest")) .Then(Clean) .Then(LogIntoAzure) .Then(RestorePackages) @@ -22,12 +24,8 @@ static void Main(string[] args) .Then(AddTemplatesNupkgs) .Then(AddTemplatesJson) .Then(AddGoZip) - .Then(GenerateZipToSign, skip: !args.Contains("--sign")) - .Then(UploadZipToSign, skip: !args.Contains("--sign")) - .Then(EnqueueSignMessage, skip: !args.Contains("--sign")) - .Then(WaitForSigning, skip: !args.Contains("--sign")) - .Then(ReplaceSignedZipAndCleanup, skip: !args.Contains("--sign")) - .Then(TestSignedArtifacts, skip: !args.Contains("--sign")) + .Then(TestPreSignedArtifacts) + .Then(CopyBinariesToSign) .Then(Test) .Then(Zip) .Then(UploadToStorage) diff --git a/build/Settings.cs b/build/Settings.cs index 777e9cb17..d1fc5acca 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -74,6 +74,10 @@ private static string config(string @default = null, [CallerMemberName] string k public static readonly string OutputDir = Path.Combine(Path.GetFullPath(".."), "artifacts"); + public static readonly string PreSignTestDir = "PreSignTest"; + + public static readonly string SignTestDir = "SignTest"; + public static readonly string ItemTemplates = $"https://www.nuget.org/api/v2/package/Microsoft.Azure.WebJobs.ItemTemplates/{ItemTemplatesVersion}"; public static readonly string ProjectTemplates = $"https://www.nuget.org/api/v2/package/Microsoft.Azure.WebJobs.ProjectTemplates/{ProjectTemplatesVersion}"; @@ -98,10 +102,12 @@ public class SignInfo public static readonly string AzureSigningJobName = "signing-jobs"; public static readonly string ToSignZipName = $"{BuildNumber}-authenticode.zip"; public static readonly string ToSignThirdPartyName = $"{BuildNumber}-third.zip"; - public static readonly string ToSignDir = "unsigned"; + public static readonly string ToSignDir = "ToSign"; public static readonly string SignedDir = "signed"; public static readonly string Authenticode = "SignAuthenticode"; + public static readonly string ToAuthenticodeSign = "Authenticode"; public static readonly string ThirdParty = "Sign3rdParty"; + public static readonly string ToThirdPartySign = "ThirdParty"; public static readonly string[] RuntimesToSign = new[] { "min.win-x86", "min.win-x64" }; public static readonly string[] FilterExtenstionsSign = new[] { ".json", ".spec", ".cfg", ".pdb", ".config", ".nupkg", ".py", ".md" }; public static readonly string SigcheckDownloadURL = "https://functionsbay.blob.core.windows.net/public/tools/sigcheck64.exe"; @@ -155,6 +161,13 @@ public class SignInfo "Marklio.Metadata.dll", "Remotion.Linq.dll", "System.IO.Abstractions.dll", + "Microsoft.CodeAnalysis.CSharp.Scripting.dll", + "Microsoft.CodeAnalysis.CSharp.Workspaces.dll", + "Microsoft.CodeAnalysis.Razor.dll", + "Microsoft.CodeAnalysis.Scripting.dll", + "Microsoft.CodeAnalysis.VisualBasic.dll", + "Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll", + "Microsoft.CodeAnalysis.Workspaces.dll", "YamlDotNet.dll", Path.Combine("tools", "python", "packapp", "distlib") }; diff --git a/repackageBinaries.ps1 b/repackageBinaries.ps1 new file mode 100644 index 000000000..4a05fe22c --- /dev/null +++ b/repackageBinaries.ps1 @@ -0,0 +1,122 @@ +Set-Location ".\build" +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function Unzip([string]$zipfilePath, [string]$outputpath) { + try { + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfilePath, $outputpath) + LogSuccess "Unzipped:$zipfilePath at $outputpath" + } + catch { + LogErrorAndExit "Unzip failed for:$zipfilePath" $_.Exception + } +} + + +function Zip([string]$directoryPath, [string]$zipPath) { + try { + LogSuccess "start zip:$directoryPath to $zipPath" + + [System.IO.Compression.ZipFile]::CreateFromDirectory($directoryPath, $zipPath, [System.IO.Compression.CompressionLevel]::Optimal, $false); + LogSuccess "Zipped:$directoryPath to $zipPath" + } + catch { + LogErrorAndExit "Zip operation failed for:$directoryPath" $_.Exception + } +} + + +function LogErrorAndExit($errorMessage, $exception) { + Write-Output $errorMessage + if ($exception -ne $null) { + Write-Output $exception|format-list -force + } + Exit 1 +} + +function LogSuccess($message) { + Write-Output `n + Write-Output $message +} + +try +{ + $artifactsPath = Resolve-Path "..\artifacts\" + $tempDirectoryPath = "..\artifacts\temp\" + + if (Test-Path $tempDirectoryPath) + { + Remove-Item $tempDirectoryPath -Force -Recurse + } + + # Runtimes with signed binaries + $runtimesIdentifiers = @("min.win-x86","min.win-x64") + $tempDirectory = New-Item $tempDirectoryPath -ItemType Directory + LogSuccess "$tempDirectoryPath created" + + # Unzip the coretools artifact to add signed binaries + foreach($rid in $runtimesIdentifiers) + { + $files= Get-ChildItem -Recurse -Path "..\artifacts\*.zip" + foreach($file in $files) + { + if ($file.Name.Contains($rid)) + { + $fileName = [io.path]::GetFileNameWithoutExtension($file.Name) + + $targetDirectory = Join-Path $tempDirectoryPath $fileName + $dir = New-Item $targetDirectory -ItemType Directory + $targetDirectory = Resolve-Path $targetDirectory + $filePath = Resolve-Path $file.FullName + Unzip $filePath $targetDirectory + + # Removing file after extraction + Remove-Item $filePath + LogSuccess "Removed $filePath" + } + } + } + + # Store file count before replacing the binaries + $fileCountBefore = (Get-ChildItem $tempDirectoryPath -Recurse | Measure-Object).Count + + # copy authenticode signed binaries into extracted directories + $authenticodeDirectory = "..\artifacts\ToSign\Authenticode\" + $authenticodeDirectories = Get-ChildItem $authenticodeDirectory -Directory + + foreach($directory in $authenticodeDirectories) + { + $sourcePath = $directory.FullName + Copy-Item -Path $sourcePath -Destination $tempDirectoryPath -Recurse -Force + } + + # copy thirdparty signed directory into extracted directories + $thirdPathDirectory = "..\artifacts\ToSign\ThirdParty\" + $thirdPathDirectories = Get-ChildItem $thirdPathDirectory -Directory + + foreach($directory in $thirdPathDirectories) + { + $sourcePath = $directory.FullName + Copy-Item -Path $sourcePath -Destination $tempDirectoryPath -Recurse -Force + } + + $fileCountAfter = (Get-ChildItem $tempDirectoryPath -Recurse | Measure-Object).Count + + if ($fileCountBefore -ne $fileCountAfter) + { + LogErrorAndExit "File count does not match. File count before copy: $fileCountBefore != file count after copy:$fileCountAfter" $_.Exception + } + + $tempDirectories = Get-ChildItem $tempDirectoryPath -Directory + foreach($directory in $tempDirectories) + { + $directoryName = $directory.Name + $zipPath = Join-Path $artifactsPath $directoryName + $zipPath = $zipPath + ".zip" + $directoryPath = $directory.FullName + Zip $directoryPath $zipPath + } + +} +catch { + LogErrorAndExit "Execution Failed" $_.Exception +} \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/DurableTests.cs b/test/Azure.Functions.Cli.Tests/E2E/DurableTests.cs index 86e8ab4e8..2868e1e27 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/DurableTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/DurableTests.cs @@ -313,6 +313,7 @@ await CliTester.Run(new RunConfiguration [SkippableFact] public async Task DurableStartNewTest_FileInput() { + Skip.If(true, reason: "This test fails intermittently, needs to be fixed"); Skip.If(string.IsNullOrEmpty(StorageConnectionString), reason: _storageReason); diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 1fdde3f62..dc022e6f6 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -45,7 +45,7 @@ await CliTester.Run(new RunConfiguration var result = await response.Content.ReadAsStringAsync(); p.Kill(); result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - } + } }, }, _output); } From 5b0d1c25fa10adcc9a235579d159d7c3e4cf98ff Mon Sep 17 00:00:00 2001 From: Pramod Valavala <43602528+PramodValavala-MSFT@users.noreply.github.com> Date: Wed, 22 Apr 2020 01:15:45 +0530 Subject: [PATCH 038/127] Fix excludedTypes settings in generated host.json (#1900) --- src/Azure.Functions.Cli/StaticResources/host.json | 6 +++--- test/Azure.Functions.Cli.Tests/E2E/InitTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/host.json b/src/Azure.Functions.Cli/StaticResources/host.json index a85a33a30..369b5be84 100644 --- a/src/Azure.Functions.Cli/StaticResources/host.json +++ b/src/Azure.Functions.Cli/StaticResources/host.json @@ -2,10 +2,10 @@ "version": "2.0", "logging": { "applicationInsights": { - "samplingExcludedTypes": "Request", "samplingSettings": { - "isEnabled": true + "isEnabled": true, + "excludedTypes": "Request" } } } -} \ No newline at end of file +} diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index 05e872e0e..066d9ebe8 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -430,7 +430,7 @@ public Task init_function_app_contains_logging_config() ContentContains = new [] { "applicationInsights", - "samplingExcludedTypes", + "excludedTypes", "Request", "logging" } From d3e47c1c5700dead61b3dbc30d566bb43943bd53 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 22 Apr 2020 23:16:25 -0700 Subject: [PATCH 039/127] Fix zip command to work with spaced paths --- src/Azure.Functions.Cli/Helpers/ZipHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs index 9e7cd5d6b..6784bb130 100644 --- a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs @@ -81,7 +81,7 @@ public static async Task CreateGoZip(IEnumerable files, string r var contentsFile = Path.GetTempFileName(); await File.WriteAllLinesAsync(contentsFile, files); - var goZipExe = new Executable(goZipLocation, $"-base-dir {rootPath} -input-file {contentsFile} -output {zipFilePath}"); + var goZipExe = new Executable(goZipLocation, $"-base-dir \"{rootPath}\" -input-file \"{contentsFile}\" -output \"{zipFilePath}\""); await goZipExe.RunAsync(); return new FileStream(zipFilePath, FileMode.Open, FileAccess.Read); } From 13e11f66508c5f90b40b53d2e73ea4a2e60d540e Mon Sep 17 00:00:00 2001 From: Graham Zuber Date: Tue, 28 Apr 2020 20:19:45 -0700 Subject: [PATCH 040/127] Fixed issue where AzureWebJobsStorage wasn't being properly set in local.settings.json. --- src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs index f7df9bfa9..50c68ed1c 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs @@ -289,7 +289,7 @@ private static async Task WriteLocalSettingsJson(WorkerRuntime workerRuntime) ? Constants.StorageEmulatorConnectionString : string.Empty; - localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.StorageEmulatorConnectionString}}}", storageConnectionStringValue); + localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.AzureWebJobsStorage}}}", storageConnectionStringValue); await WriteFiles("local.settings.json", localSettingsJsonContent); } From c2645629fab49f9641961cae5440392e230d5f2a Mon Sep 17 00:00:00 2001 From: "Ahmed El Sayed (Mamoun)" <52262708+amamounelsayed@users.noreply.github.com> Date: Wed, 29 Apr 2020 14:48:52 -0700 Subject: [PATCH 041/127] Update java worker release to 1.6.0 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 9c7d14ebb..bd6f6a90d 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -109,7 +109,7 @@ - + From 9fcf8da40d8d79bea39dcf3bfa99dbf5b4909f6f Mon Sep 17 00:00:00 2001 From: amamounelsayed <52262708+amamounelsayed@users.noreply.github.com> Date: Thu, 30 Apr 2020 13:20:28 -0700 Subject: [PATCH 042/127] Revert "Update java worker release to 1.6.0" This reverts commit c2645629fab49f9641961cae5440392e230d5f2a. --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index bd6f6a90d..9c7d14ebb 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -109,7 +109,7 @@ - + From 5667d8e0514b0d82a17e3ab5ac810cd7ea0db029 Mon Sep 17 00:00:00 2001 From: Yogesh Jagadeesan Date: Thu, 30 Apr 2020 15:42:27 -0700 Subject: [PATCH 043/127] Updated WebHost version (#1955) Updated webhost to 2.0.13351 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 9c7d14ebb..c52b342b0 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -112,7 +112,7 @@ - + From 8d1aa8a7591b6e7eab3d25bf93e0f26e99b9bed7 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 30 Apr 2020 16:36:46 -0700 Subject: [PATCH 044/127] Update PowerShellWorker to v2.0.281 (#1957) --- NuGet.Config | 1 + build/BuildSteps.cs | 1 + src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index 1151f3e12..e6a04ec5a 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 985051ecb..f2052b280 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -37,6 +37,7 @@ public static void RestorePackages() "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", "https://www.myget.org/F/xunit/api/v3/index.json", "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", + "https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/Microsoft.Azure.Functions.PowerShellWorker/nuget/v3/index.json", } .Aggregate(string.Empty, (a, b) => $"{a} --source {b}"); diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index c52b342b0..5f41f69b6 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -111,7 +111,7 @@ - + From 4da36643c32783a832094318afcd679fa9d76455 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Mon, 20 Apr 2020 13:09:29 -0700 Subject: [PATCH 045/127] Fixing the dev build --- azure-pipelines.yml | 27 +++------------------------ generateSha.ps1 | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 generateSha.ps1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 50a00f242..a3a2aa9ec 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ steps: condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) - pwsh: | .\repackageBinaries.ps1 - displayName: Sign and repackage binaries + displayName: Repackage signed binaries env: AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) BuildArtifactsStorage: $(BuildArtifactsStorage) @@ -134,29 +134,8 @@ steps: arguments: 'TestSignedArtifacts --signTest' displayName: 'Verify signed binaries' condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) -- task: PowerShell@2 - inputs: - targetType: 'inline' - script: | - function GenerateSha([string]$filePath,[string]$artifactsPath, [string]$shaFileName) - { - $sha = (Get-FileHash $filePath).Hash.ToLower() - $shaPath = Join-Path $artifactsPath "$shaFileName.sha" - Out-File -InputObject $sha -Encoding ascii -FilePath $shaPath - } - - Set-Location ".\build" - - $artifactsPath = Resolve-Path "..\artifacts\" - $zipFilesSearchPath = Join-Path $artifactsPath "*.zip" - $zipFiles = Get-ChildItem -File $zipFilesSearchPath - - foreach($zipFile in $zipFiles) - { - $zipFullPath = $zipFile.FullName - $fileName = $zipFile.Name - GenerateSha $zipFullPath $artifactsPath $fileName - } +- pwsh: | + .\generateSha.ps1 displayName: 'Generate sha files' - task: PublishTestResults@2 inputs: diff --git a/generateSha.ps1 b/generateSha.ps1 new file mode 100644 index 000000000..10c9f6159 --- /dev/null +++ b/generateSha.ps1 @@ -0,0 +1,19 @@ +function GenerateSha([string]$filePath,[string]$artifactsPath, [string]$shaFileName) +{ +$sha = (Get-FileHash $filePath).Hash.ToLower() +$shaPath = Join-Path $artifactsPath "$shaFileName.sha2" +Out-File -InputObject $sha -Encoding ascii -FilePath $shaPath +} + +Set-Location ".\build" + +$artifactsPath = Resolve-Path "..\artifacts\" +$zipFilesSearchPath = Join-Path $artifactsPath "*.zip" +$zipFiles = Get-ChildItem -File $zipFilesSearchPath + +foreach($zipFile in $zipFiles) +{ + $zipFullPath = $zipFile.FullName + $fileName = $zipFile.Name + GenerateSha $zipFullPath $artifactsPath $fileName +} \ No newline at end of file From ca4c9d0a2f47e133a40d9b345802781f3bc85fe4 Mon Sep 17 00:00:00 2001 From: Varad Meru Date: Tue, 5 May 2020 16:25:04 -0700 Subject: [PATCH 046/127] [pack] Updating V2 Python Worker version Release info: https://github.com/Azure/azure-functions-python-worker/releases/tag/1.1.1 MyGet: https://azfunc.visualstudio.com/Azure%20Functions%20Python/_releaseProgress?_a=release-environment-logs&releaseId=229&environmentId=229 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 5f41f69b6..47f51f2eb 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -113,7 +113,7 @@ - + From bc6e4c05d524f5fe6851f74100b075341d613c2a Mon Sep 17 00:00:00 2001 From: soninaren Date: Wed, 6 May 2020 15:25:32 -0700 Subject: [PATCH 047/127] Fixing the sha generation to not add new line in the end --- generateSha.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generateSha.ps1 b/generateSha.ps1 index 10c9f6159..33d4ab530 100644 --- a/generateSha.ps1 +++ b/generateSha.ps1 @@ -2,7 +2,7 @@ function GenerateSha([string]$filePath,[string]$artifactsPath, [string]$shaFileN { $sha = (Get-FileHash $filePath).Hash.ToLower() $shaPath = Join-Path $artifactsPath "$shaFileName.sha2" -Out-File -InputObject $sha -Encoding ascii -FilePath $shaPath +Out-File -InputObject $sha -Encoding ascii -FilePath $shaPath -NoNewline } Set-Location ".\build" From fffa03295751a05150ebc0a2cbd732aaf543f0d3 Mon Sep 17 00:00:00 2001 From: "Ahmed El Sayed (Mamoun)" <52262708+amamounelsayed@users.noreply.github.com> Date: Mon, 18 May 2020 13:09:36 -0700 Subject: [PATCH 048/127] Update java worker release 1.6.0 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 5f41f69b6..a9941c20d 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -109,7 +109,7 @@ - + From 4a2f7da70ec11a4902f83382244daacf001ddcad Mon Sep 17 00:00:00 2001 From: Tony Xia Date: Wed, 20 May 2020 07:53:02 +1000 Subject: [PATCH 049/127] Fixed a minor typo (#1996) --- .../Kubernetes/FuncKeys/FuncAppKeysHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs index b9fedf88b..1fd834905 100644 --- a/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs +++ b/src/Azure.Functions.Cli/Kubernetes/FuncKeys/FuncAppKeysHelper.cs @@ -53,7 +53,7 @@ public static IDictionary FuncKeysKubernetesEnvironVariables(str { AzureWebJobsSecretStorageTypeEnvVariableName, "kubernetes" } }; - //if keys needs are not to be mounted as container volume then add "AzureWebJobsKubernetesSecretName" enviornment varibale to the container + //if keys needs are not to be mounted as container volume then add "AzureWebJobsKubernetesSecretName" environment variable to the container if (!mountKeysAsContainerVolume) { funcKeysKubernetesEnvironVariables.Add(AzureWebJobsKubernetesSecretNameEnvVariableName, $"secrets/{keysSecretCollectionName}"); From 47b7d1bea1cc42dced9340ec3de58a96207e1295 Mon Sep 17 00:00:00 2001 From: Tony Xia Date: Wed, 20 May 2020 07:53:42 +1000 Subject: [PATCH 050/127] Fixed a typo in method name (#1997) --- .../Helpers/KuduLiteDeploymentHelpers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Azure.Functions.Cli/Helpers/KuduLiteDeploymentHelpers.cs b/src/Azure.Functions.Cli/Helpers/KuduLiteDeploymentHelpers.cs index 391824513..af8e2fd99 100644 --- a/src/Azure.Functions.Cli/Helpers/KuduLiteDeploymentHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/KuduLiteDeploymentHelpers.cs @@ -50,7 +50,7 @@ private static async Task GetLatestDeploymentId(HttpClient client, Site var latestDeployment = json.First(); if (latestDeployment.TryGetValue("status", out string statusString)) { - DeployStatus status = ConvertToDeployementStatus(statusString); + DeployStatus status = ConvertToDeploymentStatus(statusString); if (status == DeployStatus.Building || status == DeployStatus.Deploying || status == DeployStatus.Success || status == DeployStatus.Failed) { @@ -76,7 +76,7 @@ private static async Task GetDeploymentStatusById(HttpClient clien return DeployStatus.Unknown; } - return ConvertToDeployementStatus(statusString); + return ConvertToDeploymentStatus(statusString); } private static async Task DisplayDeploymentLog(HttpClient client, Site functionApp, string id, DateTime lastUpdate, Uri innerUrl = null, StringBuilder innerLogger = null) @@ -141,7 +141,7 @@ await RetryHelper.Retry(async () => } } - private static DeployStatus ConvertToDeployementStatus(string statusString) + private static DeployStatus ConvertToDeploymentStatus(string statusString) { if (Enum.TryParse(statusString, out DeployStatus result)) { From f9adb8f65c1f15f463d0e4605ac5b26ef86b8468 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Wed, 20 May 2020 00:08:28 -0700 Subject: [PATCH 051/127] Remove unused usings (#1998) --- src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs index 566d494fb..f8dfa7ff8 100644 --- a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs +++ b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs @@ -3,8 +3,6 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -12,14 +10,10 @@ using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; -using Azure.Functions.Cli.Interfaces; using Azure.Functions.Cli.Kubernetes.FuncKeys; using Azure.Functions.Cli.Kubernetes.Models; using Azure.Functions.Cli.Kubernetes.Models.Kubernetes; using Colors.Net; -using Dynamitey.DynamicObjects; -using Microsoft.IdentityModel.Xml; -using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Identity; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YamlDotNet.Serialization; From 98381841d458a57f374e57f1565842603e7358d5 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 26 May 2020 17:30:21 -0700 Subject: [PATCH 052/127] Update Functions Host reference to 2.0.13616 (#2006) --- .../Actions/HostActions/StartHostAction.cs | 6 +++--- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 6ac050fb1..855e7acac 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -349,7 +349,7 @@ private void DisplayDisabledFunctions(IScriptJobHost scriptHost) { if (scriptHost != null) { - foreach (var function in scriptHost.Functions.Where(f => f.Metadata.IsDisabled)) + foreach (var function in scriptHost.Functions.Where(f => f.Metadata.IsDisabled())) { ColoredConsole.WriteLine(WarningColor($"Function {function.Name} is disabled.")); } @@ -360,7 +360,7 @@ private void DisplayHttpFunctionsInfo(IScriptJobHost scriptHost, HttpOptions htt { if (scriptHost != null) { - var httpFunctions = scriptHost.Functions.Where(f => f.Metadata.IsHttpFunction() && !f.Metadata.IsDisabled); + var httpFunctions = scriptHost.Functions.Where(f => f.Metadata.IsHttpFunction() && !f.Metadata.IsDisabled()); if (httpFunctions.Any()) { ColoredConsole @@ -383,7 +383,7 @@ private void DisplayHttpFunctionsInfo(IScriptJobHost scriptHost, HttpOptions htt } string hostRoutePrefix = ""; - if (!function.Metadata.IsProxy) + if (!function.Metadata.IsProxy()) { hostRoutePrefix = httpOptions.RoutePrefix ?? "api/"; hostRoutePrefix = string.IsNullOrEmpty(hostRoutePrefix) || hostRoutePrefix.EndsWith("/") diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index a9941c20d..c8133b386 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -112,7 +112,7 @@ - + diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index dc022e6f6..755fb7133 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -133,7 +133,7 @@ await CliTester.Run(new RunConfiguration[] ExpectExit = false, OutputContains = new [] { - "The binding type(s) 'http2' are not registered. Please ensure the type is correct and the binding extension is installed." + "The binding type(s) 'http2' were not found in the configured extension bundle. Please ensure the type is correct and the correct version of extension bundle is configured." }, Test = async (_, p) => { From 5c1c0137b1128b429b73b65a76772bd2f2a47efa Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Wed, 27 May 2020 11:05:23 -0700 Subject: [PATCH 053/127] update error message recommendations (#2008) --- src/Azure.Functions.Cli/Helpers/PythonHelpers.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs index b6250133a..c6949499e 100644 --- a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs @@ -80,7 +80,7 @@ public static void AssertPythonVersion(WorkerLanguageVersionInfo pythonVersion, { if (pythonVersion?.Version == null) { - var message = "Could not find a Python version. Python 3.6.x or 3.7.x is recommended, and used in Azure Functions."; + var message = "Could not find a Python version. Python 3.6.x or 3.7.x is recommended, and used in Azure Functions 2.x."; if (errorIfNoVersion) throw new CliException(message); ColoredConsole.WriteLine(WarningColor(message)); return; @@ -97,9 +97,9 @@ public static void AssertPythonVersion(WorkerLanguageVersionInfo pythonVersion, // Python 3.x (but not 3.6 or 3.7), not recommended, may fail if (pythonVersion.Major == 3) { - if (errorIfNotSupported) throw new CliException($"Python 3.6.x or 3.7.x is required for this operation. " - + "Please install Python 3.6 or 3.7, and use a virtual environment to switch to Python 3.6 or 3.7."); - ColoredConsole.WriteLine(WarningColor("Python 3.6.x or 3.7.x is recommended, and used in Azure Functions.")); + if (errorIfNotSupported) throw new CliException($"Python 3.6.x or 3.7.x is required for the version of Azure Functions you are using (2.x). " + + "Please see if another version of Functions supports your version (https://aka.ms/functions-python-versions). Otherwise, please install Python 3.6 or 3.7 and use a virtual environment to switch to Python 3.6 or 3.7."); + ColoredConsole.WriteLine(WarningColor("Python 3.6.x or 3.7.x is recommended, and used in Azure Functions 2.x.")); } // No Python 3 From 148a1017fde75627979222e5cbaf71e9de62cf08 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 10 Jun 2020 01:43:58 -0700 Subject: [PATCH 054/127] Fix extension bundle test --- .../ExtensionsTests/InstallExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs b/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs index 5315bbbf1..ce547b912 100644 --- a/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs +++ b/test/Azure.Functions.Cli.Tests/ExtensionsTests/InstallExtensionsTests.cs @@ -56,7 +56,7 @@ public Task try_install_with_a_valid_trigger() OutputContains = new[] { "Restoring packages for", - "Restore completed" + "Build succeeded" }, OutputDoesntContain = new[] { @@ -93,7 +93,7 @@ public Task try_install_with_a_version() OutputContains = new[] { "Restoring packages for", - "Restore completed" + "Build succeeded" }, OutputDoesntContain = new[] { From a3f9f58e2ab964ccd9bca899d818ec6867ce85f1 Mon Sep 17 00:00:00 2001 From: Francisco Gamino Date: Fri, 12 Jun 2020 13:59:13 -0700 Subject: [PATCH 055/127] Update generation of host.json for PowerShell function apps to use the default host.json template; update profile.ps1 template to remove Get-Module call added Disable-AzContextAutosave to improve cold start (#2004) --- .../Actions/LocalActions/InitAction.cs | 21 ++++++++++++--- .../Azure.Functions.Cli.csproj | 4 +-- src/Azure.Functions.Cli/Common/Constants.cs | 1 + .../StaticResources/StaticResources.cs | 2 +- .../managedDependenciesConfig.json | 3 +++ .../StaticResources/powershell.host.json | 6 ----- .../StaticResources/profile.ps1 | 3 ++- .../E2E/InitTests.cs | 26 +++++++++++++++++-- 8 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/Azure.Functions.Cli/StaticResources/managedDependenciesConfig.json delete mode 100644 src/Azure.Functions.Cli/StaticResources/powershell.host.json diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs index 50c68ed1c..932a657ed 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs @@ -290,6 +290,7 @@ private static async Task WriteLocalSettingsJson(WorkerRuntime workerRuntime) : string.Empty; localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.AzureWebJobsStorage}}}", storageConnectionStringValue); + await WriteFiles("local.settings.json", localSettingsJsonContent); } @@ -414,9 +415,12 @@ private static bool ResolveManagedDependencies(WorkerRuntime workerRuntime, bool private static async Task WriteHostJson(WorkerRuntime workerRuntime, bool managedDependenciesOption, bool extensionBundle = true) { - var hostJsonContent = (workerRuntime == Helpers.WorkerRuntime.powershell && managedDependenciesOption) - ? await StaticResources.PowerShellHostJson - : await StaticResources.HostJson; + var hostJsonContent = await StaticResources.HostJson; + + if (workerRuntime == Helpers.WorkerRuntime.powershell && managedDependenciesOption) + { + hostJsonContent = await AddManagedDependencyConfig(hostJsonContent); + } if (extensionBundle) { @@ -431,7 +435,16 @@ private static async Task AddBundleConfig(string hostJsonContent) var hostJsonObj = JsonConvert.DeserializeObject(hostJsonContent); var bundleConfigContent = await StaticResources.BundleConfig; var bundleConfig = JsonConvert.DeserializeObject(bundleConfigContent); - hostJsonObj.Add("extensionBundle", bundleConfig); + hostJsonObj.Add(Constants.ExtensionBundleConfigPropertyName, bundleConfig); + return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); + } + + private static async Task AddManagedDependencyConfig(string hostJsonContent) + { + var hostJsonObj = JsonConvert.DeserializeObject(hostJsonContent); + var managedDependenciesConfigContent = await StaticResources.ManagedDependenciesConfig; + var managedDependenciesConfig = JsonConvert.DeserializeObject(managedDependenciesConfigContent); + hostJsonObj.Add(Constants.ManagedDependencyConfigPropertyName, managedDependenciesConfig); return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); } } diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index cc468ef45..a8d464a1a 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -51,8 +51,8 @@ $(AssemblyName).gitignore - - $(AssemblyName).powershell.host.json + + $(AssemblyName).managedDependenciesConfig.json $(AssemblyName).host.json diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index ee67682a1..3e319b7dc 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -48,6 +48,7 @@ internal static class Constants public const string AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; + public const string ManagedDependencyConfigPropertyName = "managedDependency"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); diff --git a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs index a5e6c7840..da0c1f342 100644 --- a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs +++ b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs @@ -51,7 +51,7 @@ private static async Task GetValue(string name) public static Task BundleConfig => GetValue("bundleConfig.json"); - public static Task PowerShellHostJson => GetValue("powershell.host.json"); + public static Task ManagedDependenciesConfig => GetValue("managedDependenciesConfig.json"); public static Task PythonDockerBuildScript => GetValue(Constants.StaticResourcesNames.PythonDockerBuild); diff --git a/src/Azure.Functions.Cli/StaticResources/managedDependenciesConfig.json b/src/Azure.Functions.Cli/StaticResources/managedDependenciesConfig.json new file mode 100644 index 000000000..4e609c71b --- /dev/null +++ b/src/Azure.Functions.Cli/StaticResources/managedDependenciesConfig.json @@ -0,0 +1,3 @@ +{ + "enabled": true +} diff --git a/src/Azure.Functions.Cli/StaticResources/powershell.host.json b/src/Azure.Functions.Cli/StaticResources/powershell.host.json deleted file mode 100644 index c92d4ac6d..000000000 --- a/src/Azure.Functions.Cli/StaticResources/powershell.host.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "2.0", - "managedDependency": { - "enabled": true - } -} diff --git a/src/Azure.Functions.Cli/StaticResources/profile.ps1 b/src/Azure.Functions.Cli/StaticResources/profile.ps1 index dbbf09385..6a525cf76 100644 --- a/src/Azure.Functions.Cli/StaticResources/profile.ps1 +++ b/src/Azure.Functions.Cli/StaticResources/profile.ps1 @@ -11,7 +11,8 @@ # Authenticate with Azure PowerShell using MSI. # Remove this if you are not planning on using MSI or Azure PowerShell. -if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) { +if ($env:MSI_SECRET) { + Disable-AzContextAutosave Connect-AzAccount -Identity } diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index 066d9ebe8..c99ecdfe2 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -252,7 +252,7 @@ public Task init_ts_app_using_lang() }, _output); } - [Fact] + [Fact] public Task javascript_adds_packagejson() { return CliTester.Run(new RunConfiguration @@ -378,7 +378,7 @@ public Task init_docker_only_no_project() } [Fact] - public Task init_function_app_powershell_supports_managed_dependencies() + public Task init_function_app_powershell_and_enable_managed_dependencies() { return CliTester.Run(new RunConfiguration { @@ -390,6 +390,9 @@ public Task init_function_app_powershell_supports_managed_dependencies() Name = "host.json", ContentContains = new [] { + "logging", + "applicationInsights", + "extensionBundle", "managedDependency", "enabled", "true" @@ -402,6 +405,25 @@ public Task init_function_app_powershell_supports_managed_dependencies() { "Az", } + }, + new FileResult + { + Name = "profile.ps1", + ContentContains = new [] + { + "env:MSI_SECRET", + "Disable-AzContextAutosave", + "Connect-AzAccount -Identity" + } + }, + new FileResult + { + Name = "local.settings.json", + ContentContains = new [] + { + "FUNCTIONS_WORKER_RUNTIME", + "powershell" + } } }, OutputContains = new[] From e68cdba49cdfa1f70a9ca34f94fcc4d115f884ac Mon Sep 17 00:00:00 2001 From: Tom Kerkhove Date: Mon, 15 Jun 2020 19:44:31 +0200 Subject: [PATCH 056/127] Bump KEDA version to 1.4.1 (#1949) Co-authored-by: SatishRanjan --- src/Azure.Functions.Cli/StaticResources/keda.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/keda.yaml b/src/Azure.Functions.Cli/StaticResources/keda.yaml index 190f13465..44cdc49d3 100644 --- a/src/Azure.Functions.Cli/StaticResources/keda.yaml +++ b/src/Azure.Functions.Cli/StaticResources/keda.yaml @@ -4791,7 +4791,7 @@ spec: - name: keda securityContext: {} - image: "docker.io/kedacore/keda:1.3.0" + image: "docker.io/kedacore/keda:1.4.1" command: - keda args: @@ -4834,7 +4834,7 @@ spec: - name: keda-metrics-apiserver securityContext: {} - image: "docker.io/kedacore/keda-metrics-adapter:1.3.0" + image: "docker.io/kedacore/keda-metrics-adapter:1.4.1" imagePullPolicy: Always env: - name: WATCH_NAMESPACE From 005fc0f1ffd640f86f0b770dc02609c864bf9dc2 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Tue, 16 Jun 2020 12:55:59 -0700 Subject: [PATCH 057/127] Update build project (#2035) --- build.ps1 | 4 ++-- build/Program.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.ps1 b/build.ps1 index 91891da5c..95551607d 100644 --- a/build.ps1 +++ b/build.ps1 @@ -5,7 +5,7 @@ if ($env:APPVEYOR_REPO_BRANCH -eq "disabled") { $javaWorkerVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.Functions.JavaWorker $javaWorkerVersion to project" -ForegroundColor Green Invoke-Expression -Command "dotnet add package Microsoft.Azure.Functions.JavaWorker -v $javaWorkerVersion -s https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg" - + $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.Functions.PowerShellWorker -Source https://ci.appveyor.com/nuget/azure-functions-powershell-wor-0842fakagqy6 -PreRelease" $powerShellWorkerVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.Functions.PowerShellWorker $powerShellWorkerVersion to project" -ForegroundColor Green @@ -26,5 +26,5 @@ else { Set-Location ".\build" } -Invoke-Expression -Command "dotnet run" +Invoke-Expression -Command "dotnet run --ci" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/build/Program.cs b/build/Program.cs index a30baf7a5..28f8cb837 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -15,20 +15,20 @@ static void Main(string[] args) .CreateForTarget(args) .Then(TestSignedArtifacts, skip: !args.Contains("--signTest")) .Then(Clean) - .Then(LogIntoAzure) + .Then(LogIntoAzure, skip: !args.Contains("--ci")) .Then(RestorePackages) - .Then(ReplaceTelemetryInstrumentationKey) + .Then(ReplaceTelemetryInstrumentationKey, skip: !args.Contains("--ci")) .Then(DotnetPublish) .Then(FilterPowershellRuntimes) .Then(AddDistLib) .Then(AddTemplatesNupkgs) .Then(AddTemplatesJson) .Then(AddGoZip) - .Then(TestPreSignedArtifacts) - .Then(CopyBinariesToSign) + .Then(TestPreSignedArtifacts, skip: !args.Contains("--ci")) + .Then(CopyBinariesToSign, skip: !args.Contains("--ci")) .Then(Test) .Then(Zip) - .Then(UploadToStorage) + .Then(UploadToStorage, skip: !args.Contains("--ci")) .Run(); } } From a206328c5e7082e0fee0126bba3c835cd18a62f5 Mon Sep 17 00:00:00 2001 From: Hyeondong Jang Date: Wed, 17 Jun 2020 06:53:37 +0900 Subject: [PATCH 058/127] Update README.md with apt installation command for v2 (#2030) Co-authored-by: Ahmed ElSayed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f161d7eb9..348c9a916 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list ##### v2 ```bash sudo apt-get update -sudo apt-get install azure-functions-core-tools +sudo apt-get install azure-functions-core-tools-2 ``` ##### v3 From 6c570c4076d028581c27ab83a760796a28318fc1 Mon Sep 17 00:00:00 2001 From: amamounelsayed <52262708+amamounelsayed@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:54:29 -0700 Subject: [PATCH 059/127] Update java worker release to 1.6.0 (#1964) Co-authored-by: Ahmed ElSayed From e67d2d547620076f2cd0671d3b3c7c8715151502 Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Tue, 16 Jun 2020 15:00:31 -0700 Subject: [PATCH 060/127] Add Debian 10 instructions (#1934) --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 348c9a916..0513a58ca 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,7 @@ brew link --overwrite azure-functions-core-tools@3 ### Linux -#### Ubuntu - -1. Set up package feed - +#### 1. Set up package feed ##### Ubuntu 19.04 @@ -100,18 +97,21 @@ wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-pr sudo dpkg -i packages-microsoft-prod.deb ``` -##### Debian 9 +##### Debian 9 / 10 ```bash +# set to 9 or 10 +DEBIAN_VERSION=10 + wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ -wget -q https://packages.microsoft.com/config/debian/9/prod.list +wget -q https://packages.microsoft.com/config/debian/$DEBIAN_VERSION/prod.list sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list ``` -2. Install +#### 2. Install ##### v2 ```bash @@ -129,35 +129,35 @@ sudo apt-get install azure-functions-core-tools-3 1. Download latest release -Download the latest release for your platform from [here](https://github.com/Azure/azure-functions-core-tools/releases). + Download the latest release for your platform from [here](https://github.com/Azure/azure-functions-core-tools/releases). 2. Unzip release zip -Using your preferred tool, unzip the downloaded release. To unzip into an `azure-functions-cli` directory using the `unzip` tool, run this command from the directory containing the downloaded release zip: + Using your preferred tool, unzip the downloaded release. To unzip into an `azure-functions-cli` directory using the `unzip` tool, run this command from the directory containing the downloaded release zip: -```bash -unzip -d azure-functions-cli Azure.Functions.Cli.linux-x64.*.zip -``` + ```bash + unzip -d azure-functions-cli Azure.Functions.Cli.linux-x64.*.zip + ``` 3. Make the `func` command executable -Zip files do not maintain the executable bit on binaries. So, you'll need to make the `func` binary, as well as `gozip` (used by func during packaging) executables. Assuming you used the instructions above to unzip: + Zip files do not maintain the executable bit on binaries. So, you'll need to make the `func` binary, as well as `gozip` (used by func during packaging) executables. Assuming you used the instructions above to unzip: -```bash -cd azure-functions-cli -chmod +x func -chmod +x gozip -./func -``` + ```bash + cd azure-functions-cli + chmod +x func + chmod +x gozip + ./func + ``` 4. Optionally add `func` to your `$PATH` -To execute the `func` command without specifying the full path to the binary, add its directory to your `$PATH` environment variable. Assuming you're still following along from above: + To execute the `func` command without specifying the full path to the binary, add its directory to your `$PATH` environment variable. Assuming you're still following along from above: -```bash -export PATH=`pwd`:$PATH -func -``` + ```bash + export PATH=`pwd`:$PATH + func + ``` [Code and test Azure Functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) From 9b134566361f2c5e30dc28f364d58cceb3d0c800 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 16 Jun 2020 15:05:21 -0700 Subject: [PATCH 061/127] Handle when directory is not available (#2012) Co-authored-by: Ahmed ElSayed --- src/Azure.Functions.Cli/Common/Utilities.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index feb832a82..47bdc5eb6 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -160,7 +160,14 @@ internal static async Task SafeExecution(Func> action) internal static string EnsureCoreToolsLocalData() { - var localPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "azure-functions-core-tools"); + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + if (string.IsNullOrEmpty(appDataDir)) + { + throw new Exception("Cannot find the Local Application Data."); + } + + var localPath = Path.Combine(appDataDir, "azure-functions-core-tools"); FileSystemHelpers.EnsureDirectory(localPath); return localPath; } From 4cde9bce3843f9ba9daa5b0857be7dc7c1721632 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 16 Jun 2020 15:05:38 -0700 Subject: [PATCH 062/127] Fix error message in core tools publish (#1852) Co-authored-by: Ahmed ElSayed --- .../Actions/AzureActions/PublishFunctionAppAction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index a63e45ec7..0586adeac 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -195,8 +195,8 @@ private async Task> ValidateFunctionAppPublish(Site } else { - throw new CliException("You're trying to publish to a v1 function app from v2 tooling.\n" + - "You can pass --force to force update the app to v2, or downgrade to v1 tooling for publishing"); + throw new CliException("You're trying to publish to a non-v2 function app from v2 tooling.\n" + + "You can pass --force to force update the app to v2, or switch to v1 or v3 tooling for publishing"); } } } From 8c33687efb1053afc5c03550518bd775c27053b2 Mon Sep 17 00:00:00 2001 From: Hanzhang Zeng <48038149+Hazhzeng@users.noreply.github.com> Date: Tue, 16 Jun 2020 15:05:54 -0700 Subject: [PATCH 063/127] Use force remote build for Linux Consumption deployment (#1959) Co-authored-by: Ahmed ElSayed --- .../AzureActions/PublishFunctionAppAction.cs | 5 ++-- .../Helpers/KuduLiteDeploymentHelpers.cs | 23 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 0586adeac..5262e3cdf 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -455,7 +455,8 @@ private async Task HandleLinuxConsumptionPublish(Site functionApp, Func pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp); var deployStatus = await PerformServerSideBuild(functionApp, zipFileFactory, pollConsumptionBuild); return deployStatus == DeployStatus.Success; @@ -703,7 +704,7 @@ public async Task PerformServerSideBuild(Site functionApp, Func WaitForRemoteBuild(HttpClient client, Sit while (statusCode != DeployStatus.Success && statusCode != DeployStatus.Failed && statusCode != DeployStatus.Unknown) { - statusCode = await GetDeploymentStatusById(client, functionApp, id); - logLastUpdate = await DisplayDeploymentLog(client, functionApp, id, logLastUpdate); + try + { + statusCode = await GetDeploymentStatusById(client, functionApp, id); + logLastUpdate = await DisplayDeploymentLog(client, functionApp, id, logLastUpdate); + } + catch (HttpRequestException) + { + return DeployStatus.Unknown; + } + await Task.Delay(TimeSpan.FromSeconds(Constants.KuduLiteDeploymentConstants.StatusRefreshSeconds)); } @@ -62,15 +69,7 @@ private static async Task GetLatestDeploymentId(HttpClient client, Site private static async Task GetDeploymentStatusById(HttpClient client, Site functionApp, string id) { - Dictionary json; - try - { - json = await InvokeRequest>(client, HttpMethod.Get, $"/deployments/{id}"); - } catch (HttpRequestException) - { - return DeployStatus.Unknown; - } - + Dictionary json = await InvokeRequest>(client, HttpMethod.Get, $"/deployments/{id}"); if (!json.TryGetValue("status", out string statusString)) { return DeployStatus.Unknown; From 42e149f5313330f1f0a5e721a7e59d9ef7d27e6f Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 16 Jun 2020 17:23:38 -0700 Subject: [PATCH 064/127] Update Functions Host reference to 2.0.13907 (#2038) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index a8d464a1a..f9673be4d 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -112,7 +112,7 @@ - + From ea232a5b4d4a5b8c90bda87d8c1bc510bcf966f9 Mon Sep 17 00:00:00 2001 From: Francisco Gamino Date: Wed, 17 Jun 2020 14:47:31 -0700 Subject: [PATCH 065/127] Add worker version for PowerShell function apps. (#2045) --- .../Actions/LocalActions/InitAction.cs | 19 +++++++++++++++++++ src/Azure.Functions.Cli/Common/Constants.cs | 1 + .../E2E/InitTests.cs | 6 ++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs index 932a657ed..5ccc61649 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs @@ -291,6 +291,11 @@ private static async Task WriteLocalSettingsJson(WorkerRuntime workerRuntime) localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.AzureWebJobsStorage}}}", storageConnectionStringValue); + if (workerRuntime == Helpers.WorkerRuntime.powershell) + { + localSettingsJsonContent = AddWorkerVersion(localSettingsJsonContent, Constants.PowerShellWorkerDefaultVersion); + } + await WriteFiles("local.settings.json", localSettingsJsonContent); } @@ -447,5 +452,19 @@ private static async Task AddManagedDependencyConfig(string hostJsonCont hostJsonObj.Add(Constants.ManagedDependencyConfigPropertyName, managedDependenciesConfig); return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); } + + private static string AddWorkerVersion(string localSettingsContent, string workerVersion) + { + var localSettingsObj = JsonConvert.DeserializeObject(localSettingsContent); + + if (localSettingsObj.TryGetValue("Values", StringComparison.OrdinalIgnoreCase, out var valuesContent)) + { + var values = valuesContent as JObject; + values.Property(Constants.FunctionsWorkerRuntime).AddAfterSelf( + new JProperty(Constants.FunctionsWorkerRuntimeVersion, workerVersion)); + } + + return JsonConvert.SerializeObject(localSettingsObj, Formatting.Indented); + } } } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 3e319b7dc..a51a9640b 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -49,6 +49,7 @@ internal static class Constants public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; + public const string PowerShellWorkerDefaultVersion = "~6"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index c99ecdfe2..863700206 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -378,7 +378,7 @@ public Task init_docker_only_no_project() } [Fact] - public Task init_function_app_powershell_and_enable_managed_dependencies() + public Task init_function_app_powershell_enable_managed_dependencies_and_set_default_version() { return CliTester.Run(new RunConfiguration { @@ -422,7 +422,9 @@ public Task init_function_app_powershell_and_enable_managed_dependencies() ContentContains = new [] { "FUNCTIONS_WORKER_RUNTIME", - "powershell" + "powershell", + "FUNCTIONS_WORKER_RUNTIME_VERSION", + "~6" } } }, From e2f5bab1e2e41e130ffd7e379beff5176730fc86 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 24 Jun 2020 12:53:09 -0700 Subject: [PATCH 066/127] Update ubuntu readme instructions (#2062) --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 0513a58ca..d0c2c6384 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,13 @@ brew link --overwrite azure-functions-core-tools@3 #### 1. Set up package feed +##### Ubuntu 20.04 + +```bash +wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +``` + ##### Ubuntu 19.04 ```bash From efa3d503717dbb4f96a67e8d508ecfefe7eaf56c Mon Sep 17 00:00:00 2001 From: Tom Kerkhove Date: Fri, 10 Jul 2020 22:35:01 +0200 Subject: [PATCH 067/127] Bump KEDA version to v1.5.0 (#2080) --- src/Azure.Functions.Cli/StaticResources/keda.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/keda.yaml b/src/Azure.Functions.Cli/StaticResources/keda.yaml index 44cdc49d3..a055b6268 100644 --- a/src/Azure.Functions.Cli/StaticResources/keda.yaml +++ b/src/Azure.Functions.Cli/StaticResources/keda.yaml @@ -4791,7 +4791,7 @@ spec: - name: keda securityContext: {} - image: "docker.io/kedacore/keda:1.4.1" + image: "docker.io/kedacore/keda:1.5.0" command: - keda args: @@ -4834,7 +4834,7 @@ spec: - name: keda-metrics-apiserver securityContext: {} - image: "docker.io/kedacore/keda-metrics-adapter:1.4.1" + image: "docker.io/kedacore/keda-metrics-adapter:1.5.0" imagePullPolicy: Always env: - name: WATCH_NAMESPACE From 1e341998d4d48103e31600c9a25aa0bd0a762918 Mon Sep 17 00:00:00 2001 From: Francisco Gamino Date: Mon, 13 Jul 2020 18:56:48 -0700 Subject: [PATCH 068/127] Fixing pipelines build (#2088) --- azure-pipelines.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a3a2aa9ec..d2679332c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -41,6 +41,18 @@ steps: # acquire access token from Azure CLI and export it to AZURE_MANAGEMENT_ACCESS_TOKEN $accessToken = (az account get-access-token --query "accessToken" | % { $_.Trim('"') }) echo "##vso[task.setvariable variable=azure_management_access_token]$accessToken" +- pwsh: | + # Set DotNetPath + $sdkBasePath = dotnet --info | Where-Object {$_ -match "Base Path"} | ForEach-Object {($_ -replace '\s+Base Path:','').trim()} + $dotnetPath = Split-Path (Split-Path $sdkBasePath) + Write-Host "dotnet path: $dotnetPath" + Write-Host "##vso[task.setvariable variable=DotNetPath]$dotnetPath" + displayName: 'Set DotNetPath' +- task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '2.2.207' + installationPath: $(DotNetPath) - pwsh: | .\build.ps1 env: From d90f585489c3e683da5db9ba2dd20a40825bb994 Mon Sep 17 00:00:00 2001 From: Nabob Date: Wed, 15 Jul 2020 09:31:11 +0800 Subject: [PATCH 069/127] Version 3 Azure functions instruction (#2081) * Version 3 Azure functions instruction Added Version3 Azure functions core tools instruction for windows * Updated Windows instruction * Updated Windows instruction Adjusted syntax --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d0c2c6384..391849e35 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,15 @@ npm i -g azure-functions-core-tools@3 --unsafe-perm true To install with chocolatey: +"**v2**" ```bash choco install azure-functions-core-tools ``` +"**v3**" +```bash +choco install azure-functions-core-tools-3 +``` + #### Notice: To debug functions under vscode, x64 bitness is required ```bash choco install azure-functions-core-tools --params "'/x64'" From ca6c315c3f1a8c73cc6449dcdd07b58a07bce098 Mon Sep 17 00:00:00 2001 From: Hanzhang Zeng <48038149+Hazhzeng@users.noreply.github.com> Date: Thu, 16 Jul 2020 00:57:28 -0700 Subject: [PATCH 070/127] [pack] Update Python worker to 1.1.4 v2 (#2078) * [pack] Update Python worker to 1.1.3 * Update python worker to 1.1.4 * trigger ci --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index f9673be4d..13d96e233 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -113,7 +113,7 @@ - + From 2536395b589fee62fadbbf42b0b437312c32e19d Mon Sep 17 00:00:00 2001 From: Brett Samblanet Date: Thu, 16 Jul 2020 18:08:41 -0700 Subject: [PATCH 071/127] updating WebHost and Node packages (#2095) --- .../Actions/HostActions/StartHostAction.cs | 19 ++++++++++++++++--- .../Azure.Functions.Cli.csproj | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 855e7acac..8ec1ea53c 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -6,8 +6,6 @@ using System.Net; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using static Azure.Functions.Cli.Common.OutputTheme; -using static Colors.Net.StringStaticMethods; using Azure.Functions.Cli.Actions.HostActions.WebHost.Security; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Diagnostics; @@ -22,6 +20,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.WebHost; using Microsoft.Azure.WebJobs.Script.WebHost.Authentication; using Microsoft.Azure.WebJobs.Script.WebHost.Controllers; @@ -34,7 +33,8 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Microsoft.Azure.WebJobs.Script.Description; +using static Azure.Functions.Cli.Common.OutputTheme; +using static Colors.Net.StringStaticMethods; namespace Azure.Functions.Cli.Actions.HostActions { @@ -526,6 +526,9 @@ public IServiceProvider ConfigureServices(IServiceCollection services) services.AddMvc() .AddApplicationPart(typeof(HostController).Assembly); + // workaround for https://github.com/Azure/azure-functions-core-tools/issues/2097 + SetBundlesEnvironmentVariables(); + services.AddWebJobsScriptHost(_builderContext.Configuration); services.Configure(o => @@ -545,6 +548,16 @@ public IServiceProvider ConfigureServices(IServiceCollection services) return services.BuildServiceProvider(); } + private void SetBundlesEnvironmentVariables() + { + var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; + if (!string.IsNullOrEmpty(bundleId)) + { + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)); + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); + } + } + public void Configure(IApplicationBuilder app) { if (_corsOrigins != null) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 13d96e233..dabbb72e5 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -110,9 +110,9 @@ - + - + From b0455000f9001d7c6b531997097becb0ebd1aa48 Mon Sep 17 00:00:00 2001 From: soninaren Date: Tue, 21 Jul 2020 17:54:20 -0700 Subject: [PATCH 072/127] changing the default bundle download path for osx --- .../Actions/HostActions/StartHostAction.cs | 2 +- src/Azure.Functions.Cli/Common/Constants.cs | 2 ++ .../ExtensionBundleConfigurationBuilder.cs | 8 +------- .../ExtensionBundle/ExtensionBundleHelper.cs | 7 +++++++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 8ec1ea53c..1001a69f9 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -553,7 +553,7 @@ private void SetBundlesEnvironmentVariables() var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; if (!string.IsNullOrEmpty(bundleId)) { - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)); + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", ExtensionBundleHelper.GetDownloadPath(bundleId)); Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); } } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index a51a9640b..3f8bfda95 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -48,6 +48,8 @@ internal static class Constants public const string AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; + public const string OSXCoreToolsTempDirectoryName = ".azure-functions-core-tools"; + public const string OSXRootPath = "~/"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; public const string PowerShellWorkerDefaultVersion = "~6"; diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs index 6204f6391..3cd8b4e95 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs @@ -1,12 +1,6 @@ using System.Collections.Generic; -using System.IO; -using Azure.Functions.Cli.Common; using Microsoft.Azure.WebJobs.Script; -using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Azure.Functions.Cli.ExtensionBundle { @@ -26,7 +20,7 @@ public void Configure(IConfigurationBuilder builder) { builder.AddInMemoryCollection(new Dictionary { - { "AzureFunctionsJobHost:extensionBundle:downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)}, + { "AzureFunctionsJobHost:extensionBundle:downloadPath", ExtensionBundleHelper.GetDownloadPath(bundleId) }, { "AzureFunctionsJobHost:extensionBundle:ensureLatest", "true"} }); } diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs index e4bbdb2fb..e038fc555 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Text; namespace Azure.Functions.Cli.ExtensionBundle @@ -42,5 +43,11 @@ public static ExtensionBundleContentProvider GetExtensionBundleContentProvider() { return new ExtensionBundleContentProvider(GetExtensionBundleManager(), NullLogger.Instance); } + + public static string GetDownloadPath(string bundleId) + { + string rootDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? Path.Combine(Constants.OSXRootPath, Constants.OSXCoreToolsTempDirectoryName) : Path.GetTempPath(); + return Path.Combine(rootDirectoryPath, "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId); + } } } From 5c7cf4827f34fd07cc81e4491b3c703302067753 Mon Sep 17 00:00:00 2001 From: Hanzhang Zeng <48038149+Hazhzeng@users.noreply.github.com> Date: Wed, 22 Jul 2020 20:43:11 -0700 Subject: [PATCH 073/127] Remove WEBSITE_RUN_FROM_PACKAGE app setting (#2100) --- .../Actions/AzureActions/PublishFunctionAppAction.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 5262e3cdf..0586adeac 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -455,8 +455,7 @@ private async Task HandleLinuxConsumptionPublish(Site functionApp, Func pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp); var deployStatus = await PerformServerSideBuild(functionApp, zipFileFactory, pollConsumptionBuild); return deployStatus == DeployStatus.Success; @@ -704,7 +703,7 @@ public async Task PerformServerSideBuild(Site functionApp, Func Date: Mon, 27 Jul 2020 22:27:28 -0700 Subject: [PATCH 074/127] Add MSI instructions (#2092) * Update chocolatey v2 * Add MSI links * Update 64-bit message --- README.md | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 391849e35..c7a95d056 100644 --- a/README.md +++ b/README.md @@ -16,56 +16,65 @@ The Azure Functions Core Tools provide a local development experience for creati **v2** (master branch): Self-contained cross-platform package -**v3**: Self-contained cross-platform package +**v3**: (v3.x branch): Self-contained cross-platform package **(recommended)** ## Installing ### Windows -To install runtime with npm: +#### To download and install with MSI: -**v2** +##### v3 + +- [Windows 64-bit](https://go.microsoft.com/fwlink/?linkid=2135274) (VS Code debugging requires 64-bit) +- [Windows 32-bit](https://go.microsoft.com/fwlink/?linkid=2135275) + +#### To install with npm: + +##### v3 ```bash -npm i -g azure-functions-core-tools@2 --unsafe-perm true +npm i -g azure-functions-core-tools@3 --unsafe-perm true ``` -**v3** +##### v2 ```bash -npm i -g azure-functions-core-tools@3 --unsafe-perm true +npm i -g azure-functions-core-tools@2 --unsafe-perm true ``` -To install with chocolatey: +#### To install with chocolatey: -"**v2**" +##### v3 ```bash -choco install azure-functions-core-tools +choco install azure-functions-core-tools-3 ``` -"**v3**" + +*Notice: To debug functions under vscode, the 64-bit version is required* ```bash -choco install azure-functions-core-tools-3 +choco install azure-functions-core-tools-3 --params "'/x64'" ``` -#### Notice: To debug functions under vscode, x64 bitness is required +##### v2 ```bash -choco install azure-functions-core-tools --params "'/x64'" +choco install azure-functions-core-tools-2 ``` + ### Mac -**Homebrew**: +#### Homebrew: -**v2** +##### v3 ```bash brew tap azure/functions -brew install azure-functions-core-tools@2 +brew install azure-functions-core-tools@3 ``` -**v3** +##### v2 ```bash brew tap azure/functions -brew install azure-functions-core-tools@3 +brew install azure-functions-core-tools@2 ``` -Homebrew allow side by side installation of v2 and v3, you can switch between the versions using +Homebrew allows side by side installation of v2 and v3, you can switch between the versions using ```bash brew link --overwrite azure-functions-core-tools@3 ``` @@ -126,16 +135,16 @@ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list #### 2. Install -##### v2 +##### v3 ```bash sudo apt-get update -sudo apt-get install azure-functions-core-tools-2 +sudo apt-get install azure-functions-core-tools-3 ``` -##### v3 +##### v2 ```bash sudo apt-get update -sudo apt-get install azure-functions-core-tools-3 +sudo apt-get install azure-functions-core-tools-2 ``` #### Other Linux Distributions From 8c653424aeb6494704c80edb202dd357043ecdf8 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Thu, 30 Jul 2020 12:33:08 -0700 Subject: [PATCH 075/127] Adding AuthLevel to HttpTrigger (#2114) --- .../LocalActions/CreateFunctionAction.cs | 47 ++++++++++-- src/Azure.Functions.Cli/Common/Constants.cs | 2 + .../Helpers/DotnetHelpers.cs | 18 ++++- .../E2E/CreateFunctionTests.cs | 74 ++++++++++++++++++- 4 files changed, 130 insertions(+), 11 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs index e830fa293..9ca696885 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs @@ -10,8 +10,10 @@ using Azure.Functions.Cli.Telemetry; using Colors.Net; using Fclp; +using Microsoft.Azure.WebJobs.Extensions.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using static Azure.Functions.Cli.Common.Constants; using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Actions.LocalActions @@ -21,18 +23,23 @@ namespace Azure.Functions.Cli.Actions.LocalActions [Action(Name = "create", Context = Context.Function, HelpText = "Create a new function from a template.")] internal class CreateFunctionAction : BaseAction { - private readonly ITemplatesManager _templatesManager; + private ITemplatesManager _templatesManager; private readonly ISecretsManager _secretsManager; public string Language { get; set; } public string TemplateName { get; set; } public string FunctionName { get; set; } public bool Csx { get; set; } + public AuthorizationLevel? AuthorizationLevel { get; set; } + + Lazy> _templates; + public CreateFunctionAction(ITemplatesManager templatesManager, ISecretsManager secretsManager) { _templatesManager = templatesManager; _secretsManager = secretsManager; + _templates = new Lazy>(() => { return _templatesManager.Templates.Result; }); } public override ICommandLineParserResult ParseArgs(string[] args) @@ -52,6 +59,11 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription("Function name") .Callback(n => FunctionName = n); + Parser + .Setup('a', "authlevel") + .WithDescription("Authorization level is applicable to templates that use Http trigger, Allowed values: [function, anonymous, admin]. Authorization level is not enforced when running functions from core tools") + .Callback(a => AuthorizationLevel = a); + Parser .Setup("csx") .WithDescription("use old style csx dotnet functions") @@ -75,7 +87,7 @@ public async override Task RunAsync() } var workerRuntime = GlobalCoreToolsSettings.CurrentWorkerRuntimeOrNone; - var templates = await _templatesManager.Templates; + if (workerRuntime != WorkerRuntime.None && !string.IsNullOrWhiteSpace(Language)) { @@ -92,13 +104,13 @@ public async override Task RunAsync() if (workerRuntime == WorkerRuntime.None) { SelectionMenuHelper.DisplaySelectionWizardPrompt("language"); - Language = SelectionMenuHelper.DisplaySelectionWizard(templates.Select(t => t.Metadata.Language).Where(l => !l.Equals("python", StringComparison.OrdinalIgnoreCase)).Distinct()); + Language = SelectionMenuHelper.DisplaySelectionWizard(_templates.Value.Select(t => t.Metadata.Language).Where(l => !l.Equals("python", StringComparison.OrdinalIgnoreCase)).Distinct()); workerRuntime = WorkerRuntimeLanguageHelper.SetWorkerRuntime(_secretsManager, Language); } else if (workerRuntime != WorkerRuntime.dotnet || Csx) { var languages = WorkerRuntimeLanguageHelper.LanguagesForWorker(workerRuntime); - var displayList = templates + var displayList = _templates.Value .Select(t => t.Metadata.Language) .Where(l => languages.Contains(l, StringComparer.OrdinalIgnoreCase)) .Distinct() @@ -127,7 +139,7 @@ public async override Task RunAsync() FunctionName = FunctionName ?? Console.ReadLine(); ColoredConsole.WriteLine(FunctionName); var namespaceStr = Path.GetFileName(Environment.CurrentDirectory); - await DotnetHelpers.DeployDotnetFunction(TemplateName.Replace(" ", string.Empty), Utilities.SanitizeClassName(FunctionName), Utilities.SanitizeNameSpace(namespaceStr)); + await DotnetHelpers.DeployDotnetFunction(TemplateName.Replace(" ", string.Empty), Utilities.SanitizeClassName(FunctionName), Utilities.SanitizeNameSpace(namespaceStr), AuthorizationLevel); } else { @@ -144,10 +156,10 @@ public async override Task RunAsync() } TelemetryHelpers.AddCommandEventToDictionary(TelemetryCommandEvents, "language", templateLanguage); - TemplateName = TemplateName ?? SelectionMenuHelper.DisplaySelectionWizard(templates.Where(t => t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase)).Select(t => t.Metadata.Name).Distinct()); + TemplateName = TemplateName ?? SelectionMenuHelper.DisplaySelectionWizard(_templates.Value.Where(t => t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase)).Select(t => t.Metadata.Name).Distinct()); ColoredConsole.WriteLine(TitleColor(TemplateName)); - var template = templates.FirstOrDefault(t => Utilities.EqualsIgnoreCaseAndSpace(t.Metadata.Name, TemplateName) && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase)); + var template = _templates.Value.FirstOrDefault(t => Utilities.EqualsIgnoreCaseAndSpace(t.Metadata.Name, TemplateName) && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase)); if (template == null) { @@ -164,6 +176,11 @@ public async override Task RunAsync() throw new CliException($"The {template.Metadata.Name} template has extensions. {Constants.Errors.ExtensionsNeedDotnet}"); } + if (AuthorizationLevel.HasValue) + { + ConfigureAuthorizationLevel(template); + } + ColoredConsole.Write($"Function name: [{template.Metadata.DefaultFunctionName}] "); FunctionName = FunctionName ?? Console.ReadLine(); FunctionName = string.IsNullOrEmpty(FunctionName) ? template.Metadata.DefaultFunctionName : FunctionName; @@ -174,6 +191,22 @@ public async override Task RunAsync() ColoredConsole.WriteLine($"The function \"{FunctionName}\" was created successfully from the \"{TemplateName}\" template."); } + private void ConfigureAuthorizationLevel(Template template) + { + var bindings = template.Function["bindings"]; + bool IsHttpTriggerTemplate = bindings.Any(b => b["type"].ToString() == "httpTrigger"); + + if (!IsHttpTriggerTemplate) + { + throw new CliException(AuthLevelErrorMessage); + } + else + { + var binding = bindings.Where(b => b["type"].ToString().Equals(HttpTriggerTemplateName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + binding["authLevel"] = AuthorizationLevel.ToString(); + } + } + private bool InferAndUpdateLanguage(WorkerRuntime workerRuntime) { // If there is a tsconfig.json present, we assume that the language is typescript diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 3f8bfda95..95d238b0d 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -52,6 +52,8 @@ internal static class Constants public const string OSXRootPath = "~/"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; public const string PowerShellWorkerDefaultVersion = "~6"; + public const string AuthLevelErrorMessage = "Unable to configure Authorization level. The selected template does not use Http Trigger"; + public const string HttpTriggerTemplateName = "HttpTrigger"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); diff --git a/src/Azure.Functions.Cli/Helpers/DotnetHelpers.cs b/src/Azure.Functions.Cli/Helpers/DotnetHelpers.cs index 7d80277a5..c7de279e3 100644 --- a/src/Azure.Functions.Cli/Helpers/DotnetHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/DotnetHelpers.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Azure.Functions.Cli.Common; using Colors.Net; +using Microsoft.Azure.WebJobs.Extensions.Http; using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Helpers @@ -38,11 +39,24 @@ await TemplateOperation(async () => }); } - public static async Task DeployDotnetFunction(string templateName, string functionName, string namespaceStr) + public static async Task DeployDotnetFunction(string templateName, string functionName, string namespaceStr, AuthorizationLevel? httpAuthorizationLevel = null) { await TemplateOperation(async () => { - var exe = new Executable("dotnet", $"new {templateName} --name {functionName} --namespace {namespaceStr}"); + string exeCommandArguments = $"new {templateName} --name {functionName} --namespace {namespaceStr}"; + if (httpAuthorizationLevel != null) + { + if (templateName.Equals(Constants.HttpTriggerTemplateName, StringComparison.OrdinalIgnoreCase)) + { + exeCommandArguments += $" --AccessRights {httpAuthorizationLevel}"; + } + else + { + throw new CliException(Constants.AuthLevelErrorMessage); + } + } + + var exe = new Executable("dotnet", exeCommandArguments); var exitCode = await exe.RunAsync(o => { }, e => ColoredConsole.Error.WriteLine(ErrorColor(e))); if (exitCode != 0) { diff --git a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs index 46334712b..c79434d05 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs @@ -1,4 +1,5 @@ -using Azure.Functions.Cli.Tests.E2E.Helpers; +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Tests.E2E.Helpers; using System; using System.IO; using System.Threading.Tasks; @@ -28,6 +29,75 @@ await CliTester.Run(new RunConfiguration }, _output); } + [Fact] + public async Task create_timerTrigger_authConfigured_returns_error() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + "new --template TimerTrigger --name testfunc --authlevel function" + }, + HasStandardError = true, + ErrorContains = new[] + { + Constants.AuthLevelErrorMessage + } + }, _output); + } + + [Fact] + public async Task create_httpTrigger_Invalid_AuthConfig_returns_error() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + "new --template httpTrigger --name testfunc --authlevel invalid" + }, + OutputContains = new[] + { + "Authorization level is applicable to templates that use Http trigger, Allowed values: [function, anonymous, admin]. Authorization level is not enforced when running functions from core tools" + } + }, _output); + } + + [Fact] + public async Task create_httpTrigger_with_authConfigured_node() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + "new --template HttpTrigger --name testfunc --authlevel function" + }, + OutputContains = new[] + { + "The function \"testfunc\" was created successfully from the \"HttpTrigger\" template." + } + }, _output); + } + + [Fact] + public async Task create_httpTrigger_with_authConfigured_dotnet() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet", + "new --template HttpTrigger --name testfunc --authlevel function" + }, + OutputContains = new[] + { + "The function \"testfunc\" was created successfully from the \"HttpTrigger\" template." + } + }, _output); + } + [Fact] public async Task create_template_function_sanitization_dotnet() { @@ -39,7 +109,7 @@ await CliTester.Run(new RunConfiguration "new --prefix 12n.e.0w-file$ --template HttpTrigger --name 12@n.other-file$" }, CommandTimeout = new TimeSpan(0, 1, 0), - CheckFiles = new[] + CheckFiles = new[] { new FileResult { From 7dd097e1d7f5ce4d6ef5bd16f425ff0a5b83da4d Mon Sep 17 00:00:00 2001 From: Yogesh Jagadeesan Date: Wed, 5 Aug 2020 15:13:20 -0700 Subject: [PATCH 076/127] func init and new for custom handlers (#2093) Added init and new actions for custom handler --- build/Settings.cs | 6 ++-- .../Actions/LocalActions/InitAction.cs | 34 +++++++------------ .../Azure.Functions.Cli.csproj | 6 ++++ src/Azure.Functions.Cli/Common/Constants.cs | 2 ++ .../Common/TemplatesManager.cs | 7 ++-- .../Extensions/StringExtensions.cs | 11 ++++++ .../Helpers/GlobalCoreToolsSettings.cs | 5 +++ .../Helpers/LanguageWorkerHelper.cs | 1 + .../Helpers/WorkerRuntimeLanguageHelper.cs | 14 +++++--- .../StaticResources/Dockerfile.custom | 6 ++++ .../StaticResources/StaticResources.cs | 4 +++ .../StaticResources/customHandlerConfig.json | 7 ++++ .../E2E/CreateFunctionTests.cs | 17 ++++++++++ 13 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 src/Azure.Functions.Cli/StaticResources/Dockerfile.custom create mode 100644 src/Azure.Functions.Cli/StaticResources/customHandlerConfig.json diff --git a/build/Settings.cs b/build/Settings.cs index d1fc5acca..312ca54d6 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -20,9 +20,9 @@ private static string config(string @default = null, [CallerMemberName] string k : value; } - public const string ItemTemplatesVersion = "2.1.1517"; - public const string ProjectTemplatesVersion = "2.1.1517"; - public const string TemplateJsonVersion = "2.1.1517"; + public const string ItemTemplatesVersion = "2.1.1579"; + public const string ProjectTemplatesVersion = "2.1.1579"; + public const string TemplateJsonVersion = "2.1.1579"; public static readonly string SrcProjectPath = Path.GetFullPath("../src/Azure.Functions.Cli/"); diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs index 5ccc61649..858d06107 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs @@ -6,12 +6,11 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; -using Azure.Functions.Cli.Telemetry; using Colors.Net; using Fclp; -using Microsoft.Azure.WebJobs.Script; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static Azure.Functions.Cli.Common.OutputTheme; @@ -324,6 +323,10 @@ private static async Task WriteDockerfile(WorkerRuntime workerRuntime, bool csx) { await WriteFiles("Dockerfile", await StaticResources.DockerfilePowershell); } + else if(workerRuntime == Helpers.WorkerRuntime.custom) + { + await WriteFiles("Dockerfile", await StaticResources.DockerfileCustom); + } else if (workerRuntime == Helpers.WorkerRuntime.None) { throw new CliException("Can't find WorkerRuntime None"); @@ -424,33 +427,20 @@ private static async Task WriteHostJson(WorkerRuntime workerRuntime, bool manage if (workerRuntime == Helpers.WorkerRuntime.powershell && managedDependenciesOption) { - hostJsonContent = await AddManagedDependencyConfig(hostJsonContent); + hostJsonContent = await hostJsonContent.AppendContent(Constants.ManagedDependencyConfigPropertyName, StaticResources.ManagedDependenciesConfig); } if (extensionBundle) { - hostJsonContent = await AddBundleConfig(hostJsonContent); + hostJsonContent = await hostJsonContent.AppendContent(Constants.ExtensionBundleConfigPropertyName, StaticResources.BundleConfig); } - await WriteFiles("host.json", hostJsonContent); - } - - private static async Task AddBundleConfig(string hostJsonContent) - { - var hostJsonObj = JsonConvert.DeserializeObject(hostJsonContent); - var bundleConfigContent = await StaticResources.BundleConfig; - var bundleConfig = JsonConvert.DeserializeObject(bundleConfigContent); - hostJsonObj.Add(Constants.ExtensionBundleConfigPropertyName, bundleConfig); - return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); - } + if(workerRuntime == Helpers.WorkerRuntime.custom) + { + hostJsonContent = await hostJsonContent.AppendContent(Constants.CustomHandlerPropertyName, StaticResources.CustomHandlerConfig); + } - private static async Task AddManagedDependencyConfig(string hostJsonContent) - { - var hostJsonObj = JsonConvert.DeserializeObject(hostJsonContent); - var managedDependenciesConfigContent = await StaticResources.ManagedDependenciesConfig; - var managedDependenciesConfig = JsonConvert.DeserializeObject(managedDependenciesConfigContent); - hostJsonObj.Add(Constants.ManagedDependencyConfigPropertyName, managedDependenciesConfig); - return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); + await WriteFiles(Constants.HostJsonFileName, hostJsonContent); } private static string AddWorkerVersion(string localSettingsContent, string workerVersion) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index dabbb72e5..b75025e19 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -27,12 +27,18 @@ $(AssemblyName).bundleConfig.json + + $(AssemblyName).customHandlerConfig.json + $(AssemblyName).ExtensionsProj.csproj $(AssemblyName).Dockerfile.csx.dotnet + + $(AssemblyName).Dockerfile.custom + $(AssemblyName).Dockerfile.dotnet diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 95d238b0d..cfc4d9adf 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -51,6 +51,7 @@ internal static class Constants public const string OSXCoreToolsTempDirectoryName = ".azure-functions-core-tools"; public const string OSXRootPath = "~/"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; + public const string CustomHandlerPropertyName = "customHandler"; public const string PowerShellWorkerDefaultVersion = "~6"; public const string AuthLevelErrorMessage = "Unable to configure Authorization level. The selected template does not use Http Trigger"; public const string HttpTriggerTemplateName = "HttpTrigger"; @@ -88,6 +89,7 @@ public static class Languages public const string CSharp = "c#"; public const string Powershell = "powershell"; public const string Java = "java"; + public const string Custom = "custom"; } public static class ArmConstants diff --git a/src/Azure.Functions.Cli/Common/TemplatesManager.cs b/src/Azure.Functions.Cli/Common/TemplatesManager.cs index 93c521191..1c417b7d0 100644 --- a/src/Azure.Functions.Cli/Common/TemplatesManager.cs +++ b/src/Azure.Functions.Cli/Common/TemplatesManager.cs @@ -6,11 +6,11 @@ using Colors.Net; using Newtonsoft.Json; using Azure.Functions.Cli.Interfaces; -using Newtonsoft.Json.Linq; using Azure.Functions.Cli.Actions.LocalActions; using Azure.Functions.Cli.ExtensionBundle; using System.Linq; using System.Reflection; +using Azure.Functions.Cli.Helpers; namespace Azure.Functions.Cli.Common { @@ -34,9 +34,9 @@ public Task> Templates private static async Task> GetTemplates() { var extensionBundleManager = ExtensionBundleHelper.GetExtensionBundleManager(); + string templatesJson; - string templatesJson; - if (extensionBundleManager.IsExtensionBundleConfigured()) + if(extensionBundleManager.IsExtensionBundleConfigured()) { var contentProvider = ExtensionBundleHelper.GetExtensionBundleContentProvider(); templatesJson = await contentProvider.GetTemplates(); @@ -45,6 +45,7 @@ private static async Task> GetTemplates() { templatesJson = GetTemplatesJson(); } + return JsonConvert.DeserializeObject>(templatesJson); } diff --git a/src/Azure.Functions.Cli/Extensions/StringExtensions.cs b/src/Azure.Functions.Cli/Extensions/StringExtensions.cs index 3acb89b14..756485e75 100644 --- a/src/Azure.Functions.Cli/Extensions/StringExtensions.cs +++ b/src/Azure.Functions.Cli/Extensions/StringExtensions.cs @@ -1,7 +1,9 @@ using Azure.Functions.Cli.Exceptions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace Azure.Functions.Cli.Extensions { @@ -41,5 +43,14 @@ public static string SanitizeImageName(this string imageName) return cleanImageName.ToLowerInvariant().Substring(0, Math.Min(cleanImageName.Length, 128)).Trim(); } + + public static async Task AppendContent(this string hostJsonContent, string contentPropertyName, Task contentSource) + { + var hostJsonObj = JsonConvert.DeserializeObject(hostJsonContent); + var additionalContent = await contentSource; + var additionalConfig = JsonConvert.DeserializeObject(additionalContent); + hostJsonObj.Add(contentPropertyName, additionalConfig); + return JsonConvert.SerializeObject(hostJsonObj, Formatting.Indented); + } } } diff --git a/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs b/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs index f25e50fac..e87fcde18 100644 --- a/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs +++ b/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs @@ -9,6 +9,7 @@ namespace Azure.Functions.Cli.Helpers public static class GlobalCoreToolsSettings { private static WorkerRuntime _currentWorkerRuntime; + public static WorkerRuntime CurrentWorkerRuntime { get @@ -70,6 +71,10 @@ public static void Init(ISecretsManager secretsManager, string[] args) { _currentWorkerRuntime = WorkerRuntime.powershell; } + else if(args.Contains("--custom")) + { + _currentWorkerRuntime = WorkerRuntime.custom; + } else { _currentWorkerRuntime = WorkerRuntimeLanguageHelper.GetCurrentWorkerRuntimeLanguage(secretsManager); diff --git a/src/Azure.Functions.Cli/Helpers/LanguageWorkerHelper.cs b/src/Azure.Functions.Cli/Helpers/LanguageWorkerHelper.cs index ace7e5be4..b0eddcb50 100644 --- a/src/Azure.Functions.Cli/Helpers/LanguageWorkerHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/LanguageWorkerHelper.cs @@ -13,6 +13,7 @@ public static class LanguageWorkerHelper { WorkerRuntime.java, "languageWorkers:java:arguments" }, { WorkerRuntime.powershell, "languageWorkers:powershell:arguments" }, { WorkerRuntime.dotnet, string.Empty }, + { WorkerRuntime.custom, string.Empty }, { WorkerRuntime.None, string.Empty } } .Select(p => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) diff --git a/src/Azure.Functions.Cli/Helpers/WorkerRuntimeLanguageHelper.cs b/src/Azure.Functions.Cli/Helpers/WorkerRuntimeLanguageHelper.cs index 322244050..04a55d0b1 100644 --- a/src/Azure.Functions.Cli/Helpers/WorkerRuntimeLanguageHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/WorkerRuntimeLanguageHelper.cs @@ -15,7 +15,8 @@ public enum WorkerRuntime node, python, java, - powershell + powershell, + custom } public static class WorkerRuntimeLanguageHelper @@ -26,7 +27,8 @@ public static class WorkerRuntimeLanguageHelper { WorkerRuntime.node, new [] { "js", "javascript", "typescript", "ts" } }, { WorkerRuntime.python, new [] { "py" } }, { WorkerRuntime.java, new string[] { } }, - { WorkerRuntime.powershell, new [] { "pwsh" } } + { WorkerRuntime.powershell, new [] { "pwsh" } }, + { WorkerRuntime.custom, new string[] { } } }; private static readonly IDictionary normalizeMap = availableWorkersRuntime @@ -39,7 +41,8 @@ public static class WorkerRuntimeLanguageHelper { WorkerRuntime.dotnet, Constants.Languages.CSharp }, { WorkerRuntime.node, Constants.Languages.JavaScript }, { WorkerRuntime.python, Constants.Languages.Python }, - { WorkerRuntime.powershell, Constants.Languages.Powershell } + { WorkerRuntime.powershell, Constants.Languages.Powershell }, + { WorkerRuntime.custom, Constants.Languages.Custom }, }; private static readonly IDictionary> languageToAlias = new Dictionary> @@ -50,7 +53,8 @@ public static class WorkerRuntimeLanguageHelper { Constants.Languages.Python, new [] { "py" } }, { Constants.Languages.Powershell, new [] { "pwsh" } }, { Constants.Languages.CSharp, new [] { "csharp", "dotnet" } }, - { Constants.Languages.Java, new string[] { } } + { Constants.Languages.Java, new string[] { } }, + { Constants.Languages.Custom, new string[] { } } }; public static readonly IDictionary WorkerRuntimeStringToLanguage = languageToAlias @@ -65,7 +69,7 @@ public static class WorkerRuntimeLanguageHelper public static string AvailableWorkersRuntimeString => string.Join(", ", availableWorkersRuntime.Keys - .Where(k => k != WorkerRuntime.java) + .Where(k => (k != WorkerRuntime.java)) .Select(s => s.ToString())); public static IEnumerable AvailableWorkersList => availableWorkersRuntime.Keys diff --git a/src/Azure.Functions.Cli/StaticResources/Dockerfile.custom b/src/Azure.Functions.Cli/StaticResources/Dockerfile.custom new file mode 100644 index 000000000..98c6fae97 --- /dev/null +++ b/src/Azure.Functions.Cli/StaticResources/Dockerfile.custom @@ -0,0 +1,6 @@ +# To enable ssh & remote debugging on app service change the base image to the one below +FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice +ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ + AzureFunctionsJobHost__Logging__Console__IsEnabled=true + +COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"] diff --git a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs index da0c1f342..27ff92961 100644 --- a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs +++ b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs @@ -31,6 +31,8 @@ private static async Task GetValue(string name) public static Task DockerfileDotNet => GetValue("Dockerfile.dotnet"); + public static Task DockerfileCustom => GetValue("Dockerfile.custom"); + public static Task DockerfileCsxDotNet => GetValue("Dockerfile.csx.dotnet"); public static Task DockerfilePython36 => GetValue("Dockerfile.python36"); @@ -51,6 +53,8 @@ private static async Task GetValue(string name) public static Task BundleConfig => GetValue("bundleConfig.json"); + public static Task CustomHandlerConfig => GetValue("customHandlerConfig.json"); + public static Task ManagedDependenciesConfig => GetValue("managedDependenciesConfig.json"); public static Task PythonDockerBuildScript => GetValue(Constants.StaticResourcesNames.PythonDockerBuild); diff --git a/src/Azure.Functions.Cli/StaticResources/customHandlerConfig.json b/src/Azure.Functions.Cli/StaticResources/customHandlerConfig.json new file mode 100644 index 000000000..888ebfac3 --- /dev/null +++ b/src/Azure.Functions.Cli/StaticResources/customHandlerConfig.json @@ -0,0 +1,7 @@ +{ + "description": { + "defaultExecutablePath": "", + "workingDirectory": "", + "arguments": [ ] + } +} \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs index c79434d05..8027fe926 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs @@ -141,6 +141,23 @@ await CliTester.Run(new RunConfiguration }, _output); } + [Fact] + public async Task create_function_custom() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime custom --no-bundle", + "new --template \"Http Trigger\" --name testfunc" + }, + OutputContains = new[] + { + "The function \"testfunc\" was created successfully from the \"Http Trigger\" template." + } + }, _output); + } + [Fact] public async Task create_template_function_js_no_space_name() { From a52408b171e3723cf0af670148283aaad7c67f91 Mon Sep 17 00:00:00 2001 From: Cedar Date: Fri, 7 Aug 2020 03:22:22 +0800 Subject: [PATCH 077/127] Update README.md to fix knative install doc broken link. (#2133) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7a95d056..802925fa9 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ func kubernetes deploy --name myfunction --registry Date: Thu, 6 Aug 2020 12:23:03 -0700 Subject: [PATCH 078/127] Upgrade PowerShellWorker to 2.0.292 (#2128) --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index b75025e19..7e7145281 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -117,7 +117,7 @@ - + From be1b76f4b961e37b351aff6aaf74cf2b29dd7bbd Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Fri, 7 Aug 2020 00:04:51 -0700 Subject: [PATCH 079/127] Change colors to be more accessible Making most of the colors to the darker shade to make it visible in both light and dark backgrounds. --- .../Actions/AzureActions/BaseAzureAction.cs | 4 +-- .../AzureActions/PublishFunctionAppAction.cs | 12 ++++----- src/Azure.Functions.Cli/Actions/HelpAction.cs | 4 +-- .../Actions/HostActions/StartHostAction.cs | 4 +-- .../Actions/LocalActions/PackAction.cs | 7 +++-- .../Common/DurableManager.cs | 27 ++++++++++--------- src/Azure.Functions.Cli/Common/OutputTheme.cs | 12 +++++---- .../Helpers/PythonHelpers.cs | 2 +- .../Helpers/SecurityHelpers.cs | 4 +-- src/Azure.Functions.Cli/Helpers/ZipHelper.cs | 7 ++--- 10 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/BaseAzureAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/BaseAzureAction.cs index 2f538c511..00962bba5 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/BaseAzureAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/BaseAzureAction.cs @@ -118,7 +118,7 @@ private async Task GetManagementURL() { if (StaticSettings.IsDebug) { - ColoredConsole.WriteLine(Yellow("Unable to retrieve the resource manager URL from az CLI")); + ColoredConsole.WriteLine(WarningColor("Unable to retrieve the resource manager URL from az CLI")); } return (false, null); } @@ -156,7 +156,7 @@ private bool TryGetTokenFromTestEnvironment(out string token) { if (StaticSettings.IsDebug) { - ColoredConsole.WriteLine(Yellow("Unable to fetch access token from az CLI")); + ColoredConsole.WriteLine(WarningColor("Unable to fetch access token from az CLI")); } return (false, null); } diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 0586adeac..c510ed9cb 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -89,7 +89,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) Parser .Setup("no-bundler") .WithDescription("[Deprecated] Skips generating a bundle when publishing python function apps with build-native-deps.") - .Callback(nb => ColoredConsole.WriteLine(Yellow($"Warning: Argument {Cyan("--no-bundler")} is deprecated and a no-op. Python function apps are not bundled anymore."))); + .Callback(nb => ColoredConsole.WriteLine(WarningColor($"Warning: Argument {AdditionalInfoColor("--no-bundler")} is deprecated and a no-op. Python function apps are not bundled anymore."))); Parser .Setup("additional-packages") .WithDescription("List of packages to install when building native dependencies. For example: \"python3-dev libevent-dev\"") @@ -290,7 +290,7 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa // Recommend Linux scm users to use --build remote instead of --build-native-deps if (BuildNativeDeps && functionApp.IsLinux && !string.IsNullOrEmpty(functionApp.ScmUri)) { - ColoredConsole.WriteLine(Yellow("Recommend using '--build remote' to resolve project dependencies remotely on Azure")); + ColoredConsole.WriteLine(WarningColor("Recommend using '--build remote' to resolve project dependencies remotely on Azure")); } Func> zipStreamFactory = () => ZipHelper.GetAppZipFile(functionAppRoot, BuildNativeDeps, PublishBuildOption, @@ -585,7 +585,7 @@ private async Task SetRunFromPackageAppSetting(Site functionApp, string runFromP functionApp.AzureAppSettings.Remove("WEBSITE_RUN_FROM_ZIP"); if (StaticSettings.IsDebug) { - ColoredConsole.WriteLine(Yellow("Removing WEBSITE_RUN_FROM_ZIP App Setting")); + ColoredConsole.WriteLine(WarningColor("Removing WEBSITE_RUN_FROM_ZIP App Setting")); } } @@ -633,7 +633,7 @@ private async Task RemoveFunctionAppAppSetting(Site functionApp, params string[] if (functionApp.AzureAppSettings.ContainsKey(appSettingName)) { var appSettingValue = functionApp.AzureAppSettings[appSettingName]; - ColoredConsole.WriteLine(Yellow($"Removing {appSettingName} app setting ({appSettingValue})")); + ColoredConsole.WriteLine(WarningColor($"Removing {appSettingName} app setting ({appSettingValue})")); functionApp.AzureAppSettings.Remove(appSettingName); isAppSettingUpdated = true; } @@ -719,7 +719,7 @@ public async Task PerformServerSideBuild(Site functionApp, Func PerformServerSideBuild(Site functionApp, Func contexts) ColoredConsole.WriteLine(TitleColor("Contexts:")); foreach (var context in contexts) { - ColoredConsole.WriteLine(string.Format($"{{0, {-longestName}}} {{1}}", context.ToLowerCaseString().Yellow(), GetDescriptionOfContext(context))); + ColoredConsole.WriteLine(string.Format($"{{0, {-longestName}}} {{1}}", context.ToLowerCaseString().DarkYellow(), GetDescriptionOfContext(context))); } ColoredConsole.WriteLine(); } @@ -311,7 +311,7 @@ private static string GetActionHelp(ActionType action, int formattingSpace) ? action.Names.Distinct().Aggregate((a, b) => string.Join(", ", a, b)) : string.Empty; var description = action.Type.GetCustomAttributes()?.FirstOrDefault()?.HelpText; - return string.Format($"{{0, {-formattingSpace}}} {{1}} {(aliases.Any() ? "Aliases:" : "")} {{2}}", name.Yellow(), description, aliases); + return string.Format($"{{0, {-formattingSpace}}} {{1}} {(aliases.Any() ? "Aliases:" : "")} {{2}}", name.DarkYellow(), description, aliases); } // http://stackoverflow.com/a/1799401 diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 1001a69f9..83850f033 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -365,7 +365,7 @@ private void DisplayHttpFunctionsInfo(IScriptJobHost scriptHost, HttpOptions htt { ColoredConsole .WriteLine() - .WriteLine(Yellow("Http Functions:")) + .WriteLine(DarkYellow("Http Functions:")) .WriteLine(); } @@ -394,7 +394,7 @@ private void DisplayHttpFunctionsInfo(IScriptJobHost scriptHost, HttpOptions htt var functionMethods = methods != null ? $"{CleanAndFormatHttpMethods(string.Join(",", methods))}" : null; var url = $"{baseUri.ToString().Replace("0.0.0.0", "localhost")}{hostRoutePrefix}{httpRoute}"; ColoredConsole - .WriteLine($"\t{Yellow($"{function.Name}:")} {Green(functionMethods)} {Green(url)}") + .WriteLine($"\t{HttpFunctionNameColor($"{function.Name}:")} {HttpFunctionUrlColor(functionMethods)} {HttpFunctionUrlColor(url)}") .WriteLine(); } } diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/PackAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/PackAction.cs index 2a128bac9..ddf538391 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/PackAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/PackAction.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using Azure.Functions.Cli.Common; -using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; using Colors.Net; using Fclp; using Microsoft.Azure.WebJobs.Script; using static Colors.Net.StringStaticMethods; +using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Actions.LocalActions { @@ -45,7 +44,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) Parser .Setup("no-bundler") .WithDescription("Skips generating a bundle when publishing python function apps with build-native-deps.") - .Callback(nb => ColoredConsole.WriteLine(Yellow($"Warning: Argument {Cyan("--no-bundler")} is deprecated and a no-op. Python function apps are not bundled anymore."))); + .Callback(nb => ColoredConsole.WriteLine(WarningColor($"Warning: Argument {AdditionalInfoColor("--no-bundler")} is deprecated and a no-op. Python function apps are not bundled anymore."))); Parser .Setup("additional-packages") .WithDescription("List of packages to install when building native dependencies. For example: \"python3-dev libevent-dev\"") @@ -116,4 +115,4 @@ public override async Task RunAsync() await FileSystemHelpers.WriteToFile(outputPath, stream); } } -} \ No newline at end of file +} diff --git a/src/Azure.Functions.Cli/Common/DurableManager.cs b/src/Azure.Functions.Cli/Common/DurableManager.cs index f66b7ed55..eb2a423ae 100644 --- a/src/Azure.Functions.Cli/Common/DurableManager.cs +++ b/src/Azure.Functions.Cli/Common/DurableManager.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static Colors.Net.StringStaticMethods; +using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Common { @@ -83,13 +84,13 @@ private void SetConnectionStringAndTaskHubName() } else { - ColoredConsole.WriteLine(Yellow($"Could not find local host metadata file '{ScriptConstants.HostMetadataFileName}'")); + ColoredConsole.WriteLine(WarningColor($"Could not find local host metadata file '{ScriptConstants.HostMetadataFileName}'")); } } catch (Exception e) { - ColoredConsole.WriteLine(Yellow($"Exception thrown while attempting to parse override connection string and task hub name from '{ScriptConstants.HostMetadataFileName}':")); - ColoredConsole.WriteLine(Yellow(e.Message)); + ColoredConsole.WriteLine(WarningColor($"Exception thrown while attempting to parse override connection string and task hub name from '{ScriptConstants.HostMetadataFileName}':")); + ColoredConsole.WriteLine(WarningColor(e.Message)); } } @@ -131,7 +132,7 @@ private void CheckAssemblies() if (assemblyFilePaths.Count() == 0) { - ColoredConsole.WriteLine(Yellow($"Could not find {DurableAzureStorageExtensionName}. The functions host must be running a" + + ColoredConsole.WriteLine(WarningColor($"Could not find {DurableAzureStorageExtensionName}. The functions host must be running a" + $" Durable Functions app in order for Durable Functions CLI commands to work.")); return; } @@ -156,7 +157,7 @@ public async Task DeleteTaskHub(string connectionStringKey, string taskHubName) await _orchestrationService.DeleteAsync(); - ColoredConsole.Write(Green($"Task hub '{_taskHubName}' successfully deleted.")); + ColoredConsole.Write(VerboseColor($"Task hub '{_taskHubName}' successfully deleted.")); } public async Task GetHistory(string connectionStringKey, string taskHubName, string instanceId) @@ -188,7 +189,7 @@ public async Task GetInstances(string connectionStringKey, string taskHubName, D // TODO? Status of each instance prints as an integer, rather than the string of the OrchestrationStatus enum ColoredConsole.WriteLine(JsonConvert.SerializeObject(queryResult.OrchestrationState, Formatting.Indented)); - ColoredConsole.WriteLine(Green($"Continuation token for next set of results: '{queryResult.ContinuationToken}'")); + ColoredConsole.WriteLine(VerboseColor($"Continuation token for next set of results: '{queryResult.ContinuationToken}'")); } public async Task GetRuntimeStatus(string connectionStringKey, string taskHubName, string instanceId, bool showInput, bool showOutput) @@ -223,7 +224,7 @@ public async Task PurgeHistory(string connectionStringKey, string taskHubName, D messageToPrint += $" and whose runtime status matched one of the following: [{statuses}]"; } - ColoredConsole.WriteLine(Green(messageToPrint)); + ColoredConsole.WriteLine(VerboseColor(messageToPrint)); ColoredConsole.WriteLine($"Instances deleted: {stats.InstancesDeleted}"); ColoredConsole.WriteLine($"Rows deleted: {stats.RowsDeleted}"); } @@ -239,7 +240,7 @@ public async Task RaiseEvent(string connectionStringKey, string taskHubName, str await _client.RaiseEventAsync(orchestrationInstance, eventName, data); - ColoredConsole.WriteLine(Green($"Raised event '{eventName}' to instance '{instanceId}'.")); + ColoredConsole.WriteLine(VerboseColor($"Raised event '{eventName}' to instance '{instanceId}'.")); } public async Task Rewind(string connectionStringKey, string taskHubName, string instanceId, string reason) @@ -267,9 +268,9 @@ public async Task Rewind(string connectionStringKey, string taskHubName, string if (oldStatus != null && oldStatus.Count > 0 && newStatus != null && newStatus.Count > 0) { - ColoredConsole.Write(Green("Status before rewind: ")); + ColoredConsole.Write(VerboseColor("Status before rewind: ")); ColoredConsole.WriteLine($"{oldStatus[0].OrchestrationStatus}"); - ColoredConsole.Write(Green("Status after rewind: ")); + ColoredConsole.Write(VerboseColor("Status after rewind: ")); ColoredConsole.WriteLine($"{newStatus[0].OrchestrationStatus}"); } } @@ -283,7 +284,7 @@ public async Task StartNew(string connectionStringKey, string taskHubName, strin var status = await _client.GetOrchestrationStateAsync(instanceId, false); if (status != null && status.Count > 0) { - ColoredConsole.WriteLine(Green($"Started '{status[0].Name}' at {status[0].CreatedTime}. " + + ColoredConsole.WriteLine(VerboseColor($"Started '{status[0].Name}' at {status[0].CreatedTime}. " + $"Instance ID: '{status[0].OrchestrationInstance.InstanceId}'.")); } else @@ -318,7 +319,7 @@ public async Task Terminate(string connectionStringKey, string taskHubName, stri if (status?.OrchestrationStatus == OrchestrationStatus.Terminated) { - ColoredConsole.WriteLine(Green($"Successfully terminated '{instanceId}'")); + ColoredConsole.WriteLine(VerboseColor($"Successfully terminated '{instanceId}'")); } else { @@ -327,7 +328,7 @@ public async Task Terminate(string connectionStringKey, string taskHubName, stri } else { - ColoredConsole.WriteLine(Yellow($"Failed to find instance '{instanceId}'. No instance was terminated.")); + ColoredConsole.WriteLine(WarningColor($"Failed to find instance '{instanceId}'. No instance was terminated.")); } } diff --git a/src/Azure.Functions.Cli/Common/OutputTheme.cs b/src/Azure.Functions.Cli/Common/OutputTheme.cs index bf7702d54..7595102d9 100644 --- a/src/Azure.Functions.Cli/Common/OutputTheme.cs +++ b/src/Azure.Functions.Cli/Common/OutputTheme.cs @@ -6,13 +6,15 @@ namespace Azure.Functions.Cli.Common public static class OutputTheme { public static RichString TitleColor(string value) => DarkCyan(value); - public static RichString VerboseColor(string value) => Green(value); - public static RichString LinksColor(string value) => Cyan(value); - public static RichString AdditionalInfoColor(string value) => Cyan(value); + public static RichString VerboseColor(string value) => DarkGreen(value); + public static RichString LinksColor(string value) => DarkCyan(value); + public static RichString AdditionalInfoColor(string value) => DarkCyan(value); public static RichString ExampleColor(string value) => DarkGreen(value); public static RichString ErrorColor(string value) => Red(value); - public static RichString QuestionColor(string value) => Magenta(value); - public static RichString WarningColor(string value) => Yellow(value); + public static RichString QuestionColor(string value) => DarkMagenta(value); + public static RichString WarningColor(string value) => DarkYellow(value); public static RichString QuietWarningColor(string value) => DarkGray(value); + public static RichString HttpFunctionNameColor(string value) => DarkYellow(value); + public static RichString HttpFunctionUrlColor(string value) => DarkGreen(value); } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs index c6949499e..a98ca6153 100644 --- a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs @@ -300,7 +300,7 @@ internal static async Task GetPythonDeploymentPackage(IEnumerable GetOrCreateCertificate(string certP .WriteLine("On Windows you can run:") .WriteLine() .Write(DarkCyan("PS> ")) - .WriteLine($"$cert = {Yellow("New-SelfSignedCertificate")} -Subject localhost -DnsName localhost -FriendlyName \"Functions Development\" -KeyUsage DigitalSignature -TextExtension @(\"2.5.29.37={{text}}1.3.6.1.5.5.7.3.1\")") + .WriteLine($"$cert = {DarkYellow("New-SelfSignedCertificate")} -Subject localhost -DnsName localhost -FriendlyName \"Functions Development\" -KeyUsage DigitalSignature -TextExtension @(\"2.5.29.37={{text}}1.3.6.1.5.5.7.3.1\")") .Write(DarkCyan("PS> ")) - .WriteLine($"{Yellow("Export-PfxCertificate")} -Cert $cert -FilePath certificate.pfx -Password (ConvertTo-SecureString -String {Red("")} -Force -AsPlainText)") + .WriteLine($"{DarkYellow("Export-PfxCertificate")} -Cert $cert -FilePath certificate.pfx -Password (ConvertTo-SecureString -String {Red("")} -Force -AsPlainText)") .WriteLine() .WriteLine("For more checkout https://docs.microsoft.com/en-us/aspnet/core/security/https") .WriteLine(); diff --git a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs index 6784bb130..01b53259f 100644 --- a/src/Azure.Functions.Cli/Helpers/ZipHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/ZipHelper.cs @@ -7,6 +7,7 @@ using Azure.Functions.Cli.Common; using Colors.Net; using static Colors.Net.StringStaticMethods; +using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Helpers { @@ -22,13 +23,13 @@ public static async Task GetAppZipFile(string functionAppRoot, bool buil if (noBuild) { - ColoredConsole.WriteLine(Yellow("Skipping build event for functions project (--no-build).")); + ColoredConsole.WriteLine(DarkYellow("Skipping build event for functions project (--no-build).")); } else if (buildOption == BuildOption.Remote) { - ColoredConsole.WriteLine(Yellow("Performing remote build for functions project.")); + ColoredConsole.WriteLine(DarkYellow("Performing remote build for functions project.")); } else if (buildOption == BuildOption.Local) { - ColoredConsole.WriteLine(Yellow("Performing local build for functions project.")); + ColoredConsole.WriteLine(DarkYellow("Performing local build for functions project.")); } if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.python && !noBuild) From b7b3c83ae9dd340cb7ad60fa2882c7d273ebe66c Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 12 Aug 2020 18:24:38 -0700 Subject: [PATCH 080/127] Revert "changing the default bundle download path for osx" (#2156) This reverts commit b0455000f9001d7c6b531997097becb0ebd1aa48. --- .../Actions/HostActions/StartHostAction.cs | 2 +- src/Azure.Functions.Cli/Common/Constants.cs | 2 -- .../ExtensionBundleConfigurationBuilder.cs | 8 +++++++- .../ExtensionBundle/ExtensionBundleHelper.cs | 7 ------- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 83850f033..d747aad36 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -553,7 +553,7 @@ private void SetBundlesEnvironmentVariables() var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; if (!string.IsNullOrEmpty(bundleId)) { - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", ExtensionBundleHelper.GetDownloadPath(bundleId)); + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)); Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); } } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index cfc4d9adf..452934e6d 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -48,8 +48,6 @@ internal static class Constants public const string AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; - public const string OSXCoreToolsTempDirectoryName = ".azure-functions-core-tools"; - public const string OSXRootPath = "~/"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; public const string CustomHandlerPropertyName = "customHandler"; public const string PowerShellWorkerDefaultVersion = "~6"; diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs index 3cd8b4e95..6204f6391 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs @@ -1,6 +1,12 @@ using System.Collections.Generic; +using System.IO; +using Azure.Functions.Cli.Common; using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Azure.Functions.Cli.ExtensionBundle { @@ -20,7 +26,7 @@ public void Configure(IConfigurationBuilder builder) { builder.AddInMemoryCollection(new Dictionary { - { "AzureFunctionsJobHost:extensionBundle:downloadPath", ExtensionBundleHelper.GetDownloadPath(bundleId) }, + { "AzureFunctionsJobHost:extensionBundle:downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)}, { "AzureFunctionsJobHost:extensionBundle:ensureLatest", "true"} }); } diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs index e038fc555..e4bbdb2fb 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using System.Text; namespace Azure.Functions.Cli.ExtensionBundle @@ -43,11 +42,5 @@ public static ExtensionBundleContentProvider GetExtensionBundleContentProvider() { return new ExtensionBundleContentProvider(GetExtensionBundleManager(), NullLogger.Instance); } - - public static string GetDownloadPath(string bundleId) - { - string rootDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? Path.Combine(Constants.OSXRootPath, Constants.OSXCoreToolsTempDirectoryName) : Path.GetTempPath(); - return Path.Combine(rootDirectoryPath, "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId); - } } } From 55f206c9eb26cea0a801090284293d2334765841 Mon Sep 17 00:00:00 2001 From: Hanzhang Zeng <48038149+Hazhzeng@users.noreply.github.com> Date: Mon, 17 Aug 2020 16:07:07 -0700 Subject: [PATCH 081/127] Warn users to stop using azure-functions-worker in requirements.txt (v2) (#2116) * Warn user when using azure-functions-worker in requirements.txt * Fix regex group in .net 2.2 --- .../AzureActions/PublishFunctionAppAction.cs | 6 + .../Azure.Functions.Cli.csproj | 3 + src/Azure.Functions.Cli/Common/Constants.cs | 1 - .../Common/PythonPackage.cs | 21 +++ .../Common/RequirementsTxtParser.cs | 59 ++++++++ .../Helpers/PythonHelpers.cs | 20 ++- .../StaticResources/StaticResources.cs | 2 + .../StaticResources/requirements.txt | 3 + .../E2E/InitTests.cs | 28 ++++ .../PublishHelperTests.cs | 2 +- .../RequirementsTxtParserTests.cs | 143 ++++++++++++++++++ 11 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 src/Azure.Functions.Cli/Common/PythonPackage.cs create mode 100644 src/Azure.Functions.Cli/Common/RequirementsTxtParser.cs create mode 100644 src/Azure.Functions.Cli/StaticResources/requirements.txt create mode 100644 test/Azure.Functions.Cli.Tests/RequirementsTxtParserTests.cs diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index c510ed9cb..04f120ce7 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -271,6 +271,12 @@ private async Task> ValidateFunctionAppPublish(Site } } + // Check if azure-functions-worker exists in requirements.txt for Python function app + if (workerRuntime == WorkerRuntime.python) + { + await PythonHelpers.WarnIfAzureFunctionsWorkerInRequirementsTxt(); + } + return result; } diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 7e7145281..138451464 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -90,6 +90,9 @@ $(AssemblyName).requirements.psd1 + + $(AssemblyName).requirements.txt + $(AssemblyName).dockerignore diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 452934e6d..647a8b843 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -33,7 +33,6 @@ internal static class Constants public const string PythonDockerImageVersionSetting = "FUNCTIONS_PYTHON_DOCKER_IMAGE"; public const string PythonDockerImageSkipPull = "FUNCTIONS_PYTHON_DOCKER_SKIP_PULL"; public const string PythonDockerRunCommand = "FUNCTIONS_PYTHON_DOCKER_RUN_COMMAND"; - public const string PythonFunctionsLibrary = "azure-functions"; public const string FunctionsCoreToolsEnvironment = "FUNCTIONS_CORETOOLS_ENVIRONMENT"; public const string EnablePersistenceChannelDebugSetting = "FUNCTIONS_CORE_TOOLS_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT"; public const string TelemetryOptOutVariable = "FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT"; diff --git a/src/Azure.Functions.Cli/Common/PythonPackage.cs b/src/Azure.Functions.Cli/Common/PythonPackage.cs new file mode 100644 index 000000000..e1178e9a2 --- /dev/null +++ b/src/Azure.Functions.Cli/Common/PythonPackage.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Functions.Cli.Common +{ + public class PythonPackage + { + // azure-functions-worker + public string Name { get; set; } + + // >=1.0.0,<1.0.3 + public string Specification { get; set; } + + // python_version < '2.8' or python_version == '2.7' + public string EnvironmentMarkers { get; set; } + + // @ file:///somewhere + public string DirectReference { get; set; } + } +} diff --git a/src/Azure.Functions.Cli/Common/RequirementsTxtParser.cs b/src/Azure.Functions.Cli/Common/RequirementsTxtParser.cs new file mode 100644 index 000000000..285c6cb21 --- /dev/null +++ b/src/Azure.Functions.Cli/Common/RequirementsTxtParser.cs @@ -0,0 +1,59 @@ +using Azure.Functions.Cli.Common; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Collections.Concurrent; +using System.IO; + +namespace Azure.Functions.Cli.Common +{ + // Requirements.txt PEP https://www.python.org/dev/peps/pep-0440/ + public static class RequirementsTxtParser + { + public static async Task> ParseRequirementsTxtFile(string functionAppRoot) + { + // Check if requirements.txt exist + string requirementsTxtPath = Path.Join(functionAppRoot, Constants.RequirementsTxt); + if (!FileSystemHelpers.FileExists(requirementsTxtPath)) + { + return new List(); + } + + // Parse requirements.txt line by line + string fileContent = await FileSystemHelpers.ReadAllTextFromFileAsync(requirementsTxtPath); + return ParseRequirementsTxtContent(fileContent); + } + + public static List ParseRequirementsTxtContent(string fileContent) + { + string pattern = @"^(?(\w|\-|_|\.)+\s*)((?(===|==|<=|>=|!=|~=|>|<)[(\d|\w\d)\.]*[^;@])?)((;(?[^@]+))?)((@(?[^$]+))?)$"; + Regex rx = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + var packages = new ConcurrentBag(); + + fileContent.Split('\r', '\n').Where(l => !string.IsNullOrWhiteSpace(l)).AsParallel().ForAll(line => { + Match match = rx.Match(line); + + if (match.Success) + { + GroupCollection groups = match.Groups; + + Group packageName = groups["name"]; + Group packageSpec = groups["spec"]; + Group packageEnvMarker = groups["envmarker"]; + Group packageDirectRef = groups["directref"]; + + packages.Add(new PythonPackage() + { + Name = packageName.Success ? packageName.Value.ToLower().Replace('_', '-').Replace('.', '-').Trim() : string.Empty, + Specification = packageSpec.Success ? packageSpec.Value.Trim() : string.Empty, + EnvironmentMarkers = packageEnvMarker.Success ? packageEnvMarker.Value.Trim() : string.Empty, + DirectReference = packageDirectRef.Success ? packageDirectRef.Value.Trim() : string.Empty + }); + } + }); + + return packages.ToList(); + } + } +} diff --git a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs index a98ca6153..788bfdce4 100644 --- a/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/PythonHelpers.cs @@ -24,10 +24,22 @@ public static async Task SetupPythonProject() var pyVersion = await GetEnvironmentPythonVersion(); AssertPythonVersion(pyVersion, errorIfNoVersion: false); - CreateRequirements(); + await CreateRequirements(); await EnsureVirtualEnvrionmentIgnored(); } + public static async Task WarnIfAzureFunctionsWorkerInRequirementsTxt() + { + if (FileSystemHelpers.FileExists(Path.Join(Environment.CurrentDirectory, Constants.RequirementsTxt))) { + List packages = await RequirementsTxtParser.ParseRequirementsTxtFile(Environment.CurrentDirectory); + PythonPackage workerPackage = packages.FirstOrDefault(p => p.Name == "azure-functions-worker"); + if (workerPackage != null) + { + ColoredConsole.WriteLine(WarningColor($"Please remove '{workerPackage.Name}{workerPackage.Specification}' from requirements.txt as it may conflict with the Azure Functions platform.")); + } + } + } + public static async Task EnsureVirtualEnvrionmentIgnored() { if (InVirtualEnvironment) @@ -64,11 +76,13 @@ public static async Task EnsureVirtualEnvrionmentIgnored() } } - private static void CreateRequirements() + private async static Task CreateRequirements() { if (!FileSystemHelpers.FileExists(Constants.RequirementsTxt)) { - FileSystemHelpers.WriteAllTextToFile(Constants.RequirementsTxt, Constants.PythonFunctionsLibrary); + ColoredConsole.WriteLine($"Writing {Constants.RequirementsTxt}"); + string requirementsTxtContent = await StaticResources.PythonRequirementsTxt; + await FileSystemHelpers.WriteAllTextToFileAsync(Constants.RequirementsTxt, requirementsTxtContent); } else { diff --git a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs index 27ff92961..c359f519c 100644 --- a/src/Azure.Functions.Cli/StaticResources/StaticResources.cs +++ b/src/Azure.Functions.Cli/StaticResources/StaticResources.cs @@ -71,6 +71,8 @@ private static async Task GetValue(string name) public static Task PowerShellRequirementsPsd1 => GetValue("requirements.psd1"); + public static Task PythonRequirementsTxt => GetValue("requirements.txt"); + public static Task PrintFunctionJson => GetValue("print-functions.sh"); diff --git a/src/Azure.Functions.Cli/StaticResources/requirements.txt b/src/Azure.Functions.Cli/StaticResources/requirements.txt new file mode 100644 index 000000000..edaa0de4a --- /dev/null +++ b/src/Azure.Functions.Cli/StaticResources/requirements.txt @@ -0,0 +1,3 @@ +# Do not include azure-functions-worker as it may conflict with the Azure Functions platform + +azure-functions \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index 863700206..0ac2820c0 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -508,5 +508,33 @@ public Task init_python_app_twice() } }, _output); } + + [Fact] + public Task init_python_app_generates_requirements_txt() + { + return CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime python" + }, + OutputContains = new[] + { + "Writing requirements.txt" + }, + CheckFiles = new FileResult[] + { + new FileResult + { + Name = "requirements.txt", + ContentContains = new [] + { + "# Do not include azure-functions-worker as it may conflict with the Azure Functions platform", + "azure-functions" + } + } + }, + }, _output); + } } } diff --git a/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs b/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs index 44a16e0fe..bdbe706a1 100644 --- a/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/PublishHelperTests.cs @@ -16,7 +16,7 @@ public class PublishHelperTests [InlineData("", false)] public void IsLinuxFxVersionUsingCustomImageTest(string linuxFxVersion, bool expected) { - Assert.Equal(expected, PublishHelper.IsLinuxFxVersionUsingCustomImage(linuxFxVersion)); + Assert.Equal(expected, PublishHelper.IsLinuxFxVersionUsingCustomImage(linuxFxVersion)); } [Theory] diff --git a/test/Azure.Functions.Cli.Tests/RequirementsTxtParserTests.cs b/test/Azure.Functions.Cli.Tests/RequirementsTxtParserTests.cs new file mode 100644 index 000000000..9705991a5 --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/RequirementsTxtParserTests.cs @@ -0,0 +1,143 @@ +using Azure.Functions.Cli.Common; +using System.Collections.Generic; +using Xunit; + +namespace Azure.Functions.Cli.Tests +{ + public class RequirementsTxtParserTests + { + [Fact] + public void ShouldReturnEmptyListOnEmptyContent() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent(""); + Assert.Empty(result); + } + + [Fact] + public void ShouldIgnoreComment() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "# This is a comment"); + Assert.Empty(result); + } + + [Fact] + public void ShouldIgnoreExtraIndex() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "--extra-index = https://extra.index.org"); + Assert.Empty(result); + } + + [Fact] + public void ShouldIgnoreInvalidPackage() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy<>3.5.2"); + Assert.Empty(result); + } + + [Fact] + public void ShouldReturnPackageWithNoSpecification() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy"); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal("numpy", numpyPackage.Name); + Assert.Empty(numpyPackage.Specification); + Assert.Empty(numpyPackage.EnvironmentMarkers); + Assert.Empty(numpyPackage.DirectReference); + } + + [Theory] + [InlineData("numpy===1.19.1", "numpy", "===1.19.1")] + [InlineData("numpy~=1.19.1", "numpy", "~=1.19.1")] + [InlineData("numpy!=1.19.1", "numpy", "!=1.19.1")] + [InlineData("numpy>=1.19.1", "numpy", ">=1.19.1")] + [InlineData("numpy<=1.19.1", "numpy", "<=1.19.1")] + [InlineData("numpy>1.19.1", "numpy", ">1.19.1")] + [InlineData("numpy<1.19.1", "numpy", "<1.19.1")] + public void ShouldReturnPackageWithSpecification(string content, string expectedName, string expectedSpec) + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + content); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal(expectedName, numpyPackage.Name); + Assert.Equal(expectedSpec, numpyPackage.Specification); + Assert.Empty(numpyPackage.EnvironmentMarkers); + Assert.Empty(numpyPackage.DirectReference); + } + + [Fact] + public void ShouldReturnPackageWithEnvironmentMarker() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy; python_version == '2.7'"); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal("numpy", numpyPackage.Name); + Assert.Empty(numpyPackage.Specification); + Assert.Equal("python_version == '2.7'", numpyPackage.EnvironmentMarkers); + Assert.Empty(numpyPackage.DirectReference); + } + + [Fact] + public void ShouldReturnPackageWithSpecAndEnvironmentMarker() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy>=3.5.1; python_version == '2.7'"); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal("numpy", numpyPackage.Name); + Assert.Equal(">=3.5.1", numpyPackage.Specification); + Assert.Equal("python_version == '2.7'", numpyPackage.EnvironmentMarkers); + Assert.Empty(numpyPackage.DirectReference); + } + + [Fact] + public void ShouldReturnPackageDirectReference() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy @ https://direct.reference/numpy"); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal("numpy", numpyPackage.Name); + Assert.Empty(numpyPackage.Specification); + Assert.Empty(numpyPackage.EnvironmentMarkers); + Assert.Equal("https://direct.reference/numpy", numpyPackage.DirectReference); + } + + [Fact] + public void PackageSpecWithDirectReference() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "numpy==3.5.2 @ https://direct.reference/numpy"); + Assert.Single(result); + + PythonPackage numpyPackage = result[0]; + Assert.Equal("numpy", numpyPackage.Name); + Assert.Equal("==3.5.2", numpyPackage.Specification); + Assert.Empty(numpyPackage.EnvironmentMarkers); + Assert.Equal("https://direct.reference/numpy", numpyPackage.DirectReference); + } + + [Fact] + public void PackageNameShouldBeFormalizedIntoDashes() + { + List result = RequirementsTxtParser.ParseRequirementsTxtContent("" + + "Azure-Functions-Worker\r\n" + + "azure.functions.worker\n" + + "azure_functions_worker"); + + Assert.Equal(3, result.Count); + Assert.All(result, r => Assert.Equal("azure-functions-worker", r.Name)); + } + } +} From c6211c16d31ad757442c3854cf09e205c3e8b591 Mon Sep 17 00:00:00 2001 From: Naren Soni Date: Fri, 21 Aug 2020 22:03:28 +0000 Subject: [PATCH 082/127] changing the default bundle download path (#2160) --- .../Actions/HostActions/StartHostAction.cs | 2 +- src/Azure.Functions.Cli/Common/Constants.cs | 1 + .../ExtensionBundleConfigurationBuilder.cs | 8 +------- .../ExtensionBundle/ExtensionBundleHelper.cs | 9 ++++++--- .../ExtensionBundleHelperTests.cs | 20 +++++++++++++++++++ 5 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 test/Azure.Functions.Cli.Tests/ExtensionBundleHelperTests.cs diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index d747aad36..da12fc545 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -553,7 +553,7 @@ private void SetBundlesEnvironmentVariables() var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; if (!string.IsNullOrEmpty(bundleId)) { - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)); + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", ExtensionBundleHelper.GetBundleDownloadPath(bundleId)); Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); } } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 647a8b843..62e3d39c6 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -47,6 +47,7 @@ internal static class Constants public const string AzureFunctionsEnvorinmentEnvironmentVariable = "AZURE_FUNCTIONS_ENVIRONMENT"; public const string ExtensionBundleConfigPropertyName = "extensionBundle"; public const string AspNetCoreEnvironmentEnvironmentVariable = "ASPNETCORE_ENVIRONMENT"; + public const string UserCoreToolsDirectory = ".azure-functions-core-tools"; public const string ManagedDependencyConfigPropertyName = "managedDependency"; public const string CustomHandlerPropertyName = "customHandler"; public const string PowerShellWorkerDefaultVersion = "~6"; diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs index 6204f6391..5b15d6160 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleConfigurationBuilder.cs @@ -1,12 +1,6 @@ using System.Collections.Generic; -using System.IO; -using Azure.Functions.Cli.Common; using Microsoft.Azure.WebJobs.Script; -using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Azure.Functions.Cli.ExtensionBundle { @@ -26,7 +20,7 @@ public void Configure(IConfigurationBuilder builder) { builder.AddInMemoryCollection(new Dictionary { - { "AzureFunctionsJobHost:extensionBundle:downloadPath", Path.Combine(Path.GetTempPath(), "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId)}, + { "AzureFunctionsJobHost:extensionBundle:downloadPath", ExtensionBundleHelper.GetBundleDownloadPath(bundleId) }, { "AzureFunctionsJobHost:extensionBundle:ensureLatest", "true"} }); } diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs index e4bbdb2fb..c1c8962f3 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs @@ -6,13 +6,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using System; -using System.Collections.Generic; using System.IO; -using System.Text; namespace Azure.Functions.Cli.ExtensionBundle { - class ExtensionBundleHelper + internal class ExtensionBundleHelper { public static ExtensionBundleOptions GetExtensionBundleOptions(ScriptApplicationHostOptions hostOptions = null) { @@ -42,5 +40,10 @@ public static ExtensionBundleContentProvider GetExtensionBundleContentProvider() { return new ExtensionBundleContentProvider(GetExtensionBundleManager(), NullLogger.Instance); } + + public static string GetBundleDownloadPath(string bundleId) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Constants.UserCoreToolsDirectory, "Functions", ScriptConstants.ExtensionBundleDirectory, bundleId); + } } } diff --git a/test/Azure.Functions.Cli.Tests/ExtensionBundleHelperTests.cs b/test/Azure.Functions.Cli.Tests/ExtensionBundleHelperTests.cs new file mode 100644 index 000000000..5205c9a4d --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/ExtensionBundleHelperTests.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.ExtensionBundle; +using Azure.Functions.Cli.Helpers; +using Xunit; + +namespace Azure.Functions.Cli.Tests +{ + public class ExtensionBundleHelperTests + { + [Fact] + public void VerifyGetBundleDownloadPathReturnCorrectPath() + { + var downloadPath = ExtensionBundleHelper.GetBundleDownloadPath("BundleId"); + var expectedPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azure-functions-core-tools", "Functions", "ExtensionBundles", "BundleId"); + Assert.Equal(expectedPath, downloadPath); + } + } +} \ No newline at end of file From 35f509429827a13a1c0407cad2853092b49faf6c Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 25 Aug 2020 13:33:17 -0700 Subject: [PATCH 083/127] Fix powershell test (#2166) --- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 755fb7133..3d0e2ae8d 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -306,7 +306,7 @@ await CliTester.Run(new RunConfiguration { Commands = new[] { - "init . --worker-runtime powershell", + "init . --worker-runtime powershell --managed-dependencies false", "new --template \"Http trigger\" --name HttpTrigger", "start" }, @@ -318,8 +318,8 @@ await CliTester.Run(new RunConfiguration (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); var response = await client.GetAsync("/api/HttpTrigger?name=Test"); var result = await response.Content.ReadAsStringAsync(); - p.Kill(); result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); + p.Kill(); } }, }, _output); From 1654570e08c5512c18e7e7c321c9b2897d6f908d Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 25 Aug 2020 16:19:20 -0700 Subject: [PATCH 084/127] Update Functions Host references (#2165) Moving WebJobs.Script.WebHost to 2.0.14248 and Java Worker to 1.7.3-SNAPSHOT --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 138451464..6f5da05ef 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -118,10 +118,10 @@ - + - + From da00cec4b95000670e9e27c97774bccec0e37848 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Tue, 25 Aug 2020 19:09:20 -0700 Subject: [PATCH 085/127] [V2]Less verbose logs on func start (#2171) --- .../Actions/HostActions/StartHostAction.cs | 262 ++++-------------- .../Actions/HostActions/Startup.cs | 140 ++++++++++ .../Common/DisplayFunctionsInfoUtilities.cs | 90 ++++++ src/Azure.Functions.Cli/Common/Utilities.cs | 108 +++++++- .../Diagnostics/ColoredConsoleLogger.cs | 48 +++- .../ColoredConsoleLoggerProvider.cs | 8 +- .../Diagnostics/LoggingBuilder.cs | 9 +- .../Diagnostics/LoggingFilterHelper.cs | 101 +++++++ .../ExtensionBundle/ExtensionBundleHelper.cs | 7 +- .../ColoredConsoleLoggerTests.cs | 47 ++++ .../E2E/StartTests.cs | 4 +- .../LoggingFilterHelperTests.cs | 91 ++++++ .../UtilitiesTests.cs | 81 ++++++ 13 files changed, 752 insertions(+), 244 deletions(-) create mode 100644 src/Azure.Functions.Cli/Actions/HostActions/Startup.cs create mode 100644 src/Azure.Functions.Cli/Common/DisplayFunctionsInfoUtilities.cs create mode 100644 src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs create mode 100644 test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs create mode 100644 test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs create mode 100644 test/Azure.Functions.Cli.Tests/UtilitiesTests.cs diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index da12fc545..c93fc5575 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -6,27 +6,19 @@ using System.Net; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Azure.Functions.Cli.Actions.HostActions.WebHost.Security; using Azure.Functions.Cli.Common; -using Azure.Functions.Cli.Diagnostics; -using Azure.Functions.Cli.ExtensionBundle; using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; using Azure.Functions.Cli.NativeMethods; using Colors.Net; using Fclp; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.Configuration; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.WebHost; -using Microsoft.Azure.WebJobs.Script.WebHost.Authentication; -using Microsoft.Azure.WebJobs.Script.WebHost.Controllers; -using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection; -using Microsoft.Azure.WebJobs.Script.WebHost.Security; -using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -45,6 +37,7 @@ internal class StartHostAction : BaseAction private const int DefaultPort = 7071; private const int DefaultTimeout = 20; private readonly ISecretsManager _secretsManager; + private IConfigurationRoot _hostJsonConfig; public int Port { get; set; } @@ -65,6 +58,9 @@ internal class StartHostAction : BaseAction public bool NoBuild { get; set; } public bool EnableAuth { get; set; } + + public bool? VerboseLogging { get; set; } + public List EnabledFunctions { get; private set; } public bool SkipAzureStorageCheck { get; private set; } @@ -76,7 +72,6 @@ public StartHostAction(ISecretsManager secretsManager) public override ICommandLineParserResult ParseArgs(string[] args) { var hostSettings = _secretsManager.GetHostStartSettings(); - Parser .Setup('p', "port") .WithDescription($"Local port to listen on. Default: {DefaultPort}") @@ -145,17 +140,33 @@ public override ICommandLineParserResult ParseArgs(string[] args) .SetDefault(false) .Callback(skip => SkipAzureStorageCheck = skip); - return base.ParseArgs(args); + Parser + .Setup("verbose") + .WithDescription("When false, hides system logs other than warnings and errors.") + .SetDefault(false) + .Callback(v => VerboseLogging = v); + + var parserResult = base.ParseArgs(args); + bool verboseLoggingArgExists = parserResult.UnMatchedOptions.Any(o => o.LongName.Equals("verbose", StringComparison.OrdinalIgnoreCase)); + // Input args do not contain --verbose flag + if (!VerboseLogging.Value && verboseLoggingArgExists) + { + VerboseLogging = null; + } + return parserResult; } private async Task BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate) { IDictionary settings = await GetConfigurationSettings(hostOptions.ScriptPath, baseAddress); + settings.AddRange(LanguageWorkerHelper.GetWorkerConfiguration(LanguageWorkerSetting)); UpdateEnvironmentVariables(settings); - var defaultBuilder = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder(Array.Empty()); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging); + var defaultBuilder = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder(Array.Empty()); + if (UseHttps) { defaultBuilder @@ -167,7 +178,6 @@ private async Task BuildWebHost(ScriptApplicationHostOptions hostOptio }); }); } - return defaultBuilder .UseSetting(WebHostDefaults.ApplicationKey, typeof(Startup).Assembly.GetName().Name) .UseUrls(listenAddress.ToString()) @@ -178,10 +188,9 @@ private async Task BuildWebHost(ScriptApplicationHostOptions hostOptio .ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); - loggingBuilder.AddDefaultWebJobsFilters(); - loggingBuilder.AddProvider(new ColoredConsoleLoggerProvider((cat, level) => level >= LogLevel.Information)); + loggingFilterHelper.AddConsoleLoggingProvider(loggingBuilder); }) - .ConfigureServices((context, services) => services.AddSingleton(new Startup(context, hostOptions, CorsOrigins, CorsCredentials, EnableAuth))) + .ConfigureServices((context, services) => services.AddSingleton(new Startup(context, hostOptions, CorsOrigins, CorsCredentials, EnableAuth, loggingFilterHelper))) .Build(); } @@ -247,15 +256,19 @@ private void UpdateEnvironmentVariables(IDictionary secrets) public override async Task RunAsync() { await PreRunConditions(); - Utilities.PrintLogo(); + if (VerboseLogging.HasValue && VerboseLogging.Value) + { + Utilities.PrintLogo(); + } Utilities.PrintVersion(); - ValidateHostJsonConfiguration(); - var settings = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); + ScriptApplicationHostOptions hostOptions = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); + + ValidateAndBuildHostJsonConfigurationIfFileExists(hostOptions); (var listenUri, var baseUri, var certificate) = await Setup(); - IWebHost host = await BuildWebHost(settings, listenUri, baseUri, certificate); + IWebHost host = await BuildWebHost(hostOptions, listenUri, baseUri, certificate); var runTask = host.RunAsync(); var hostService = host.Services.GetRequiredService(); @@ -264,22 +277,29 @@ public override async Task RunAsync() var scriptHost = hostService.Services.GetRequiredService(); var httpOptions = hostService.Services.GetRequiredService>(); - DisplayHttpFunctionsInfo(scriptHost, httpOptions.Value, baseUri); - DisplayDisabledFunctions(scriptHost); - + if (scriptHost != null && scriptHost.Functions.Any()) + { + DisplayFunctionsInfoUtilities.DisplayFunctionsInfo(scriptHost.Functions, httpOptions.Value, baseUri); + } + if (VerboseLogging == null || !VerboseLogging.Value) + { + ColoredConsole.WriteLine(AdditionalInfoColor("For detailed output, run func with --verbose flag.")); + } await runTask; } - private void ValidateHostJsonConfiguration() + private void ValidateAndBuildHostJsonConfigurationIfFileExists(ScriptApplicationHostOptions hostOptions) { 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}"); + throw new CliException($"Host.json file in missing. Please make sure host.json file is present at {Environment.CurrentDirectory}"); } - if (IsPreCompiledApp && BundleConfigurationExists(hostJsonPath)) + //BuildHostJsonConfigutation only if host.json file exists. + _hostJsonConfig = Utilities.BuildHostJsonConfigutation(hostOptions); + if (IsPreCompiledApp && Utilities.JobHostConfigSectionExists(_hostJsonConfig, ConfigurationSectionNames.ExtensionBundle)) { 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")}"); } @@ -317,13 +337,7 @@ private async Task PreRunConditions() throw new CliException($"Port {Port} is unavailable. Close the process using that port, or specify another port using --port [-p]."); } } - - private bool BundleConfigurationExists(string hostJsonPath) - { - var hostJson = FileSystemHelpers.ReadAllTextFromFile(hostJsonPath); - return hostJson.Contains(Constants.ExtensionBundleConfigPropertyName, StringComparison.OrdinalIgnoreCase); - } - + private bool IsPreCompiledFunctionApp() { bool isPrecompiled = false; @@ -344,68 +358,7 @@ private bool IsPreCompiledFunctionApp() } return isPrecompiled; } - - private void DisplayDisabledFunctions(IScriptJobHost scriptHost) - { - if (scriptHost != null) - { - foreach (var function in scriptHost.Functions.Where(f => f.Metadata.IsDisabled())) - { - ColoredConsole.WriteLine(WarningColor($"Function {function.Name} is disabled.")); - } - } - } - - private void DisplayHttpFunctionsInfo(IScriptJobHost scriptHost, HttpOptions httpOptions, Uri baseUri) - { - if (scriptHost != null) - { - var httpFunctions = scriptHost.Functions.Where(f => f.Metadata.IsHttpFunction() && !f.Metadata.IsDisabled()); - if (httpFunctions.Any()) - { - ColoredConsole - .WriteLine() - .WriteLine(DarkYellow("Http Functions:")) - .WriteLine(); - } - - foreach (var function in httpFunctions) - { - var binding = function.Metadata.Bindings.FirstOrDefault(b => b.Type != null && b.Type.Equals("httpTrigger", StringComparison.OrdinalIgnoreCase)); - var httpRoute = binding?.Raw?.GetValue("route", StringComparison.OrdinalIgnoreCase)?.ToString(); - httpRoute = httpRoute ?? function.Name; - - string[] methods = null; - var methodsRaw = binding?.Raw?.GetValue("methods", StringComparison.OrdinalIgnoreCase)?.ToString(); - if (string.IsNullOrEmpty(methodsRaw) == false) - { - methods = methodsRaw.Split(','); - } - - string hostRoutePrefix = ""; - if (!function.Metadata.IsProxy()) - { - hostRoutePrefix = httpOptions.RoutePrefix ?? "api/"; - hostRoutePrefix = string.IsNullOrEmpty(hostRoutePrefix) || hostRoutePrefix.EndsWith("/") - ? hostRoutePrefix - : $"{hostRoutePrefix}/"; - } - - var functionMethods = methods != null ? $"{CleanAndFormatHttpMethods(string.Join(",", methods))}" : null; - var url = $"{baseUri.ToString().Replace("0.0.0.0", "localhost")}{hostRoutePrefix}{httpRoute}"; - ColoredConsole - .WriteLine($"\t{HttpFunctionNameColor($"{function.Name}:")} {HttpFunctionUrlColor(functionMethods)} {HttpFunctionUrlColor(url)}") - .WriteLine(); - } - } - } - - private string CleanAndFormatHttpMethods(string httpMethods) - { - return httpMethods.Replace(Environment.NewLine, string.Empty).Replace(" ", string.Empty) - .Replace("\"", string.Empty).ToUpperInvariant(); - } - + internal static async Task CheckNonOptionalSettings(IEnumerable> secrets, string scriptPath, bool skipAzureWebJobsStorageCheck = false) { try @@ -480,122 +433,5 @@ internal static async Task CheckNonOptionalSettings(IEnumerable>(AuthLevelAuthenticationDefaults.AuthenticationScheme, configureOptions: _ => { }) - .AddScheme>(ArmAuthenticationDefaults.AuthenticationScheme, _ => { }); - } - - services.AddWebJobsScriptHostAuthorization(); - - services.AddMvc() - .AddApplicationPart(typeof(HostController).Assembly); - - // workaround for https://github.com/Azure/azure-functions-core-tools/issues/2097 - SetBundlesEnvironmentVariables(); - - services.AddWebJobsScriptHost(_builderContext.Configuration); - - services.Configure(o => - { - o.ScriptPath = _hostOptions.ScriptPath; - o.LogPath = _hostOptions.LogPath; - o.IsSelfHost = _hostOptions.IsSelfHost; - o.SecretsPath = _hostOptions.SecretsPath; - }); - - services.AddSingleton>(_ => new ExtensionBundleConfigurationBuilder(_hostOptions)); - services.AddSingleton, DisableConsoleConfigurationBuilder>(); - services.AddSingleton, LoggingBuilder>(); - - services.AddSingleton(); - - return services.BuildServiceProvider(); - } - - private void SetBundlesEnvironmentVariables() - { - var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; - if (!string.IsNullOrEmpty(bundleId)) - { - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", ExtensionBundleHelper.GetBundleDownloadPath(bundleId)); - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); - } - } - - public void Configure(IApplicationBuilder app) - { - if (_corsOrigins != null) - { - app.UseCors(builder => - { - var origins = builder.WithOrigins(_corsOrigins) - .AllowAnyHeader() - .AllowAnyMethod(); - if (_corsCredentials) - { - origins.AllowCredentials(); - } - }); - } - - IApplicationLifetime applicationLifetime = app.ApplicationServices - .GetRequiredService(); - - app.UseWebJobsScriptHost(applicationLifetime); - } - - private class ThrowingDependencyValidator : DependencyValidator - { - public override void Validate(IServiceCollection services) - { - try - { - base.Validate(services); - } - catch (InvalidHostServicesException ex) - { - // Rethrow this as an InvalidOperationException to bypass the handling - // in the host. This will stop invalid services in the CLI only. - throw new InvalidOperationException("Invalid host services.", ex); - } - } - } - } } -} +} \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Actions/HostActions/Startup.cs b/src/Azure.Functions.Cli/Actions/HostActions/Startup.cs new file mode 100644 index 000000000..7531e42c3 --- /dev/null +++ b/src/Azure.Functions.Cli/Actions/HostActions/Startup.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using Azure.Functions.Cli.Actions.HostActions.WebHost.Security; +using Azure.Functions.Cli.Diagnostics; +using Azure.Functions.Cli.ExtensionBundle; +using Azure.Functions.Cli.Helpers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.WebHost; +using Microsoft.Azure.WebJobs.Script.WebHost.Authentication; +using Microsoft.Azure.WebJobs.Script.WebHost.Controllers; +using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection; +using Microsoft.Azure.WebJobs.Script.WebHost.Security; +using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Azure.Functions.Cli.Actions.HostActions +{ + public class Startup : IStartup + { + private readonly WebHostBuilderContext _builderContext; + private readonly ScriptApplicationHostOptions _hostOptions; + private readonly string[] _corsOrigins; + private readonly bool _corsCredentials; + private readonly bool _enableAuth; + private readonly LoggingFilterHelper _loggingFilterHelper; + + public Startup(WebHostBuilderContext builderContext, ScriptApplicationHostOptions hostOptions, string corsOrigins, bool corsCredentials, bool enableAuth, LoggingFilterHelper loggingFilterHelper) + { + _builderContext = builderContext; + _hostOptions = hostOptions; + _enableAuth = enableAuth; + _loggingFilterHelper = loggingFilterHelper; + + if (!string.IsNullOrEmpty(corsOrigins)) + { + _corsOrigins = corsOrigins.Split(',', StringSplitOptions.RemoveEmptyEntries); + _corsCredentials = corsCredentials; + } + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + if (_corsOrigins != null) + { + services.AddCors(); + } + + if (_enableAuth) + { + services.AddWebJobsScriptHostAuthentication(); + } + else + { + services.AddAuthentication() + .AddScriptJwtBearer() + .AddScheme>(AuthLevelAuthenticationDefaults.AuthenticationScheme, configureOptions: _ => { }) + .AddScheme>(ArmAuthenticationDefaults.AuthenticationScheme, _ => { }); + } + + services.AddWebJobsScriptHostAuthorization(); + + services.AddMvc() + .AddApplicationPart(typeof(HostController).Assembly); + + // workaround for https://github.com/Azure/azure-functions-core-tools/issues/2097 + SetBundlesEnvironmentVariables(); + + services.AddWebJobsScriptHost(_builderContext.Configuration); + + services.Configure(o => + { + o.ScriptPath = _hostOptions.ScriptPath; + o.LogPath = _hostOptions.LogPath; + o.IsSelfHost = _hostOptions.IsSelfHost; + o.SecretsPath = _hostOptions.SecretsPath; + }); + + services.AddSingleton>(_ => new ExtensionBundleConfigurationBuilder(_hostOptions)); + services.AddSingleton, DisableConsoleConfigurationBuilder>(); + services.AddSingleton>(_ => new LoggingBuilder(_loggingFilterHelper)); + + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private void SetBundlesEnvironmentVariables() + { + var bundleId = ExtensionBundleHelper.GetExtensionBundleOptions(_hostOptions).Id; + if (!string.IsNullOrEmpty(bundleId)) + { + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__downloadPath", ExtensionBundleHelper.GetBundleDownloadPath(bundleId)); + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__extensionBundle__ensureLatest", "true"); + } + } + + public void Configure(IApplicationBuilder app) + { + if (_corsOrigins != null) + { + app.UseCors(builder => + { + var origins = builder.WithOrigins(_corsOrigins) + .AllowAnyHeader() + .AllowAnyMethod(); + if (_corsCredentials) + { + origins.AllowCredentials(); + } + }); + } + + IApplicationLifetime applicationLifetime = app.ApplicationServices + .GetRequiredService(); + + app.UseWebJobsScriptHost(applicationLifetime); + } + + private class ThrowingDependencyValidator : DependencyValidator + { + public override void Validate(IServiceCollection services) + { + try + { + base.Validate(services); + } + catch (InvalidHostServicesException ex) + { + // Rethrow this as an InvalidOperationException to bypass the handling + // in the host. This will stop invalid services in the CLI only. + throw new InvalidOperationException("Invalid host services.", ex); + } + } + } + } +} diff --git a/src/Azure.Functions.Cli/Common/DisplayFunctionsInfoUtilities.cs b/src/Azure.Functions.Cli/Common/DisplayFunctionsInfoUtilities.cs new file mode 100644 index 000000000..e9dfaa930 --- /dev/null +++ b/src/Azure.Functions.Cli/Common/DisplayFunctionsInfoUtilities.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Azure.Functions.Cli.Extensions; +using Colors.Net; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Script; +using static Colors.Net.StringStaticMethods; +using static Azure.Functions.Cli.Common.OutputTheme; +using Microsoft.Azure.WebJobs.Script.Description; +using System.Collections.Generic; + +namespace Azure.Functions.Cli +{ + public static class DisplayFunctionsInfoUtilities + { + internal static void DisplayFunctionsInfo(ICollection functions, HttpOptions httpOptions, Uri baseUri) + { + var allValidFunctions = functions.Where(f => !f.Metadata.IsDisabled()); + if (allValidFunctions.Any()) + { + ColoredConsole + .WriteLine() + .WriteLine(DarkYellow("Functions:")) + .WriteLine(); + } + DisplayHttpFunctions(functions, httpOptions, baseUri); + DisplayNonHttpFunctionsInfo(functions); + DisplayDisabledFunctions(functions); + } + + private static void DisplayHttpFunctions(ICollection functions, HttpOptions httpOptions, Uri baseUri) + { + var httpFunctions = functions.Where(f => f.Metadata.IsHttpFunction() && !f.Metadata.IsDisabled()); + foreach (var function in httpFunctions) + { + var binding = function.Metadata.Bindings.FirstOrDefault(b => b.Type != null && b.Type.Equals("httpTrigger", StringComparison.OrdinalIgnoreCase)); + var httpRoute = binding?.Raw?.GetValue("route", StringComparison.OrdinalIgnoreCase)?.ToString(); + httpRoute = httpRoute ?? function.Name; + + string[] methods = null; + var methodsRaw = binding?.Raw?.GetValue("methods", StringComparison.OrdinalIgnoreCase)?.ToString(); + if (string.IsNullOrEmpty(methodsRaw) == false) + { + methods = methodsRaw.Split(','); + } + + string hostRoutePrefix = ""; + if (!function.Metadata.IsProxy()) + { + hostRoutePrefix = httpOptions.RoutePrefix ?? "api/"; + hostRoutePrefix = string.IsNullOrEmpty(hostRoutePrefix) || hostRoutePrefix.EndsWith("/") + ? hostRoutePrefix + : $"{hostRoutePrefix}/"; + } + + var functionMethods = methods != null ? $"{CleanAndFormatHttpMethods(string.Join(",", methods))}" : null; + var url = $"{baseUri.ToString().Replace("0.0.0.0", "localhost")}{hostRoutePrefix}{httpRoute}"; + ColoredConsole + .WriteLine($"\t{HttpFunctionNameColor($"{function.Name}:")} {HttpFunctionUrlColor(functionMethods)} {HttpFunctionUrlColor(url)}") + .WriteLine(); + } + } + + private static string CleanAndFormatHttpMethods(string httpMethods) + { + return httpMethods.Replace(Environment.NewLine, string.Empty).Replace(" ", string.Empty) + .Replace("\"", string.Empty).ToUpperInvariant(); + } + + private static void DisplayNonHttpFunctionsInfo(ICollection functions) + { + var nonHttpFunctions = functions.Where(f => !f.Metadata.IsHttpFunction() && !f.Metadata.IsDisabled()); + foreach (var function in nonHttpFunctions) + { + var trigger = function.Metadata.Bindings.FirstOrDefault(b => b.Type != null && b.Type.EndsWith("Trigger", ignoreCase: true, null)); + ColoredConsole + .WriteLine($"\t{Yellow($"{function.Name}:")} {trigger?.Type}") + .WriteLine(); + } + } + + private static void DisplayDisabledFunctions(ICollection functions) + { + foreach (var function in functions.Where(f => f.Metadata.IsDisabled())) + { + ColoredConsole.WriteLine(WarningColor($"Function {function.Name} is disabled.")); + } + } + } +} diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index 47bdc5eb6..7dc94ebfa 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -1,8 +1,4 @@ -using Azure.Functions.Cli.Common; -using Colors.Net; -using Colors.Net.StringColorExtensions; -using Microsoft.Azure.WebJobs.Script; -using System; +using System; using System.Diagnostics; using System.IO; using System.Linq; @@ -10,11 +6,26 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Azure.Functions.Cli.Common; +using Colors.Net; +using Colors.Net.StringColorExtensions; +using Microsoft.Azure.WebJobs.Logging; +using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.Configuration; +using Microsoft.Azure.WebJobs.Script.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Azure.Functions.Cli { internal static class Utilities { + public const string LogLevelSection = "loglevel"; + public const string LogLevelDefaultSection = "Default"; + internal static void PrintLogo() { ColoredConsole.WriteLine($@" @@ -99,7 +110,7 @@ internal static string SanitizeLiteral(string unsanitized, string allowed = "", Match match = Regex.Match(sanitizedString, removeRegex); string matchString; // Keep removing the matching regex until no more match is found - while(!string.IsNullOrEmpty(matchString = match.Value)) + while (!string.IsNullOrEmpty(matchString = match.Value)) { sanitizedString = sanitizedString.Replace(matchString, new string(fillerChar, matchString.Length)); match = Regex.Match(sanitizedString, removeRegex); @@ -171,5 +182,90 @@ internal static string EnsureCoreToolsLocalData() FileSystemHelpers.EnsureDirectory(localPath); return localPath; } + + internal static LogLevel GetHostJsonDefaultLogLevel(IConfigurationRoot hostJsonConfig) + { + string defaultLogLevelKey = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Logging, LogLevelSection, LogLevelDefaultSection); + try + { + if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[defaultLogLevelKey].ToString(), true, out object outLevel)) + { + return (LogLevel)outLevel; + } + } + catch + { + } + // Default log level + return LogLevel.Information; + } + + internal static bool LogLevelExists(IConfigurationRoot hostJsonConfig, string category) + { + string categoryKey = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Logging, LogLevelSection, category); + try + { + if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[categoryKey].ToString(), true, out object outLevel)) + { + return true; + } + } + catch { } + return false; + } + + internal static bool JobHostConfigSectionExists(IConfigurationRoot hostJsonConfig, string sectioName) + { + string configSection = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, sectioName); + try + { + if (hostJsonConfig.GetSection(configSection).Exists()) + { + return true; + } + } + catch { } + return false; + } + + /// + /// For user logs, returns true if actualLevel of the log is >= default user log level - Information unless overridden in host.json + /// For system logs, returns true if actualLevel of the log is >= default system log level - Warning unless overridden in host.json + /// + /// + /// + /// + /// + /// + internal static bool DefaultLoggingFilter(string category, LogLevel actualLevel, LogLevel userLogMinLevel, LogLevel systemLogMinLevel) + { + if (LogCategories.IsFunctionUserCategory(category)) + { + return actualLevel >= userLogMinLevel; + } + return actualLevel >= systemLogMinLevel; + } + + /// + /// Returns true for user logs >= Trace level. Returns false, if log level is explicitly set to None. + /// + /// + /// + internal static bool UserLoggingFilter(LogLevel actualLevel) + { + if (actualLevel == LogLevel.None) + { + return false; + } + return actualLevel >= LogLevel.Trace; + } + + internal static IConfigurationRoot BuildHostJsonConfigutation(ScriptApplicationHostOptions hostOptions) + { + IConfigurationBuilder builder = new ConfigurationBuilder(); + builder.Add(new HostJsonFileConfigurationSource(hostOptions, SystemEnvironment.Instance, loggerFactory: NullLoggerFactory.Instance, metricsLogger: new MetricsLogger())); + var configuration = builder.Build(); + return configuration; + } } } diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs index 1edcf003d..3a71ac93b 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs @@ -5,50 +5,72 @@ using Microsoft.Extensions.Logging; using Azure.Functions.Cli.Common; using static Azure.Functions.Cli.Common.OutputTheme; +using System.Linq; namespace Azure.Functions.Cli.Diagnostics { public class ColoredConsoleLogger : ILogger { - private readonly Func _filter; private readonly bool _verboseErrors; private readonly string _category; + private readonly LoggingFilterHelper _loggingFilterHelper; + private readonly string[] allowedLogsPrefixes = new string[] { "Worker process started and initialized.", "Host lock lease acquired by instance ID" }; - public ColoredConsoleLogger(string category, Func filter = null) + public ColoredConsoleLogger(string category, LoggingFilterHelper loggingFilterHelper) { _category = category; - _filter = filter; + _loggingFilterHelper = loggingFilterHelper; _verboseErrors = StaticSettings.IsDebug; } public bool IsEnabled(LogLevel logLevel) { - if (_filter == null) - { - return true; - } - - return _filter(_category, logLevel); + return _loggingFilterHelper.IsEnabled(_category, logLevel); } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (!IsEnabled(logLevel)) + string formattedMessage = formatter(state, exception); + + if (string.IsNullOrEmpty(formattedMessage)) { return; } - string formattedMessage = formatter(state, exception); + if (DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)) + { + LogToConsole(logLevel, exception, formattedMessage); + return; + } - if (string.IsNullOrEmpty(formattedMessage)) + if (!IsEnabled(logLevel)) { return; } + LogToConsole(logLevel, exception, formattedMessage); + } + + private void LogToConsole(LogLevel logLevel, Exception exception, string formattedMessage) + { foreach (var line in GetMessageString(logLevel, formattedMessage, exception)) { - ColoredConsole.WriteLine($"[{DateTime.UtcNow}] {line}"); + var outputline = $"{line}"; + if (_loggingFilterHelper.VerboseLogging) + { + outputline = $"[{DateTime.UtcNow}] {outputline}"; + } + ColoredConsole.WriteLine($"{outputline}"); + } + } + + internal bool DoesMessageStartsWithAllowedLogsPrefix(string formattedMessage) + { + if (formattedMessage == null) + { + throw new ArgumentNullException(nameof(formattedMessage)); } + return allowedLogsPrefixes.Any(s => formattedMessage.StartsWith(s, StringComparison.OrdinalIgnoreCase)); } private IEnumerable GetMessageString(LogLevel level, string formattedMessage, Exception exception) diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs index bc967a43e..44192d6b5 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs @@ -5,16 +5,16 @@ namespace Azure.Functions.Cli.Diagnostics { public class ColoredConsoleLoggerProvider : ILoggerProvider { - private readonly Func _filter; + private readonly LoggingFilterHelper _loggingFilterHelper; - public ColoredConsoleLoggerProvider(Func filter = null) + public ColoredConsoleLoggerProvider(LoggingFilterHelper loggingFilterHelper) { - _filter = filter; + _loggingFilterHelper = loggingFilterHelper; } public ILogger CreateLogger(string categoryName) { - return new ColoredConsoleLogger(categoryName, _filter); + return new ColoredConsoleLogger(categoryName, _loggingFilterHelper); } public void Dispose() diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs index fcc36ca9a..bee841dec 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs @@ -10,9 +10,16 @@ namespace Azure.Functions.Cli.Diagnostics { internal class LoggingBuilder : IConfigureBuilder { + private LoggingFilterHelper _loggingFilterHelper; + + public LoggingBuilder(LoggingFilterHelper loggingFilterHelper) + { + _loggingFilterHelper = loggingFilterHelper; + } + public void Configure(ILoggingBuilder builder) { - builder.AddProvider(new ColoredConsoleLoggerProvider()); + _loggingFilterHelper.AddConsoleLoggingProvider(builder); builder.Services.AddSingleton(provider => { diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs new file mode 100644 index 000000000..3847b1132 --- /dev/null +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -0,0 +1,101 @@ +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Diagnostics; +using Microsoft.Azure.WebJobs.Script.Eventing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace Azure.Functions.Cli +{ + public class LoggingFilterHelper + { + private const string DefaultLogLevelKey = "default"; + private IConfigurationRoot _hostJsonConfig = null; + + // CI EnvironmentSettings + // https://github.com/watson/ci-info/blob/master/index.js#L52-L59 + public const string Ci = "CI"; // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari + public const string Ci_Continuous_Integration = "CONTINUOUS_INTEGRATION"; // Travis CI, Cirrus CI + public const string Ci_Build_Number = "BUILD_NUMBER"; // Travis CI, Cirrus CI + public const string Ci_Run_Id = "RUN_ID"; // TaskCluster, dsari + + public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging) + { + _hostJsonConfig = hostJsonConfig; + VerboseLogging = verboseLogging.HasValue && verboseLogging.Value; + + if (IsCiEnvironment(verboseLogging.HasValue)) + { + VerboseLogging = true; + } + if (VerboseLogging) + { + SystemLogDefaultLogLevel = LogLevel.Information; + } + bool defaultLogLevelExists = Utilities.LogLevelExists(hostJsonConfig, DefaultLogLevelKey); + if (defaultLogLevelExists) + { + DefaultLogLevel = Utilities.GetHostJsonDefaultLogLevel(hostJsonConfig); + SystemLogDefaultLogLevel = DefaultLogLevel; + UserLogDefaultLogLevel = DefaultLogLevel; + } + } + + /// + /// Default level for system logs + /// + public LogLevel SystemLogDefaultLogLevel { get; } = LogLevel.Warning; + + /// + /// Default level for user logs + /// + public LogLevel UserLogDefaultLogLevel { get; } = LogLevel.Information; + + /// + /// Default log level set in host.json. If not present, deafaults to Information + /// + public LogLevel DefaultLogLevel { get; private set; } = LogLevel.Information; + + /// + /// Is set to true if `func start` is started with `--verbose` flag. If set, SystemLogDefaultLogLevel is set to Information + /// + public bool VerboseLogging { get; private set; } + + internal void AddConsoleLoggingProvider(ILoggingBuilder loggingBuilder) + { + // Filter is needed to force all the logs. + loggingBuilder.AddFilter((category, level) => true).AddProvider(new ColoredConsoleLoggerProvider(this)); + } + + internal bool IsEnabled(string category, LogLevel logLevel) + { + if (_hostJsonConfig != null && Utilities.LogLevelExists(_hostJsonConfig, category)) + { + // If category exists in `loglevel` section, ensure defaults do not apply. + return Utilities.UserLoggingFilter(logLevel); + } + if (DefaultLogLevel == LogLevel.None) + { + return false; + } + return Utilities.DefaultLoggingFilter(category, logLevel, UserLogDefaultLogLevel, SystemLogDefaultLogLevel); + } + + internal bool IsCiEnvironment(bool verboseLoggingArgExists) + { + if (verboseLoggingArgExists) + { + return VerboseLogging; + } + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Ci)) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Ci_Continuous_Integration)) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Ci_Build_Number)) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Ci_Run_Id))) + { + return true; + } + return false; + } + } +} diff --git a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs index c1c8962f3..ad9b1290e 100644 --- a/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs +++ b/src/Azure.Functions.Cli/ExtensionBundle/ExtensionBundleHelper.cs @@ -15,16 +15,13 @@ internal class ExtensionBundleHelper public static ExtensionBundleOptions GetExtensionBundleOptions(ScriptApplicationHostOptions hostOptions = null) { hostOptions = hostOptions ?? SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); - IConfigurationBuilder builder = new ConfigurationBuilder(); - builder.Add(new HostJsonFileConfigurationSource(hostOptions, SystemEnvironment.Instance, loggerFactory: NullLoggerFactory.Instance, metricsLogger: new MetricsLogger())); - var configuration = builder.Build(); - + IConfigurationRoot configuration = Utilities.BuildHostJsonConfigutation(hostOptions); var configurationHelper = new ExtensionBundleConfigurationHelper(configuration, SystemEnvironment.Instance); var options = new ExtensionBundleOptions(); configurationHelper.Configure(options); return options; } - + public static ExtensionBundleManager GetExtensionBundleManager() { var extensionBundleOption = GetExtensionBundleOptions(); diff --git a/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs new file mode 100644 index 000000000..aeca45386 --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs @@ -0,0 +1,47 @@ +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Diagnostics; +using Microsoft.Extensions.Configuration; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Azure.Functions.Cli.Tests +{ + public class ColoredConsoleLoggerTests + { + [Theory] + [InlineData("somelog", false)] + [InlineData("Worker process started and initialized.", true)] + [InlineData("Worker PROCESS started and initialized.", true)] + [InlineData("Worker process started.", false)] + [InlineData("Host lock lease acquired by instance ID", true)] + [InlineData("Host lock lease acquired by instance id", true)] + [InlineData("Host lock lease", false)] + public async Task DoesMessageStartsWithWhiteListedPrefix_Tests(string formattedMessage, bool expected) + { + string defaultJson = "{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}"; + await FileSystemHelpers.WriteToFile("host.json", new MemoryStream(Encoding.ASCII.GetBytes(defaultJson))); + var testConfiguration = new ConfigurationBuilder().AddJsonFile("host.json").Build(); + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(testConfiguration, true)); + Assert.Equal(expected, coloredConsoleLogger.DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)); + } + + [Theory] + [InlineData("somelog", false)] + [InlineData("Worker process started and initialized.", true)] + [InlineData("Worker PROCESS started and initialized.", true)] + [InlineData("Worker process started.", false)] + [InlineData("Host lock lease acquired by instance ID", true)] + [InlineData("Host lock lease acquired by instance id", true)] + [InlineData("Host lock lease", false)] + public async Task IsEnabled_Tests(string formattedMessage, bool expected) + { + string defaultJson = "{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}"; + await FileSystemHelpers.WriteToFile("host.json", new MemoryStream(Encoding.ASCII.GetBytes(defaultJson))); + var testConfiguration = new ConfigurationBuilder().AddJsonFile("host.json").Build(); + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(testConfiguration, true)); + Assert.Equal(expected, coloredConsoleLogger.DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)); + } + } +} diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 3d0e2ae8d..a70ec599a 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -33,7 +33,7 @@ await CliTester.Run(new RunConfiguration ExpectExit = false, OutputContains = new[] { - "Http Functions:", + "Functions:", "HttpTrigger: [GET,POST] http://localhost:7071/api/HttpTrigger" }, Test = async (workingDir, p) => @@ -59,7 +59,7 @@ await CliTester.Run(new RunConfiguration { "init . --worker-runtime node", "new --template \"Http trigger\" --name HttpTrigger", - "start --language-worker -- \"--inspect=5050\"" + "start --verbose --language-worker -- \"--inspect=5050\"" }, ExpectExit = false, OutputContains = new[] diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs new file mode 100644 index 000000000..3d4f99c36 --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -0,0 +1,91 @@ +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Diagnostics; +using Microsoft.Azure.WebJobs.Script; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Text; +using Xunit; + +namespace Azure.Functions.Cli.Tests +{ + public class LoggingFilterHelperTests + { + private ScriptApplicationHostOptions _hostOptions; + + public LoggingFilterHelperTests() + { + _hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + } + + [Theory] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", true, LogLevel.None)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"DEBUG\"}}}", true, LogLevel.Debug)] + [InlineData("{\"version\": \"2.0\"}", null, LogLevel.Information)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", true, LogLevel.Information)] + public void LoggingFilterHelper_Tests(string hostJsonContent, bool? verboseLogging, LogLevel expectedDefaultLogLevel) + { + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); + if (verboseLogging == null) + { + Assert.False(loggingFilterHelper.VerboseLogging); + } + Assert.Equal(loggingFilterHelper.DefaultLogLevel, expectedDefaultLogLevel); + if (hostJsonContent.Contains("Default")) + { + Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); + Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); + } + else + { + Assert.Equal(LogLevel.Information, loggingFilterHelper.UserLogDefaultLogLevel); + if (verboseLogging.HasValue && verboseLogging.Value) + { + Assert.Equal(LogLevel.Information, loggingFilterHelper.SystemLogDefaultLogLevel); + } + else + { + Assert.Equal(LogLevel.Warning, loggingFilterHelper.SystemLogDefaultLogLevel); + } + } + } + + [Theory] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", "test", LogLevel.Information, false)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.Startup", LogLevel.Information, true)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Information, false)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Warning, true)] + public void IsEnabled_Tests(string hostJsonContent, string category, LogLevel logLevel, bool expected) + { + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, false); + Assert.Equal(expected, loggingFilterHelper.IsEnabled(category, logLevel)); + } + + [Theory] + [InlineData(false, null, false)] + [InlineData(true, false, false)] + [InlineData(true, null, true)] + public void IsCI_Tests(bool isCiEnv, bool? verboseLogging, bool expected) + { + if (isCiEnv) + { + Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, "90l99"); + } + string defaultJson = "{\"version\": \"2.0\"}"; + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, defaultJson); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); + Assert.Equal(expected, loggingFilterHelper.IsCiEnvironment(verboseLogging.HasValue)); + Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, ""); + } + } +} diff --git a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs new file mode 100644 index 000000000..923e0e426 --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs @@ -0,0 +1,81 @@ +using Azure.Functions.Cli.Common; +using Microsoft.Azure.WebJobs.Script; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.IO; +using System.Text; +using Xunit; + +namespace Azure.Functions.Cli.Tests +{ + public class UtilitiesTests + { + [Theory] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", LogLevel.None)] + [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"NONE\"}}}", LogLevel.None)] + [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}", LogLevel.Debug)] + [InlineData("{\"version\": \"2.0\"}", LogLevel.Information)] + public void GetHostJsonDefaultLogLevel_Test(string hostJsonContent, LogLevel expectedLogLevel) + { + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); + ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); + LogLevel actualLogLevel = Utilities.GetHostJsonDefaultLogLevel(configuration); + Assert.Equal(actualLogLevel, expectedLogLevel); + } + + [Theory] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.General\": \"Debug\"}}}", "Host.General", true)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", false)] + [InlineData("{\"version\": \"2.0\"}", "Function.HttpFunction", false)] + public void LogLevelExists_Test(string hostJsonContent, string category, bool expected) + { + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); + ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); + Assert.Equal(expected, Utilities.LogLevelExists(configuration, category)); + } + + [Theory] + [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "extensionBundle", true)] + [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "ExtensionBundle", true)] + [InlineData("{\"version\": \"2.0\"}", "extensionBundle", false)] + public void JobHostConfigSectionExists_Test(string hostJsonContent, string section, bool expected) + { + FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); + ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); + Assert.Equal(expected, Utilities.JobHostConfigSectionExists(configuration, section)); + } + + [Theory] + [InlineData(LogLevel.None, false)] + [InlineData(LogLevel.Debug, true)] + [InlineData(LogLevel.Information, true)] + public void UserLoggingFilter_Test(LogLevel inputLogLevel, bool expected) + { + Assert.Equal(expected, Utilities.UserLoggingFilter(inputLogLevel)); + } + + [Theory] + [InlineData("Function.Function1", LogLevel.None, true)] + [InlineData("Function.Function1", LogLevel.Warning, true)] + [InlineData("Function.Function1.User", LogLevel.Information, true)] + [InlineData("Host.General", LogLevel.Information, false)] + [InlineData("Host.Startup", LogLevel.Error, true)] + [InlineData("Host.General", LogLevel.Warning, true)] + public void DefaultLoggingFilter_Test(string inputCategory, LogLevel inputLogLevel, bool expected) + { + Assert.Equal(expected, Utilities.DefaultLoggingFilter(inputCategory, inputLogLevel, LogLevel.Information, LogLevel.Warning)); + } + } +} From 674ea7a0dd605e6c61349839129596841c1a8f30 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 26 Aug 2020 16:46:54 -0700 Subject: [PATCH 086/127] Skip LoggingFilter flaky tests (#2172) (#2177) --- .../LoggingFilterHelperTests.cs | 129 ++++++++++++------ .../UtilitiesTests.cs | 109 +++++++++++---- 2 files changed, 175 insertions(+), 63 deletions(-) diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs index 3d4f99c36..99204e468 100644 --- a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -1,12 +1,10 @@ using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Diagnostics; using Microsoft.Azure.WebJobs.Script; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.IO; -using System.Text; using Xunit; namespace Azure.Functions.Cli.Tests @@ -14,78 +12,133 @@ namespace Azure.Functions.Cli.Tests public class LoggingFilterHelperTests { private ScriptApplicationHostOptions _hostOptions; + private string _workerDir; + private string _hostJsonFilePath; public LoggingFilterHelperTests() { - _hostOptions = new ScriptApplicationHostOptions + try { - ScriptPath = Directory.GetCurrentDirectory() - }; + _workerDir = Path.GetTempFileName(); + DeleteIfExists(_workerDir); + Directory.CreateDirectory(_workerDir); + _hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = _workerDir + }; + } + catch + { + _hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + } + _hostJsonFilePath = Path.Combine(_hostOptions.ScriptPath, Constants.HostJsonFileName); + } + + private void DeleteIfExists(string filePath) + { + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + else if (Directory.Exists(filePath)) + { + Directory.Delete(filePath, recursive: true); + } + } + catch + { + } } - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", true, LogLevel.None)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"DEBUG\"}}}", true, LogLevel.Debug)] [InlineData("{\"version\": \"2.0\"}", null, LogLevel.Information)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", true, LogLevel.Information)] public void LoggingFilterHelper_Tests(string hostJsonContent, bool? verboseLogging, LogLevel expectedDefaultLogLevel) { - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); - if (verboseLogging == null) + try { - Assert.False(loggingFilterHelper.VerboseLogging); - } - Assert.Equal(loggingFilterHelper.DefaultLogLevel, expectedDefaultLogLevel); - if (hostJsonContent.Contains("Default")) - { - Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); - Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); - } - else - { - Assert.Equal(LogLevel.Information, loggingFilterHelper.UserLogDefaultLogLevel); - if (verboseLogging.HasValue && verboseLogging.Value) + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); + if (verboseLogging == null) { - Assert.Equal(LogLevel.Information, loggingFilterHelper.SystemLogDefaultLogLevel); + Assert.False(loggingFilterHelper.VerboseLogging); + } + Assert.Equal(loggingFilterHelper.DefaultLogLevel, expectedDefaultLogLevel); + if (hostJsonContent.Contains("Default")) + { + Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); + Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); } else { - Assert.Equal(LogLevel.Warning, loggingFilterHelper.SystemLogDefaultLogLevel); + Assert.Equal(LogLevel.Information, loggingFilterHelper.UserLogDefaultLogLevel); + if (verboseLogging.HasValue && verboseLogging.Value) + { + Assert.Equal(LogLevel.Information, loggingFilterHelper.SystemLogDefaultLogLevel); + } + else + { + Assert.Equal(LogLevel.Warning, loggingFilterHelper.SystemLogDefaultLogLevel); + } } } + finally + { + DeleteIfExists(_workerDir); + } } - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", "test", LogLevel.Information, false)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.Startup", LogLevel.Information, true)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Information, false)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Warning, true)] public void IsEnabled_Tests(string hostJsonContent, string category, LogLevel logLevel, bool expected) { - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, false); - Assert.Equal(expected, loggingFilterHelper.IsEnabled(category, logLevel)); + try + { + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, false); + Assert.Equal(expected, loggingFilterHelper.IsEnabled(category, logLevel)); + } + finally + { + DeleteIfExists(_workerDir); + } } - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData(false, null, false)] [InlineData(true, false, false)] [InlineData(true, null, true)] public void IsCI_Tests(bool isCiEnv, bool? verboseLogging, bool expected) { - if (isCiEnv) + try + { + if (isCiEnv) + { + Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, "90l99"); + } + string defaultJson = "{\"version\": \"2.0\"}"; + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, defaultJson); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); + Assert.Equal(expected, loggingFilterHelper.IsCiEnvironment(verboseLogging.HasValue)); + Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, ""); + } + finally { - Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, "90l99"); + DeleteIfExists(_workerDir); } - string defaultJson = "{\"version\": \"2.0\"}"; - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, defaultJson); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); - Assert.Equal(expected, loggingFilterHelper.IsCiEnvironment(verboseLogging.HasValue)); - Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, ""); } } } diff --git a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs index 923e0e426..0108ce4fd 100644 --- a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs +++ b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs @@ -1,60 +1,101 @@ using Azure.Functions.Cli.Common; using Microsoft.Azure.WebJobs.Script; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.IO; -using System.Text; using Xunit; namespace Azure.Functions.Cli.Tests { public class UtilitiesTests { - [Theory] + private string _workerDir; + private ScriptApplicationHostOptions _hostOptions; + private string _hostJsonFilePath; + + public UtilitiesTests() + { + try + { + _workerDir = Path.GetTempFileName(); + if (File.Exists(_workerDir)) + { + File.Delete(_workerDir); + } + else if (Directory.Exists(_workerDir)) + { + Directory.Delete(_workerDir, recursive: true); + } + Directory.CreateDirectory(_workerDir); + _hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = _workerDir + }; + } + catch + { + _hostOptions = new ScriptApplicationHostOptions + { + ScriptPath = Directory.GetCurrentDirectory() + }; + } + _hostJsonFilePath = Path.Combine(_hostOptions.ScriptPath, Constants.HostJsonFileName); + } + + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", LogLevel.None)] [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"NONE\"}}}", LogLevel.None)] [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}", LogLevel.Debug)] [InlineData("{\"version\": \"2.0\"}", LogLevel.Information)] public void GetHostJsonDefaultLogLevel_Test(string hostJsonContent, LogLevel expectedLogLevel) { - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); - ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + try { - ScriptPath = Directory.GetCurrentDirectory() - }; - var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); - LogLevel actualLogLevel = Utilities.GetHostJsonDefaultLogLevel(configuration); - Assert.Equal(actualLogLevel, expectedLogLevel); + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + LogLevel actualLogLevel = Utilities.GetHostJsonDefaultLogLevel(configuration); + Assert.Equal(actualLogLevel, expectedLogLevel); + } + finally + { + DeleteIfExists(_workerDir); + } } - [Theory] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.General\": \"Debug\"}}}", "Host.General", true)] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.General\": \"Debug\"}}}", "Host.General", true)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", false)] [InlineData("{\"version\": \"2.0\"}", "Function.HttpFunction", false)] public void LogLevelExists_Test(string hostJsonContent, string category, bool expected) { - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); - ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + try { - ScriptPath = Directory.GetCurrentDirectory() - }; - var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); - Assert.Equal(expected, Utilities.LogLevelExists(configuration, category)); + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); + + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + Assert.Equal(expected, Utilities.LogLevelExists(configuration, category)); + } + finally + { + DeleteIfExists(_workerDir); + } } - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "extensionBundle", true)] [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "ExtensionBundle", true)] [InlineData("{\"version\": \"2.0\"}", "extensionBundle", false)] public void JobHostConfigSectionExists_Test(string hostJsonContent, string section, bool expected) { - FileSystemHelpers.WriteAllTextToFile(Constants.HostJsonFileName, hostJsonContent); - ScriptApplicationHostOptions hostOptions = new ScriptApplicationHostOptions + try { - ScriptPath = Directory.GetCurrentDirectory() - }; - var configuration = Utilities.BuildHostJsonConfigutation(hostOptions); - Assert.Equal(expected, Utilities.JobHostConfigSectionExists(configuration, section)); + FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); + var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); + Assert.Equal(expected, Utilities.JobHostConfigSectionExists(configuration, section)); + } + finally + { + DeleteIfExists(_workerDir); + } } [Theory] @@ -77,5 +118,23 @@ public void DefaultLoggingFilter_Test(string inputCategory, LogLevel inputLogLev { Assert.Equal(expected, Utilities.DefaultLoggingFilter(inputCategory, inputLogLevel, LogLevel.Information, LogLevel.Warning)); } + + private void DeleteIfExists(string filePath) + { + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + else if (Directory.Exists(filePath)) + { + Directory.Delete(filePath, recursive: true); + } + } + catch + { + } + } } } From e08bfb827887e002b72031959b1ee717ce716f96 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 2 Sep 2020 12:02:11 -0700 Subject: [PATCH 087/127] Fix null ref when debugging from VS (#2186) (#2190) --- src/Azure.Functions.Cli/Common/Utilities.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index 7dc94ebfa..03b091426 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -205,12 +205,14 @@ internal static bool LogLevelExists(IConfigurationRoot hostJsonConfig, string ca string categoryKey = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Logging, LogLevelSection, category); try { - if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[categoryKey].ToString(), true, out object outLevel)) + if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[categoryKey], true, out object outLevel)) { return true; } } - catch { } + catch + { + } return false; } From b43d30f4df6368b4f6cc8ee212c284bcfc65b5b2 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 2 Sep 2020 16:39:40 -0700 Subject: [PATCH 088/127] [V2]Add default webjobs loglevel filter and treat logs with category ILogger as user logs (#2195) --- .../Actions/HostActions/StartHostAction.cs | 4 ++- src/Azure.Functions.Cli/Common/Utilities.cs | 27 ++++++++++++++++--- .../Diagnostics/LoggingFilterHelper.cs | 3 ++- .../ColoredConsoleLoggerTests.cs | 4 +-- .../UtilitiesTests.cs | 11 ++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index c93fc5575..08972f44f 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Diagnostics; using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; @@ -188,7 +189,8 @@ private async Task BuildWebHost(ScriptApplicationHostOptions hostOptio .ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); - loggingFilterHelper.AddConsoleLoggingProvider(loggingBuilder); + // This is needed to filter system logs only for known categories + loggingBuilder.AddFilter((category, level) => Utilities.SystemLoggingFilter(category, level, LogLevel.Trace)).AddProvider(new ColoredConsoleLoggerProvider(loggingFilterHelper)); }) .ConfigureServices((context, services) => services.AddSingleton(new Startup(context, hostOptions, CorsOrigins, CorsCredentials, EnableAuth, loggingFilterHelper))) .Build(); diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index 03b091426..978733867 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -16,8 +16,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Azure.Functions.Cli { @@ -25,6 +23,13 @@ internal static class Utilities { public const string LogLevelSection = "loglevel"; public const string LogLevelDefaultSection = "Default"; + internal static readonly string[] SystemCategoryPrefixes = new[] + { + "Microsoft.Azure.WebJobs.", + "Function.", + "Worker.", + "Host." + }; internal static void PrintLogo() { @@ -245,7 +250,13 @@ internal static bool DefaultLoggingFilter(string category, LogLevel actualLevel, { return actualLevel >= userLogMinLevel; } - return actualLevel >= systemLogMinLevel; + if (IsSystemLogCategory(category)) + { + // System logs + return actualLevel >= systemLogMinLevel; + } + // consider any other category as user log + return actualLevel >= userLogMinLevel; } /// @@ -262,6 +273,16 @@ internal static bool UserLoggingFilter(LogLevel actualLevel) return actualLevel >= LogLevel.Trace; } + internal static bool SystemLoggingFilter(string category, LogLevel actualLevel, LogLevel minLevel) + { + return actualLevel >= minLevel && IsSystemLogCategory(category); + } + + internal static bool IsSystemLogCategory(string category) + { + return SystemCategoryPrefixes.Where(p => category.StartsWith(p)).Any(); + } + internal static IConfigurationRoot BuildHostJsonConfigutation(ScriptApplicationHostOptions hostOptions) { IConfigurationBuilder builder = new ConfigurationBuilder(); diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 3847b1132..5bab9fbdc 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; namespace Azure.Functions.Cli { @@ -64,7 +65,7 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi internal void AddConsoleLoggingProvider(ILoggingBuilder loggingBuilder) { - // Filter is needed to force all the logs. + // Filter is needed to force all the logs at jobhost level loggingBuilder.AddFilter((category, level) => true).AddProvider(new ColoredConsoleLoggerProvider(this)); } diff --git a/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs index aeca45386..c3a95b193 100644 --- a/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs +++ b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs @@ -10,7 +10,7 @@ namespace Azure.Functions.Cli.Tests { public class ColoredConsoleLoggerTests { - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("somelog", false)] [InlineData("Worker process started and initialized.", true)] [InlineData("Worker PROCESS started and initialized.", true)] @@ -27,7 +27,7 @@ public async Task DoesMessageStartsWithWhiteListedPrefix_Tests(string formattedM Assert.Equal(expected, coloredConsoleLogger.DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)); } - [Theory] + [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData("somelog", false)] [InlineData("Worker process started and initialized.", true)] [InlineData("Worker PROCESS started and initialized.", true)] diff --git a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs index 0108ce4fd..694337efb 100644 --- a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs +++ b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs @@ -119,6 +119,17 @@ public void DefaultLoggingFilter_Test(string inputCategory, LogLevel inputLogLev Assert.Equal(expected, Utilities.DefaultLoggingFilter(inputCategory, inputLogLevel, LogLevel.Information, LogLevel.Warning)); } + [Theory] + [InlineData("Function.Function1", true)] + [InlineData("Random", false)] + [InlineData("Host.Startup", true)] + [InlineData("Microsoft.Azure.WebJobs.TestLogger", true)] + [InlineData("Microsoft.Azure.TestLogger", false)] + public void IsSystemLogCategory_Test(string inputCategory, bool expected) + { + Assert.Equal(expected, Utilities.IsSystemLogCategory(inputCategory)); + } + private void DeleteIfExists(string filePath) { try From 3a00ee48b208494bb9fef999a9db824a5c12cf61 Mon Sep 17 00:00:00 2001 From: Francisco Gamino Date: Tue, 8 Sep 2020 19:57:24 -0700 Subject: [PATCH 089/127] Update profile.ps1 template (#2210) --- src/Azure.Functions.Cli/StaticResources/profile.ps1 | 2 +- test/Azure.Functions.Cli.Tests/E2E/InitTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/StaticResources/profile.ps1 b/src/Azure.Functions.Cli/StaticResources/profile.ps1 index 6a525cf76..1670fc99c 100644 --- a/src/Azure.Functions.Cli/StaticResources/profile.ps1 +++ b/src/Azure.Functions.Cli/StaticResources/profile.ps1 @@ -12,7 +12,7 @@ # Authenticate with Azure PowerShell using MSI. # Remove this if you are not planning on using MSI or Azure PowerShell. if ($env:MSI_SECRET) { - Disable-AzContextAutosave + Disable-AzContextAutosave -Scope Process | Out-Null Connect-AzAccount -Identity } diff --git a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs index 0ac2820c0..45651271c 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/InitTests.cs @@ -412,7 +412,7 @@ public Task init_function_app_powershell_enable_managed_dependencies_and_set_def ContentContains = new [] { "env:MSI_SECRET", - "Disable-AzContextAutosave", + "Disable-AzContextAutosave -Scope Process | Out-Null", "Connect-AzAccount -Identity" } }, From 4e739abb40dfc7f74339e932633f40d552fb1956 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 9 Sep 2020 13:07:35 -0700 Subject: [PATCH 090/127] Filter ConsoleLogs from user code as function logs (#2212) --- src/Azure.Functions.Cli/Common/Utilities.cs | 3 ++- src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index 978733867..c0299e599 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -13,6 +13,7 @@ using Microsoft.Azure.WebJobs.Script; using Microsoft.Azure.WebJobs.Script.Configuration; using Microsoft.Azure.WebJobs.Script.Diagnostics; +using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -246,7 +247,7 @@ internal static bool JobHostConfigSectionExists(IConfigurationRoot hostJsonConfi /// internal static bool DefaultLoggingFilter(string category, LogLevel actualLevel, LogLevel userLogMinLevel, LogLevel systemLogMinLevel) { - if (LogCategories.IsFunctionUserCategory(category)) + if (LogCategories.IsFunctionUserCategory(category) || category.Equals(WorkerConstants.FunctionConsoleLogCategoryName, StringComparison.OrdinalIgnoreCase)) { return actualLevel >= userLogMinLevel; } diff --git a/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs b/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs index e87fcde18..d6b957a9e 100644 --- a/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs +++ b/src/Azure.Functions.Cli/Helpers/GlobalCoreToolsSettings.cs @@ -16,7 +16,7 @@ public static WorkerRuntime CurrentWorkerRuntime { if (_currentWorkerRuntime == WorkerRuntime.None) { - ColoredConsole.Error.WriteLine(QuietWarningColor("Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell]")); + ColoredConsole.Error.WriteLine(QuietWarningColor("Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]")); } return _currentWorkerRuntime; } From 2e4cf7e936cc7d645b15f98e0ecbbe853ac09d43 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 10 Sep 2020 19:39:25 -0700 Subject: [PATCH 091/127] Update Functions Host references (#2218) * Upgrade Microsoft.Azure.WebJobs.Script.WebHost to 2.0.14494 * Upgrade PowerShellWorker to 2.0.448 --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 6f5da05ef..4fa74a9b4 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -120,8 +120,8 @@ - - + + From 19d0b160293e539ec7fd36b377ea0ee73f02a490 Mon Sep 17 00:00:00 2001 From: Graham Zuber Date: Wed, 10 Jun 2020 15:31:13 -0700 Subject: [PATCH 092/127] func new will now run init if the proper files have not already been created for the function app. --- .../LocalActions/CreateFunctionAction.cs | 14 ++++++++ .../Actions/LocalActions/InitAction.cs | 24 +++++++------ .../E2E/CreateFunctionTests.cs | 35 +++++++++++++++++++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs index 9ca696885..4970341fc 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/CreateFunctionAction.cs @@ -26,6 +26,8 @@ internal class CreateFunctionAction : BaseAction private ITemplatesManager _templatesManager; private readonly ISecretsManager _secretsManager; + private readonly InitAction _initAction; + public string Language { get; set; } public string TemplateName { get; set; } public string FunctionName { get; set; } @@ -40,6 +42,7 @@ public CreateFunctionAction(ITemplatesManager templatesManager, ISecretsManager _templatesManager = templatesManager; _secretsManager = secretsManager; _templates = new Lazy>(() => { return _templatesManager.Templates.Result; }); + _initAction = new InitAction(_templatesManager, _secretsManager); } public override ICommandLineParserResult ParseArgs(string[] args) @@ -68,6 +71,9 @@ public override ICommandLineParserResult ParseArgs(string[] args) .Setup("csx") .WithDescription("use old style csx dotnet functions") .Callback(csx => Csx = csx); + + _initAction.ParseArgs(args); + return base.ParseArgs(args); } @@ -87,7 +93,15 @@ public async override Task RunAsync() } var workerRuntime = GlobalCoreToolsSettings.CurrentWorkerRuntimeOrNone; + if (!FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, "local.settings.json"))) + { + // we're assuming "func init" has not been run + await _initAction.RunAsync(); + workerRuntime = _initAction.ResolvedWorkerRuntime; + Language = _initAction.ResolvedLanguage; + } + var templates = await _templatesManager.Templates; if (workerRuntime != WorkerRuntime.None && !string.IsNullOrWhiteSpace(Language)) { diff --git a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs index 858d06107..2ca135159 100644 --- a/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs +++ b/src/Azure.Functions.Cli/Actions/LocalActions/InitAction.cs @@ -42,6 +42,10 @@ internal class InitAction : BaseAction public bool? ManagedDependencies { get; set; } + public WorkerRuntime ResolvedWorkerRuntime { get; set; } + + public string ResolvedLanguage { get; set; } + internal static readonly Dictionary, Task> fileToContentMap = new Dictionary, Task> { { new Lazy(() => ".gitignore"), StaticResources.GitIgnore } @@ -149,30 +153,28 @@ private async Task InitDockerFileOnly() private async Task InitFunctionAppProject() { - WorkerRuntime workerRuntime; - string language = string.Empty; if (Csx) { - workerRuntime = Helpers.WorkerRuntime.dotnet; + ResolvedWorkerRuntime = Helpers.WorkerRuntime.dotnet; } else { - (workerRuntime, language) = ResolveWorkerRuntimeAndLanguage(WorkerRuntime, Language); + (ResolvedWorkerRuntime, ResolvedLanguage) = ResolveWorkerRuntimeAndLanguage(WorkerRuntime, Language); } - TelemetryHelpers.AddCommandEventToDictionary(TelemetryCommandEvents, "WorkerRuntime", workerRuntime.ToString()); + TelemetryHelpers.AddCommandEventToDictionary(TelemetryCommandEvents, "WorkerRuntime", ResolvedWorkerRuntime.ToString()); - if (workerRuntime == Helpers.WorkerRuntime.dotnet && !Csx) + if (ResolvedWorkerRuntime == Helpers.WorkerRuntime.dotnet && !Csx) { await DotnetHelpers.DeployDotnetProject(Utilities.SanitizeLiteral(Path.GetFileName(Environment.CurrentDirectory), allowed: "-"), Force); } else { - bool managedDependenciesOption = ResolveManagedDependencies(workerRuntime, ManagedDependencies); - await InitLanguageSpecificArtifacts(workerRuntime, language, managedDependenciesOption); + bool managedDependenciesOption = ResolveManagedDependencies(ResolvedWorkerRuntime, ManagedDependencies); + await InitLanguageSpecificArtifacts(ResolvedWorkerRuntime, ResolvedLanguage, managedDependenciesOption); await WriteFiles(); - await WriteHostJson(workerRuntime, managedDependenciesOption, ExtensionBundle); - await WriteLocalSettingsJson(workerRuntime); + await WriteHostJson(ResolvedWorkerRuntime, managedDependenciesOption, ExtensionBundle); + await WriteLocalSettingsJson(ResolvedWorkerRuntime); } await WriteExtensionsJson(); @@ -183,7 +185,7 @@ private async Task InitFunctionAppProject() } if (InitDocker) { - await WriteDockerfile(workerRuntime, Csx); + await WriteDockerfile(ResolvedWorkerRuntime, Csx); } } diff --git a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs index 8027fe926..6f4e0bc3a 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs @@ -256,5 +256,40 @@ await CliTester.Run(new RunConfiguration } }, _output); } + + [Fact] + public async Task create_function_no_init_csx() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "new --csx --template \"http trigger\" --name testfunc" + }, + CheckFiles = new FileResult[] + { + new FileResult + { + Name = Path.Combine("testfunc", "function.json"), + ContentContains = new [] + { + "httpTrigger" + } + }, + new FileResult + { + Name = Path.Combine("local.settings.json"), + ContentContains = new [] + { + "\"FUNCTIONS_WORKER_RUNTIME\": \"dotnet\"" + } + } + }, + OutputContains = new[] + { + "The function \"testfunc\" was created successfully from the \"http trigger\" template." + } + }, _output); + } } } From c00b06616a741dcdf70a3061fc415adde84010f8 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Sun, 20 Sep 2020 12:49:26 -0700 Subject: [PATCH 093/127] Fix logging filters-rely on LoggerFilterOptions (#2231) --- .../Actions/HostActions/StartHostAction.cs | 10 +- src/Azure.Functions.Cli/Common/Utilities.cs | 54 +------- .../Diagnostics/ColoredConsoleLogger.cs | 48 +++++-- .../ColoredConsoleLoggerProvider.cs | 8 +- .../Diagnostics/LoggerRuleSelector.cs | 87 ++++++++++++ .../Diagnostics/LoggingBuilder.cs | 10 +- .../Diagnostics/LoggingFilterHelper.cs | 33 +---- .../Diagnostics/ProviderAliasUtilities.cs | 18 +++ .../ColoredConsoleLoggerTests.cs | 77 +++++++++-- .../E2E/StartTests.cs | 126 +++++++++++++++++- .../LoggingFilterHelperTests.cs | 20 --- .../UtilitiesTests.cs | 23 ++-- 12 files changed, 378 insertions(+), 136 deletions(-) create mode 100644 src/Azure.Functions.Cli/Diagnostics/LoggerRuleSelector.cs create mode 100644 src/Azure.Functions.Cli/Diagnostics/ProviderAliasUtilities.cs diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 08972f44f..a0276dff6 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -189,8 +189,16 @@ private async Task BuildWebHost(ScriptApplicationHostOptions hostOptio .ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); + loggingBuilder.Services.AddSingleton(p => + { + //Cache LoggerFilterOptions to be used by the logger to filter logs based on content + var filterOptions = p.GetService>().Value; + // Set min level to SystemLogDefaultLogLevel. + filterOptions.MinLevel = loggingFilterHelper.SystemLogDefaultLogLevel; + return new ColoredConsoleLoggerProvider(loggingFilterHelper, filterOptions); + }); // This is needed to filter system logs only for known categories - loggingBuilder.AddFilter((category, level) => Utilities.SystemLoggingFilter(category, level, LogLevel.Trace)).AddProvider(new ColoredConsoleLoggerProvider(loggingFilterHelper)); + loggingBuilder.AddDefaultWebJobsFilters(LogLevel.Trace); }) .ConfigureServices((context, services) => services.AddSingleton(new Startup(context, hostOptions, CorsOrigins, CorsCredentials, EnableAuth, loggingFilterHelper))) .Build(); diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index c0299e599..7dea3039d 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -24,13 +24,6 @@ internal static class Utilities { public const string LogLevelSection = "loglevel"; public const string LogLevelDefaultSection = "Default"; - internal static readonly string[] SystemCategoryPrefixes = new[] - { - "Microsoft.Azure.WebJobs.", - "Function.", - "Worker.", - "Host." - }; internal static void PrintLogo() { @@ -189,29 +182,12 @@ internal static string EnsureCoreToolsLocalData() return localPath; } - internal static LogLevel GetHostJsonDefaultLogLevel(IConfigurationRoot hostJsonConfig) - { - string defaultLogLevelKey = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Logging, LogLevelSection, LogLevelDefaultSection); - try - { - if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[defaultLogLevelKey].ToString(), true, out object outLevel)) - { - return (LogLevel)outLevel; - } - } - catch - { - } - // Default log level - return LogLevel.Information; - } - - internal static bool LogLevelExists(IConfigurationRoot hostJsonConfig, string category) + internal static bool LogLevelExists(IConfigurationRoot hostJsonConfig, string category, out LogLevel outLogLevel) { string categoryKey = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Logging, LogLevelSection, category); try { - if (Enum.TryParse(typeof(LogLevel), hostJsonConfig[categoryKey], true, out object outLevel)) + if (Enum.TryParse(hostJsonConfig[categoryKey], true, out outLogLevel)) { return true; } @@ -219,6 +195,7 @@ internal static bool LogLevelExists(IConfigurationRoot hostJsonConfig, string ca catch { } + outLogLevel = LogLevel.Information; return false; } @@ -247,7 +224,9 @@ internal static bool JobHostConfigSectionExists(IConfigurationRoot hostJsonConfi /// internal static bool DefaultLoggingFilter(string category, LogLevel actualLevel, LogLevel userLogMinLevel, LogLevel systemLogMinLevel) { - if (LogCategories.IsFunctionUserCategory(category) || category.Equals(WorkerConstants.FunctionConsoleLogCategoryName, StringComparison.OrdinalIgnoreCase)) + if (LogCategories.IsFunctionUserCategory(category) + || LogCategories.IsFunctionCategory(category) + || category.Equals(WorkerConstants.FunctionConsoleLogCategoryName, StringComparison.OrdinalIgnoreCase)) { return actualLevel >= userLogMinLevel; } @@ -260,28 +239,9 @@ internal static bool DefaultLoggingFilter(string category, LogLevel actualLevel, return actualLevel >= userLogMinLevel; } - /// - /// Returns true for user logs >= Trace level. Returns false, if log level is explicitly set to None. - /// - /// - /// - internal static bool UserLoggingFilter(LogLevel actualLevel) - { - if (actualLevel == LogLevel.None) - { - return false; - } - return actualLevel >= LogLevel.Trace; - } - - internal static bool SystemLoggingFilter(string category, LogLevel actualLevel, LogLevel minLevel) - { - return actualLevel >= minLevel && IsSystemLogCategory(category); - } - internal static bool IsSystemLogCategory(string category) { - return SystemCategoryPrefixes.Where(p => category.StartsWith(p)).Any(); + return ScriptConstants.SystemLogCategoryPrefixes.Where(p => category.StartsWith(p)).Any(); } internal static IConfigurationRoot BuildHostJsonConfigutation(ScriptApplicationHostOptions hostOptions) diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs index 3a71ac93b..f56b43132 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs @@ -14,18 +14,53 @@ public class ColoredConsoleLogger : ILogger private readonly bool _verboseErrors; private readonly string _category; private readonly LoggingFilterHelper _loggingFilterHelper; + private readonly LoggerFilterOptions _loggerFilterOptions; private readonly string[] allowedLogsPrefixes = new string[] { "Worker process started and initialized.", "Host lock lease acquired by instance ID" }; + private static readonly LoggerRuleSelector RuleSelector = new LoggerRuleSelector(); + private static readonly Type ProviderType = typeof(ColoredConsoleLoggerProvider); - public ColoredConsoleLogger(string category, LoggingFilterHelper loggingFilterHelper) + public ColoredConsoleLogger(string category, LoggingFilterHelper loggingFilterHelper, LoggerFilterOptions loggerFilterOptions) { _category = category; - _loggingFilterHelper = loggingFilterHelper; + _loggerFilterOptions = loggerFilterOptions ?? throw new ArgumentNullException(nameof(loggerFilterOptions)); + _loggingFilterHelper = loggingFilterHelper ?? throw new ArgumentNullException(nameof(loggingFilterHelper)); _verboseErrors = StaticSettings.IsDebug; } + internal LoggerFilterRule SelectRule(string categoryName, LoggerFilterOptions loggerFilterOptions) + { + RuleSelector.Select(loggerFilterOptions, ProviderType, categoryName, + out LogLevel? minLevel, out Func filter); + + return new LoggerFilterRule(ProviderType.FullName, categoryName, minLevel, filter); + } + + internal bool IsEnabled(string category, LogLevel logLevel) + { + LoggerFilterRule filterRule = SelectRule(category, _loggerFilterOptions); + + if (filterRule.LogLevel != null && logLevel < filterRule.LogLevel) + { + return false; + } + if (filterRule.Filter != null) + { + bool enabled = filterRule.Filter(ProviderType.FullName, category, logLevel); + if (!enabled) + { + return false; + } + } + if (filterRule.LogLevel != null) + { + return Utilities.DefaultLoggingFilter(category, logLevel, filterRule.LogLevel.Value, filterRule.LogLevel.Value); + } + return Utilities.DefaultLoggingFilter(category, logLevel, _loggingFilterHelper.UserLogDefaultLogLevel, _loggingFilterHelper.SystemLogDefaultLogLevel); + } + public bool IsEnabled(LogLevel logLevel) { - return _loggingFilterHelper.IsEnabled(_category, logLevel); + return IsEnabled(_category, logLevel); } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) @@ -55,12 +90,7 @@ private void LogToConsole(LogLevel logLevel, Exception exception, string formatt { foreach (var line in GetMessageString(logLevel, formattedMessage, exception)) { - var outputline = $"{line}"; - if (_loggingFilterHelper.VerboseLogging) - { - outputline = $"[{DateTime.UtcNow}] {outputline}"; - } - ColoredConsole.WriteLine($"{outputline}"); + ColoredConsole.WriteLine($"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff")}] {line}"); } } diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs index 44192d6b5..8b6cdd8e4 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLoggerProvider.cs @@ -6,15 +6,17 @@ namespace Azure.Functions.Cli.Diagnostics public class ColoredConsoleLoggerProvider : ILoggerProvider { private readonly LoggingFilterHelper _loggingFilterHelper; + private readonly LoggerFilterOptions _loggerFilterOptions; - public ColoredConsoleLoggerProvider(LoggingFilterHelper loggingFilterHelper) + public ColoredConsoleLoggerProvider(LoggingFilterHelper loggingFilterHelper, LoggerFilterOptions loggerFilterOptions) { - _loggingFilterHelper = loggingFilterHelper; + _loggerFilterOptions = loggerFilterOptions ?? throw new ArgumentNullException(nameof(loggerFilterOptions)); + _loggingFilterHelper = loggingFilterHelper ?? throw new ArgumentNullException(nameof(loggingFilterHelper)); } public ILogger CreateLogger(string categoryName) { - return new ColoredConsoleLogger(categoryName, _loggingFilterHelper); + return new ColoredConsoleLogger(categoryName, _loggingFilterHelper, _loggerFilterOptions); } public void Dispose() diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggerRuleSelector.cs b/src/Azure.Functions.Cli/Diagnostics/LoggerRuleSelector.cs new file mode 100644 index 000000000..7efee6a26 --- /dev/null +++ b/src/Azure.Functions.Cli/Diagnostics/LoggerRuleSelector.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// This file is modified copy of: +// https://github.com/aspnet/Logging/blob/cc350d7ef616ef292c1b4ae7130b8c2b45fc1164/src/Microsoft.Extensions.Logging/LoggerRuleSelector.cs. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal class LoggerRuleSelector + { + public void Select(LoggerFilterOptions options, Type providerType, string category, out LogLevel? minLevel, out Func filter) + { + filter = null; + minLevel = options.MinLevel; + + // Filter rule selection: + // 1. Select rules for current logger type, if there is none, select ones without logger type specified + // 2. Select rules with longest matching categories + // 3. If there nothing matched by category take all rules without category + // 3. If there is only one rule use it's level and filter + // 4. If there are multiple rules use last + // 5. If there are no applicable rules use global minimal level + + LoggerFilterRule current = null; + foreach (var rule in options.Rules) + { + if (IsBetter(rule, current, null, category)) + { + current = rule; + } + } + + if (current != null) + { + filter = current.Filter; + minLevel = current.LogLevel; + } + } + + private static bool IsBetter(LoggerFilterRule rule, LoggerFilterRule current, string logger, string category) + { + // Skip rules with inapplicable type or category + if (rule.ProviderName != null && rule.ProviderName != logger) + { + return false; + } + + if (rule.CategoryName != null && !category.StartsWith(rule.CategoryName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (current?.ProviderName != null) + { + if (rule.ProviderName == null) + { + return false; + } + } + else + { + // We want to skip category check when going from no provider to having provider + if (rule.ProviderName != null) + { + return true; + } + } + + if (current?.CategoryName != null) + { + if (rule.CategoryName == null) + { + return false; + } + + if (current.CategoryName.Length > rule.CategoryName.Length) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs index bee841dec..436996b57 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Azure.WebJobs.Script; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Azure.Functions.Cli.Diagnostics { @@ -19,7 +20,14 @@ public LoggingBuilder(LoggingFilterHelper loggingFilterHelper) public void Configure(ILoggingBuilder builder) { - _loggingFilterHelper.AddConsoleLoggingProvider(builder); + builder.Services.AddSingleton(p => + { + //Cache LoggerFilterOptions to be used by the logger to filter logs based on content + var filterOptions = p.GetService>(); + return new ColoredConsoleLoggerProvider(_loggingFilterHelper, filterOptions.Value); + }); + + builder.AddFilter((category, level) => true); builder.Services.AddSingleton(provider => { diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 5bab9fbdc..65871b133 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -1,5 +1,6 @@ using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Diagnostics; +using Dynamitey; using Microsoft.Azure.WebJobs.Script.Eventing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -11,9 +12,6 @@ namespace Azure.Functions.Cli { public class LoggingFilterHelper { - private const string DefaultLogLevelKey = "default"; - private IConfigurationRoot _hostJsonConfig = null; - // CI EnvironmentSettings // https://github.com/watson/ci-info/blob/master/index.js#L52-L59 public const string Ci = "CI"; // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari @@ -23,7 +21,6 @@ public class LoggingFilterHelper public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging) { - _hostJsonConfig = hostJsonConfig; VerboseLogging = verboseLogging.HasValue && verboseLogging.Value; if (IsCiEnvironment(verboseLogging.HasValue)) @@ -34,12 +31,10 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi { SystemLogDefaultLogLevel = LogLevel.Information; } - bool defaultLogLevelExists = Utilities.LogLevelExists(hostJsonConfig, DefaultLogLevelKey); - if (defaultLogLevelExists) + if (Utilities.LogLevelExists(hostJsonConfig, Utilities.LogLevelDefaultSection, out LogLevel logLevel)) { - DefaultLogLevel = Utilities.GetHostJsonDefaultLogLevel(hostJsonConfig); - SystemLogDefaultLogLevel = DefaultLogLevel; - UserLogDefaultLogLevel = DefaultLogLevel; + SystemLogDefaultLogLevel = logLevel; + UserLogDefaultLogLevel = logLevel; } } @@ -63,26 +58,6 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi /// public bool VerboseLogging { get; private set; } - internal void AddConsoleLoggingProvider(ILoggingBuilder loggingBuilder) - { - // Filter is needed to force all the logs at jobhost level - loggingBuilder.AddFilter((category, level) => true).AddProvider(new ColoredConsoleLoggerProvider(this)); - } - - internal bool IsEnabled(string category, LogLevel logLevel) - { - if (_hostJsonConfig != null && Utilities.LogLevelExists(_hostJsonConfig, category)) - { - // If category exists in `loglevel` section, ensure defaults do not apply. - return Utilities.UserLoggingFilter(logLevel); - } - if (DefaultLogLevel == LogLevel.None) - { - return false; - } - return Utilities.DefaultLoggingFilter(category, logLevel, UserLogDefaultLogLevel, SystemLogDefaultLogLevel); - } - internal bool IsCiEnvironment(bool verboseLoggingArgExists) { if (verboseLoggingArgExists) diff --git a/src/Azure.Functions.Cli/Diagnostics/ProviderAliasUtilities.cs b/src/Azure.Functions.Cli/Diagnostics/ProviderAliasUtilities.cs new file mode 100644 index 000000000..b12e3cee8 --- /dev/null +++ b/src/Azure.Functions.Cli/Diagnostics/ProviderAliasUtilities.cs @@ -0,0 +1,18 @@ +// 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.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Logging +{ + internal static class ProviderAliasUtilities + { + internal static string GetAlias(Type providerType) + { + var attribute = providerType.GetCustomAttributes(inherit: false).FirstOrDefault(); + return attribute?.Alias; + } + } +} diff --git a/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs index c3a95b193..1671249b5 100644 --- a/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs +++ b/test/Azure.Functions.Cli.Tests/ColoredConsoleLoggerTests.cs @@ -1,16 +1,26 @@ using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Diagnostics; using Microsoft.Extensions.Configuration; -using System.IO; -using System.Text; +using Microsoft.Extensions.Logging; using System.Threading.Tasks; using Xunit; +using System.Collections.Generic; namespace Azure.Functions.Cli.Tests { public class ColoredConsoleLoggerTests { - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] + IConfigurationRoot _testConfiguration; + + public ColoredConsoleLoggerTests() + { + _testConfiguration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + { "AzureFunctionsJobHost:Logging:LogLevel:Host.Startup", "Debug" } + }).Build(); + } + + [Theory] [InlineData("somelog", false)] [InlineData("Worker process started and initialized.", true)] [InlineData("Worker PROCESS started and initialized.", true)] @@ -18,16 +28,14 @@ public class ColoredConsoleLoggerTests [InlineData("Host lock lease acquired by instance ID", true)] [InlineData("Host lock lease acquired by instance id", true)] [InlineData("Host lock lease", false)] - public async Task DoesMessageStartsWithWhiteListedPrefix_Tests(string formattedMessage, bool expected) + public void DoesMessageStartsWithWhiteListedPrefix_Tests(string formattedMessage, bool expected) { - string defaultJson = "{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}"; - await FileSystemHelpers.WriteToFile("host.json", new MemoryStream(Encoding.ASCII.GetBytes(defaultJson))); - var testConfiguration = new ConfigurationBuilder().AddJsonFile("host.json").Build(); - ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(testConfiguration, true)); + + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(_testConfiguration, true), new LoggerFilterOptions()); Assert.Equal(expected, coloredConsoleLogger.DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)); } - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] + [Theory] [InlineData("somelog", false)] [InlineData("Worker process started and initialized.", true)] [InlineData("Worker PROCESS started and initialized.", true)] @@ -35,13 +43,54 @@ public async Task DoesMessageStartsWithWhiteListedPrefix_Tests(string formattedM [InlineData("Host lock lease acquired by instance ID", true)] [InlineData("Host lock lease acquired by instance id", true)] [InlineData("Host lock lease", false)] - public async Task IsEnabled_Tests(string formattedMessage, bool expected) + public void IsEnabled_Tests(string formattedMessage, bool expected) { - string defaultJson = "{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}"; - await FileSystemHelpers.WriteToFile("host.json", new MemoryStream(Encoding.ASCII.GetBytes(defaultJson))); - var testConfiguration = new ConfigurationBuilder().AddJsonFile("host.json").Build(); - ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(testConfiguration, true)); + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(_testConfiguration, true), new LoggerFilterOptions()); Assert.Equal(expected, coloredConsoleLogger.DoesMessageStartsWithAllowedLogsPrefix(formattedMessage)); } + + [Theory] + [InlineData("Random", LogLevel.Information, true)] + [InlineData("Random", LogLevel.Debug, false)] + [InlineData("Host.Startup", LogLevel.Information, true)] + [InlineData("Host.Startup", LogLevel.Trace, false)] + [InlineData("Host.General", LogLevel.Trace, false)] + [InlineData("Host.General", LogLevel.Debug, false)] + [InlineData("Host.General", LogLevel.Information, false)] + public void IsEnabled_LoggerFilterTests_Tests(string inputCategory, LogLevel inputLogLevel, bool expected) + { + LoggerFilterRule loggerFilterRule1 = new LoggerFilterRule(null, "Host.", LogLevel.None, null); + LoggerFilterRule loggerFilterRule2 = new LoggerFilterRule(null, "Host.Startup", LogLevel.Debug, null); + + LoggerFilterOptions customFilterOptions = new LoggerFilterOptions(); + customFilterOptions.MinLevel = LogLevel.Information; + customFilterOptions.Rules.Add(loggerFilterRule1); + customFilterOptions.Rules.Add(loggerFilterRule2); + + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger(inputCategory, new LoggingFilterHelper(_testConfiguration, true), customFilterOptions); + bool result = coloredConsoleLogger.IsEnabled(inputCategory, inputLogLevel); + Assert.Equal(expected, result); + } + + [Fact] + public void SelectRule_Tests() + { + List loggerFilterRules = new List(); + LoggerFilterRule loggerFilterRule = new LoggerFilterRule(null, "Host.", LogLevel.Trace, null); + loggerFilterRules.Add(loggerFilterRule); + + LoggerFilterOptions customFilterOptions = new LoggerFilterOptions(); + customFilterOptions.MinLevel = LogLevel.Information; + customFilterOptions.Rules.Add(loggerFilterRule); + + ColoredConsoleLogger coloredConsoleLogger = new ColoredConsoleLogger("test", new LoggingFilterHelper(_testConfiguration, true), customFilterOptions); + var startupLogsRule = coloredConsoleLogger.SelectRule("Host.Startup", customFilterOptions); + Assert.NotNull(startupLogsRule.LogLevel); + Assert.Equal(LogLevel.Trace, startupLogsRule.LogLevel); + + var functionLogsRule = coloredConsoleLogger.SelectRule("Function.TestFunction", customFilterOptions); + Assert.NotNull(functionLogsRule.LogLevel); + Assert.Equal(LogLevel.Information, functionLogsRule.LogLevel); + } } } diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index a70ec599a..f2d593aa7 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -36,6 +36,10 @@ await CliTester.Run(new RunConfiguration "Functions:", "HttpTrigger: [GET,POST] http://localhost:7071/api/HttpTrigger" }, + OutputDoesntContain = new string[] + { + "Initializing function HTTP routes" + }, Test = async (workingDir, p) => { using (var client = new HttpClient() { BaseAddress = new Uri("http://localhost:7071/") }) @@ -45,7 +49,7 @@ await CliTester.Run(new RunConfiguration var result = await response.Content.ReadAsStringAsync(); p.Kill(); result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - } + } }, }, _output); } @@ -75,6 +79,126 @@ await CliTester.Run(new RunConfiguration } + [Fact] + public async Task start_nodejs_loglevel_overrriden_in_settings() + { + await CliTester.Run(new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + "settings add AzureFunctionsJobHost__logging__logLevel__Default Debug", + "new --template \"Http trigger\" --name HttpTrigger", + "start" + }, + ExpectExit = false, + OutputContains = new[] + { + "Worker path for language worker node" + }, + Test = async (_, p) => + { + await Task.Delay(TimeSpan.FromSeconds(15)); + p.Kill(); + }, + }, _output); + } + + [Fact] + public async Task start_loglevel_overrriden_in_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\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}"; + await File.WriteAllTextAsync(filePath, hostJsonContent); + }, + }, + new RunConfiguration + { + Commands = new[] + { + "start" + }, + ExpectExit = false, + OutputContains = new [] + { + "Workers Directory set to" + }, + Test = async (_, p) => + { + // give the host time to load functions and print any errors + await Task.Delay(TimeSpan.FromSeconds(10)); + p.Kill(); + } + }, + }, _output, startHost: true); + } + + [Fact] + public async Task start_loglevel_None_overrriden_in_host_json() + { + var functionName = "HttpTrigger"; + + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + $"new --template Httptrigger --name {functionName}", + }, + Test = async (workingDir, p) => + { + var filePath = Path.Combine(workingDir, "host.json"); + string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"None\"}}}"; + await File.WriteAllTextAsync(filePath, hostJsonContent); + }, + }, + new RunConfiguration + { + Commands = new[] + { + "start" + }, + ExpectExit = false, + OutputContains = new [] + { + "Worker process started and initialized" + }, + OutputDoesntContain = new string[] + { + "Initializing function HTTP routes" + }, + Test = async (_, p) => + { + try + { + // give the host time to load functions and print any errors + await Task.Delay(TimeSpan.FromSeconds(15)); + p.Kill(); + } + catch + { + //ignore + } + } + }, + }, _output, startHost: true); + } + [Fact] public async Task start_dotnet_csharp() { diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs index 99204e468..33a21ecda 100644 --- a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -96,26 +96,6 @@ public void LoggingFilterHelper_Tests(string hostJsonContent, bool? verboseLoggi } } - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", "test", LogLevel.Information, false)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.Startup", LogLevel.Information, true)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Information, false)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", LogLevel.Warning, true)] - public void IsEnabled_Tests(string hostJsonContent, string category, LogLevel logLevel, bool expected) - { - try - { - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, false); - Assert.Equal(expected, loggingFilterHelper.IsEnabled(category, logLevel)); - } - finally - { - DeleteIfExists(_workerDir); - } - } - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] [InlineData(false, null, false)] [InlineData(true, false, false)] diff --git a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs index 694337efb..5a20f7918 100644 --- a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs +++ b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs @@ -1,6 +1,7 @@ using Azure.Functions.Cli.Common; using Microsoft.Azure.WebJobs.Script; using Microsoft.Extensions.Logging; +using System; using System.IO; using Xunit; @@ -52,7 +53,8 @@ public void GetHostJsonDefaultLogLevel_Test(string hostJsonContent, LogLevel exp { FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LogLevel actualLogLevel = Utilities.GetHostJsonDefaultLogLevel(configuration); + LogLevel actualLogLevel; + bool result = Utilities.LogLevelExists(configuration, Utilities.LogLevelDefaultSection, out actualLogLevel); Assert.Equal(actualLogLevel, expectedLogLevel); } finally @@ -65,6 +67,8 @@ public void GetHostJsonDefaultLogLevel_Test(string hostJsonContent, LogLevel exp [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.General\": \"Debug\"}}}", "Host.General", true)] [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", false)] [InlineData("{\"version\": \"2.0\"}", "Function.HttpFunction", false)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Function.HttpFunction\": \"Debug\"}}}", "Function.HttpFunction.User", true)] + [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Function.HttpFunction\": \"Debug\"}}}", "Function.HTTPFunction.USER", true)] public void LogLevelExists_Test(string hostJsonContent, string category, bool expected) { try @@ -72,7 +76,13 @@ public void LogLevelExists_Test(string hostJsonContent, string category, bool ex FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - Assert.Equal(expected, Utilities.LogLevelExists(configuration, category)); + LogLevel logLevel; + bool result = Utilities.LogLevelExists(configuration, category, out logLevel); + Assert.Equal(expected, result); + if (result) + { + Assert.Equal(LogLevel.Debug, logLevel); + } } finally { @@ -98,15 +108,6 @@ public void JobHostConfigSectionExists_Test(string hostJsonContent, string secti } } - [Theory] - [InlineData(LogLevel.None, false)] - [InlineData(LogLevel.Debug, true)] - [InlineData(LogLevel.Information, true)] - public void UserLoggingFilter_Test(LogLevel inputLogLevel, bool expected) - { - Assert.Equal(expected, Utilities.UserLoggingFilter(inputLogLevel)); - } - [Theory] [InlineData("Function.Function1", LogLevel.None, true)] [InlineData("Function.Function1", LogLevel.Warning, true)] From 3a59efba0cbf74dfaf1911de5f073df97259d9f3 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Tue, 22 Sep 2020 13:16:59 -0700 Subject: [PATCH 094/127] Add variable to display ascii logo (#2234) (#2238) Co-authored-by: Anthony Chu --- .../Actions/HostActions/StartHostAction.cs | 8 +++++++- src/Azure.Functions.Cli/Common/Constants.cs | 2 ++ src/Azure.Functions.Cli/Common/Utilities.cs | 5 +++-- .../Diagnostics/ColoredConsoleLogger.cs | 3 ++- src/Azure.Functions.Cli/Helpers/EnvironmentHelper.cs | 8 ++++++++ test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 3 ++- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index a0276dff6..044571bb5 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -266,10 +266,16 @@ private void UpdateEnvironmentVariables(IDictionary secrets) public override async Task RunAsync() { await PreRunConditions(); - if (VerboseLogging.HasValue && VerboseLogging.Value) + + var isVerbose = VerboseLogging.HasValue && VerboseLogging.Value; + if (isVerbose || EnvironmentHelper.GetEnvironmentVariableAsBool(Constants.DisplayLogo)) { Utilities.PrintLogo(); } + + // Suppress AspNetCoreSupressStatusMessages + EnvironmentHelper.SetEnvironmentVariableAsBoolIfNotExists(Constants.AspNetCoreSupressStatusMessages); + Utilities.PrintVersion(); ScriptApplicationHostOptions hostOptions = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory); diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 62e3d39c6..07f3f107c 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -53,6 +53,8 @@ internal static class Constants public const string PowerShellWorkerDefaultVersion = "~6"; public const string AuthLevelErrorMessage = "Unable to configure Authorization level. The selected template does not use Http Trigger"; public const string HttpTriggerTemplateName = "HttpTrigger"; + public const string DisplayLogo = "FUNCTIONS_CORE_TOOLS_DISPLAY_LOGO"; + public const string AspNetCoreSupressStatusMessages = "ASPNETCORE_SUPPRESSSTATUSMESSAGES"; public static string CliVersion => typeof(Constants).GetTypeInfo().Assembly.GetName().Version.ToString(3); diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index 7dea3039d..e8721ef1b 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -46,8 +46,9 @@ internal static void PrintLogo() internal static void PrintVersion() { ColoredConsole - .WriteLine($"Azure Functions Core Tools ({Constants.CliDetailedVersion})") - .WriteLine($"Function Runtime Version: {ScriptHost.Version}"); + .WriteLine($"\nAzure Functions Core Tools") + .WriteLine($"Core Tools Version: {Constants.CliDetailedVersion}".DarkGray()) + .WriteLine($"Function Runtime Version: {ScriptHost.Version}\n".DarkGray()); } private static RichString AlternateLogoColor(string str, int firstColorCount = -1) diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs index f56b43132..d28476f83 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs @@ -6,6 +6,7 @@ using Azure.Functions.Cli.Common; using static Azure.Functions.Cli.Common.OutputTheme; using System.Linq; +using Colors.Net.StringColorExtensions; namespace Azure.Functions.Cli.Diagnostics { @@ -90,7 +91,7 @@ private void LogToConsole(LogLevel logLevel, Exception exception, string formatt { foreach (var line in GetMessageString(logLevel, formattedMessage, exception)) { - ColoredConsole.WriteLine($"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff")}] {line}"); + ColoredConsole.WriteLine($"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff")}] ".DarkGray() + line); } } diff --git a/src/Azure.Functions.Cli/Helpers/EnvironmentHelper.cs b/src/Azure.Functions.Cli/Helpers/EnvironmentHelper.cs index 647c2313f..04d50e29f 100644 --- a/src/Azure.Functions.Cli/Helpers/EnvironmentHelper.cs +++ b/src/Azure.Functions.Cli/Helpers/EnvironmentHelper.cs @@ -17,5 +17,13 @@ public static bool GetEnvironmentVariableAsBool(string keyName) return val.Equals("1") || val.Equals("true", StringComparison.OrdinalIgnoreCase); } + + public static void SetEnvironmentVariableAsBoolIfNotExists(string keyName) + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(keyName))) + { + Environment.SetEnvironmentVariable(keyName, "true"); + } + } } } diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index f2d593aa7..015d3bbf4 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -38,7 +38,8 @@ await CliTester.Run(new RunConfiguration }, OutputDoesntContain = new string[] { - "Initializing function HTTP routes" + "Initializing function HTTP routes", + "Content root path:" // ASPNETCORE_SUPPRESSSTATUSMESSAGES is set to true by default }, Test = async (workingDir, p) => { From 2f5369afbdeb3e9ded70dccc93388f2b186b2093 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 30 Sep 2020 12:36:40 -0700 Subject: [PATCH 095/127] throw error when using unsupported build flag (#2241) --- .../Actions/AzureActions/PublishFunctionAppAction.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 04f120ce7..630e015ac 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -119,6 +119,11 @@ public override async Task RunAsync() // Get function app var functionApp = await AzureHelper.GetFunctionApp(FunctionAppName, AccessToken, ManagementURL, Slot, Subscription); + if (!functionApp.IsLinux && (PublishBuildOption == BuildOption.Container || PublishBuildOption == BuildOption.Remote)) + { + throw new CliException($"--build {PublishBuildOption} is not supported for Windows Function Apps."); + } + // Get the GitIgnoreParser from the functionApp root var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory); var ignoreParser = PublishHelper.GetIgnoreParser(functionAppRoot); From 063d022b3dad84b812490c32ee173cda07f3f6e7 Mon Sep 17 00:00:00 2001 From: Pragna Gopa Date: Wed, 30 Sep 2020 14:40:08 -0700 Subject: [PATCH 096/127] [Logging]Fix DateTime format, enable tests (#2246) --- .../Diagnostics/ColoredConsoleLogger.cs | 3 +- .../Diagnostics/LoggingFilterHelper.cs | 5 - .../E2E/StartTests.cs | 46 ++++++ .../LoggingFilterHelperTests.cs | 111 ++++---------- test/Azure.Functions.Cli.Tests/TestUtils.cs | 21 +++ .../UtilitiesTests.cs | 138 ++++-------------- 6 files changed, 124 insertions(+), 200 deletions(-) create mode 100644 test/Azure.Functions.Cli.Tests/TestUtils.cs diff --git a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs index d28476f83..bfb2ebada 100644 --- a/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs +++ b/src/Azure.Functions.Cli/Diagnostics/ColoredConsoleLogger.cs @@ -7,6 +7,7 @@ using static Azure.Functions.Cli.Common.OutputTheme; using System.Linq; using Colors.Net.StringColorExtensions; +using System.Globalization; namespace Azure.Functions.Cli.Diagnostics { @@ -91,7 +92,7 @@ private void LogToConsole(LogLevel logLevel, Exception exception, string formatt { foreach (var line in GetMessageString(logLevel, formattedMessage, exception)) { - ColoredConsole.WriteLine($"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff")}] ".DarkGray() + line); + ColoredConsole.WriteLine($"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffZ", CultureInfo.InvariantCulture)}] ".DarkGray() + line); } } diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 65871b133..057e6375c 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -48,11 +48,6 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi /// public LogLevel UserLogDefaultLogLevel { get; } = LogLevel.Information; - /// - /// Default log level set in host.json. If not present, deafaults to Information - /// - public LogLevel DefaultLogLevel { get; private set; } = LogLevel.Information; - /// /// Is set to true if `func start` is started with `--verbose` flag. If set, SystemLogDefaultLogLevel is set to Information /// diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 015d3bbf4..17a02cdfb 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -147,6 +147,52 @@ await CliTester.Run(new RunConfiguration[] }, _output, startHost: true); } + [Fact] + public async Task start_loglevel_overrriden_in_host_json_category_filter() + { + 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\",\"logging\": {\"logLevel\": {\"Default\": \"None\", \"Host.Startup\": \"Information\"}}}"; + await File.WriteAllTextAsync(filePath, hostJsonContent); + }, + }, + new RunConfiguration + { + Commands = new[] + { + "start" + }, + ExpectExit = false, + OutputContains = new [] + { + "Found the following functions:" + }, + OutputDoesntContain = new string[] + { + "Reading host configuration file" + }, + Test = async (_, p) => + { + // give the host time to load functions and print any errors + await Task.Delay(TimeSpan.FromSeconds(10)); + p.Kill(); + } + }, + }, _output, startHost: true); + } + [Fact] public async Task start_loglevel_None_overrriden_in_host_json() { diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs index 33a21ecda..4c63dc9df 100644 --- a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -1,102 +1,53 @@ -using Azure.Functions.Cli.Common; -using Azure.Functions.Cli.Diagnostics; -using Microsoft.Azure.WebJobs.Script; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Azure.WebJobs.Script.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; -using System.IO; +using System.Collections.Generic; using Xunit; namespace Azure.Functions.Cli.Tests { public class LoggingFilterHelperTests { - private ScriptApplicationHostOptions _hostOptions; - private string _workerDir; - private string _hostJsonFilePath; - - public LoggingFilterHelperTests() + [Theory] + [InlineData("default", true, LogLevel.None)] + [InlineData("Default", true, LogLevel.Debug)] + [InlineData(null, null, LogLevel.Information)] + [InlineData("Host.Startup", true, LogLevel.Information)] + public void LoggingFilterHelper_Tests(string categoryKey, bool? verboseLogging, LogLevel expectedDefaultLogLevel) { - try + + var settings = new Dictionary(); + if (!string.IsNullOrEmpty(categoryKey)) { - _workerDir = Path.GetTempFileName(); - DeleteIfExists(_workerDir); - Directory.CreateDirectory(_workerDir); - _hostOptions = new ScriptApplicationHostOptions - { - ScriptPath = _workerDir - }; + settings.Add(ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "logging", "loglevel", categoryKey), expectedDefaultLogLevel.ToString()); } - catch + var testConfiguration = TestUtils.CreateSetupWithConfiguration(settings); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(testConfiguration, verboseLogging); + if (verboseLogging == null) { - _hostOptions = new ScriptApplicationHostOptions - { - ScriptPath = Directory.GetCurrentDirectory() - }; - } - _hostJsonFilePath = Path.Combine(_hostOptions.ScriptPath, Constants.HostJsonFileName); - } - - private void DeleteIfExists(string filePath) - { - try - { - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - else if (Directory.Exists(filePath)) - { - Directory.Delete(filePath, recursive: true); - } + Assert.False(loggingFilterHelper.VerboseLogging); } - catch + if ( !string.IsNullOrEmpty(categoryKey) && categoryKey.Equals("Default", StringComparison.OrdinalIgnoreCase)) { + Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); + Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); } - } - - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", true, LogLevel.None)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"DEBUG\"}}}", true, LogLevel.Debug)] - [InlineData("{\"version\": \"2.0\"}", null, LogLevel.Information)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", true, LogLevel.Information)] - public void LoggingFilterHelper_Tests(string hostJsonContent, bool? verboseLogging, LogLevel expectedDefaultLogLevel) - { - try + else { - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); - if (verboseLogging == null) + Assert.Equal(LogLevel.Information, loggingFilterHelper.UserLogDefaultLogLevel); + if (verboseLogging.HasValue && verboseLogging.Value) { - Assert.False(loggingFilterHelper.VerboseLogging); - } - Assert.Equal(loggingFilterHelper.DefaultLogLevel, expectedDefaultLogLevel); - if (hostJsonContent.Contains("Default")) - { - Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); - Assert.Equal(loggingFilterHelper.DefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); + Assert.Equal(LogLevel.Information, loggingFilterHelper.SystemLogDefaultLogLevel); } else { - Assert.Equal(LogLevel.Information, loggingFilterHelper.UserLogDefaultLogLevel); - if (verboseLogging.HasValue && verboseLogging.Value) - { - Assert.Equal(LogLevel.Information, loggingFilterHelper.SystemLogDefaultLogLevel); - } - else - { - Assert.Equal(LogLevel.Warning, loggingFilterHelper.SystemLogDefaultLogLevel); - } + Assert.Equal(LogLevel.Warning, loggingFilterHelper.SystemLogDefaultLogLevel); } } - finally - { - DeleteIfExists(_workerDir); - } } - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] + [Theory] [InlineData(false, null, false)] [InlineData(true, false, false)] [InlineData(true, null, true)] @@ -108,16 +59,14 @@ public void IsCI_Tests(bool isCiEnv, bool? verboseLogging, bool expected) { Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, "90l99"); } - string defaultJson = "{\"version\": \"2.0\"}"; - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, defaultJson); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(configuration, verboseLogging); + var testConfiguration = TestUtils.CreateSetupWithConfiguration(null); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(testConfiguration, verboseLogging); Assert.Equal(expected, loggingFilterHelper.IsCiEnvironment(verboseLogging.HasValue)); - Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, ""); + } finally { - DeleteIfExists(_workerDir); + Environment.SetEnvironmentVariable(LoggingFilterHelper.Ci_Build_Number, ""); } } } diff --git a/test/Azure.Functions.Cli.Tests/TestUtils.cs b/test/Azure.Functions.Cli.Tests/TestUtils.cs new file mode 100644 index 000000000..dd729e409 --- /dev/null +++ b/test/Azure.Functions.Cli.Tests/TestUtils.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Functions.Cli.Tests +{ + public static class TestUtils + { + public static IConfigurationRoot CreateSetupWithConfiguration(Dictionary settings = null) + { + var builder = new ConfigurationBuilder(); + if (settings != null) + { + builder.AddInMemoryCollection(settings); + } + + return builder.Build(); + } + } +} diff --git a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs index 5a20f7918..02e12e1a7 100644 --- a/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs +++ b/test/Azure.Functions.Cli.Tests/UtilitiesTests.cs @@ -1,111 +1,41 @@ -using Azure.Functions.Cli.Common; -using Microsoft.Azure.WebJobs.Script; +using Microsoft.Azure.WebJobs.Script.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using System; -using System.IO; +using System.Collections.Generic; using Xunit; namespace Azure.Functions.Cli.Tests { public class UtilitiesTests { - private string _workerDir; - private ScriptApplicationHostOptions _hostOptions; - private string _hostJsonFilePath; - - public UtilitiesTests() - { - try - { - _workerDir = Path.GetTempFileName(); - if (File.Exists(_workerDir)) - { - File.Delete(_workerDir); - } - else if (Directory.Exists(_workerDir)) - { - Directory.Delete(_workerDir, recursive: true); - } - Directory.CreateDirectory(_workerDir); - _hostOptions = new ScriptApplicationHostOptions - { - ScriptPath = _workerDir - }; - } - catch - { - _hostOptions = new ScriptApplicationHostOptions - { - ScriptPath = Directory.GetCurrentDirectory() - }; - } - _hostJsonFilePath = Path.Combine(_hostOptions.ScriptPath, Constants.HostJsonFileName); - } - - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Default\": \"None\"}}}", LogLevel.None)] - [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"NONE\"}}}", LogLevel.None)] - [InlineData("{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}", LogLevel.Debug)] - [InlineData("{\"version\": \"2.0\"}", LogLevel.Information)] - public void GetHostJsonDefaultLogLevel_Test(string hostJsonContent, LogLevel expectedLogLevel) + [Theory] + [InlineData(LogLevel.None)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public void GetHostJsonDefaultLogLevel_Test(LogLevel expectedLogLevel) { - try - { - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LogLevel actualLogLevel; - bool result = Utilities.LogLevelExists(configuration, Utilities.LogLevelDefaultSection, out actualLogLevel); - Assert.Equal(actualLogLevel, expectedLogLevel); - } - finally - { - DeleteIfExists(_workerDir); - } + var settings = new Dictionary(); + settings.Add(ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "logging", "loglevel", "default"), expectedLogLevel.ToString()); + var testConfiguration = TestUtils.CreateSetupWithConfiguration(settings); + LogLevel actualLogLevel; + bool result = Utilities.LogLevelExists(testConfiguration, Utilities.LogLevelDefaultSection, out actualLogLevel); + Assert.Equal(actualLogLevel, expectedLogLevel); } - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.General\": \"Debug\"}}}", "Host.General", true)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Host.Startup\": \"Debug\"}}}", "Host.General", false)] - [InlineData("{\"version\": \"2.0\"}", "Function.HttpFunction", false)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Function.HttpFunction\": \"Debug\"}}}", "Function.HttpFunction.User", true)] - [InlineData("{\"version\": \"2.0\",\"Logging\": {\"LogLevel\": {\"Function.HttpFunction\": \"Debug\"}}}", "Function.HTTPFunction.USER", true)] - public void LogLevelExists_Test(string hostJsonContent, string category, bool expected) - { - try - { - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); - - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - LogLevel logLevel; - bool result = Utilities.LogLevelExists(configuration, category, out logLevel); - Assert.Equal(expected, result); - if (result) - { - Assert.Equal(LogLevel.Debug, logLevel); - } - } - finally - { - DeleteIfExists(_workerDir); - } - } - - [Theory(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2174")] - [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "extensionBundle", true)] - [InlineData("{\"version\": \"2.0\",\"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\"version\": \"[1.*, 2.0.0)\"}}", "ExtensionBundle", true)] - [InlineData("{\"version\": \"2.0\"}", "extensionBundle", false)] - public void JobHostConfigSectionExists_Test(string hostJsonContent, string section, bool expected) + [Theory] + [InlineData("ExtensionBundle", true)] + [InlineData("extensionBundle", false)] + public void JobHostConfigSectionExists_Test(string section, bool expected) { - try - { - FileSystemHelpers.WriteAllTextToFile(_hostJsonFilePath, hostJsonContent); - var configuration = Utilities.BuildHostJsonConfigutation(_hostOptions); - Assert.Equal(expected, Utilities.JobHostConfigSectionExists(configuration, section)); - } - finally + var settings = new Dictionary(); + if (expected) { - DeleteIfExists(_workerDir); + settings.Add(ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "extensionBundle", "id"), "Microsoft.Azure.Functions.ExtensionBundle"); + settings.Add(ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "extensionBundle", "version"), "[1.*, 2.0.0)"); } + + var testConfiguration = TestUtils.CreateSetupWithConfiguration(settings); + Assert.Equal(expected, Utilities.JobHostConfigSectionExists(testConfiguration, section)); } [Theory] @@ -130,23 +60,5 @@ public void IsSystemLogCategory_Test(string inputCategory, bool expected) { Assert.Equal(expected, Utilities.IsSystemLogCategory(inputCategory)); } - - private void DeleteIfExists(string filePath) - { - try - { - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - else if (Directory.Exists(filePath)) - { - Directory.Delete(filePath, recursive: true); - } - } - catch - { - } - } } } From 6499b57e6984aa08db442666f2ff096a6dacd5cb Mon Sep 17 00:00:00 2001 From: Brett Samblanet Date: Tue, 6 Oct 2020 14:21:39 -0700 Subject: [PATCH 097/127] updating WebHost and language worker packages --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 4fa74a9b4..46f325b5f 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -118,11 +118,11 @@ - + - - - + + + From 49dc7db5c7fe928c8c5e8e44d2ce10f16f3b9cff Mon Sep 17 00:00:00 2001 From: Fabio Cavalcante Date: Fri, 23 Oct 2020 14:11:25 -0700 Subject: [PATCH 098/127] Update EnvironmentNativeMethods.cs --- .../NativeMethods/EnvironmentNativeMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/NativeMethods/EnvironmentNativeMethods.cs b/src/Azure.Functions.Cli/NativeMethods/EnvironmentNativeMethods.cs index cbe4f62c0..5331e7de5 100644 --- a/src/Azure.Functions.Cli/NativeMethods/EnvironmentNativeMethods.cs +++ b/src/Azure.Functions.Cli/NativeMethods/EnvironmentNativeMethods.cs @@ -27,9 +27,9 @@ public static void SetEnvironmentVariable(string key, string value) // Any changes through setenv() are ignored. if (StaticSettings.IsDebug) { - ColoredConsole.WriteLine("Setting unsupported .NET environemt variables (empty string) is not implemented for this platform."); + ColoredConsole.WriteLine("Setting unsupported .NET environment variables (empty string) is not implemented for this platform."); } } } } -} \ No newline at end of file +} From d9fb5979f07d2716fabdd0c8164db18ee57529db Mon Sep 17 00:00:00 2001 From: Katy Shimizu Date: Tue, 27 Oct 2020 11:26:27 -0700 Subject: [PATCH 099/127] Adding v2 upgrade warning. (#2273) Co-authored-by: Katy Shimizu --- build/BuildSteps.cs | 3 ++- src/Azure.Functions.Cli/Common/Utilities.cs | 5 +++++ src/Azure.Functions.Cli/Program.cs | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index f2052b280..2e20e0f79 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -506,7 +506,8 @@ private static string CurrentVersion ? Path.Combine(Settings.OutputDir, "win-x86", "func.exe") : Path.Combine(Settings.OutputDir, "linux-x64", "func"); - _version = Shell.GetOutput(funcPath, "--version"); + var match = Regex.Match(Shell.GetOutput(funcPath, "--version"), "^((([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)$", RegexOptions.Multiline); + _version = match.Value; } return _version; } diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index e8721ef1b..1e13da91f 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -51,6 +51,11 @@ internal static void PrintVersion() .WriteLine($"Function Runtime Version: {ScriptHost.Version}\n".DarkGray()); } + internal static void PrintUpgradeWarning() + { + ColoredConsole.WriteLine(OutputTheme.ErrorColor("You are using an outdated version of the Azure Functions Core Tools. For more information, please see: https://aka.ms/func-v2-upgrade")); + } + private static RichString AlternateLogoColor(string str, int firstColorCount = -1) { if (str.Length == 1) diff --git a/src/Azure.Functions.Cli/Program.cs b/src/Azure.Functions.Cli/Program.cs index f46f59ee9..931fbbeb1 100644 --- a/src/Azure.Functions.Cli/Program.cs +++ b/src/Azure.Functions.Cli/Program.cs @@ -1,10 +1,8 @@ using System; +using System.Threading; using Autofac; -using Colors.Net; -using Azure.Functions.Cli.Arm; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Interfaces; -using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli { @@ -15,6 +13,8 @@ internal static void Main(string[] args) FirstTimeCliExperience(); SetupGlobalExceptionHandler(); SetCoreToolsEnvironmentVaraibles(); + Utilities.PrintUpgradeWarning(); + Thread.Sleep(1000); ConsoleApp.Run(args, InitializeAutofacContainer()); } From 74e5df4f2d00d9b3d1efe76aeabbcc738b48e1dc Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Fri, 30 Oct 2020 10:52:25 -0700 Subject: [PATCH 100/127] Add test to ensure strong name assemblies are not delayed (#2265) --- build/BuildSteps.cs | 66 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 2e20e0f79..30199a4d7 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -21,6 +21,9 @@ public static class BuildSteps { private static readonly string _wwwroot = Environment.ExpandEnvironmentVariables(@"%HOME%\site\wwwroot"); + private static readonly string _delaySignedOutput = "delay-signed"; + private static readonly string _testSignedOutput = "test-signed"; + public static void Clean() { Directory.Delete(Settings.OutputDir, recursive: true); @@ -278,13 +281,7 @@ public static void TestPreSignedArtifacts() unSignedFiles.ForEach(filePath => File.Delete(filePath)); - var unSignedPackages = GetUnsignedBinaries(targetDir); - if (unSignedPackages.Count() != 0) - { - var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); - ColoredConsole.Error.WriteLine($"This files are missing valid signatures: {Environment.NewLine}{missingSignature}"); - throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); - } + CheckSignedBinaries(targetDir); } } @@ -301,17 +298,29 @@ public static void TestSignedArtifacts() Directory.CreateDirectory(targetDir); ZipFile.ExtractToDirectory(zipFilePath, targetDir); - var unSignedPackages = GetUnsignedBinaries(targetDir); - if (unSignedPackages.Count() != 0) - { - var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); - ColoredConsole.Error.WriteLine($"This files are missing valid signatures: {Environment.NewLine}{missingSignature}"); - throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); - } + CheckSignedBinaries(targetDir); } } } + private static void CheckSignedBinaries(string targetDir) + { + var unSignedPackages = GetUnsignedBinaries(targetDir); + if (unSignedPackages.Count() != 0) + { + var missingSignature = string.Join($",{Environment.NewLine}", unSignedPackages); + ColoredConsole.Error.WriteLine($"These files are missing valid signatures: {Environment.NewLine}{missingSignature}"); + throw new Exception($"sigcheck.exe test failed. Following files are unsigned: {Environment.NewLine}{missingSignature}"); + } + + var delaySigned = GetDelaySignedBinaries(targetDir); + if (delaySigned.Count() != 0) + { + var delayed = string.Join($",{Environment.NewLine}", delaySigned); + ColoredConsole.Error.WriteLine($"These files with strong names are delay-signed or test-signed: {Environment.NewLine}{delayed}"); + throw new Exception($"sn.exe test failed. Following files are delay-signed or test-signed: {Environment.NewLine}{delayed}"); + } + } public static async Task UploadZipToSignAsync() { @@ -471,6 +480,35 @@ public static List GetUnsignedBinaries(string targetDir) return unSignedPackages; } + private static List GetDelaySignedBinaries(string targetDir) + { + // This will only work in Windows and assumes that "sn" tool is present in this expected location. + // We are ok doing this right now as this doesn't run in a normal build scenario and only when we need to validate + // signing. This only happens today in our release pipeline. + string snLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft SDKs\\Windows\\v10.0A\\bin\\Netfx 4.8 Tools\\sn.exe"); + + Console.WriteLine($"Checking if any assemblies in '{targetDir} are delay-signed or test-signed using tool '{snLocation}'"); + + string[] files = Directory.GetFiles(targetDir, "*", SearchOption.AllDirectories); + var delaySigned = new List(); + + foreach (string file in files) + { + var commandOutput = Shell.GetOutput(snLocation, $" -q -vf {file}", ignoreExitCode: true); + if (commandOutput.Contains(_delaySignedOutput, StringComparison.OrdinalIgnoreCase) + || commandOutput.Contains(_testSignedOutput, StringComparison.OrdinalIgnoreCase)) + { + delaySigned.Add(file); + } + if (!string.IsNullOrEmpty(commandOutput) && !commandOutput.Contains(file)) + { + throw new Exception($"Something went wrong while running 'sn.exe'. Command output does not contain the file name as expected. Output: {commandOutput}"); + } + } + + return delaySigned; + } + public static void Zip() { var version = CurrentVersion; From c191a0af89d59fce1e528dfad8cd23010e04dd99 Mon Sep 17 00:00:00 2001 From: Hanzhang Zeng <48038149+Hazhzeng@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:01:33 -0800 Subject: [PATCH 101/127] Update Python Worker to 1.1.9 (v2) (#2292) * Update Python Worker to 1.1.7 (v2) * Update worker version to 1.1.8 * Bump to version 2.1.1.9 to reduce worker size --- src/Azure.Functions.Cli/Azure.Functions.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj index 46f325b5f..2a10f8310 100644 --- a/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj +++ b/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj @@ -122,7 +122,7 @@ - + From f5984038c7377e9437f71191bff1d5818dd1fa8d Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Thu, 10 Dec 2020 21:39:58 -0800 Subject: [PATCH 102/127] [v2] Only show upgrade warning when an action is invoked (#2327) --- src/Azure.Functions.Cli/ConsoleApp.cs | 3 +++ src/Azure.Functions.Cli/Program.cs | 3 --- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/ConsoleApp.cs b/src/Azure.Functions.Cli/ConsoleApp.cs index e40516e5d..08c51cb8d 100644 --- a/src/Azure.Functions.Cli/ConsoleApp.cs +++ b/src/Azure.Functions.Cli/ConsoleApp.cs @@ -55,6 +55,9 @@ public static async Task RunAsync(string[] args, IContainer container) var action = app.Parse(); if (action != null) { + Utilities.PrintUpgradeWarning(); + await Task.Delay(TimeSpan.FromSeconds(1)); + if (action is IInitializableAction) { var initializableAction = action as IInitializableAction; diff --git a/src/Azure.Functions.Cli/Program.cs b/src/Azure.Functions.Cli/Program.cs index 931fbbeb1..136eb71cc 100644 --- a/src/Azure.Functions.Cli/Program.cs +++ b/src/Azure.Functions.Cli/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using Autofac; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Interfaces; @@ -13,8 +12,6 @@ internal static void Main(string[] args) FirstTimeCliExperience(); SetupGlobalExceptionHandler(); SetCoreToolsEnvironmentVaraibles(); - Utilities.PrintUpgradeWarning(); - Thread.Sleep(1000); ConsoleApp.Run(args, InitializeAutofacContainer()); } diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 17a02cdfb..79f09d6b3 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -470,7 +470,7 @@ await CliTester.Run(new RunConfiguration[] }, _output); } - [Fact] + [Fact(Skip = "https://github.com/Azure/azure-functions-core-tools/issues/2320")] public async Task start_powershell() { await CliTester.Run(new RunConfiguration From 9b06e87eecac03ad6ea1825df8cb1e3c389e5fa1 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Wed, 3 Feb 2021 11:51:58 -0800 Subject: [PATCH 103/127] Use kubernetes proxy for non-LoadBalancer services --- .../KubernetesDeployAction.cs | 13 +- .../Helpers/NetworkHelpers.cs | 14 ++ .../Kubernetes/KubectlHelper.cs | 76 ++++++++- .../Kubernetes/KubernetesHelpers.cs | 154 ++++++++++++------ .../Models/Kubernetes/ContainerV1.cs | 36 ++++ .../Kubernetes/Models/Kubernetes/ServiceV1.cs | 3 + 6 files changed, 244 insertions(+), 52 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs index b27f54acf..b759de3d2 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs @@ -43,6 +43,7 @@ class KubernetesDeployAction : BaseAction public bool IgnoreErrors { get; private set; } = false; public int? MaxReplicaCount { get; private set; } public int? MinReplicaCount { get; private set; } + public bool ShowServiceFqdn { get; set; } = false; public KubernetesDeployAction(ISecretsManager secretsManager) { @@ -80,6 +81,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) SetFlag("use-config-map", "Use a ConfigMap/V1 instead of a Secret/V1 object for function app settings configurations", c => UseConfigMap = c); SetFlag("dry-run", "Show the deployment template", f => DryRun = f); SetFlag("ignore-errors", "Proceed with the deployment if a resource returns an error. Default: false", f => IgnoreErrors = f); + SetFlag("show-service-fqdn", "display Http Trigger URL with kubernetes FQDN rather than IP. Default: false", f => ShowServiceFqdn = f); return base.ParseArgs(args); } @@ -148,9 +150,18 @@ public override async Task RunAsync() { await KubectlHelper.KubectlApply(resource, showOutput: true, ignoreError: IgnoreErrors, @namespace: Namespace); } + var httpService = resources + .Where(i => i is ServiceV1) + .Cast() + .FirstOrDefault(s => s.Metadata.Name.Contains("http")); + var httpDeployment = resources + .Where(i => i is DeploymentV1Apps) + .Cast() + .FirstOrDefault(d => d.Metadata.Name.Contains("http")); + await KubernetesHelper.WaitForDeploymentRolleout(httpDeployment); //Print the function keys message to the console - await KubernetesHelper.PrintFunctionsInfo($"{Name}-http", Namespace, funcKeys, triggers); + await KubernetesHelper.PrintFunctionsInfo(httpDeployment, httpService, funcKeys, triggers, ShowServiceFqdn); } } diff --git a/src/Azure.Functions.Cli/Helpers/NetworkHelpers.cs b/src/Azure.Functions.Cli/Helpers/NetworkHelpers.cs index 384adbdf8..af0aec8c3 100644 --- a/src/Azure.Functions.Cli/Helpers/NetworkHelpers.cs +++ b/src/Azure.Functions.Cli/Helpers/NetworkHelpers.cs @@ -19,5 +19,19 @@ public static bool IsPortAvailable(int port) return false; } } + + public static int GetAvailablePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + try + { + listener.Start(); + return ((IPEndPoint)listener.LocalEndpoint).Port; + } + finally + { + listener.Stop(); + } + } } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Kubernetes/KubectlHelper.cs b/src/Azure.Functions.Cli/Kubernetes/KubectlHelper.cs index ea8dd933a..e42f1c684 100644 --- a/src/Azure.Functions.Cli/Kubernetes/KubectlHelper.cs +++ b/src/Azure.Functions.Cli/Kubernetes/KubectlHelper.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Kubernetes.Models.Kubernetes; using Colors.Net; using Newtonsoft.Json; using static Azure.Functions.Cli.Common.OutputTheme; @@ -33,17 +35,17 @@ public static async Task KubectlGet(string resource) return JsonConvert.DeserializeObject(output); } - public static async Task<(string output, string error, int exitCode)> RunKubectl(string cmd, bool ignoreError = false, bool showOutput = false, string stdIn = null) + public static async Task<(string output, string error, int exitCode)> RunKubectl(string cmd, bool ignoreError = false, bool showOutput = false, string stdIn = null, TimeSpan? timeout = null) { - var docker = new Executable("kubectl", cmd); + var kubectl = new Executable("kubectl", cmd); var sbError = new StringBuilder(); var sbOutput = new StringBuilder(); - var exitCode = await docker.RunAsync(l => output(l), e => error(e), stdIn: stdIn); + var exitCode = await kubectl.RunAsync(l => output(l), e => error(e), stdIn: stdIn, timeout: timeout); if (exitCode != 0 && !ignoreError) { - throw new CliException($"Error running {docker.Command}.\n" + + throw new CliException($"Error running {kubectl.Command}.\n" + $"output: {sbOutput.ToString()}\n{sbError.ToString()}"); } @@ -65,7 +67,73 @@ void error(string line) ColoredConsole.Error.WriteLine(line); } } + } + + public static Task RunKubectlProxy(IKubernetesResource resource, int targetPort, int localPort, TimeSpan? timeout = null) + { + var kubectl = new Executable("kubectl", $"port-forward {GetResourceFullName(resource)} {localPort}:{targetPort}"); + var sbError = new StringBuilder(); + var sbOutput = new StringBuilder(); + var tcs = new TaskCompletionSource(); + var deadline = DateTime.Now.Add(timeout ?? TimeSpan.FromSeconds(20)); + + var exitCodeTask = kubectl.RunAsync(l => Output(l, false), e => Output(e, true)); + Task.Run(() => TimeoutFunc()); + return tcs.Task; + + void Output(string line, bool isError) + { + if (!isError && line?.Contains("Forwarding from") == true) + { + tcs.TrySetResult(kubectl.Process); + } + else if (!isError) + { + sbOutput.AppendLine(line); + } + else + { + sbError.AppendLine(line); + } + } + + async Task TimeoutFunc() + { + while(DateTime.Now < deadline) + { + if (tcs.Task.IsCompleted) + { + return; + } + else if (exitCodeTask.IsFaulted || exitCodeTask.IsCompleted) + { + tcs.TrySetException(new Exception($"Unable to proxy request to kubernetes api-server: exitCode: {exitCodeTask.Result}, {sbOutput}, {sbError}")); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + if (!tcs.Task.IsCompleted) + { + tcs.TrySetException(new TimeoutException("Timedout trying to establish proxy to kubernetes api-server")); + } + } + string GetResourceFullName(IKubernetesResource r) + { + switch (r) + { + case DeploymentV1Apps deployment: + return $"deployment/{deployment.Metadata.Name} --namespace {deployment.Metadata.Namespace}"; + case ServiceV1 service: + return $"service/{service.Metadata.Name} --namespace {service.Metadata.Namespace}"; + case PodTemplateV1 pod: + return $"pod/{pod.Metadata.Name} --namespace {pod.Metadata.Namespace}"; + default: + throw new ArgumentException($"type {r.GetType()} is not supported"); + } + } } } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs index f8dfa7ff8..9a304b9aa 100644 --- a/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs +++ b/src/Azure.Functions.Cli/Kubernetes/KubernetesHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; @@ -273,6 +274,12 @@ internal static string GetKedaResources(string @namespace) return (scaledobject != null ? result.Append(scaledobject) : result, resultantFunctionKeys); } + internal static async Task WaitForDeploymentRolleout(DeploymentV1Apps deployment) + { + await KubectlHelper.RunKubectl($"rollout status deployment {deployment.Metadata.Name} --namespace {deployment.Metadata.Namespace}", showOutput: true, timeout: TimeSpan.FromSeconds(60)); + await Task.Delay(TimeSpan.FromSeconds(2.5)); + } + internal static async Task RemoveKeda(string @namespace) { foreach (var name in _allResourceNames) @@ -302,12 +309,9 @@ private async static Task> GetExistingFunctionKeys(s return new Dictionary(); } - internal async static Task PrintFunctionsInfo(string serviceName, string @namespace, IDictionary funcKeys, TriggersPayload triggers) + internal async static Task PrintFunctionsInfo(DeploymentV1Apps deployment, ServiceV1 service, IDictionary funcKeys, TriggersPayload triggers, bool showServiceFqdn) { - if (string.IsNullOrWhiteSpace(serviceName) - || string.IsNullOrWhiteSpace(@namespace) - || funcKeys?.Any() == false - || triggers == null) + if (funcKeys?.Any() == false || triggers == null) { return; } @@ -316,29 +320,34 @@ internal async static Task PrintFunctionsInfo(string serviceName, string @namesp .Where(b => b.Value["bindings"]?.Any(e => e?["type"].ToString().IndexOf("httpTrigger", StringComparison.OrdinalIgnoreCase) != -1) == true) .Select(item => item.Key); - var loadBalancerIp = await GetLoadBalancerIp(serviceName, @namespace, 24); - if (string.IsNullOrEmpty(loadBalancerIp)) - { - ColoredConsole.WriteLine(WarningColor($"The service: {serviceName} is not yet ready, please re-run the deployment to get the function keys.")); - return; - } - - var masterKey = funcKeys["host.master"]; - if (httpFunctions?.Any() == true) + var localPort = NetworkHelpers.GetAvailablePort(); + Process proxy = null; + try { - foreach (var functionName in httpFunctions) + proxy = await KubectlHelper.RunKubectlProxy( + deployment, + service.Spec.Ports.FirstOrDefault()?.Port ?? 80, + localPort + ); + string baseAddress; + if (showServiceFqdn) + { + baseAddress = $"{service.Metadata.Name}.{service.Metadata.Namespace}.svc.cluster.local"; + } + else { - var getFunctionAdminUri = $"http://{loadBalancerIp}/admin/functions/{functionName}?code={masterKey}"; - var httpResponseMessage = await GetHttpResponse(new HttpRequestMessage(HttpMethod.Get, getFunctionAdminUri), 20); + baseAddress = await GetServiceIp(service); + } - if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.NotFound) + var masterKey = funcKeys["host.master"]; + if (httpFunctions?.Any() == true) + { + foreach (var functionName in httpFunctions) { - ColoredConsole.WriteLine(WarningColor($"The service: {functionName} is not yet ready in the runtime yet, please re-run the deployment to get the function keys.")); - return; - } + var getFunctionAdminUri = $"http://127.0.0.1:{localPort}/admin/functions/{functionName}?code={masterKey}"; + var httpResponseMessage = await GetHttpResponse(new HttpRequestMessage(HttpMethod.Get, getFunctionAdminUri), 20); + httpResponseMessage.EnsureSuccessStatusCode(); - if (httpResponseMessage.IsSuccessStatusCode) - { var responseContent = await httpResponseMessage.Content.ReadAsStringAsync(); var functionsInfo = JsonConvert.DeserializeObject(responseContent); @@ -363,13 +372,14 @@ internal async static Task PrintFunctionsInfo(string serviceName, string @namesp ColoredConsole.WriteLine($"\t{functionName} - [{VerboseColor(trigger.ToString())}]"); if (!string.IsNullOrEmpty(functionsInfo.InvokeUrlTemplate)) { + var url = new Uri(new Uri($"http://{baseAddress}"), new Uri(functionsInfo.InvokeUrlTemplate).PathAndQuery).ToString(); if (showFunctionKey) { - ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor($"{functionsInfo.InvokeUrlTemplate}?code={funcKeys[$"functions.{functionName.ToLower()}.default"]}")}"); + ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor($"{url}?code={funcKeys[$"functions.{functionName.ToLower()}.default"]}")}"); } else { - ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor(functionsInfo.InvokeUrlTemplate)}"); + ColoredConsole.WriteLine($"\tInvoke url: {VerboseColor(url)}"); } } ColoredConsole.WriteLine(); @@ -377,11 +387,28 @@ internal async static Task PrintFunctionsInfo(string serviceName, string @namesp } } } + finally + { + if (proxy != null && !proxy.HasExited) + { + proxy.Kill(); + } + } + //Print the master key as well for the user ColoredConsole.WriteLine($"\tMaster key: {VerboseColor($"{funcKeys[$"host.master"]}")}"); } + public async static Task> GetPods(this DeploymentV1Apps deployment) + { + var selector = deployment.Spec.Selector.MatchLabels + .Aggregate(string.Empty, (a, kv) => $"{a}{kv.Key}={kv.Value},"); + + var pods = await KubectlHelper.KubectlGet>($"pods --selector {selector}"); + return pods.Items; + } + private async static Task GetHttpResponse(HttpRequestMessage httpRequestMessage, int retryCount = 5) { HttpResponseMessage httpResponseMsg = new HttpResponseMessage(); @@ -393,18 +420,28 @@ private async static Task GetHttpResponse(HttpRequestMessag int currentRetry = 0; using (var httpClient = new HttpClient(new HttpClientHandler())) { + httpClient.Timeout = TimeSpan.FromSeconds(2); while (currentRetry++ < retryCount) { - httpResponseMsg = await httpClient.SendAsync(httpRequestMessage.Clone()); - if (httpResponseMsg.IsSuccessStatusCode || - (httpResponseMsg.StatusCode != System.Net.HttpStatusCode.BadGateway - && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.RequestTimeout - && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.GatewayTimeout - && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.NotFound)) + try { - return httpResponseMsg; + httpResponseMsg = await httpClient.SendAsync(httpRequestMessage.Clone()); + if (httpResponseMsg.IsSuccessStatusCode || + (httpResponseMsg.StatusCode != System.Net.HttpStatusCode.BadGateway + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.RequestTimeout + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.GatewayTimeout + && httpResponseMsg.StatusCode != System.Net.HttpStatusCode.NotFound)) + { + return httpResponseMsg; + } + } + catch (Exception e) + { + if (StaticSettings.IsDebug) + { + ColoredConsole.Error.WriteLine(e); + } } - await Task.Delay(new Random().Next(500, 2000)); } } @@ -412,30 +449,27 @@ private async static Task GetHttpResponse(HttpRequestMessag return httpResponseMsg; } - private async static Task GetLoadBalancerIp(string serviceName, string @namespace, int retryCount = 12) + private async static Task GetServiceIp(ServiceV1 service, int retryCount = 12) { - if (string.IsNullOrWhiteSpace(serviceName) - || string.IsNullOrWhiteSpace(@namespace)) - { - return string.Empty; - } - - ColoredConsole.WriteLine(AdditionalInfoColor($"Getting loadbalancer ip for the service: {serviceName}")); int currentRetry = 0; while (currentRetry++ < retryCount) { - (string output, bool serviceExists) = await ResourceExists("service", serviceName, @namespace, true); + (string output, bool serviceExists) = await ResourceExists("service", service.Metadata.Name, service.Metadata.Namespace, true); if (serviceExists) { - var service = TryParse(output); - if (service?.Status?.LoadBalancer?.Ingress?.Any() == true) + service = TryParse(output); + if (service?.Spec?.Type?.Equals("LoadBalancer", StringComparison.OrdinalIgnoreCase) == true && service?.Status?.LoadBalancer?.Ingress?.Any() == true) { - return service?.Status?.LoadBalancer?.Ingress.First().Ip; + return service.Status.LoadBalancer.Ingress.First().Ip; + } + else if (service?.Spec?.Type?.Equals("LoadBalancer", StringComparison.OrdinalIgnoreCase) == false && !string.IsNullOrEmpty(service?.Spec?.ClusterIp)) + { + return service.Spec.ClusterIp; } } + ColoredConsole.WriteLine(AdditionalInfoColor($"Waiting for the service to be ready: {service.Metadata.Name}")); await Task.Delay(5000); - ColoredConsole.WriteLine(AdditionalInfoColor($"Waiting for the service to be ready: {serviceName}")); } return string.Empty; @@ -539,7 +573,33 @@ private static DeploymentV1Apps GetDeployment(string name, string @namespace, st { ContainerPort = 80 } - } + }, + ReadinessProbe = new Probe + { + FailureThreshold = 3, + HttpGet = new HttpAction + { + Path = "/", + port = 80, + Scheme = "HTTP" + }, + PeriodSeconds = 10, + SuccessThreshold = 1, + TimeoutSeconds = 240 + }, + StartupProbe = new Probe + { + FailureThreshold = 3, + HttpGet = new HttpAction + { + Path = "/", + port = 80, + Scheme = "HTTP" + }, + PeriodSeconds = 10, + SuccessThreshold = 1, + TimeoutSeconds = 240 + } } }, ImagePullSecrets = string.IsNullOrEmpty(pullSecret) diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs index 50a9bacc5..eb79815a5 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ContainerV1.cs @@ -29,6 +29,12 @@ public class ContainerV1 [JsonProperty("volumeMounts")] public IEnumerable VolumeMounts { get; internal set; } + + [JsonProperty("readinessProbe")] + public Probe ReadinessProbe { get; set;} + + [JsonProperty("startupProbe")] + public Probe StartupProbe { get; set;} } public class ContainerVolumeMountV1 @@ -118,4 +124,34 @@ public class VolumeMountV1 [JsonProperty("readOnly")] public bool ReadOnly { get; set; } } + + public class Probe + { + [JsonProperty("failureThreshold")] + public int? FailureThreshold { get; set; } + + [JsonProperty("periodSeconds")] + public int? PeriodSeconds { get; set; } + + [JsonProperty("successThreshold")] + public int? SuccessThreshold { get; set; } + + [JsonProperty("timeoutSeconds")] + public int? TimeoutSeconds { get; set; } + + [JsonProperty("httpGet")] + public HttpAction HttpGet { get; set; } + } + + public class HttpAction + { + [JsonProperty("path")] + public string Path { get; set; } + + [JsonProperty("port")] + public int? port { get; set; } + + [JsonProperty("scheme")] + public string Scheme { get; set; } + } } \ No newline at end of file diff --git a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs index 2e05d9c79..864ba2872 100644 --- a/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs +++ b/src/Azure.Functions.Cli/Kubernetes/Models/Kubernetes/ServiceV1.cs @@ -19,6 +19,9 @@ public class ServiceSpecV1 : IKubernetesSpec [JsonProperty("type")] public string Type { get; set; } + + [JsonProperty("clusterIP", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string ClusterIp { get; set; } } public class ServicePortV1 From d67bf48d3c80afe7ea831e8db8932ca16b8471d9 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Fri, 5 Feb 2021 13:27:04 -0800 Subject: [PATCH 104/127] Fix nullref on deployments not containing HTTP --- .../KubernetesActions/KubernetesDeployAction.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs index b759de3d2..a0ae3af99 100644 --- a/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs +++ b/src/Azure.Functions.Cli/Actions/KubernetesActions/KubernetesDeployAction.cs @@ -150,6 +150,7 @@ public override async Task RunAsync() { await KubectlHelper.KubectlApply(resource, showOutput: true, ignoreError: IgnoreErrors, @namespace: Namespace); } + var httpService = resources .Where(i => i is ServiceV1) .Cast() @@ -158,10 +159,13 @@ public override async Task RunAsync() .Where(i => i is DeploymentV1Apps) .Cast() .FirstOrDefault(d => d.Metadata.Name.Contains("http")); - await KubernetesHelper.WaitForDeploymentRolleout(httpDeployment); - //Print the function keys message to the console - await KubernetesHelper.PrintFunctionsInfo(httpDeployment, httpService, funcKeys, triggers, ShowServiceFqdn); + if (httpDeployment != null && httpDeployment != null) + { + await KubernetesHelper.WaitForDeploymentRolleout(httpDeployment); + //Print the function keys message to the console + await KubernetesHelper.PrintFunctionsInfo(httpDeployment, httpService, funcKeys, triggers, ShowServiceFqdn); + } } } From 72f7c87497a674130b9a6a0e7d862fa4a1882838 Mon Sep 17 00:00:00 2001 From: Ken Dale Date: Thu, 31 Dec 2020 08:04:07 -0500 Subject: [PATCH 105/127] Grammar fix --- .../Actions/HostActions/StartHostAction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 044571bb5..42f38737e 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -93,7 +93,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) Parser .Setup('t', "timeout") - .WithDescription($"Timeout for on the functions host to start in seconds. Default: {DefaultTimeout} seconds.") + .WithDescription($"Timeout for the functions host to start in seconds. Default: {DefaultTimeout} seconds.") .SetDefault(DefaultTimeout) .Callback(t => Timeout = t); @@ -450,4 +450,4 @@ internal static async Task CheckNonOptionalSettings(IEnumerable Date: Thu, 10 Dec 2020 21:14:01 -0500 Subject: [PATCH 106/127] Update MSDN link MSDN forums have been migrated to Microsoft Q&A --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 802925fa9..23f8793fe 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope For questions on Azure Functions or the tools, you can ask questions here: -- [Azure Functions MSDN Forum](https://social.msdn.microsoft.com/Forums/azure/en-US/home?forum=AzureFunctions) +- [Azure Functions Q&A Forum](https://docs.microsoft.com/en-us/answers/topics/azure-functions.html) - [Azure-Functions tag on StackOverflow](http://stackoverflow.com/questions/tagged/azure-functions) File bugs at [Azure Functions Core Tools repo on GitHub](https://github.com/Azure/azure-functions-core-tools/issues). From aacc8c22eeddd53bd14af260e6cd18cbf276af49 Mon Sep 17 00:00:00 2001 From: Joe Bowbeer Date: Fri, 19 Feb 2021 16:00:30 -0800 Subject: [PATCH 107/127] Update README.md Fix azure-aci link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23f8793fe..05a75ae85 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ Azure Functions running on Kubernetes can take advantage of true serverless cont Functions deployed to Kubernetes already contain all the tolerations needed to be schedulable to Virtual Kubelet nodes. All you need to do is to set up VKubelet on your Kubernetes cluster: -* [Install VKubelet with ACI](https://github.com/virtual-kubelet/virtual-kubelet/tree/master/providers/azure) +* [Install VKubelet with ACI](https://github.com/virtual-kubelet/azure-aci) * [Install VKubelet with ACI on AKS](https://docs.microsoft.com/en-us/cli/azure/aks?view=azure-cli-latest#az-aks-install-connector) From d8b78b49aa9df51bb36986a2b257da978c867657 Mon Sep 17 00:00:00 2001 From: Fabio Cavalcante Date: Mon, 22 Mar 2021 14:27:41 -0700 Subject: [PATCH 108/127] Removing old .NET Core MyGet sources (#2511) --- .nuget/NuGet.Config | 1 - NuGet.Config | 1 - build/BuildSteps.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config index 2fb01aecc..81a5cd374 100644 --- a/.nuget/NuGet.Config +++ b/.nuget/NuGet.Config @@ -11,6 +11,5 @@ - \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config index e6a04ec5a..d11d6b0f2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,7 +6,6 @@ - \ No newline at end of file diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 30199a4d7..2376e446d 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -39,7 +39,6 @@ public static void RestorePackages() "https://www.myget.org/F/fusemandistfeed/api/v2", "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", "https://www.myget.org/F/xunit/api/v3/index.json", - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", "https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/Microsoft.Azure.Functions.PowerShellWorker/nuget/v3/index.json", } .Aggregate(string.Empty, (a, b) => $"{a} --source {b}"); From 601746f91108f7599da1c2383a1623047d2cc393 Mon Sep 17 00:00:00 2001 From: Robert Steele Date: Tue, 30 Mar 2021 16:30:30 -0700 Subject: [PATCH 109/127] Remove V2 beta note (#2204) --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 05a75ae85..3a069cc4d 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,6 @@ sudo apt-get install azure-functions-core-tools-2 **NOTE**: npm can be used on all platforms. On unix platforms, you may need to specify `--unsafe-perm` if you are running npm with sudo. That's due to npm behavior of post install script. - -**NOTE**: If you're running the v2 on Windows, Linux, or Mac, make sure to [enable the `beta` runtime](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions#target-the-version-20-runtime) in function app settings, otherwise you may not see the same results as running locally. - ## Getting Started on Kubernetes Using the Core Tools, you can easily configure a Kubernetes cluster and run Azure Functions on it. From 6a070fb40308438c758ba8ab69399845bd698391 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Ushio Date: Fri, 2 Apr 2021 09:51:42 -0700 Subject: [PATCH 110/127] fix the issue of rabbitmq trigger w/o stroage account (#2531) --- src/Azure.Functions.Cli/Common/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 07f3f107c..cf393281a 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -70,7 +70,7 @@ internal static class Constants { WorkerRuntime.powershell, new [] { "mcr.microsoft.com/azure-functions/powershell", "microsoft/azure-functions-powershell" } } }; - public static readonly string[] TriggersWithoutStorage = new[] { "httptrigger", "kafkatrigger" }; + public static readonly string[] TriggersWithoutStorage = new[] { "httptrigger", "kafkatrigger", "rabbitmqtrigger" }; public static class Errors { From 735920bfbeac5d79eb3c978047301f1cd9957dcd Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Thu, 8 Apr 2021 18:27:14 -0700 Subject: [PATCH 111/127] Throw error if building from wrong branch --- publish-scripts/driver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/publish-scripts/driver.py b/publish-scripts/driver.py index f4bd9879c..3cf7d04da 100644 --- a/publish-scripts/driver.py +++ b/publish-scripts/driver.py @@ -7,7 +7,10 @@ def main(*args): # assume follow semantic versioning 2.0.0 - constants.VERSION = args[1] + ver = args[1] + if not ver.startswith('2'): + raise Exception(f"This script only builds packages for major version '2'. Instead received '{ver}'") + constants.VERSION = ver constants.DRIVERROOTDIR = os.path.dirname(os.path.abspath(__file__)) platformSystem = platform.system() if platformSystem == "Linux": From 5e114c97ed391449fefcae1a93767f6e9fd8f49a Mon Sep 17 00:00:00 2001 From: Maddie Gordon <50681653+madelinegordon@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:40:35 -0700 Subject: [PATCH 112/127] Support secretless extensions (#2652) * add check to config section for conn string * how to simulate managed identity? * added unit test * edit error message * PR changes * space between tests --- .../Actions/HostActions/StartHostAction.cs | 30 ++++++++++++++-- .../ActionsTests/StartHostActionTests.cs | 35 +++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 42f38737e..3097143fd 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -377,10 +377,11 @@ private bool IsPreCompiledFunctionApp() internal static async Task CheckNonOptionalSettings(IEnumerable> secrets, string scriptPath, bool skipAzureWebJobsStorageCheck = false) { + string storageConnectionKey = "AzureWebJobsStorage"; try { // FirstOrDefault returns a KeyValuePair which is a struct so it can't be null. - var azureWebJobsStorage = secrets.FirstOrDefault(pair => pair.Key.Equals("AzureWebJobsStorage", StringComparison.OrdinalIgnoreCase)).Value; + var azureWebJobsStorage = secrets.FirstOrDefault(pair => pair.Key.Equals(storageConnectionKey, StringComparison.OrdinalIgnoreCase)).Value; var functionJsonFiles = await FileSystemHelpers .GetDirectories(scriptPath) .Select(d => Path.Combine(d, "function.json")) @@ -400,11 +401,12 @@ internal static async Task CheckNonOptionalSettings(IEnumerable b.IndexOf("Trigger", StringComparison.OrdinalIgnoreCase) != -1) .All(t => Constants.TriggersWithoutStorage.Any(tws => tws.Equals(t, StringComparison.OrdinalIgnoreCase))); - if (!skipAzureWebJobsStorageCheck && string.IsNullOrWhiteSpace(azureWebJobsStorage) && !allNonStorageTriggers) + if (!skipAzureWebJobsStorageCheck && string.IsNullOrWhiteSpace(azureWebJobsStorage) && + !StorageConnectionExists(secrets, storageConnectionKey) && !allNonStorageTriggers) { throw new CliException($"Missing value for AzureWebJobsStorage in {SecretsManager.AppSettingsFileName}. " + $"This is required for all triggers other than {string.Join(", ", Constants.TriggersWithoutStorage)}. " - + $"You can run 'func azure functionapp fetch-app-settings ' or specify a connection string in {SecretsManager.AppSettingsFileName}."); + + $"You can run 'func azure functionapp fetch-app-settings ', specify a connection string in {SecretsManager.AppSettingsFileName}, or use managed identity to authenticate."); } foreach ((var filePath, var functionJson) in functionsJsons) @@ -441,6 +443,28 @@ internal static async Task CheckNonOptionalSettings(IEnumerable> secrets, string connectionStringKey) + { + // convert secrets into IConfiguration object, check for storage connection in config section + var convertedEnv = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in secrets) + { + var convertedKey = kvp.Key.Replace("__", ":"); + if (!convertedEnv.ContainsKey(convertedKey)) + { + convertedEnv.Add(convertedKey, kvp.Value); + } + } + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(convertedEnv).Build(); + var connectionStringSection = configuration?.GetSection("ConnectionStrings").GetSection(connectionStringKey); + if (!connectionStringSection.Exists()) + { + connectionStringSection = configuration?.GetSection(connectionStringKey); + } + return connectionStringSection.Exists(); + } + private async Task<(Uri listenUri, Uri baseUri, X509Certificate2 cert)> Setup() { var protocol = UseHttps ? "https" : "http"; diff --git a/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs b/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs index 869a5b852..9688b8ed0 100644 --- a/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs +++ b/test/Azure.Functions.Cli.Tests/ActionsTests/StartHostActionTests.cs @@ -19,7 +19,7 @@ namespace Azure.Functions.Cli.Tests.ActionsTests public class StartHostActionTests : IDisposable { [SkippableFact] - public async Task CheckNonOptionalSettingsThrowsOnMissingAzureWebJobsStorage() + public async Task CheckNonOptionalSettingsThrowsOnMissingAzureWebJobsStorageAndManagedIdentity() { Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), reason: "Environment.CurrentDirectory throws in linux in test cases for some reason. Revisit this once we figure out why it's failing"); @@ -48,6 +48,37 @@ public async Task CheckNonOptionalSettingsThrowsOnMissingAzureWebJobsStorage() $"This is required for all triggers other than {string.Join(", ", Constants.TriggersWithoutStorage)}."); } + [Fact] + public async Task CheckNonOptionalSettingsDoesntThrowMissingStorageUsingManagedIdentity() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), + reason: "Environment.CurrentDirectory throws in linux in test cases for some reason. Revisit this once we figure out why it's failing"); + var fileSystem = GetFakeFileSystem(new[] + { + ("x:\\folder1", "{'bindings': [{'type': 'blobTrigger'}]}"), + ("x:\\folder2", "{'bindings': [{'type': 'httpTrigger'}]}") + }); + + var secrets = new Dictionary() + { + { "AzureWebJobsStorage:blobServiceUri", "myuri" }, + { "AzureWebJobsStorage__queueServiceUri", "queueuri" } + }; + + FileSystemHelpers.Instance = fileSystem; + + Exception exception = null; + try + { + await StartHostAction.CheckNonOptionalSettings(secrets, "x:\\"); + } + catch (Exception e) + { + exception = e; + } + exception.Should().BeNull(); + } + [Fact] public async Task CheckNonOptionalSettingsDoesntThrowOnMissingAzureWebJobsStorage() { @@ -99,7 +130,7 @@ public async Task CheckNonOptionalSettingsDoesntThrowOnMissingAzureWebJobsStorag public async Task CheckNonOptionalSettingsPrintsWarningForMissingSettings() { Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), - reason: "Environment.CurrentDirectory throws in linux in test cases for some reason. Revisit this once we figure out why it's failling"); + reason: "Environment.CurrentDirectory throws in linux in test cases for some reason. Revisit this once we figure out why it's failing"); var fileSystem = GetFakeFileSystem(new[] { From 12e1939d9ef95b012790e8701b1cd329d5581cc0 Mon Sep 17 00:00:00 2001 From: Bala G Date: Fri, 20 Aug 2021 18:44:42 -0700 Subject: [PATCH 113/127] Retain Azure Files settings for Linux consumption remote build (#2589) Reverting https://github.com/Azure/azure-functions-core-tools/pull/1437/files which was originally required to workaround a limitation. Co-authored-by: Paula Gombar --- .../Actions/AzureActions/PublishFunctionAppAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs index 630e015ac..b9304c1d5 100644 --- a/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs @@ -466,7 +466,7 @@ private async Task HandleLinuxConsumptionPublish(Site functionApp, Func pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp); var deployStatus = await PerformServerSideBuild(functionApp, zipFileFactory, pollConsumptionBuild); return deployStatus == DeployStatus.Success; From 177dd3586c29984215c4f9df738310d8f6788729 Mon Sep 17 00:00:00 2001 From: soninaren Date: Thu, 26 Aug 2021 11:18:46 -0700 Subject: [PATCH 114/127] Fixing the devops pipeline build name --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d2679332c..aa9779cbe 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,4 +1,4 @@ -name: $(Build.SourceBranchName)_$(Build.Reason)_$(devops_buildNumber) +name: $(Build.SourceBranchName)_$(Build.Reason) pr: branches: From c32d31eb24a578c25956ba21af80be4529a36cdc Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Thu, 7 Oct 2021 14:27:59 -0700 Subject: [PATCH 115/127] Update README with v4 preview install instructions (#2750) --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 3a069cc4d..fc2e05282 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,19 @@ The Azure Functions Core Tools provide a local development experience for creati **v3**: (v3.x branch): Self-contained cross-platform package **(recommended)** +**v4**: (v4.x branch): Self-contained cross-platform package (preview) + ## Installing ### Windows #### To download and install with MSI: +##### v4 (preview) + +- [Windows 64-bit](https://go.microsoft.com/fwlink/?linkid=2174087) (VS Code debugging requires 64-bit) +- [Windows 32-bit](https://go.microsoft.com/fwlink/?linkid=2174159) + ##### v3 - [Windows 64-bit](https://go.microsoft.com/fwlink/?linkid=2135274) (VS Code debugging requires 64-bit) @@ -31,6 +38,11 @@ The Azure Functions Core Tools provide a local development experience for creati #### To install with npm: +##### v4 (preview) +```bash +npm i -g azure-functions-core-tools@4 --unsafe-perm true +``` + ##### v3 ```bash npm i -g azure-functions-core-tools@3 --unsafe-perm true @@ -43,6 +55,11 @@ npm i -g azure-functions-core-tools@2 --unsafe-perm true #### To install with chocolatey: +##### v4 (preview) +```bash +choco install azure-functions-core-tools-4 +``` + ##### v3 ```bash choco install azure-functions-core-tools-3 @@ -62,6 +79,13 @@ choco install azure-functions-core-tools-2 #### Homebrew: +#### v4 (preview) + +```bash +brew tap azure/functions +brew install azure-functions-core-tools@4 +``` + ##### v3 ```bash brew tap azure/functions @@ -135,6 +159,12 @@ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list #### 2. Install +##### v4 (preview) +```bash +sudo apt-get update +sudo apt-get install azure-functions-core-tools-4 +``` + ##### v3 ```bash sudo apt-get update From a2634fba228decf73ec886fac413628257eb9e1c Mon Sep 17 00:00:00 2001 From: Lilian Kasem Date: Wed, 20 Oct 2021 14:48:46 -0700 Subject: [PATCH 116/127] Add GitHub PR template (#2751) --- .github/pull_request_template.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..640f174dc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ + + +### Issue describing the changes in this PR + +resolves #issue_for_this_pr + +### Pull request checklist + +* [ ] My changes **do not** require documentation changes + * [ ] Otherwise: Documentation issue linked to PR +* [ ] My changes **should not** be added to the release notes for the next release + * [ ] Otherwise: I've added my notes to `release_notes.md` +* [ ] My changes **do not** need to be backported to a previous version + * [ ] Otherwise: Backport tracked by issue/PR #issue_or_pr +* [ ] I have added all required tests (Unit tests, E2E tests) + + +### Additional information + +Additional PR information From cc03b39f7c2451c78ae100ce8fa30dd6214304b0 Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Mon, 29 Nov 2021 12:17:44 -0800 Subject: [PATCH 117/127] Remove "preview" from v4 (#2840) --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc2e05282..e0809298d 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ The Azure Functions Core Tools provide a local development experience for creati **v2** (master branch): Self-contained cross-platform package -**v3**: (v3.x branch): Self-contained cross-platform package **(recommended)** +**v3**: (v3.x branch): Self-contained cross-platform package -**v4**: (v4.x branch): Self-contained cross-platform package (preview) +**v4**: (v4.x branch): Self-contained cross-platform package **(recommended)** ## Installing @@ -26,7 +26,7 @@ The Azure Functions Core Tools provide a local development experience for creati #### To download and install with MSI: -##### v4 (preview) +##### v4 - [Windows 64-bit](https://go.microsoft.com/fwlink/?linkid=2174087) (VS Code debugging requires 64-bit) - [Windows 32-bit](https://go.microsoft.com/fwlink/?linkid=2174159) @@ -38,7 +38,7 @@ The Azure Functions Core Tools provide a local development experience for creati #### To install with npm: -##### v4 (preview) +##### v4 ```bash npm i -g azure-functions-core-tools@4 --unsafe-perm true ``` @@ -55,7 +55,7 @@ npm i -g azure-functions-core-tools@2 --unsafe-perm true #### To install with chocolatey: -##### v4 (preview) +##### v4 ```bash choco install azure-functions-core-tools-4 ``` @@ -79,7 +79,7 @@ choco install azure-functions-core-tools-2 #### Homebrew: -#### v4 (preview) +#### v4 ```bash brew tap azure/functions @@ -159,7 +159,7 @@ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list #### 2. Install -##### v4 (preview) +##### v4 ```bash sudo apt-get update sudo apt-get install azure-functions-core-tools-4 From 5f4f3fa52f8a2f88ce6681eadc470ce07a08fc4d Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Fri, 17 Dec 2021 13:35:53 -0800 Subject: [PATCH 118/127] Add SBOM generation task to V2 (#2880) * Add SBOM Generation to V3 (#2879) * Add SBOM manifest to generated artifacts (#2869) * Initial changes to add SBOM manifest to generated artifacts * Added .nuspec file for packing with manifests * Add SBOM generation for release builds * Add reference to .nuspec file * Made generateMsiFiles.ps1 script cleaner * Remove AddSBOM variable * Addressing some comments * Uncommented key steps in the pipeline and adjusted branch name comparison * Addressing further comments * Removed from the pipeline * Removed Write-Log from pipelineUtilities.psm1 * Altered installation location of dotnet * Add variable that can override SBOM generation in non-SBOM scenarios * Ensure parsing boolean environmental variables is done properly * Add [ref] for parsing boolean environmental variables * Moved condition for generating .msi files to azure-pipelines.yml * Ensure parsing of boolean environmental variable is done correctly * Added debugging statement * Removed debugging statement and fixed build command logic * Fixed condition determining build command * Ensure conditions for a full release simulation match * Correct artifacts path location * Altered the pool and vmImage * Slight modifications for V3 * Change version name in .nuspec file to be V3 instead of V4 * Update target framework in Settings.cs * Changed the target framework to netcoreapp 2.2 * Altered spacing on azure-pipelines.yml * More altered spacing * Removed IntegrationBuildNumber --- azure-pipelines.yml | 51 +++++++++++-------- build.ps1 | 23 ++++++++- build/BuildSteps.cs | 29 +++++++++++ build/Program.cs | 2 + build/Settings.cs | 8 +++ pipelineUtilities.psm1 | 113 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 pipelineUtilities.psm1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index aa9779cbe..c877f6e3b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,17 +3,16 @@ name: $(Build.SourceBranchName)_$(Build.Reason) pr: branches: include: - - master - dev trigger: branches: include: - dev - - master pool: - vmImage: 'vs2017-win2016' + name: '1ES-Hosted-AzFunc' + vmImage: 'MMS2022TLS' variables: devops_buildNumber: $[counter(format(''), 1500)] @@ -21,12 +20,34 @@ variables: APPVEYOR_REPO_COMMIT: $(Build.SourceVersion) steps: +- pwsh: | + $isReleaseBuild = $false + if ($env:BuildSourceBranchName -like "release_2.0*") + { + $isReleaseBuild = $true + } + Write-Host "##vso[task.setvariable variable=IsReleaseBuild]$isReleaseBuild" + Write-Host "IsReleaseBuild: $isReleaseBuild" + displayName: 'Set IsReleaseBuild variable' + env: + BuildSourceBranchName: $(Build.SourceBranchName) - pwsh: | Write-Host "Target branch: '$(APPVEYOR_REPO_BRANCH)'" displayName: Set up environment variables - task: NodeTool@0 inputs: versionSpec: '10.x' +- pwsh: | + Import-Module ".\pipelineUtilities.psm1" -Force + Install-Dotnet + displayName: 'Install .NET 2.2' +- pwsh: | + Import-Module ".\pipelineUtilities.psm1" -Force + Install-SBOMUtil -SBOMUtilSASUrl $env:SBOMUtilSASUrl + env: + SBOMUtilSASUrl: $(SBOMUtilSASUrl) + condition: or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true')) + displayName: 'Install SBOM ManifestTool' - task: NuGetToolInstaller@1 inputs: versionSpec: @@ -41,23 +62,13 @@ steps: # acquire access token from Azure CLI and export it to AZURE_MANAGEMENT_ACCESS_TOKEN $accessToken = (az account get-access-token --query "accessToken" | % { $_.Trim('"') }) echo "##vso[task.setvariable variable=azure_management_access_token]$accessToken" -- pwsh: | - # Set DotNetPath - $sdkBasePath = dotnet --info | Where-Object {$_ -match "Base Path"} | ForEach-Object {($_ -replace '\s+Base Path:','').trim()} - $dotnetPath = Split-Path (Split-Path $sdkBasePath) - Write-Host "dotnet path: $dotnetPath" - Write-Host "##vso[task.setvariable variable=DotNetPath]$dotnetPath" - displayName: 'Set DotNetPath' -- task: UseDotNet@2 - inputs: - packageType: 'sdk' - version: '2.2.207' - installationPath: $(DotNetPath) - pwsh: | .\build.ps1 env: AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) BuildArtifactsStorage: $(BuildArtifactsStorage) + IsReleaseBuild: $(IsReleaseBuild) + SimulateReleaseBuild: $(SimulateReleaseBuild) DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) displayName: 'Executing build script' @@ -94,7 +105,7 @@ steps: SessionTimeout: '60' MaxConcurrency: '50' MaxRetryAttempts: '5' - condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) + condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) - task: EsrpCodeSigning@1 displayName: 'Third party signing' inputs: @@ -129,7 +140,7 @@ steps: SessionTimeout: '60' MaxConcurrency: '50' MaxRetryAttempts: '5' - condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) + condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) - pwsh: | .\repackageBinaries.ps1 displayName: Repackage signed binaries @@ -138,14 +149,14 @@ steps: BuildArtifactsStorage: $(BuildArtifactsStorage) DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) - condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) + condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) - task: DotNetCoreCLI@2 inputs: command: 'run' workingDirectory: '.\build' arguments: 'TestSignedArtifacts --signTest' displayName: 'Verify signed binaries' - condition: and(succeeded(),or (eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/3.0'))) + condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) - pwsh: | .\generateSha.ps1 displayName: 'Generate sha files' @@ -165,4 +176,4 @@ steps: inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' - publishLocation: 'Container' \ No newline at end of file + publishLocation: 'Container' diff --git a/build.ps1 b/build.ps1 index 95551607d..7e66d3687 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,4 +1,3 @@ - if ($env:APPVEYOR_REPO_BRANCH -eq "disabled") { Set-Location ".\src\Azure.Functions.Cli" $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.Functions.JavaWorker -Source https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg -PreRelease" @@ -26,5 +25,25 @@ else { Set-Location ".\build" } -Invoke-Expression -Command "dotnet run --ci" +$buildCommand = $null + +$isReleaseBuild = $null +$simulateReleaseBuild = $null +if (-not([bool]::TryParse($env:IsReleaseBuild, [ref] $isReleaseBuild) -and + [bool]::TryParse($env:SimulateReleaseBuild, [ref] $simulateReleaseBuild))) +{ + throw "IsReleaseBuild and GenerateSBOM can only be set to true or false." +} + +if ($isReleaseBuild -or $simulateReleaseBuild) +{ + $buildCommand = "dotnet run --ci --generateSBOM" +} +else +{ + $buildCommand = "dotnet run --ci" +} + +Write-Host "Running $buildCommand" +Invoke-Expression -Command $buildCommand if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index 2376e446d..4ee455f55 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -550,6 +550,35 @@ private static string CurrentVersion } } + public static void GenerateSBOMManifestForZips() + { + Directory.CreateDirectory(Settings.SBOMManifestTelemetryDir); + // Generate the SBOM manifest for each runtime + foreach (var runtime in Settings.TargetRuntimes) + { + var packageName = $"Azure.Functions.Cli.{runtime}.{CurrentVersion}"; + var buildPath = Path.Combine(Settings.OutputDir, runtime); + var manifestFolderPath = Path.Combine(buildPath, "_manifest"); + var telemetryFilePath = Path.Combine(Settings.SBOMManifestTelemetryDir, Guid.NewGuid().ToString() + ".json"); + + // Delete the manifest folder if it exists + if (Directory.Exists(manifestFolderPath)) + { + Directory.Delete(manifestFolderPath, recursive: true); + } + + // Generate the SBOM manifest + Shell.Run("dotnet", + $"{Settings.SBOMManifestToolPath} generate -PackageName {packageName} -BuildDropPath {buildPath}" + + $" -BuildComponentPath {buildPath} -Verbosity Information -t {telemetryFilePath}"); + } + } + + public static void DeleteSBOMTelemetryFolder() + { + Directory.Delete(Settings.SBOMManifestTelemetryDir, recursive: true); + } + public static void UploadToStorage() { if (!string.IsNullOrEmpty(Settings.BuildArtifactsStorage)) diff --git a/build/Program.cs b/build/Program.cs index 28f8cb837..98081b0fa 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -27,7 +27,9 @@ static void Main(string[] args) .Then(TestPreSignedArtifacts, skip: !args.Contains("--ci")) .Then(CopyBinariesToSign, skip: !args.Contains("--ci")) .Then(Test) + .Then(GenerateSBOMManifestForZips, skip: !args.Contains("--generateSBOM")) .Then(Zip) + .Then(DeleteSBOMTelemetryFolder, skip: !args.Contains("--generateSBOM")) .Then(UploadToStorage, skip: !args.Contains("--ci")) .Run(); } diff --git a/build/Settings.cs b/build/Settings.cs index 312ca54d6..c2e535961 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -24,6 +24,8 @@ private static string config(string @default = null, [CallerMemberName] string k public const string ProjectTemplatesVersion = "2.1.1579"; public const string TemplateJsonVersion = "2.1.1579"; + public static readonly string SBOMManifestToolPath = Path.GetFullPath("../ManifestTool/Microsoft.ManifestTool.dll"); + public static readonly string SrcProjectPath = Path.GetFullPath("../src/Azure.Functions.Cli/"); public static readonly string ConstantsFile = Path.Combine(SrcProjectPath, "Common", "Constants.cs"); @@ -74,6 +76,12 @@ private static string config(string @default = null, [CallerMemberName] string k public static readonly string OutputDir = Path.Combine(Path.GetFullPath(".."), "artifacts"); + public static readonly string SBOMManifestTelemetryDir = Path.Combine(OutputDir, "SBOMManifestTelemetry"); + + public static string TargetFramework = "netcoreapp2.2"; + + public static readonly string NupkgPublishDir = Path.GetFullPath($"../src/Azure.Functions.Cli/bin/Release/{TargetFramework}/publish"); + public static readonly string PreSignTestDir = "PreSignTest"; public static readonly string SignTestDir = "SignTest"; diff --git a/pipelineUtilities.psm1 b/pipelineUtilities.psm1 new file mode 100644 index 000000000..19cd9b12f --- /dev/null +++ b/pipelineUtilities.psm1 @@ -0,0 +1,113 @@ +# +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +#Requires -Version 6.0 + +using namespace System.Runtime.InteropServices + +$DLL_NAME = "Microsoft.ManifestTool.dll" +$MANIFESTOOLNAME = "ManifestTool" +$MANIFESTOOL_DIRECTORY = Join-Path $PSScriptRoot $MANIFESTOOLNAME +$MANIFEST_TOOL_PATH = "$MANIFESTOOL_DIRECTORY/$DLL_NAME" + +function Get-ManifestToolPath +{ + if (Test-Path $MANIFEST_TOOL_PATH) + { + return $MANIFEST_TOOL_PATH + } + throw "The SBOM Manifest Tool is not installed. Please run Install-SBOMUtil -SBOMUtilSASUrl " +} + +function Install-SBOMUtil +{ + param( + [string] + $SBOMUtilSASUrl + ) + + if ([string]::IsNullOrEmpty($SBOMUtilSASUrl)) + { + throw "The `$SBOMUtilSASUrl parameter cannot be null or empty." + } + + Write-Host "Installing $MANIFESTOOLNAME..." + Remove-Item -Recurse -Force $MANIFESTOOL_DIRECTORY -ErrorAction Ignore + + Invoke-RestMethod -Uri $SBOMUtilSASUrl -OutFile "$MANIFESTOOL_DIRECTORY.zip" + Expand-Archive "$MANIFESTOOL_DIRECTORY.zip" -DestinationPath $MANIFESTOOL_DIRECTORY + + if (-not (Test-Path $MANIFEST_TOOL_PATH)) + { + throw "$MANIFESTOOL_DIRECTORY does not contain '$DLL_NAME'" + } + + Write-Host 'Done.' + + return $MANIFEST_TOOL_PATH +} + +$DotnetSDKVersionRequirements = @{ + + # .NET SDK 3.1 is required by the Microsoft.ManifestTool.dll tool + '2.2' = @{ + MinimalPatch = '207' + DefaultPatch = '207' + } +} + +function AddLocalDotnetDirPath { + $LocalDotnetDirPath = if ($IsWindows) { "$env:ProgramFiles/dotnet" } else { "/usr/share/dotnet" } + if (($env:PATH -split [IO.Path]::PathSeparator) -notcontains $LocalDotnetDirPath) { + $env:PATH = $LocalDotnetDirPath + [IO.Path]::PathSeparator + $env:PATH + } +} + +function Find-Dotnet +{ + AddLocalDotnetDirPath + $listSdksOutput = dotnet --list-sdks + $installedDotnetSdks = $listSdksOutput | ForEach-Object { $_.Split(" ")[0] } + Write-Host "Detected dotnet SDKs: $($installedDotnetSdks -join ', ')" + foreach ($majorMinorVersion in $DotnetSDKVersionRequirements.Keys) { + $minimalVersion = "$majorMinorVersion.$($DotnetSDKVersionRequirements[$majorMinorVersion].MinimalPatch)" + $firstAcceptable = $installedDotnetSdks | + Where-Object { $_.StartsWith("$majorMinorVersion.") } | + Where-Object { [System.Management.Automation.SemanticVersion]::new($_) -ge [System.Management.Automation.SemanticVersion]::new($minimalVersion) } | + Select-Object -First 1 + if (-not $firstAcceptable) { + throw "Cannot find the dotnet SDK for .NET Core $majorMinorVersion. Version $minimalVersion or higher is required. Please specify '-Bootstrap' to install build dependencies." + } + } +} + +function Install-Dotnet { + [CmdletBinding()] + param( + [string]$Channel = 'release' + ) + try { + Find-Dotnet + return # Simply return if we find dotnet SDk with the correct version + } catch { } + $obtainUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain" + try { + $installScript = if ($IsWindows) { "dotnet-install.ps1" } else { "dotnet-install.sh" } + Invoke-WebRequest -Uri $obtainUrl/$installScript -OutFile $installScript + foreach ($majorMinorVersion in $DotnetSDKVersionRequirements.Keys) { + $version = "$majorMinorVersion.$($DotnetSDKVersionRequirements[$majorMinorVersion].DefaultPatch)" + Write-Host "Installing dotnet SDK version $version" + if ($IsWindows) { + & .\$installScript -InstallDir "$env:ProgramFiles/dotnet" -Channel $Channel -Version $Version + } else { + bash ./$installScript --install-dir "/usr/share/dotnet" -c $Channel -v $Version + } + } + AddLocalDotnetDirPath + } + finally { + Remove-Item $installScript -Force -ErrorAction SilentlyContinue + } +} \ No newline at end of file From 75d4e99d9a765441d1890bfcfca4df60895fea2e Mon Sep 17 00:00:00 2001 From: Troy Witthoeft Date: Mon, 3 Jan 2022 18:05:55 -0500 Subject: [PATCH 119/127] Update README.md (#2887) removing the choco v4 install instructions as the choco v4 package is currently not available. Please see #2868 --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index e0809298d..a14e5c61d 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,6 @@ npm i -g azure-functions-core-tools@2 --unsafe-perm true #### To install with chocolatey: -##### v4 -```bash -choco install azure-functions-core-tools-4 -``` - ##### v3 ```bash choco install azure-functions-core-tools-3 From d6366d4bc4a1593ddbe302763ba0a04b23f0f32e Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Thu, 13 Jan 2022 19:42:46 -0500 Subject: [PATCH 120/127] Remove appveyor.yml from V2 (#2900) * Removed appveyor references from the build pipeline * Replaced Invoke-Expression to &{} and corrected an exception's message * Remove quotes from commands * Removed quotations from build command * Altered so it would run --- README.md | 2 +- appveyor.yml | 23 ----------------------- azure-pipelines.yml | 6 +++--- build.ps1 | 26 +++++++++++++------------- build/Settings.cs | 4 ++-- 5 files changed, 19 insertions(+), 42 deletions(-) delete mode 100644 appveyor.yml diff --git a/README.md b/README.md index a14e5c61d..464397442 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ |---|---| |master|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/azure-functions-core-tools?branchName=master)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=11&branchName=master) |dev|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/azure-functions-core-tools?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=11&branchName=dev) -|v1.x|[![Build status](https://ci.appveyor.com/api/projects/status/max86pwo54y44j36/branch/v1.x?svg=true)](https://ci.appveyor.com/project/appsvc/azure-functions-cli/branch/v1.x)| +|v1.x|[![Build status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/azure-functions-core-tools?branchName=v1.x)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=11&branchName=v1.x)| # Azure Functions Core Tools diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 3c815d61d..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: "{build}" - -branches: - only: - - v1.x - -pull_requests: - do_not_increment_build_number: true -image: Visual Studio 2017 - -clone_folder: c:\azure-functions-cli - -build_script: -- "SET PATH=C:\\Python36;C:\\Python36\\Scripts;%PATH%" -- ps: | - .\build.ps1 - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - -artifacts: -- path: artifacts\Azure.Functions.*.zip - name: Release -- path: artifacts\Azure.Functions.*.sha2 - name: Release \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c877f6e3b..c19fef330 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,8 +16,8 @@ pool: variables: devops_buildNumber: $[counter(format(''), 1500)] - APPVEYOR_REPO_BRANCH: $[coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranchName'])] - APPVEYOR_REPO_COMMIT: $(Build.SourceVersion) + DEVOPS_REPO_BRANCH: $[coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranchName'])] + DEVOPS_REPO_COMMIT: $(Build.SourceVersion) steps: - pwsh: | @@ -32,7 +32,7 @@ steps: env: BuildSourceBranchName: $(Build.SourceBranchName) - pwsh: | - Write-Host "Target branch: '$(APPVEYOR_REPO_BRANCH)'" + Write-Host "Target branch: '$(DEVOPS_REPO_BRANCH)'" displayName: Set up environment variables - task: NodeTool@0 inputs: diff --git a/build.ps1 b/build.ps1 index 7e66d3687..a0037f459 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,24 +1,24 @@ -if ($env:APPVEYOR_REPO_BRANCH -eq "disabled") { +if ($env:DEVOPS_REPO_BRANCH -eq "disabled") { Set-Location ".\src\Azure.Functions.Cli" - $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.Functions.JavaWorker -Source https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg -PreRelease" + $result = & { NuGet list Microsoft.Azure.Functions.JavaWorker -Source https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg -PreRelease } $javaWorkerVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.Functions.JavaWorker $javaWorkerVersion to project" -ForegroundColor Green - Invoke-Expression -Command "dotnet add package Microsoft.Azure.Functions.JavaWorker -v $javaWorkerVersion -s https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg" + & { dotnet add package Microsoft.Azure.Functions.JavaWorker -v $javaWorkerVersion -s https://ci.appveyor.com/NuGet/azure-functions-java-worker-fejnnsvmrkqg } - $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.Functions.PowerShellWorker -Source https://ci.appveyor.com/nuget/azure-functions-powershell-wor-0842fakagqy6 -PreRelease" + $result = & { NuGet list Microsoft.Azure.Functions.PowerShellWorker -Source https://ci.appveyor.com/nuget/azure-functions-powershell-wor-0842fakagqy6 -PreRelease } $powerShellWorkerVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.Functions.PowerShellWorker $powerShellWorkerVersion to project" -ForegroundColor Green - Invoke-Expression -Command "dotnet add package Microsoft.Azure.Functions.PowerShellWorker -v $powerShellWorkerVersion -s https://ci.appveyor.com/nuget/azure-functions-powershell-wor-0842fakagqy6" + & { dotnet add package Microsoft.Azure.Functions.PowerShellWorker -v $powerShellWorkerVersion -s https://ci.appveyor.com/nuget/azure-functions-powershell-wor-0842fakagqy6 } - $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.Functions.NodeJsWorker -Source https://ci.appveyor.com/nuget/azure-functions-nodejs-worker-0fcvx371y52p -PreRelease" + $result = & { NuGet list Microsoft.Azure.Functions.NodeJsWorker -Source https://ci.appveyor.com/nuget/azure-functions-nodejs-worker-0fcvx371y52p -PreRelease } $nodeJsWorkerVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.Functions.NodeJsWorker $nodeJsWorkerVersion to project" -ForegroundColor Green - Invoke-Expression -Command "dotnet add package Microsoft.Azure.Functions.NodeJsWorker -v $nodeJsWorkerVersion -s https://ci.appveyor.com/nuget/azure-functions-nodejs-worker-0fcvx371y52p" + & { dotnet add package Microsoft.Azure.Functions.NodeJsWorker -v $nodeJsWorkerVersion -s https://ci.appveyor.com/nuget/azure-functions-nodejs-worker-0fcvx371y52p } - $result = Invoke-Expression -Command "NuGet list Microsoft.Azure.WebJobs.Script.WebHost -Source https://ci.appveyor.com/NuGet/azure-webjobs-sdk-script-g6rygw981l9t -PreRelease" + $result = & { NuGet list Microsoft.Azure.WebJobs.Script.WebHost -Source https://ci.appveyor.com/NuGet/azure-webjobs-sdk-script-g6rygw981l9t -PreRelease } $WebHostVersion = $result.Split()[1] Write-host "Adding Microsoft.Azure.WebJobs.Script.WebHost $WebHostVersion to project" -ForegroundColor Green - Invoke-Expression -Command "dotnet add package Microsoft.Azure.WebJobs.Script.WebHost -v $WebHostVersion -s https://ci.appveyor.com/NuGet/azure-webjobs-sdk-script-g6rygw981l9t" + & { dotnet add package Microsoft.Azure.WebJobs.Script.WebHost -v $WebHostVersion -s https://ci.appveyor.com/NuGet/azure-webjobs-sdk-script-g6rygw981l9t } Set-Location "..\..\build" } else { @@ -32,18 +32,18 @@ $simulateReleaseBuild = $null if (-not([bool]::TryParse($env:IsReleaseBuild, [ref] $isReleaseBuild) -and [bool]::TryParse($env:SimulateReleaseBuild, [ref] $simulateReleaseBuild))) { - throw "IsReleaseBuild and GenerateSBOM can only be set to true or false." + throw "IsReleaseBuild and SimulateReleaseBuild can only be set to true or false." } if ($isReleaseBuild -or $simulateReleaseBuild) { - $buildCommand = "dotnet run --ci --generateSBOM" + $buildCommand = { dotnet run --ci --generateSBOM } } else { - $buildCommand = "dotnet run --ci" + $buildCommand = { dotnet run --ci } } Write-Host "Running $buildCommand" -Invoke-Expression -Command $buildCommand +& $buildCommand if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/build/Settings.cs b/build/Settings.cs index c2e535961..3cc97ec66 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -94,9 +94,9 @@ private static string config(string @default = null, [CallerMemberName] string k public static readonly string TelemetryKeyToReplace = "00000000-0000-0000-0000-000000000000"; - public static string BuildNumber => config(null, "devops_buildNumber") ?? config("9999", "APPVEYOR_BUILD_NUMBER"); + public static string BuildNumber => config("9999", "devops_buildNumber"); - public static string CommitId => config(null, "Build.SourceVersion") ?? config("N/A", "APPVEYOR_REPO_COMMIT"); + public static string CommitId => config("N/A", "Build.SourceVersion"); public static string TelemetryInstrumentationKey => config(null, "TELEMETRY_INSTRUMENTATION_KEY"); From b7ae1b97e63b9ddcff505302efc05894be6a6c87 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Wed, 19 Jan 2022 14:06:30 -0500 Subject: [PATCH 121/127] Move SimulateReleaseBuild logic to the pipeline V2 (#2909) * Moved SimulateReleaseBuild logic to azure-pipelines.yml * Fixed small typo * Corrected another small typo --- azure-pipelines.yml | 21 ++++++++++++++------- build.ps1 | 8 +++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c19fef330..3552c948a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,16 +21,24 @@ variables: steps: - pwsh: | + $simulateReleaseBuild = $null + if (-not([bool]::TryParse($env:SimulateReleaseBuild, [ref] $simulateReleaseBuild))) + { + throw "SimulateReleaseBuild can only be set to true or false." + } + $isReleaseBuild = $false - if ($env:BuildSourceBranchName -like "release_2.0*") + if ($env:BuildSourceBranchName -like "release_2.0*" -or $simulateReleaseBuild) { $isReleaseBuild = $true } + Write-Host "Setting IsReleaseBuild to $isReleaseBuild because the branch name is $env:BuildSourceBranchName and SimulateReleaseBuild is $env:SimulateReleaseBuild" Write-Host "##vso[task.setvariable variable=IsReleaseBuild]$isReleaseBuild" Write-Host "IsReleaseBuild: $isReleaseBuild" displayName: 'Set IsReleaseBuild variable' env: BuildSourceBranchName: $(Build.SourceBranchName) + SimulateReleaseBuild: $(SimulateReleaseBuild) - pwsh: | Write-Host "Target branch: '$(DEVOPS_REPO_BRANCH)'" displayName: Set up environment variables @@ -46,7 +54,7 @@ steps: Install-SBOMUtil -SBOMUtilSASUrl $env:SBOMUtilSASUrl env: SBOMUtilSASUrl: $(SBOMUtilSASUrl) - condition: or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true')) + condition: eq(variables['IsReleaseBuild'], 'true') displayName: 'Install SBOM ManifestTool' - task: NuGetToolInstaller@1 inputs: @@ -68,7 +76,6 @@ steps: AzureBlobSigningConnectionString: $(AzureBlobSigningConnectionString) BuildArtifactsStorage: $(BuildArtifactsStorage) IsReleaseBuild: $(IsReleaseBuild) - SimulateReleaseBuild: $(SimulateReleaseBuild) DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) displayName: 'Executing build script' @@ -105,7 +112,7 @@ steps: SessionTimeout: '60' MaxConcurrency: '50' MaxRetryAttempts: '5' - condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) + condition: and(succeeded(), eq(variables['IsReleaseBuild'], 'true')) - task: EsrpCodeSigning@1 displayName: 'Third party signing' inputs: @@ -140,7 +147,7 @@ steps: SessionTimeout: '60' MaxConcurrency: '50' MaxRetryAttempts: '5' - condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) + condition: and(succeeded(), eq(variables['IsReleaseBuild'], 'true')) - pwsh: | .\repackageBinaries.ps1 displayName: Repackage signed binaries @@ -149,14 +156,14 @@ steps: BuildArtifactsStorage: $(BuildArtifactsStorage) DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) - condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) + condition: and(succeeded(), eq(variables['IsReleaseBuild'], 'true')) - task: DotNetCoreCLI@2 inputs: command: 'run' workingDirectory: '.\build' arguments: 'TestSignedArtifacts --signTest' displayName: 'Verify signed binaries' - condition: and(succeeded(), or(eq(variables['IsReleaseBuild'], 'true'), eq(variables['SimulateReleaseBuild'], 'true'))) + condition: and(succeeded(), eq(variables['IsReleaseBuild'], 'true')) - pwsh: | .\generateSha.ps1 displayName: 'Generate sha files' diff --git a/build.ps1 b/build.ps1 index a0037f459..84cffc010 100644 --- a/build.ps1 +++ b/build.ps1 @@ -28,14 +28,12 @@ else { $buildCommand = $null $isReleaseBuild = $null -$simulateReleaseBuild = $null -if (-not([bool]::TryParse($env:IsReleaseBuild, [ref] $isReleaseBuild) -and - [bool]::TryParse($env:SimulateReleaseBuild, [ref] $simulateReleaseBuild))) +if (-not([bool]::TryParse($env:IsReleaseBuild, [ref] $isReleaseBuild))) { - throw "IsReleaseBuild and SimulateReleaseBuild can only be set to true or false." + throw "IsReleaseBuild can only be set to true or false." } -if ($isReleaseBuild -or $simulateReleaseBuild) +if ($isReleaseBuild) { $buildCommand = { dotnet run --ci --generateSBOM } } From 77fac70eafbf74c2356a0aaeb0de90910279750c Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Mon, 24 Jan 2022 11:21:46 -0800 Subject: [PATCH 122/127] Update VS Code config on dev branch (#2920) --- .vscode/extensions.json | 6 ++++++ .vscode/launch.json | 27 +++++++++++++-------------- .vscode/settings.json | 3 +++ .vscode/tasks.json | 39 ++++++++++++++++++++++++++++++++------- 4 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..98d5035a7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp", + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 8593f1c2a..6fa45dd7d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,4 @@ { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { @@ -10,25 +7,27 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceRoot}/src/Azure.Functions.Cli/bin/Debug/netcoreapp2.0/func.dll", + "program": "${workspaceFolder}/src/Azure.Functions.Cli/bin/Debug/netcoreapp2.2/func.dll", "env": { "CLI_DEBUG": "1" }, - "args": [ - "azure", - "login" - ], - "cwd": "${workspaceRoot}/src/Azure.Functions.Cli", - // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "args": "${input:funcArgs}", + "cwd": "${workspaceFolder}/src/Azure.Functions.Cli", "console": "internalConsole", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart" + "stopAtEntry": false }, { "name": ".NET Core Attach", "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" + "request": "attach" + } + ], + "inputs": [ + { + "id": "funcArgs", + "type": "promptString", + "description": "Args to pass to the 'func' command", + "default": "--version" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b83cf1320 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "azureFunctions.suppressProject": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3813a8e4d..b9255a967 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,15 +1,40 @@ { - "version": "0.1.0", - "command": "dotnet", - "isShellCommand": true, - "args": [], + "version": "2.0.0", "tasks": [ { - "taskName": "build", + "label": "build", + "command": "dotnet", + "type": "process", "args": [ - "${workspaceRoot}/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj" + "build", + "${workspaceFolder}/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj" ], - "isBuildCommand": true, "problemMatcher": "$msCompile" } ] From 98675a7f9c2cac3960ffed36580e23c4bd389283 Mon Sep 17 00:00:00 2001 From: Matthew Burleigh Date: Tue, 25 Jan 2022 14:33:39 -0500 Subject: [PATCH 123/127] fix type in RunAsync() (#2623) Other console messages are in the form of "Press any key to "; I added the word "key" the console output. Co-authored-by: Michael Peng --- src/Azure.Functions.Cli/ConsoleApp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Azure.Functions.Cli/ConsoleApp.cs b/src/Azure.Functions.Cli/ConsoleApp.cs index 08c51cb8d..90ed862e2 100644 --- a/src/Azure.Functions.Cli/ConsoleApp.cs +++ b/src/Azure.Functions.Cli/ConsoleApp.cs @@ -84,7 +84,7 @@ public static async Task RunAsync(string[] args, IContainer container) if (args.Any(a => a.Equals("--pause-on-error", StringComparison.OrdinalIgnoreCase))) { - ColoredConsole.Write("Press any to continue...."); + ColoredConsole.Write("Press any key to continue...."); Console.ReadKey(true); } From 2d0593625e9e87cb2af1aa8b70fbf40d96a33cb0 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Thu, 27 Jan 2022 19:00:10 -0500 Subject: [PATCH 124/127] Added winget instructions to install Core Tools V3 and V4 (#2930) * Added winget instructions to install Core Tools V3 and V4 * Add extra # to subheadings * Correct small pre-existing typo --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 464397442..83ddc715c 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,25 @@ choco install azure-functions-core-tools-3 --params "'/x64'" choco install azure-functions-core-tools-2 ``` +#### To install with winget: + +##### v4 + +```bash +winget install Microsoft.AzureFunctionsCoreTools +``` + +##### v3 + +```bash +winget install Microsoft.AzureFunctionsCoreTools -v 3.0.3904 +``` + ### Mac #### Homebrew: -#### v4 +##### v4 ```bash brew tap azure/functions From b19e6d2cac3b0a2d8dd7200c522362de51a84cc0 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Thu, 3 Feb 2022 15:21:57 -0500 Subject: [PATCH 125/127] Removed reference to release_notes.md as that file does not exist (#2939) --- .github/pull_request_template.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 640f174dc..3ef9805ce 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,13 +8,6 @@ resolves #issue_for_this_pr * [ ] My changes **do not** require documentation changes * [ ] Otherwise: Documentation issue linked to PR -* [ ] My changes **should not** be added to the release notes for the next release - * [ ] Otherwise: I've added my notes to `release_notes.md` * [ ] My changes **do not** need to be backported to a previous version * [ ] Otherwise: Backport tracked by issue/PR #issue_or_pr -* [ ] I have added all required tests (Unit tests, E2E tests) - - -### Additional information - -Additional PR information +* [ ] I have added all required tests (Unit tests, E2E tests) \ No newline at end of file From 7cc6b91db80afcff97a4d9d6f335c9275a1dd57f Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Mon, 4 Apr 2022 10:05:49 -0700 Subject: [PATCH 126/127] npm audit fix for minimist (#3005) --- .../npm/npm-shrinkwrap.json | 467 +++++++++++++++++- 1 file changed, 463 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json index eeacf7778..6a46254f7 100644 --- a/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json +++ b/src/Azure.Functions.Cli/npm/npm-shrinkwrap.json @@ -1,8 +1,467 @@ { "name": "azure-functions-core-tools", "version": "2.7.2184", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "azure-functions-core-tools", + "version": "2.7.2184", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "chalk": "3.0.0", + "command-exists": "1.2.8", + "glob": "7.1.6", + "https-proxy-agent": "5.0.0", + "progress": "2.0.3", + "rimraf": "3.0.2", + "tmp": "0.1.0", + "unzipper": "0.10.10" + }, + "bin": { + "azfun": "lib/main.js", + "azurefunctions": "lib/main.js", + "func": "lib/main.js" + }, + "engines": { + "node": ">=6.9.1" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "node_modules/agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", + "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", + "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/command-exists": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz", + "integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dependencies": { + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "engines": { + "node": "*" + } + }, + "node_modules/unzipper": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", + "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + }, "dependencies": { "@types/color-name": { "version": "1.1.1", @@ -221,9 +680,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mkdirp": { "version": "0.5.4", From 389cae9ab7f6e3e8f3ecb9adb25c1aedf11157a5 Mon Sep 17 00:00:00 2001 From: gorillapower Date: Tue, 5 Apr 2022 11:44:51 -0300 Subject: [PATCH 127/127] Added PartitionCount lookup from host.json --- src/Azure.Functions.Cli/Common/DurableManager.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Azure.Functions.Cli/Common/DurableManager.cs b/src/Azure.Functions.Cli/Common/DurableManager.cs index eb2a423ae..2970e4fa4 100644 --- a/src/Azure.Functions.Cli/Common/DurableManager.cs +++ b/src/Azure.Functions.Cli/Common/DurableManager.cs @@ -30,6 +30,8 @@ internal class DurableManager : IDurableManager private string _connectionStringKey; + private int? _partitionCount; + public const string DefaultConnectionStringKey = "AzureWebJobsStorage"; public const string DefaultTaskHubName = "DurableFunctionsHub"; @@ -80,6 +82,12 @@ private void SetConnectionStringAndTaskHubName() ?? _connectionStringKey; _taskHubName = durableTask.GetValue("HubName", StringComparison.OrdinalIgnoreCase)?.ToString() ?? _taskHubName; + + if (durableTask.TryGetValue("storageProvider", StringComparison.OrdinalIgnoreCase, out JToken storageProviderToken)) + { + JObject storageProviderObject = storageProviderToken as JObject; + _partitionCount = storageProviderObject?.GetValue("partitionCount", StringComparison.OrdinalIgnoreCase)?.Value(); + } } } else @@ -110,6 +118,11 @@ private void SetStorageServiceAndTaskHubClient(out AzureStorageOrchestrationServ StorageConnectionString = connectionString, }; + if (_partitionCount.HasValue) + { + settings.PartitionCount = _partitionCount.Value; + } + orchestrationService = new AzureStorageOrchestrationService(settings); taskHubClient = new TaskHubClient(orchestrationService); }