From 827bcdad2be5df7f974f56926c080901ab07ec77 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Tue, 13 Apr 2021 14:15:39 -0700 Subject: [PATCH] only return valid keda triggers --- .../Functions/FunctionTriggerProvider.cs | 30 ------ .../Functions/KedaFunctionTriggerProvider.cs | 92 ++++++++++--------- Kudu.Core/Functions/SyncTriggerHandler.cs | 28 +----- .../DockerContainerRestartTrigger.cs | 4 +- Kudu.Core/Properties/AssemblyInfo.cs | 4 + .../KedaFunctionTriggersProviderTests.cs | 35 +++++-- 6 files changed, 88 insertions(+), 105 deletions(-) delete mode 100644 Kudu.Core/Functions/FunctionTriggerProvider.cs create mode 100644 Kudu.Core/Properties/AssemblyInfo.cs diff --git a/Kudu.Core/Functions/FunctionTriggerProvider.cs b/Kudu.Core/Functions/FunctionTriggerProvider.cs deleted file mode 100644 index 18d32af0..00000000 --- a/Kudu.Core/Functions/FunctionTriggerProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Kudu.Core.Functions -{ - public class FunctionTriggerProvider - { - /// - /// Returns the function triggers from function.json files - /// - /// The the returns json string differs as per the format expected based upon providers. - /// e.g. for "KEDA" the json string is the serialzed string of IEnumerableobject - /// The functions file path - /// The josn string of triggers in function.json - public static T GetFunctionTriggers(string providerName, string functionzipFilePath) - { - if (string.IsNullOrWhiteSpace(providerName) || string.IsNullOrWhiteSpace(functionzipFilePath)) - { - return default(T); - } - - switch (providerName.ToLower()) - { - case "keda": - var functionTriggerProvider = new KedaFunctionTriggerProvider(); - return (T)functionTriggerProvider.GetFunctionTriggers(functionzipFilePath); - default: - functionTriggerProvider = new KedaFunctionTriggerProvider(); - return (T)functionTriggerProvider.GetFunctionTriggers(functionzipFilePath); - } - } - } -} diff --git a/Kudu.Core/Functions/KedaFunctionTriggerProvider.cs b/Kudu.Core/Functions/KedaFunctionTriggerProvider.cs index ae590b0a..76de8067 100644 --- a/Kudu.Core/Functions/KedaFunctionTriggerProvider.cs +++ b/Kudu.Core/Functions/KedaFunctionTriggerProvider.cs @@ -1,20 +1,15 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using System.Text; namespace Kudu.Core.Functions { - /// - /// Returns " for KEDA scalers" - /// - public class KedaFunctionTriggerProvider + public static class KedaFunctionTriggerProvider { - public IEnumerable GetFunctionTriggers(string zipFilePath) + public static IEnumerable GetFunctionTriggers(string zipFilePath) { if (!File.Exists(zipFilePath)) { @@ -43,12 +38,37 @@ public IEnumerable GetFunctionTriggers(string zipFilePath) { using (var reader = new StreamReader(stream)) { - triggerBindings.AddRange(ParseFunctionJson(GetFunctionName(entry), reader.ReadToEnd())); + triggerBindings.AddRange(ParseFunctionJson(GetFunctionName(entry), JObject.Parse(reader.ReadToEnd()))); } } } } + bool IsFunctionJson(string fullName) + { + return fullName.EndsWith(Constants.FunctionsConfigFile) && + fullName.Count(c => c == '/' || c == '\\') == 1; + } + + bool IsHostJson(string fullName) + { + return fullName.Equals(Constants.FunctionsHostConfigFile, StringComparison.OrdinalIgnoreCase); + } + + return CreateScaleTriggers(triggerBindings, hostJsonText); + } + + public static IEnumerable GetFunctionTriggers(IEnumerable functionsJson, string hostJsonText) + { + var triggerBindings = functionsJson + .Select(o => ParseFunctionJson(o["functionName"]?.ToString(), o)) + .SelectMany(i => i); + + return CreateScaleTriggers(triggerBindings, hostJsonText); + } + + internal static IEnumerable CreateScaleTriggers(IEnumerable triggerBindings, string hostJsonText) + { var durableTriggers = triggerBindings.Where(b => IsDurable(b)); var standardTriggers = triggerBindings.Where(b => !IsDurable(b)); @@ -61,17 +81,6 @@ public IEnumerable GetFunctionTriggers(string zipFilePath) kedaScaleTriggers.Add(durableScaleTrigger); } - bool IsFunctionJson(string fullName) - { - return fullName.EndsWith(Constants.FunctionsConfigFile) && - fullName.Count(c => c == '/' || c == '\\') == 1; - } - - bool IsHostJson(string fullName) - { - return fullName.Equals(Constants.FunctionsHostConfigFile, StringComparison.OrdinalIgnoreCase); - } - bool IsDurable(FunctionTrigger function) => function.Type.Equals("orchestrationTrigger", StringComparison.OrdinalIgnoreCase) || function.Type.Equals("activityTrigger", StringComparison.OrdinalIgnoreCase) || @@ -80,10 +89,9 @@ bool IsDurable(FunctionTrigger function) => return kedaScaleTriggers; } - private IEnumerable ParseFunctionJson(string functionName, string functionJson) + internal static IEnumerable ParseFunctionJson(string functionName, JObject functionJson) { - var json = JObject.Parse(functionJson); - if (json.TryGetValue("disabled", out JToken value)) + if (functionJson.TryGetValue("disabled", out JToken value)) { string stringValue = value.ToString(); if (!bool.TryParse(stringValue, out bool disabled)) @@ -99,13 +107,13 @@ private IEnumerable ParseFunctionJson(string functionName, stri } } - var excluded = json.TryGetValue("excluded", out value) && (bool)value; + var excluded = functionJson.TryGetValue("excluded", out value) && (bool)value; if (excluded) { yield break; } - foreach (JObject binding in (JArray)json["bindings"]) + foreach (JObject binding in (JArray)functionJson["bindings"]) { var type = (string)binding["type"]; if (type.EndsWith("Trigger", StringComparison.OrdinalIgnoreCase)) @@ -115,20 +123,24 @@ private IEnumerable ParseFunctionJson(string functionName, stri } } - private static IEnumerable GetStandardScaleTriggers(IEnumerable standardTriggers) + internal static IEnumerable GetStandardScaleTriggers(IEnumerable standardTriggers) { foreach (FunctionTrigger function in standardTriggers) { - var scaleTrigger = new ScaleTrigger + var triggerType = GetKedaTriggerType(function.Type); + if (!string.IsNullOrEmpty(triggerType)) { - Type = GetKedaTriggerType(function.Type), - Metadata = PopulateMetadataDictionary(function.Binding) - }; - yield return scaleTrigger; + var scaleTrigger = new ScaleTrigger + { + Type = triggerType, + Metadata = PopulateMetadataDictionary(function.Binding, function.FunctionName) + }; + yield return scaleTrigger; + } } } - private static string GetFunctionName(ZipArchiveEntry zipEntry) + internal static string GetFunctionName(ZipArchiveEntry zipEntry) { if (string.IsNullOrWhiteSpace(zipEntry?.FullName)) { @@ -138,7 +150,7 @@ private static string GetFunctionName(ZipArchiveEntry zipEntry) return zipEntry.FullName.Split('/').Length == 2 ? zipEntry.FullName.Split('/')[0] : zipEntry.FullName.Split('\\')[0]; } - public static string GetKedaTriggerType(string triggerType) + internal static string GetKedaTriggerType(string triggerType) { if (string.IsNullOrEmpty(triggerType)) { @@ -167,15 +179,12 @@ public static string GetKedaTriggerType(string triggerType) case "rabbitmqtrigger": return "rabbitmq"; - case "httpTrigger": - return "httpTrigger"; - default: - return triggerType; + return string.Empty; } } - private static bool TryGetDurableKedaTrigger(string hostJsonText, out ScaleTrigger scaleTrigger) + internal static bool TryGetDurableKedaTrigger(string hostJsonText, out ScaleTrigger scaleTrigger) { scaleTrigger = null; if (string.IsNullOrEmpty(hostJsonText)) @@ -214,7 +223,7 @@ private static bool TryGetDurableKedaTrigger(string hostJsonText, out ScaleTrigg } // match https://github.com/Azure/azure-functions-core-tools/blob/6bfab24b2743f8421475d996402c398d2fe4a9e0/src/Azure.Functions.Cli/Kubernetes/KEDA/V2/KedaV2Resource.cs#L91 - public static IDictionary PopulateMetadataDictionary(JToken t) + internal static IDictionary PopulateMetadataDictionary(JToken t, string functionName) { const string ConnectionField = "connection"; const string ConnectionFromEnvField = "connectionFromEnv"; @@ -259,10 +268,11 @@ public static IDictionary PopulateMetadataDictionary(JToken t) metadata.Remove("type"); metadata.Remove("name"); + metadata["functionName"] = functionName; return metadata; } - private class FunctionTrigger + internal class FunctionTrigger { public FunctionTrigger(string functionName, JObject binding, string type) { @@ -276,7 +286,7 @@ public FunctionTrigger(string functionName, JObject binding, string type) public string Type { get; } } - public class TriggerTypes + static class TriggerTypes { public const string AzureBlobStorage = "blobtrigger"; public const string AzureEventHubs = "eventhubtrigger"; diff --git a/Kudu.Core/Functions/SyncTriggerHandler.cs b/Kudu.Core/Functions/SyncTriggerHandler.cs index 1ed10815..b384a275 100644 --- a/Kudu.Core/Functions/SyncTriggerHandler.cs +++ b/Kudu.Core/Functions/SyncTriggerHandler.cs @@ -51,7 +51,7 @@ public async Task SyncTriggers(string functionTriggersPayload) public Tuple, string> GetScaleTriggers(string functionTriggersPayload) { - var scaleTriggers = new List(); + IEnumerable scaleTriggers = new List(); try { if (string.IsNullOrEmpty(functionTriggersPayload)) @@ -59,30 +59,10 @@ public Tuple, string> GetScaleTriggers(string function return new Tuple, string>(null, "Function trigger payload is null or empty."); } - var triggersJson = JArray.Parse(functionTriggersPayload); - foreach (JObject trigger in triggersJson) - { - var type = (string)trigger["type"]; - if (type.EndsWith("Trigger", StringComparison.OrdinalIgnoreCase)) - { - var scaleTrigger = new ScaleTrigger - { - Type = KedaFunctionTriggerProvider.GetKedaTriggerType(type), - Metadata = new Dictionary() - }; - - foreach (var property in trigger) - { - if (property.Value.Type == JTokenType.String) - { - scaleTrigger.Metadata.Add(property.Key, property.Value.ToString()); - } - } - - scaleTriggers.Add(scaleTrigger); - } - } + var triggersJson = JArray.Parse(functionTriggersPayload).Select(o => o.ToObject()); + // TODO: https://github.com/Azure/azure-functions-host/issues/7288 should change how we parse hostJsonText here. + scaleTriggers = KedaFunctionTriggerProvider.GetFunctionTriggers(triggersJson, string.Empty); if (!scaleTriggers.Any()) { return new Tuple, string>(null, "No triggers in the payload"); diff --git a/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs b/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs index 021a4900..a7571808 100644 --- a/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs +++ b/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs @@ -33,7 +33,7 @@ public static void RequestContainerRestart(IEnvironment environment, string reas { string appName = environment.K8SEAppName; string buildNumber = environment.CurrId; - var functionTriggers = FunctionTriggerProvider.GetFunctionTriggers>("keda", repositoryUrl); + var functionTriggers = KedaFunctionTriggerProvider.GetFunctionTriggers(repositoryUrl); var buildMetadata = new BuildMetadata() { AppName = appName, @@ -41,7 +41,7 @@ public static void RequestContainerRestart(IEnvironment environment, string reas AppSubPath = appSubPath }; - //Only for function apps functionTriggers will be non-null/non-empty + //Only for function apps functionTriggers will be non-null/non-empty if (functionTriggers?.Any() == true) { K8SEDeploymentHelper.UpdateFunctionAppTriggers(appName, functionTriggers, buildMetadata); diff --git a/Kudu.Core/Properties/AssemblyInfo.cs b/Kudu.Core/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..9e48b835 --- /dev/null +++ b/Kudu.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Kudu.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs b/Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs index ea744857..d9482ec2 100644 --- a/Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs +++ b/Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs @@ -23,13 +23,12 @@ public void DurableFunctionApp() CreateJsonFileEntry(archive, "f1/function.json", @"{""bindings"":[{""type"":""orchestrationTrigger"",""name"":""context""}],""disabled"":false}"); CreateJsonFileEntry(archive, "f2/function.json", @"{""bindings"":[{""type"":""entityTrigger"",""name"":""ctx""}],""disabled"":false}"); CreateJsonFileEntry(archive, "f3/function.json", @"{""bindings"":[{""type"":""activityTrigger"",""name"":""input""}],""disabled"":false}"); - CreateJsonFileEntry(archive, "f4/function.json", @"{""bindings"":[{""type"":""httpTrigger"",""methods"":[""post""],""authLevel"":""anonymous"",""name"":""req""}],""disabled"":false}"); + CreateJsonFileEntry(archive, "f4/function.json", @"{""bindings"":[{""type"":""queueTrigger"",""connection"":""AzureWebjobsStorage"",""queueName"":""queue"",""name"":""queueItem""}],""disabled"":false}"); } try { - var provider = new KedaFunctionTriggerProvider(); - IEnumerable result = provider.GetFunctionTriggers(zipFilePath); + IEnumerable result = KedaFunctionTriggerProvider.GetFunctionTriggers(zipFilePath); Assert.Equal(2, result.Count()); ScaleTrigger mssqlTrigger = Assert.Single(result, trigger => trigger.Type.Equals("mssql", StringComparison.OrdinalIgnoreCase)); @@ -43,8 +42,8 @@ public void DurableFunctionApp() string connectionStringName = Assert.Contains("connectionStringFromEnv", mssqlTrigger.Metadata); Assert.Equal("SQLDB_Connection", connectionStringName); - ScaleTrigger httpTrigger = Assert.Single(result, trigger => trigger.Type.Equals("httpTrigger", StringComparison.OrdinalIgnoreCase)); - string functionName = Assert.Contains("functionName", httpTrigger.Metadata); + ScaleTrigger queueTrigger = Assert.Single(result, trigger => trigger.Type.Equals("azure-queue", StringComparison.OrdinalIgnoreCase)); + string functionName = Assert.Contains("functionName", queueTrigger.Metadata); Assert.Equal("f4", functionName); } finally @@ -74,7 +73,7 @@ public void PopulateMetadataDictionary_KedaV1_CorrectlyPopulatesRabbitMQMetadata JToken jsonObj = JToken.Parse(jsonText); - IDictionary metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj); + IDictionary metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj, "f1"); Assert.Equal(4, metadata.Count); Assert.True(metadata.ContainsKey("type")); @@ -100,13 +99,33 @@ public void PopulateMetadataDictionary_KedaV2_CorrectlyPopulatesRabbitMQMetadata JToken jsonObj = JToken.Parse(jsonText); - IDictionary metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj); + IDictionary metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj, "f1"); - Assert.Equal(2, metadata.Count); + Assert.Equal(3, metadata.Count); Assert.True(metadata.ContainsKey("queueName")); Assert.True(metadata.ContainsKey("hostFromEnv")); Assert.Equal("myQueue", metadata["queueName"]); Assert.Equal("RabbitMQConnection", metadata["hostFromEnv"]); } + + [Fact] + public void PopulateMetadataDictionary_KedaV2_OnlyKedaSupportedTriggers() + { + string jsonText = @" + { + ""functionName"": ""f1"", + ""bindings"": [{ + ""type"": ""httpTrigger"", + ""methods"": [""GET""], + ""authLevel"": ""anonymous"" + }] + }"; + + var jsonObj = JObject.Parse(jsonText); + + var triggers = KedaFunctionTriggerProvider.GetFunctionTriggers(new[] { jsonObj }, string.Empty); + + Assert.Equal(0, triggers.Count()); + } } }