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

Fix: [Functions][Kubernetes]SyncTrigger issue #208

13 changes: 13 additions & 0 deletions Kudu.Core/Functions/KedaFunctionTriggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ public static IEnumerable<ScaleTrigger> GetFunctionTriggers(IEnumerable<JObject>
return CreateScaleTriggers(triggerBindings, hostJsonText, appSettings);
}

public static IEnumerable<ScaleTrigger> GetFunctionTriggersFromSyncTriggerPayload(string synctriggerPayload,
string hostJsonText, IDictionary<string, string> appSettings)
{
return CreateScaleTriggers(ParseSyncTriggerPayload(synctriggerPayload), hostJsonText, appSettings);
}

internal static IEnumerable<ScaleTrigger> CreateScaleTriggers(IEnumerable<FunctionTrigger> triggerBindings, string hostJsonText, IDictionary<string, string> appSettings)
{

Expand All @@ -138,6 +144,13 @@ bool IsDurable(FunctionTrigger function) =>
return kedaScaleTriggers;
}

internal static IEnumerable<FunctionTrigger> ParseSyncTriggerPayload(string payload)
{
var payloadJson = JObject.Parse(payload);
var triggers = (JArray)payloadJson["triggers"];
return triggers.Select(o => o.ToObject<JObject>())
.Select(o => new FunctionTrigger(o["functionName"].ToString(), o, o["type"].ToString()));
}

internal static IEnumerable<FunctionTrigger> ParseFunctionJson(string functionName, JObject functionJson)
{
Expand Down
8 changes: 4 additions & 4 deletions Kudu.Core/Functions/SyncTriggerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,9 @@ public Tuple<IEnumerable<ScaleTrigger>, string> GetScaleTriggers(string function
return new Tuple<IEnumerable<ScaleTrigger>, string>(null, "Function trigger payload is null or empty.");
}

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, _appSettings);
scaleTriggers =
KedaFunctionTriggerProvider.GetFunctionTriggersFromSyncTriggerPayload(functionTriggersPayload,
string.Empty, _appSettings);
if (!scaleTriggers.Any())
{
return new Tuple<IEnumerable<ScaleTrigger>, string>(null, "No triggers in the payload");
Expand All @@ -78,5 +77,6 @@ public Tuple<IEnumerable<ScaleTrigger>, string> GetScaleTriggers(string function

return new Tuple<IEnumerable<ScaleTrigger>, string>(scaleTriggers, null); ;
}

}
}
69 changes: 66 additions & 3 deletions Kudu.Core/Kube/SecretProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace Kudu.Core.Kube
{

public class SecretProvider
{
private readonly HttpClient _httpClient = new HttpClient();
private readonly string _secretKubeApiUrlPlaceHolder = "https://kubernetes.default.svc.cluster.local/api/v1/namespaces/{0}/secrets/{1}";
private readonly string _rbacServiceActTokenFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
private const string _caFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";

public async Task<string> GetSecretContent(string secretName, string secretNamespace)
{
var responseBodyContent = "";
var secretKubeApiUrl = string.Format(_secretKubeApiUrlPlaceHolder, secretNamespace, secretName);
var accessToken = await GetAccessToken();
_httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
var responseMessage = await _httpClient.GetAsync(secretKubeApiUrl);
var httpClient = CreateHttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
var responseMessage = await httpClient.GetAsync(secretKubeApiUrl);

if (responseMessage.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Expand All @@ -45,5 +48,65 @@ private async Task<string> GetAccessToken()

return accessToken;
}

private static HttpClient CreateHttpClient()
{
var client = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = ServerCertificateValidationCallback
});

return client;
}

private static bool ServerCertificateValidationCallback(
HttpRequestMessage request,
X509Certificate2 certificate,
X509Chain certChain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
// certificate is already valid
return true;
}
else if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch ||
sslPolicyErrors == SslPolicyErrors.RemoteCertificateNotAvailable)
{
// api-server cert must exist and have the right subject
return false;
}
else if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
{
// only remaining error state is RemoteCertificateChainErrors
// check custom CA
var privateChain = new X509Chain();
privateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

var caCert = new X509Certificate2(_caFilePath);
// https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chainpolicy?view=netcore-2.2
// Add CA cert to the chain store to include it in the chain check.
privateChain.ChainPolicy.ExtraStore.Add(caCert);
// Build the chain for `certificate` which should be the self-signed kubernetes api-server cert.
privateChain.Build(certificate);

foreach (X509ChainStatus chainStatus in privateChain.ChainStatus)
{
if (chainStatus.Status != X509ChainStatusFlags.NoError &&
// root CA cert is not always trusted.
chainStatus.Status != X509ChainStatusFlags.UntrustedRoot)
{
return false;
}
}

return true;
}
else
{
// Unknown sslPolicyErrors
return false;
}
}
}
}
19 changes: 17 additions & 2 deletions Kudu.Core/Kube/SyncTriggerAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async static Task<bool> AuthenticateCaller(Dictionary<string, IEnumerable

//If the encryption key secret is null or empty in the Kubernetes - return false
var secretProvider = new SecretProvider();
var encryptionKeySecretContent = await secretProvider.GetSecretContent(funcAppName + "-encryptionkey".ToLower(), funcAppNamespace);
var encryptionKeySecretContent = await secretProvider.GetSecretContent(funcAppName + "-secrets".ToLower(), funcAppNamespace);
if (string.IsNullOrEmpty(encryptionKeySecretContent))
{
return false;
Expand All @@ -64,10 +64,25 @@ public async static Task<bool> AuthenticateCaller(Dictionary<string, IEnumerable
return false;
}

var decryptedToken = Decrypt(Encoding.UTF8.GetBytes(functionEncryptionKey), funcAppAuthToken);
var decryptedToken = Decrypt(GetKeyBytes(functionEncryptionKey), funcAppAuthToken);

return ValidateToken(decryptedToken);
}

public static byte[] GetKeyBytes(string hexOrBase64)
{
// only support 32 bytes (256 bits) key length
if (hexOrBase64.Length == 64)
{
return Enumerable.Range(0, hexOrBase64.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hexOrBase64.Substring(x, 2), 16))
.ToArray();
}

return Convert.FromBase64String(hexOrBase64);
}

