diff --git a/Kudu.Contracts/Deployment/DeploymentInfoBase.cs b/Kudu.Contracts/Deployment/DeploymentInfoBase.cs index fc8b5730..441754f1 100644 --- a/Kudu.Contracts/Deployment/DeploymentInfoBase.cs +++ b/Kudu.Contracts/Deployment/DeploymentInfoBase.cs @@ -72,5 +72,9 @@ public bool IsValid() // won't update until after a process restart. Therefore, we copy the needed // files into a separate folders and run sync triggers from there. public string SyncFunctionsTriggersPath { get; set; } = null; + + // If doWarmUp is set to true, the after Linux Consumption function app deployment, + // will initiate a GET request to http://appname.azurewebsites.net + public bool DoWarmUp { get; set; } } } \ No newline at end of file diff --git a/Kudu.Core/Deployment/DeploymentManager.cs b/Kudu.Core/Deployment/DeploymentManager.cs index ae716a35..db16fba5 100644 --- a/Kudu.Core/Deployment/DeploymentManager.cs +++ b/Kudu.Core/Deployment/DeploymentManager.cs @@ -690,7 +690,7 @@ private async Task Build( // 1. packaging the output folder // 2. upload the artifact to user's storage account // 3. reset the container workers after deployment - await LinuxConsumptionDeploymentHelper.SetupLinuxConsumptionFunctionAppDeployment(_environment, _settings, context); + await LinuxConsumptionDeploymentHelper.SetupLinuxConsumptionFunctionAppDeployment(_environment, _settings, context, deploymentInfo.DoWarmUp); } await PostDeploymentHelper.SyncFunctionsTriggers( diff --git a/Kudu.Core/Deployment/Generator/OryxBuilder.cs b/Kudu.Core/Deployment/Generator/OryxBuilder.cs index 5698d95b..a9e52723 100644 --- a/Kudu.Core/Deployment/Generator/OryxBuilder.cs +++ b/Kudu.Core/Deployment/Generator/OryxBuilder.cs @@ -34,7 +34,7 @@ public override Task Build(DeploymentContext context) context.RepositoryPath = RepositoryPath; // Initialize Oryx Args. - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(environment); if (!args.SkipKuduSync) { diff --git a/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs b/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs index 4d4d5cca..9eb3e8bb 100644 --- a/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs +++ b/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs @@ -4,11 +4,11 @@ namespace Kudu.Core.Deployment.Oryx { public class OryxArgumentsFactory { - public static IOryxArguments CreateOryxArguments() + public static IOryxArguments CreateOryxArguments(IEnvironment env) { if (FunctionAppHelper.LooksLikeFunctionApp()) { - if (FunctionAppHelper.HasScmRunFromPackage()) + if (env.IsOnLinuxConsumption) { return new LinuxConsumptionFunctionAppOryxArguments(); } else { diff --git a/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs b/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs index c569de85..266f9ada 100644 --- a/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs +++ b/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs @@ -22,9 +22,14 @@ public class LinuxConsumptionDeploymentHelper /// Specifically used for Linux Consumption to support Server Side build scenario /// /// - public static async Task SetupLinuxConsumptionFunctionAppDeployment(IEnvironment env, IDeploymentSettingsManager settings, DeploymentContext context) + public static async Task SetupLinuxConsumptionFunctionAppDeployment( + IEnvironment env, + IDeploymentSettingsManager settings, + DeploymentContext context, + bool shouldWarmUp) { - string sas = System.Environment.GetEnvironmentVariable(Constants.ScmRunFromPackage); + string sas = settings.GetValue(Constants.ScmRunFromPackage) ?? System.Environment.GetEnvironmentVariable(Constants.ScmRunFromPackage); + string builtFolder = context.OutputPath; string packageFolder = env.DeploymentsPath; string packageFileName = OryxBuildConstants.FunctionAppBuildSettings.LinuxConsumptionArtifactName; @@ -43,6 +48,13 @@ public static async Task SetupLinuxConsumptionFunctionAppDeployment(IEnvironment // Remove Linux consumption plan functionapp workers for the site await RemoveLinuxConsumptionFunctionAppWorkers(context); + + // Invoke a warmup call to the main function site + if (shouldWarmUp) + { + await Task.Delay(TimeSpan.FromSeconds(5)); + await WarmUpFunctionAppSite(context); + } } private static async Task LogDependenciesFile(string builtFolder) @@ -179,7 +191,7 @@ private static async Task UploadLinuxConsumptionFunctionAppBuiltContent(Deployme // Check if SCM_RUN_FROM_PACKAGE does exist if (string.IsNullOrEmpty(sas)) { - context.Logger.Log($"Failed to upload because SCM_RUN_FROM_PACKAGE is not provided."); + context.Logger.Log($"Failed to upload because SCM_RUN_FROM_PACKAGE is not provided or misconfigured by function app setting."); throw new DeploymentFailedException(new ArgumentException("Failed to upload because SAS is empty.")); } @@ -210,7 +222,7 @@ private static async Task RemoveLinuxConsumptionFunctionAppWorkers(DeploymentCon string webSiteHostName = System.Environment.GetEnvironmentVariable(SettingsKeys.WebsiteHostname); string sitename = ServerConfiguration.GetApplicationName(); - context.Logger.Log($"Reseting all workers for {webSiteHostName}"); + context.Logger.Log($"Resetting all workers for {webSiteHostName}"); try { @@ -231,6 +243,24 @@ await OperationManager.AttemptAsync(async () => } } + private static async Task WarmUpFunctionAppSite(DeploymentContext context) + { + string webSiteHostName = System.Environment.GetEnvironmentVariable(SettingsKeys.WebsiteHostname); + + context.Logger.Log($"Warming up your function app {webSiteHostName}"); + + try + { + await OperationManager.AttemptAsync(async () => + { + await PostDeploymentHelper.WarmUpSiteAsync(webSiteHostName); + }, retries: 3, delayBeforeRetry: 2000); + } catch (HttpRequestException hre) + { + context.Logger.Log($"Warm up function site failed due to {hre.Message}"); + } + } + private static string PackageArtifactFromFolder(IEnvironment environment, IDeploymentSettingsManager settings, DeploymentContext context, string srcDirectory, string artifactDirectory, string artifactFilename) { context.Logger.Log("Writing the artifacts to a squashfs file"); diff --git a/Kudu.Core/Helpers/PostDeploymentHelper.cs b/Kudu.Core/Helpers/PostDeploymentHelper.cs index 0b8aa1fa..76da1c18 100644 --- a/Kudu.Core/Helpers/PostDeploymentHelper.cs +++ b/Kudu.Core/Helpers/PostDeploymentHelper.cs @@ -384,6 +384,31 @@ public static async Task RemoveAllWorkersAsync(string websiteHostname, string si return; } + /// + /// Invoke main site url to warm up function app + /// + /// sitename.azurewebsites.net + /// + public static async Task WarmUpSiteAsync(string websiteHostname) + { + Uri baseUri = null; + if (!Uri.TryCreate($"http://{websiteHostname}", UriKind.Absolute, out baseUri)) + { + throw new ArgumentException($"Malformed URI is used in WarmUpSite"); + } + Trace(TraceEventType.Information, "Calling WarmUpSite to warm up your application"); + + // Initiate GET request + using (var client = HttpClientFactory()) + using (var response = await client.GetAsync(baseUri)) + { + response.EnsureSuccessStatusCode(); + Trace(TraceEventType.Information, "WarmUpSite, statusCode = {0}", response.StatusCode); + } + + return; + } + private static void VerifyEnvironments() { if (string.IsNullOrEmpty(HttpHost)) diff --git a/Kudu.Core/Infrastructure/FunctionAppHelper.cs b/Kudu.Core/Infrastructure/FunctionAppHelper.cs index 3a4fe65e..11a7b57d 100644 --- a/Kudu.Core/Infrastructure/FunctionAppHelper.cs +++ b/Kudu.Core/Infrastructure/FunctionAppHelper.cs @@ -11,11 +11,6 @@ public static bool LooksLikeFunctionApp() return !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(Constants.FunctionRunTimeVersion)); } - public static bool HasScmRunFromPackage() - { - return !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(Constants.ScmRunFromPackage)); - } - public static bool IsCSharpFunctionFromProjectFile(string projectPath) { return VsHelper.IncludesAnyReferencePackage(projectPath, "Microsoft.NET.Sdk.Functions"); diff --git a/Kudu.Services/Deployment/PushDeploymentController.cs b/Kudu.Services/Deployment/PushDeploymentController.cs index 6a8ea8d3..36a443af 100644 --- a/Kudu.Services/Deployment/PushDeploymentController.cs +++ b/Kudu.Services/Deployment/PushDeploymentController.cs @@ -56,6 +56,7 @@ public PushDeploymentController( [DisableFormValueModelBinding] public async Task ZipPushDeploy( [FromQuery] bool isAsync = false, + [FromQuery] bool warmUp = false, [FromQuery] string author = null, [FromQuery] string authorEmail = null, [FromQuery] string deployer = DefaultDeployer, @@ -79,7 +80,8 @@ public async Task ZipPushDeploy( Author = author, AuthorEmail = authorEmail, Message = message, - ZipURL = null + ZipURL = null, + DoWarmUp = warmUp }; if (_settings.RunFromLocalZip()) @@ -102,6 +104,7 @@ public async Task ZipPushDeploy( public async Task ZipPushDeployViaUrl( [FromBody] JObject requestJson, [FromQuery] bool isAsync = false, + [FromQuery] bool warmUp = false, [FromQuery] string author = null, [FromQuery] string authorEmail = null, [FromQuery] string deployer = DefaultDeployer, @@ -128,6 +131,7 @@ public async Task ZipPushDeployViaUrl( AuthorEmail = authorEmail, Message = message, ZipURL = zipUrl, + DoWarmUp = warmUp }; return await PushDeployAsync(deploymentInfo, isAsync, HttpContext); } diff --git a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsAppServiceTests.cs b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsAppServiceTests.cs index bef6d177..b6c04bf0 100644 --- a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsAppServiceTests.cs +++ b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsAppServiceTests.cs @@ -28,7 +28,7 @@ public void DefaultTest() [Theory] [InlineData("NODE", "8.15", true, false, BuildOptimizationsFlags.CompressModules)] - [InlineData("PYTHON", "3.6", true, false, BuildOptimizationsFlags.None)] + [InlineData("PYTHON", "3.6", true, false, BuildOptimizationsFlags.CompressModules)] [InlineData("PHP", "7.3", true, false, BuildOptimizationsFlags.None)] [InlineData("DOTNETCORE", "2.2", true, true, BuildOptimizationsFlags.UseTempDirectory)] public void ArgumentPropertyTest(string language, string version, @@ -82,8 +82,8 @@ public void CommandGenerationTest(string language, string version, string expect } [Theory] - [InlineData("1.0", "oryx build RepositoryPath -o OutputPath --platform dotnet --platform-version 1.1 -i BuildTempPath")] - [InlineData("2.0", "oryx build RepositoryPath -o OutputPath --platform dotnet --platform-version 2.1 -i BuildTempPath")] + [InlineData("1.0", "oryx build RepositoryPath -o OutputPath --platform dotnet --platform-version 1.1 -i BuildTempPath --log-file /tmp/test.log")] + [InlineData("2.0", "oryx build RepositoryPath -o OutputPath --platform dotnet --platform-version 2.1 -i BuildTempPath --log-file /tmp/test.log")] public void DotnetcoreVersionPromotionTest(string version, string expectedCommand) { var mockedEnvironment = new Dictionary() diff --git a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs index 5eaac526..6638ba90 100644 --- a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs +++ b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs @@ -1,4 +1,5 @@ -using Kudu.Core.Deployment; +using Kudu.Core; +using Kudu.Core.Deployment; using Kudu.Core.Deployment.Oryx; using System; using System.Collections.Generic; @@ -12,7 +13,8 @@ public class OryxArgumentsFactoryTests [Fact] public void OryxArgumentShouldBeAppService() { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = new TestMockedIEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); Assert.IsType(args); } @@ -22,7 +24,8 @@ public void OryxArgumentShouldBeFunctionApp() using (new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "PYTHON")) using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); Assert.IsType(args); } } @@ -34,7 +37,8 @@ public void OryxArgumentShouldBeLinuxConsumption() using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) using (new TestScopedEnvironmentVariable("SCM_RUN_FROM_PACKAGE", "http://microsoft.com")) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); Assert.IsType(args); } } @@ -55,7 +59,8 @@ public void OryxArgumentRunOryxBuild(bool expectedRunOryxBuild, params string[] using (new TestScopedEnvironmentVariable(env)) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); Assert.Equal(expectedRunOryxBuild, args.RunOryxBuild); } } @@ -76,7 +81,8 @@ public void OryxArgumentSkipKuduSync(bool expectedSkipKuduSync, params string[] using (new TestScopedEnvironmentVariable(env)) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); Assert.Equal(expectedSkipKuduSync, args.SkipKuduSync); } } @@ -88,7 +94,8 @@ public void BuildCommandForAppService() { OutputPath = "outputpath" }; - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); string command = args.GenerateOryxBuildCommand(deploymentContext); Assert.Equal(@"oryx build outputpath -o outputpath", command); } @@ -104,7 +111,8 @@ public void BuildCommandForFunctionApp() using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); string command = args.GenerateOryxBuildCommand(deploymentContext); Assert.Equal(@"oryx build outputpath -o outputpath -i buildtemppath", command); } @@ -121,7 +129,8 @@ public void BuildCommandForLinuxConsumptionFunctionApp() using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) using (new TestScopedEnvironmentVariable("SCM_RUN_FROM_PACKAGE", "http://microsoft.com")) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); string command = args.GenerateOryxBuildCommand(deploymentContext); Assert.Equal(@"oryx build repositorypath -o repositorypath", command); } @@ -139,7 +148,8 @@ public void BuildCommandForPythonFunctionApp() using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) using (new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "python")) { - IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + IEnvironment ienv = TestMockedEnvironment.GetMockedEnvironment(); + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(ienv); string command = args.GenerateOryxBuildCommand(deploymentContext); Assert.Equal(@"oryx build outputpath -o outputpath --platform python --platform-version 3.6 -i buildtemppath -p packagedir=.python_packages\lib\python3.6\site-packages", command); } diff --git a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsLinuxConsumptionFunctionAppTests.cs b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsLinuxConsumptionFunctionAppTests.cs index 7b5cba52..632dd33c 100644 --- a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsLinuxConsumptionFunctionAppTests.cs +++ b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsLinuxConsumptionFunctionAppTests.cs @@ -23,7 +23,7 @@ public void DefaultTest() OutputPath = "OutputPath" }; string command = args.GenerateOryxBuildCommand(mockedContext); - Assert.Equal("oryx build RepositoryPath -o RepositoryPath", command); + Assert.Equal("oryx build RepositoryPath -o OutputPath", command); } [Theory] diff --git a/Kudu.Tests/TestMockedIEnvironment.cs b/Kudu.Tests/TestMockedIEnvironment.cs new file mode 100644 index 00000000..33608270 --- /dev/null +++ b/Kudu.Tests/TestMockedIEnvironment.cs @@ -0,0 +1,70 @@ +using Kudu.Core; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kudu.Tests +{ + public class TestMockedIEnvironment : IEnvironment + { + public string _RootPath = "/"; + public string _SiteRootPath = "/site"; + public string _RepositoryPath = "/site/repository"; + public string _WebRootPath = "/site/wwwroot"; + public string _DeploymentsPath = "/site/deployments"; + public string _DeploymentToolsPath = "/site/deployments/tools"; + public string _SiteExtensionSettingsPath = "/site/siteextensions"; + public string _DiagnosticsPath = "/site/diagnostics"; + public string _LocksPath = "/site/locks"; + public string _SshKeyPath = "/.ssh"; + public string _TempPath = "/tmp"; + public string _ZipTempPath = "/tmp/zipdeploy"; + public string _ScriptPath = "/site/scripts"; + public string _NodeModulesPath = "/site/node_modules"; + public string _LogFilesPath = "/logfiles"; + public string _ApplicationLogFilesPath = "/logfiles/application"; + public string _TracePath = "/logfiles/kudu/trace"; + public string _AnalyticsPath = "/site/siteExtLogs"; + public string _DeploymentTracePath = "/logfiles/kudu/deployment"; + public string _DataPath = "/data"; + public string _JobsDataPath = "/data/jobs"; + public string _JobsBinariesPath = "/site/wwwroot/app_data/jobs"; + public string _SecondaryJobsBinariesPath = "/site/jobs"; + public string _FunctionsPath = "/site/wwwroot"; + public string _AppBaseUrlPrefix = "siteName.azurewebsites.net"; + public string _RequestId = "00000000-0000-0000-0000-000000000000"; + public string _KuduConsoleFullPath = "KuduConsole/kudu.dll"; + public string _SitePackagesPath = "/data/SitePackages"; + public bool _IsOnLinuxConsumption = false; + + public string RootPath => _RootPath; + public string SiteRootPath => _SiteRootPath; + public string RepositoryPath { get => _RepositoryPath; set => _RepositoryPath = value; } + public string WebRootPath => _WebRootPath; + public string DeploymentsPath => _DeploymentsPath; + public string DeploymentToolsPath => _DeploymentToolsPath; + public string SiteExtensionSettingsPath => _SiteExtensionSettingsPath; + public string DiagnosticsPath => _DiagnosticsPath; + public string LocksPath => _LocksPath; + public string SSHKeyPath => _SshKeyPath; + public string TempPath => _TempPath; + public string ZipTempPath => _ZipTempPath; + public string ScriptPath => _ScriptPath; + public string NodeModulesPath => _NodeModulesPath; + public string LogFilesPath => _LogFilesPath; + public string ApplicationLogFilesPath => _ApplicationLogFilesPath; + public string TracePath => _TracePath; + public string AnalyticsPath => _AnalyticsPath; + public string DeploymentTracePath => _DeploymentTracePath; + public string DataPath => _DataPath; + public string JobsDataPath => _JobsDataPath; + public string JobsBinariesPath => _JobsBinariesPath; + public string SecondaryJobsBinariesPath => _SecondaryJobsBinariesPath; + public string FunctionsPath => _FunctionsPath; + public string AppBaseUrlPrefix => _AppBaseUrlPrefix; + public string RequestId => _RequestId; + public string KuduConsoleFullPath => _KuduConsoleFullPath; + public string SitePackagesPath => _SitePackagesPath; + public bool IsOnLinuxConsumption => _IsOnLinuxConsumption; + } +}