From 36cd4c4cac910b060041eea4007eeda8706518e0 Mon Sep 17 00:00:00 2001 From: SatishRanjan Date: Fri, 3 Apr 2020 10:53:27 -0700 Subject: [PATCH] Update "triggers" information for the function apps (#112) * Update "triggers" information for the function apps * updating function trigger using new buildctl updatejson command * Adding buildNumber in the functionApp trigger patch json payload * Adding buildNumber in the functionApp trigger patch json payload * Updating the method name to include triggers name to it --- Kudu.Core/Deployment/DeploymentManager.cs | 80 ++++++------ Kudu.Core/Functions/CodeSpec.cs | 10 ++ .../Functions/FunctionTriggerProvider.cs | 30 +++++ .../Functions/KedaFunctionTriggerProvider.cs | 117 ++++++++++++++++++ Kudu.Core/Functions/PackageReference.cs | 10 ++ Kudu.Core/Functions/PatchAppJson.cs | 13 ++ Kudu.Core/Functions/PatchSpec.cs | 16 +++ Kudu.Core/Functions/ScaleTrigger.cs | 14 +++ Kudu.Core/Functions/TriggerOptions.cs | 17 +++ .../DockerContainerRestartTrigger.cs | 19 ++- Kudu.Core/K8SE/BuildCtlArgumentsHelper.cs | 5 + Kudu.Core/K8SE/K8SEDeploymentHelper.cs | 65 +++++++++- 12 files changed, 347 insertions(+), 49 deletions(-) create mode 100644 Kudu.Core/Functions/CodeSpec.cs create mode 100644 Kudu.Core/Functions/FunctionTriggerProvider.cs create mode 100644 Kudu.Core/Functions/KedaFunctionTriggerProvider.cs create mode 100644 Kudu.Core/Functions/PackageReference.cs create mode 100644 Kudu.Core/Functions/PatchAppJson.cs create mode 100644 Kudu.Core/Functions/PatchSpec.cs create mode 100644 Kudu.Core/Functions/ScaleTrigger.cs create mode 100644 Kudu.Core/Functions/TriggerOptions.cs diff --git a/Kudu.Core/Deployment/DeploymentManager.cs b/Kudu.Core/Deployment/DeploymentManager.cs index 180c6d63..49a178ad 100644 --- a/Kudu.Core/Deployment/DeploymentManager.cs +++ b/Kudu.Core/Deployment/DeploymentManager.cs @@ -9,6 +9,7 @@ using Kudu.Contracts.Infrastructure; using Kudu.Contracts.Settings; using Kudu.Contracts.Tracing; +using Kudu.Core.Functions; using Kudu.Core.Helpers; using Kudu.Core.Hooks; using Kudu.Core.Infrastructure; @@ -111,7 +112,7 @@ public IEnumerable GetResults() public DeployResult GetResult(string id) { - return GetResult(id, _status.ActiveDeploymentId, IsDeploying); + return GetResult(id, _status.ActiveDeploymentId, IsDeploying); } public IEnumerable GetLogEntries(string id) @@ -194,30 +195,30 @@ public async Task DeployAsync( Console.WriteLine("Deploy Async"); using (var deploymentAnalytics = new DeploymentAnalytics(_analytics, _settings)) { - Exception exception = null; - ITracer tracer = _traceFactory.GetTracer(); - IDisposable deployStep = null; - ILogger innerLogger = null; - string targetBranch = null; + Exception exception = null; + ITracer tracer = _traceFactory.GetTracer(); + IDisposable deployStep = null; + ILogger innerLogger = null; + string targetBranch = null; + + // If we don't get a changeset, find out what branch we should be deploying and get the commit ID from it + if (changeSet == null) + { + targetBranch = _settings.GetBranch(); + + changeSet = repository.GetChangeSet(targetBranch); - // If we don't get a changeset, find out what branch we should be deploying and get the commit ID from it if (changeSet == null) { - targetBranch = _settings.GetBranch(); - - changeSet = repository.GetChangeSet(targetBranch); - - if (changeSet == null) - { - throw new InvalidOperationException(String.Format( - "The current deployment branch is '{0}', but nothing has been pushed to it", - targetBranch)); - } + throw new InvalidOperationException(String.Format( + "The current deployment branch is '{0}', but nothing has been pushed to it", + targetBranch)); } + } - string id = changeSet.Id; - _environment.CurrId = id; - IDeploymentStatusFile statusFile = null; + string id = changeSet.Id; + _environment.CurrId = id; + IDeploymentStatusFile statusFile = null; try { deployStep = tracer.Step($"DeploymentManager.Deploy(id:{id})"); @@ -284,9 +285,8 @@ public async Task DeployAsync( logger.Log(Resources.Log_TriggeringContainerRestart); } - string appName = _environment.SiteRootPath.Replace("/home/apps/", "").Split("/")[0]; ; - - DockerContainerRestartTrigger.RequestContainerRestart(_environment, RestartTriggerReason); + string appName = _environment.SiteRootPath.Replace("/home/apps/", "").Split("/")[0]; + DockerContainerRestartTrigger.RequestContainerRestart(_environment, RestartTriggerReason, deploymentInfo.RepositoryUrl); logger.Log($"Deployment Pod Rollout Started! Use kubectl watch deplotment {appName} to monitor the rollout status"); } } @@ -314,21 +314,21 @@ public async Task DeployAsync( } } - // Reload status file with latest updates - statusFile = _status.Open(id, _environment); - using (tracer.Step("Reloading status file with latest updates")) + // Reload status file with latest updates + statusFile = _status.Open(id, _environment); + using (tracer.Step("Reloading status file with latest updates")) + { + if (statusFile != null) { - if (statusFile != null) - { - await _hooksManager.PublishEventAsync(HookEventTypes.PostDeployment, statusFile); - } + await _hooksManager.PublishEventAsync(HookEventTypes.PostDeployment, statusFile); } + } - if (exception != null) - { - tracer.TraceError(exception); - throw new DeploymentFailedException(exception); - } + if (exception != null) + { + tracer.TraceError(exception); + throw new DeploymentFailedException(exception); + } } } @@ -681,11 +681,11 @@ private async Task Build( NextManifestFilePath = GetDeploymentManifestPath(id), PreviousManifestFilePath = GetActiveDeploymentManifestPath(), IgnoreManifest = deploymentInfo != null && deploymentInfo.CleanupTargetDirectory, - // Ignoring the manifest will cause kudusync to delete sub-directories / files - // in the destination directory that are not present in the source directory, - // without checking the manifest to see if the file was copied over to the destination - // during a previous kudusync operation. This effectively performs a clean deployment - // from the source to the destination directory + // Ignoring the manifest will cause kudusync to delete sub-directories / files + // in the destination directory that are not present in the source directory, + // without checking the manifest to see if the file was copied over to the destination + // during a previous kudusync operation. This effectively performs a clean deployment + // from the source to the destination directory Tracer = tracer, Logger = logger, GlobalLogger = _globalLogger, diff --git a/Kudu.Core/Functions/CodeSpec.cs b/Kudu.Core/Functions/CodeSpec.cs new file mode 100644 index 00000000..4ba5c9e6 --- /dev/null +++ b/Kudu.Core/Functions/CodeSpec.cs @@ -0,0 +1,10 @@ +namespace Kudu.Core.Functions +{ + using Newtonsoft.Json; + + public class CodeSpec + { + [JsonProperty(PropertyName = "packageRef")] + public PackageReference PackageRef { get; set; } + } +} diff --git a/Kudu.Core/Functions/FunctionTriggerProvider.cs b/Kudu.Core/Functions/FunctionTriggerProvider.cs new file mode 100644 index 00000000..85e21737 --- /dev/null +++ b/Kudu.Core/Functions/FunctionTriggerProvider.cs @@ -0,0 +1,30 @@ +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)) + { + return default; + } + + 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 new file mode 100644 index 00000000..d1a932fb --- /dev/null +++ b/Kudu.Core/Functions/KedaFunctionTriggerProvider.cs @@ -0,0 +1,117 @@ +using Newtonsoft.Json; +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 IEnumerable GetFunctionTriggers(string zipFilePath) + { + if (!File.Exists(zipFilePath)) + { + return null; + } + + List kedaScaleTriggers = new List(); + using (var zip = ZipFile.OpenRead(zipFilePath)) + { + var entries = zip.Entries + .Where(e => IsFunctionJson(e.FullName)); + + foreach (var entry in entries) + { + using (var stream = entry.Open()) + { + using (var reader = new StreamReader(stream)) + { + var functionTriggers = ParseFunctionJson(GetFunctionName(entry), reader.ReadToEnd()); + if (functionTriggers?.Any() == true) + { + kedaScaleTriggers.AddRange(functionTriggers); + } + } + } + } + } + + bool IsFunctionJson(string fullName) + { + return fullName.EndsWith(Constants.FunctionsConfigFile) && + fullName.Count(c => c == '/' || c == '\\') == 1; + } + + return kedaScaleTriggers; + } + + public IEnumerable ParseFunctionJson(string functionName, string functionJson) + { + var json = JObject.Parse(functionJson); + if (json.TryGetValue("disabled", out JToken value)) + { + string stringValue = value.ToString(); + if (!bool.TryParse(stringValue, out bool disabled)) + { + string expandValue = System.Environment.GetEnvironmentVariable(stringValue); + disabled = string.Equals(expandValue, "1", StringComparison.OrdinalIgnoreCase) || + string.Equals(expandValue, "true", StringComparison.OrdinalIgnoreCase); + } + + if (disabled) + { + return null; + } + } + + var excluded = json.TryGetValue("excluded", out value) && (bool)value; + if (excluded) + { + return null; + } + + var triggers = new List(); + foreach (JObject binding in (JArray)json["bindings"]) + { + var type = (string)binding["type"]; + if (type.EndsWith("Trigger", StringComparison.OrdinalIgnoreCase)) + { + var scaleTrigger = new ScaleTrigger + { + Type = type, + Metadata = new Dictionary() + }; + foreach (var property in binding) + { + if (property.Value.Type == JTokenType.String) + { + scaleTrigger.Metadata.Add(property.Key, property.Value.ToString()); + } + } + + scaleTrigger.Metadata.Add("functionName", functionName); + triggers.Add(scaleTrigger); + } + } + + return triggers; + } + + private static string GetFunctionName(ZipArchiveEntry zipEnetry) + { + if (string.IsNullOrWhiteSpace(zipEnetry?.FullName)) + { + return string.Empty; + } + + return zipEnetry.FullName.Split('/').Length == 2 ? zipEnetry.FullName.Split('/')[0] : zipEnetry.FullName.Split('\\')[0]; + } + } +} diff --git a/Kudu.Core/Functions/PackageReference.cs b/Kudu.Core/Functions/PackageReference.cs new file mode 100644 index 00000000..667b9ac1 --- /dev/null +++ b/Kudu.Core/Functions/PackageReference.cs @@ -0,0 +1,10 @@ +namespace Kudu.Core.Functions +{ + using Newtonsoft.Json; + + public class PackageReference + { + [JsonProperty(PropertyName = "buildVersion")] + public string BuildVersion { get; set; } + } +} diff --git a/Kudu.Core/Functions/PatchAppJson.cs b/Kudu.Core/Functions/PatchAppJson.cs new file mode 100644 index 00000000..b0fb6705 --- /dev/null +++ b/Kudu.Core/Functions/PatchAppJson.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kudu.Core.Functions +{ + public class PatchAppJson + { + [JsonProperty(PropertyName = "spec")] + public PatchSpec PatchSpec { get; set; } + } +} diff --git a/Kudu.Core/Functions/PatchSpec.cs b/Kudu.Core/Functions/PatchSpec.cs new file mode 100644 index 00000000..111bba6e --- /dev/null +++ b/Kudu.Core/Functions/PatchSpec.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kudu.Core.Functions +{ + public class PatchSpec + { + [JsonProperty(PropertyName = "triggerOptions")] + public TriggerOptions TriggerOptions { get; set; } + + [JsonProperty(PropertyName = "code")] + public CodeSpec Code { get; set; } + } +} diff --git a/Kudu.Core/Functions/ScaleTrigger.cs b/Kudu.Core/Functions/ScaleTrigger.cs new file mode 100644 index 00000000..3d97c5c0 --- /dev/null +++ b/Kudu.Core/Functions/ScaleTrigger.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Kudu.Core.Functions +{ + public class ScaleTrigger + { + [JsonProperty(PropertyName = "type")] + public string Type { get; set; } + + [JsonProperty(PropertyName = "metadata")] + public IDictionary Metadata { get; set; } + } +} diff --git a/Kudu.Core/Functions/TriggerOptions.cs b/Kudu.Core/Functions/TriggerOptions.cs new file mode 100644 index 00000000..68493871 --- /dev/null +++ b/Kudu.Core/Functions/TriggerOptions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Kudu.Core.Functions +{ + public class TriggerOptions + { + [JsonProperty(PropertyName = "triggers")] + public IEnumerable Triggers { get; set; } + + [JsonProperty(PropertyName = "pollingInterval")] + public int? PollingInterval { get; set; } + + [JsonProperty(PropertyName = "cooldownPeriod")] + public int? cooldownPeriod { get; set; } + } +} diff --git a/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs b/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs index 4d955f39..f5020170 100644 --- a/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs +++ b/Kudu.Core/Infrastructure/DockerContainerRestartTrigger.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; +using Kudu.Core.Functions; using Kudu.Core.Helpers; using Kudu.Core.K8SE; @@ -22,14 +25,24 @@ public static class DockerContainerRestartTrigger "The last modification Kudu made to this file was at {0}, for the following reason: {1}.", System.Environment.NewLine); - public static void RequestContainerRestart(IEnvironment environment, string reason) + public static void RequestContainerRestart(IEnvironment environment, string reason, string repositoryUrl = null) { - if(K8SEDeploymentHelper.IsK8SEEnvironment()) + if (K8SEDeploymentHelper.IsK8SEEnvironment()) { string appName = environment.SiteRootPath.Replace("/home/apps/", "").Split("/")[0]; string buildNumber = environment.CurrId; + var functionTriggers = FunctionTriggerProvider.GetFunctionTriggers>("keda", repositoryUrl); + + //Only for function apps functionTriggers will be non-null/non-empty + if (functionTriggers?.Any() == true) + { + K8SEDeploymentHelper.UpdateFunctionApp(appName, functionTriggers, buildNumber); + } + else + { + K8SEDeploymentHelper.UpdateBuildNumber(appName, buildNumber); + } - K8SEDeploymentHelper.UpdateBuildNumber(appName, buildNumber); return; } diff --git a/Kudu.Core/K8SE/BuildCtlArgumentsHelper.cs b/Kudu.Core/K8SE/BuildCtlArgumentsHelper.cs index cb6658af..a08d3766 100644 --- a/Kudu.Core/K8SE/BuildCtlArgumentsHelper.cs +++ b/Kudu.Core/K8SE/BuildCtlArgumentsHelper.cs @@ -26,5 +26,10 @@ internal static void AddAppPropertyValueArgument(StringBuilder args, string appP args.AppendFormat(" -appPropertyValue {0}", appPropertyValue); } + internal static void AddJsonToPatchValueArgument(StringBuilder args, string jsonToPatch) + { + args.AppendFormat(" -jsonToPatch {0}", jsonToPatch); + } + } } diff --git a/Kudu.Core/K8SE/K8SEDeploymentHelper.cs b/Kudu.Core/K8SE/K8SEDeploymentHelper.cs index ebf59d1b..89cfbc60 100644 --- a/Kudu.Core/K8SE/K8SEDeploymentHelper.cs +++ b/Kudu.Core/K8SE/K8SEDeploymentHelper.cs @@ -1,8 +1,12 @@ using Kudu.Contracts.Tracing; using Kudu.Core.Deployment; +using Kudu.Core.Functions; using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; namespace Kudu.Core.K8SE @@ -50,6 +54,27 @@ public static void UpdateBuildNumber(string appName, string buildNumber) RunBuildCtlCommand(cmd.ToString(), "Updating build version..."); } + /// + /// Updates the triggers for the function apps + /// + /// The app name to update + /// The IEnumerable + /// Build number to update + public static void UpdateFunctionAppTriggers(string appName, IEnumerable functionTriggers, string buildNumber) + { + var functionAppPatchJson = GetFunctionAppPatchJson(functionTriggers, buildNumber); + if (string.IsNullOrEmpty(functionAppPatchJson)) + { + return; + } + + var cmd = new StringBuilder(); + BuildCtlArgumentsHelper.AddBuildCtlCommand(cmd, "updatejson"); + BuildCtlArgumentsHelper.AddAppNameArgument(cmd, appName); + BuildCtlArgumentsHelper.AddFunctionAppTriggerToPatchValueArgument(cmd, functionAppPatchJson); + RunBuildCtlCommand(cmd.ToString(), "Updating function app triggers..."); + } + private static string RunBuildCtlCommand(string args, string msg) { Console.WriteLine($"{msg} : {args}"); @@ -71,13 +96,13 @@ private static string RunBuildCtlCommand(string args, string msg) string error = process.StandardError.ReadToEnd(); process.WaitForExit(); - if (string.IsNullOrEmpty(error)) - { - return output; + if (string.IsNullOrEmpty(error)) + { + return output; } - else - { - throw new Exception(error); + else + { + throw new Exception(error); } } @@ -96,5 +121,33 @@ public static string GetAppName(HttpContext context) return appName; } + + private static string GetFunctionAppPatchJson(IEnumerable functionTriggers, string buildNumber) + { + if (functionTriggers == null || !functionTriggers.Any()) + { + return null; + } + + var patchAppJson = new PatchAppJson + { + PatchSpec = new PatchSpec + { + TriggerOptions = new TriggerOptions + { + Triggers = functionTriggers + }, + Code = new CodeSpec + { + PackageRef = new PackageReference + { + BuildVersion = buildNumber + } + } + } + }; + + return JsonConvert.SerializeObject(patchAppJson, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } } }