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;
+ }
+}