Skip to content

Commit

Permalink
Fix: [Functions][Kubernetes]SyncTrigger issue (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
TsuyoshiUshio authored May 19, 2021
1 parent a17a854 commit 96aae86
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 13 deletions.
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

0 comments on commit 96aae86

Please sign in to comment.