private static bool ValidateToken(string token)
{
if (string.IsNullOrEmpty(token))
Expand Down
2 changes: 1 addition & 1 deletion Kudu.Services.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ public void Configure(IApplicationBuilder app,

routes.MapHttpRouteDual("sync-function-triggers-put", "operations/settriggers",
new { controller = "Function", action = "SyncTrigger" },
new { verb = new HttpMethodRouteConstraint("PUT") });
new { verb = new HttpMethodRouteConstraint("POST") });

// catch all unregistered url to properly handle not found
// routes.MapRoute("error-404", "{*path}", new {controller = "Error404", action = "Handle"});
Expand Down
2 changes: 1 addition & 1 deletion Kudu.Services/Function/FunctionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public FunctionController(ITracer tracer,
/// Sync triggers to the k8se function apps
/// </summary>
/// <returns></returns>
[HttpPut]
[HttpPost]
public async Task<IActionResult> SyncTrigger()
{
try
Expand Down
69 changes: 68 additions & 1 deletion Kudu.Tests/Core/Function/KedaFunctionTriggersProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,81 @@ public void PopulateMetadataDictionary_KedaV2_OnlyKedaSupportedTriggers()
""authLevel"": ""anonymous""
}]
}";

var jsonObj = JObject.Parse(jsonText);

var triggers = KedaFunctionTriggerProvider.GetFunctionTriggers(new[] { jsonObj }, string.Empty, new Dictionary<string, string>());

Assert.Equal(0, triggers.Count());
}

