Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

only return valid keda triggers #194

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions Kudu.Core/Functions/FunctionTriggerProvider.cs

This file was deleted.

92 changes: 51 additions & 41 deletions Kudu.Core/Functions/KedaFunctionTriggerProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Returns "<see cref="IEnumerable<ScaleTrigger>"/> for KEDA scalers"
/// </summary>
public class KedaFunctionTriggerProvider
public static class KedaFunctionTriggerProvider
{
public IEnumerable<ScaleTrigger> GetFunctionTriggers(string zipFilePath)
public static IEnumerable<ScaleTrigger> GetFunctionTriggers(string zipFilePath)
{
if (!File.Exists(zipFilePath))
{
Expand Down Expand Up @@ -43,12 +38,37 @@ public IEnumerable<ScaleTrigger> 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<ScaleTrigger> GetFunctionTriggers(IEnumerable<JObject> functionsJson, string hostJsonText)
{
var triggerBindings = functionsJson
.Select(o => ParseFunctionJson(o["functionName"]?.ToString(), o))
.SelectMany(i => i);

return CreateScaleTriggers(triggerBindings, hostJsonText);
}

internal static IEnumerable<ScaleTrigger> CreateScaleTriggers(IEnumerable<FunctionTrigger> triggerBindings, string hostJsonText)
{
var durableTriggers = triggerBindings.Where(b => IsDurable(b));
var standardTriggers = triggerBindings.Where(b => !IsDurable(b));

Expand All @@ -61,17 +81,6 @@ public IEnumerable<ScaleTrigger> 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) ||
Expand All @@ -80,10 +89,9 @@ bool IsDurable(FunctionTrigger function) =>
return kedaScaleTriggers;
}

private IEnumerable<FunctionTrigger> ParseFunctionJson(string functionName, string functionJson)
internal static IEnumerable<FunctionTrigger> 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))
Expand All @@ -99,13 +107,13 @@ private IEnumerable<FunctionTrigger> 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))
Expand All @@ -115,20 +123,24 @@ private IEnumerable<FunctionTrigger> ParseFunctionJson(string functionName, stri
}
}

private static IEnumerable<ScaleTrigger> GetStandardScaleTriggers(IEnumerable<FunctionTrigger> standardTriggers)
internal static IEnumerable<ScaleTrigger> GetStandardScaleTriggers(IEnumerable<FunctionTrigger> 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))
{
Expand All @@ -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))
{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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<string, string> PopulateMetadataDictionary(JToken t)
internal static IDictionary<string, string> PopulateMetadataDictionary(JToken t, string functionName)
{
const string ConnectionField = "connection";
const string ConnectionFromEnvField = "connectionFromEnv";
Expand Down Expand Up @@ -259,10 +268,11 @@ public static IDictionary<string, string> 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)
{
Expand All @@ -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";
Expand Down
28 changes: 4 additions & 24 deletions Kudu.Core/Functions/SyncTriggerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,38 +51,18 @@ public async Task<string> SyncTriggers(string functionTriggersPayload)

public Tuple<IEnumerable<ScaleTrigger>, string> GetScaleTriggers(string functionTriggersPayload)
{
var scaleTriggers = new List<ScaleTrigger>();
IEnumerable<ScaleTrigger> scaleTriggers = new List<ScaleTrigger>();
try
{
if (string.IsNullOrEmpty(functionTriggersPayload))
{
return new Tuple<IEnumerable<ScaleTrigger>, 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<string, string>()
};

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<JObject>());

// 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<IEnumerable<ScaleTrigger>, string>(null, "No triggers in the payload");
Expand Down
4 changes: 2 additions & 2 deletions Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ public static void RequestContainerRestart(IEnvironment environment, string reas
{
string appName = environment.K8SEAppName;
string buildNumber = environment.CurrId;
var functionTriggers = FunctionTriggerProvider.GetFunctionTriggers<IEnumerable<ScaleTrigger>>("keda", repositoryUrl);
var functionTriggers = KedaFunctionTriggerProvider.GetFunctionTriggers(repositoryUrl);
var buildMetadata = new BuildMetadata()
{
AppName = appName,
BuildVersion = buildNumber,
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);
Expand Down
4 changes: 4 additions & 0 deletions Kudu.Core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Kudu.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
35 changes: 27 additions & 8 deletions Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScaleTrigger> result = provider.GetFunctionTriggers(zipFilePath);
IEnumerable<ScaleTrigger> result = KedaFunctionTriggerProvider.GetFunctionTriggers(zipFilePath);
Assert.Equal(2, result.Count());

ScaleTrigger mssqlTrigger = Assert.Single(result, trigger => trigger.Type.Equals("mssql", StringComparison.OrdinalIgnoreCase));
Expand All @@ -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
Expand Down Expand Up @@ -74,7 +73,7 @@ public void PopulateMetadataDictionary_KedaV1_CorrectlyPopulatesRabbitMQMetadata

JToken jsonObj = JToken.Parse(jsonText);

IDictionary<string, string> metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj);
IDictionary<string, string> metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj, "f1");

Assert.Equal(4, metadata.Count);
Assert.True(metadata.ContainsKey("type"));
Expand All @@ -100,13 +99,33 @@ public void PopulateMetadataDictionary_KedaV2_CorrectlyPopulatesRabbitMQMetadata

JToken jsonObj = JToken.Parse(jsonText);

IDictionary<string, string> metadata = KedaFunctionTriggerProvider.PopulateMetadataDictionary(jsonObj);
IDictionary<string, string> 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());
}
}
}