[Fact]
public void ConversionFromSyncTriggerPayloadToFunctionTrigger()
{
var inputPayload = @"
{
""triggers"": [
{
""name"": ""myQueueItem"",
""type"": ""queueTrigger"",
""direction"": ""in"",
""queueName"": ""js-queue-items"",
""connection"": ""AzureWebJobsStorage"",
""functionName"": ""QueueTrigger""
}
],
""functions"": [
{
""name"": ""QueueTrigger"",
""script_root_path_href"": ""https://tsushiququecon1901.limaarcncsenv19--k53aemd.northcentralusstage.k4apps.io/admin/vfs/home/site/wwwroot/QueueTrigger/"",
""script_href"": ""https://tsushiququecon1901.limaarcncsenv19--k53aemd.northcentralusstage.k4apps.io/admin/vfs/home/site/wwwroot/QueueTrigger/index.js"",
""config_href"": ""https://tsushiququecon1901.limaarcncsenv19--k53aemd.northcentralusstage.k4apps.io/admin/vfs/home/site/wwwroot/QueueTrigger/function.json"",
""test_data_href"": ""https://tsushiququecon1901.limaarcncsenv19--k53aemd.northcentralusstage.k4apps.io/admin/vfs/tmp/FunctionsData/QueueTrigger.dat"",
""href"": ""https://tsushiququecon1901.limaarcncsenv19--k53aemd.northcentralusstage.k4apps.io/admin/functions/QueueTrigger"",
""invoke_url_template"": null,
""language"": ""node"",
""config"": {
""bindings"": [
{
""name"": ""myQueueItem"",
""type"": ""queueTrigger"",
""direction"": ""in"",
""queueName"": ""js-queue-items"",
""connection"": ""AzureWebJobsStorage""
}
]
},
""files"": null,
""test_data"": """",
""isDisabled"": false,
""isDirect"": false,
""isProxy"": false
}
]
}
";
var expectedJObjectString = @"
{
""name"": ""myQueueItem"",
""type"": ""queueTrigger"",
""direction"": ""in"",
""queueName"": ""js-queue-items"",
""connection"": ""AzureWebJobsStorage"",
""functionName"": ""QueueTrigger""
}
";

var expectedJObject = JObject.Parse(expectedJObjectString);
IEnumerable<KedaFunctionTriggerProvider.FunctionTrigger> results = KedaFunctionTriggerProvider.ParseSyncTriggerPayload(inputPayload);
var actual = results.FirstOrDefault();
Assert.NotNull(actual);
Assert.Equal("QueueTrigger", actual.FunctionName);
Assert.Equal("myQueueItem", actual.Binding["name"]);
Assert.Equal("js-queue-items", actual.Binding["queueName"]);
Assert.Equal("in", actual.Binding["direction"]);
Assert.Equal("AzureWebJobsStorage", actual.Binding["connection"]);
}

[Fact]
public void UpdateFunctionTriggerBindingExpression_Replace_Expression()
{
Expand Down
3 changes: 2 additions & 1 deletion Kudu.Tests/Core/Function/SyncTriggerHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using Kudu.Core.Functions;
using System.Linq;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Kudu.Tests.Core.Function
{
public class SyncTriggerHandlerTests
{
[Theory]
[InlineData("[{\"type\":\"httpTrigger\",\"methods\":[\"get\",\"post\"],\"authLevel\":\"function\",\"name\":\"req\",\"functionName\":\"Function1 - Oct152020 - 1\"},{\"type\":\"queueTrigger\",\"connection\":\"AzureWebJobsStorage\",\"queueName\":\"myqueue - 1\",\"name\":\"myQueueItem\",\"functionName\":\"Function2\"}]")]
[InlineData("{\"triggers\":[{\"type\":\"httpTrigger\",\"methods\":[\"get\",\"post\"],\"authLevel\":\"function\",\"name\":\"req\",\"functionName\":\"Function1 - Oct152020 - 1\"},{\"type\":\"queueTrigger\",\"connection\":\"AzureWebJobsStorage\",\"queueName\":\"myqueue - 1\",\"name\":\"myQueueItem\",\"functionName\":\"Function2\"}], \"functions\":[]}")]
[InlineData("Invalid json")]
[InlineData(null)]
public void GetScaleTriggersTest(string functionTriggerPayload)
Expand Down