diff --git a/Common/Constants.cs b/Common/Constants.cs index d5680466..1c3154f2 100644 --- a/Common/Constants.cs +++ b/Common/Constants.cs @@ -118,6 +118,7 @@ public static TimeSpan MaxAllowedExecutionTime public const string FunctionsPortal = "FunctionsPortal"; public const string FunctionKeyNewFormat = "~0.7"; public const string FunctionRunTimeVersion = "FUNCTIONS_EXTENSION_VERSION"; + public const string ScmRunFromPackage = "SCM_RUN_FROM_PACKAGE"; public const string WebSiteSku = "WEBSITE_SKU"; public const string WebSiteElasticScaleEnabled = "WEBSITE_ELASTIC_SCALING_ENABLED"; public const string DynamicSku = "Dynamic"; diff --git a/Kudu.Contracts/Settings/SettingsKeys.cs b/Kudu.Contracts/Settings/SettingsKeys.cs index ec002bfa..9ddbf698 100644 --- a/Kudu.Contracts/Settings/SettingsKeys.cs +++ b/Kudu.Contracts/Settings/SettingsKeys.cs @@ -49,6 +49,7 @@ public static class SettingsKeys // Antares container specific settings public const string PlaceholderMode = "WEBSITE_PLACEHOLDER_MODE"; public const string ContainerReady = "WEBSITE_CONTAINER_READY"; + public const string WebsiteHostname = "WEBSITE_HOSTNAME"; public const string AuthEncryptionKey = "WEBSITE_AUTH_ENCRYPTION_KEY"; public const string ContainerEncryptionKey = "CONTAINER_ENCRYPTION_KEY"; } diff --git a/Kudu.Core/Deployment/Generator/ExternalCommandBuilder.cs b/Kudu.Core/Deployment/Generator/ExternalCommandBuilder.cs index 3c426f15..c1ded864 100644 --- a/Kudu.Core/Deployment/Generator/ExternalCommandBuilder.cs +++ b/Kudu.Core/Deployment/Generator/ExternalCommandBuilder.cs @@ -17,6 +17,7 @@ namespace Kudu.Core.Deployment.Generator // // ExternalCommandBuilder // CustomBuilder + // OryxBuilder // GeneratorSiteBuilder // BaseBasicBuilder // BasicBuilder diff --git a/Kudu.Core/Deployment/Generator/OryxBuilder.cs b/Kudu.Core/Deployment/Generator/OryxBuilder.cs index 2d5e29b9..ba9beb3c 100644 --- a/Kudu.Core/Deployment/Generator/OryxBuilder.cs +++ b/Kudu.Core/Deployment/Generator/OryxBuilder.cs @@ -1,11 +1,13 @@ -using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Net.Http; +using Microsoft.WindowsAzure.Storage.Blob; +using Microsoft.WindowsAzure.Storage; +using Kudu.Core.Infrastructure; using Kudu.Core.Helpers; using Kudu.Contracts.Settings; using Kudu.Core.Deployment.Oryx; -using Kudu.Core.Infrastructure; -using System.IO; -using System; -using Kudu.Core.Commands; namespace Kudu.Core.Deployment.Generator { @@ -42,7 +44,7 @@ public override Task Build(DeploymentContext context) RunCommand(context, kuduSyncCommand, false, "Oryx-Build: Running kudu sync..."); } - + if (args.RunOryxBuild) { PreOryxBuild(context); @@ -65,6 +67,12 @@ public override Task Build(DeploymentContext context) } } + // Detect if package upload is necessary for server side build + if (FunctionAppHelper.HasScmRunFromPackage() && FunctionAppHelper.LooksLikeFunctionApp()) + { + SetupLinuxConsumptionFunctionAppDeployment(context).Wait(); + } + return Task.CompletedTask; } @@ -91,10 +99,17 @@ private void SetupFunctionAppExpressArtifacts(DeploymentContext context) FileSystemHelpers.EnsureDirectory(sitePackages); string zipAppName = $"{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.zip"; - string zipFile = Path.Combine(sitePackages, zipAppName); + PackageArtifactFromFolder(context, OryxBuildConstants.FunctionAppBuildSettings.ExpressBuildSetup, sitePackages, zipAppName, zipQuota:3); + + File.WriteAllText(packageNameFile, zipAppName); + File.WriteAllText(packagePathFile, sitePackages); + } + private string PackageArtifactFromFolder(DeploymentContext context, string srcDirectory, string destDirectory, string destFilename, int zipQuota = 0) + { context.Logger.Log("Writing the artifacts to a zip file"); - var exe = ExternalCommandFactory.BuildExternalCommandExecutable(OryxBuildConstants.FunctionAppBuildSettings.ExpressBuildSetup, sitePackages, context.Logger); + string zipFile = Path.Combine(destDirectory, destFilename); + var exe = ExternalCommandFactory.BuildExternalCommandExecutable(srcDirectory, destDirectory, context.Logger); try { exe.ExecuteWithProgressWriter(context.Logger, context.Tracer, $"zip -r {zipFile} .", String.Empty); @@ -106,10 +121,33 @@ private void SetupFunctionAppExpressArtifacts(DeploymentContext context) } // Just to be sure that we don't keep adding zip files here - DeploymentHelper.PurgeZipsIfNecessary(sitePackages, context.Tracer, totalAllowedZips: 3); + if (zipQuota > 0) + { + DeploymentHelper.PurgeZipsIfNecessary(destDirectory, context.Tracer, totalAllowedZips: zipQuota); + } - File.WriteAllText(packageNameFile, zipAppName); - File.WriteAllText(packagePathFile, sitePackages); + return zipFile; + } + + /// + /// Specifically used for Linux Consumption to support Server Side build scenario + /// + /// + private async Task SetupLinuxConsumptionFunctionAppDeployment(DeploymentContext context) + { + string sas = System.Environment.GetEnvironmentVariable(Constants.ScmRunFromPackage); + string builtFolder = context.RepositoryPath; + string packageFolder = Environment.DeploymentsPath; + string packageFileName = $"{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.zip"; + + // Package built content from oryx build artifact + string filePath = PackageArtifactFromFolder(context, builtFolder, packageFolder, packageFileName); + + // Upload from DeploymentsPath + await UploadLinuxConsumptionFunctionAppBuiltContent(context, sas, filePath); + + // Remove Linux consumption plan functionapp workers for the site + await RemoveLinuxConsumptionFunctionAppWorkers(context); } //public override void PostBuild(DeploymentContext context) @@ -118,5 +156,63 @@ private void SetupFunctionAppExpressArtifacts(DeploymentContext context) // context.Logger.Log($"Skipping post build. Project type: {ProjectType}"); // FileLogHelper.Log("Completed PostBuild oryx...."); //} + + private async Task UploadLinuxConsumptionFunctionAppBuiltContent(DeploymentContext context, string sas, string filePath) + { + context.Logger.Log($"Uploading built content {filePath} -> {sas}"); + + // 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."); + throw new DeploymentFailedException(new ArgumentException("Failed to upload because SAS is empty.")); + } + + // Parse SAS + Uri sasUri = null; + if (!Uri.TryCreate(sas, UriKind.Absolute, out sasUri)) + { + context.Logger.Log($"Malformed SAS when uploading built content."); + throw new DeploymentFailedException(new ArgumentException("Failed to upload because SAS is malformed.")); + } + + // Upload blob to Azure Storage + CloudBlockBlob blob = new CloudBlockBlob(sasUri); + try + { + await blob.UploadFromFileAsync(filePath); + } catch (StorageException se) + { + context.Logger.Log($"Failed to upload because Azure Storage responds {se.RequestInformation.HttpStatusCode}."); + context.Logger.Log(se.Message); + throw new DeploymentFailedException(se); + } + } + + private async Task RemoveLinuxConsumptionFunctionAppWorkers(DeploymentContext context) + { + string webSiteHostName = System.Environment.GetEnvironmentVariable(SettingsKeys.WebsiteHostname); + string sitename = ServerConfiguration.GetApplicationName(); + + context.Logger.Log($"Reseting all workers for {webSiteHostName}"); + + try + { + await OperationManager.AttemptAsync(async () => + { + await PostDeploymentHelper.RemoveAllWorkersAsync(webSiteHostName, sitename); + }, retries: 3, delayBeforeRetry: 2000); + } + catch (ArgumentException ae) + { + context.Logger.Log($"Reset all workers has malformed webSiteHostName or sitename {ae.Message}"); + throw new DeploymentFailedException(ae); + } + catch (HttpRequestException hre) + { + context.Logger.Log($"Reset all workers endpoint responded with {hre.Message}"); + throw new DeploymentFailedException(hre); + } + } } } diff --git a/Kudu.Core/Deployment/Oryx/FunctionAppOryxArguments.cs b/Kudu.Core/Deployment/Oryx/FunctionAppOryxArguments.cs index 85339caf..915db022 100644 --- a/Kudu.Core/Deployment/Oryx/FunctionAppOryxArguments.cs +++ b/Kudu.Core/Deployment/Oryx/FunctionAppOryxArguments.cs @@ -4,13 +4,13 @@ namespace Kudu.Core.Deployment.Oryx { - class FunctionAppOryxArguments : IOryxArguments + public class FunctionAppOryxArguments : IOryxArguments { public bool RunOryxBuild { get; set; } public BuildOptimizationsFlags Flags { get; set; } - private readonly WorkerRuntime FunctionsWorkerRuntime; + protected readonly WorkerRuntime FunctionsWorkerRuntime; public bool SkipKuduSync { get; set; } public FunctionAppOryxArguments() @@ -23,7 +23,7 @@ public FunctionAppOryxArguments() SkipKuduSync = Flags == BuildOptimizationsFlags.UseExpressBuild; } - public string GenerateOryxBuildCommand(DeploymentContext context) + public virtual string GenerateOryxBuildCommand(DeploymentContext context) { StringBuilder args = new StringBuilder(); @@ -36,7 +36,7 @@ public string GenerateOryxBuildCommand(DeploymentContext context) return args.ToString(); } - private void AddOryxBuildCommand(StringBuilder args, DeploymentContext context, string source, string destination) + protected void AddOryxBuildCommand(StringBuilder args, DeploymentContext context, string source, string destination) { // If it is express build, we don't directly need to write to /home/site/wwwroot // So, we build into a different directory to avoid overlap @@ -55,7 +55,7 @@ private void AddOryxBuildCommand(StringBuilder args, DeploymentContext context, OryxArgumentsHelper.AddOryxBuildCommand(args, source, destination); } - private void AddLanguage(StringBuilder args, WorkerRuntime workerRuntime) + protected void AddLanguage(StringBuilder args, WorkerRuntime workerRuntime) { switch (workerRuntime) { @@ -73,7 +73,7 @@ private void AddLanguage(StringBuilder args, WorkerRuntime workerRuntime) } } - private void AddLanguageVersion(StringBuilder args, WorkerRuntime workerRuntime) + protected void AddLanguageVersion(StringBuilder args, WorkerRuntime workerRuntime) { var workerVersion = ResolveWorkerRuntimeVersion(FunctionsWorkerRuntime); if (!string.IsNullOrEmpty(workerVersion)) @@ -82,7 +82,7 @@ private void AddLanguageVersion(StringBuilder args, WorkerRuntime workerRuntime) } } - private void AddBuildOptimizationFlags(StringBuilder args, DeploymentContext context, BuildOptimizationsFlags optimizationFlags) + protected void AddBuildOptimizationFlags(StringBuilder args, DeploymentContext context, BuildOptimizationsFlags optimizationFlags) { switch (Flags) { @@ -99,7 +99,7 @@ private void AddBuildOptimizationFlags(StringBuilder args, DeploymentContext con } } - private void AddWorkerRuntimeArgs(StringBuilder args, WorkerRuntime workerRuntime) + protected void AddWorkerRuntimeArgs(StringBuilder args, WorkerRuntime workerRuntime) { switch (workerRuntime) { diff --git a/Kudu.Core/Deployment/Oryx/FunctionAppSupportedWorkerRuntime.cs b/Kudu.Core/Deployment/Oryx/FunctionAppSupportedWorkerRuntime.cs index c45d5c14..9375cfe8 100644 --- a/Kudu.Core/Deployment/Oryx/FunctionAppSupportedWorkerRuntime.cs +++ b/Kudu.Core/Deployment/Oryx/FunctionAppSupportedWorkerRuntime.cs @@ -14,7 +14,11 @@ public class FunctionAppSupportedWorkerRuntime { public static WorkerRuntime ParseWorkerRuntime(string value) { - if (value.StartsWith("NODE", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(value)) + { + return WorkerRuntime.None; + } + else if (value.StartsWith("NODE", StringComparison.OrdinalIgnoreCase)) { return WorkerRuntime.Node; } diff --git a/Kudu.Core/Deployment/Oryx/IOryxArguments.cs b/Kudu.Core/Deployment/Oryx/IOryxArguments.cs index 32c133f4..70c48826 100644 --- a/Kudu.Core/Deployment/Oryx/IOryxArguments.cs +++ b/Kudu.Core/Deployment/Oryx/IOryxArguments.cs @@ -4,7 +4,7 @@ namespace Kudu.Core.Deployment.Oryx { - interface IOryxArguments + public interface IOryxArguments { bool RunOryxBuild { get; set; } diff --git a/Kudu.Core/Deployment/Oryx/LinuxConsumptionFunctionAppOryxArguments.cs b/Kudu.Core/Deployment/Oryx/LinuxConsumptionFunctionAppOryxArguments.cs new file mode 100644 index 00000000..929aca5a --- /dev/null +++ b/Kudu.Core/Deployment/Oryx/LinuxConsumptionFunctionAppOryxArguments.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kudu.Core.Deployment.Oryx +{ + public class LinuxConsumptionFunctionAppOryxArguments : FunctionAppOryxArguments + { + public LinuxConsumptionFunctionAppOryxArguments() : base() + { + SkipKuduSync = true; + Flags = BuildOptimizationsFlags.Off; + } + + public override string GenerateOryxBuildCommand(DeploymentContext context) + { + StringBuilder args = new StringBuilder(); + + base.AddOryxBuildCommand(args, context, source: context.RepositoryPath, destination: context.RepositoryPath); + base.AddLanguage(args, base.FunctionsWorkerRuntime); + base.AddLanguageVersion(args, base.FunctionsWorkerRuntime); + base.AddBuildOptimizationFlags(args, context, Flags); + base.AddWorkerRuntimeArgs(args, base.FunctionsWorkerRuntime); + + return args.ToString(); + } + } +} diff --git a/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs b/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs index 1dca6f3e..4d4d5cca 100644 --- a/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs +++ b/Kudu.Core/Deployment/Oryx/OryxArgumentsFactory.cs @@ -2,13 +2,18 @@ namespace Kudu.Core.Deployment.Oryx { - class OryxArgumentsFactory + public class OryxArgumentsFactory { public static IOryxArguments CreateOryxArguments() { if (FunctionAppHelper.LooksLikeFunctionApp()) { - return new FunctionAppOryxArguments(); + if (FunctionAppHelper.HasScmRunFromPackage()) + { + return new LinuxConsumptionFunctionAppOryxArguments(); + } else { + return new FunctionAppOryxArguments(); + } } return new AppServiceOryxArguments(); } diff --git a/Kudu.Core/Helpers/PostDeploymentHelper.cs b/Kudu.Core/Helpers/PostDeploymentHelper.cs index 3143358f..c3e102e7 100644 --- a/Kudu.Core/Helpers/PostDeploymentHelper.cs +++ b/Kudu.Core/Helpers/PostDeploymentHelper.cs @@ -11,6 +11,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Web; using Kudu.Contracts.Settings; using Kudu.Core.Deployment; using Kudu.Core.Infrastructure; @@ -348,6 +349,41 @@ public static async Task PerformAutoSwap(string requestId, TraceListener tracer) } } + /// + /// Remove all site workers after cloudbuilt content is uploaded + /// + /// WEBSITE_HOSTNAME + /// WEBSITE_SITE_NAME + /// Thrown when RemoveAllWorkers url is malformed. + /// Thrown when request to RemoveAllWorkers is not OK. + public static async Task RemoveAllWorkersAsync(string websiteHostname, string sitename) + { + // Generate URL encoded auth token + string websiteAuthEncryptionKey = System.Environment.GetEnvironmentVariable(SettingsKeys.AuthEncryptionKey); + DateTime expiry = DateTime.UtcNow.AddMinutes(5); + string authToken = SimpleWebTokenHelper.CreateToken(expiry, websiteAuthEncryptionKey.ToKeyBytes()); + string authTokenEncoded = HttpUtility.UrlEncode(authToken); + + // Generate RemoveAllWorker request URI + string baseUrl = $"http://{websiteHostname}/operations/removeworker/{sitename}/allStandard?token={authTokenEncoded}"; + Uri baseUri = null; + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out baseUri)) + { + throw new ArgumentException($"Malformed URI is used in RemoveAllWorkers"); + } + Trace(TraceEventType.Information, "Calling RemoveAllWorkers to refresh the function app"); + + // Initiate GET request + using (var client = HttpClientFactory()) + using (var response = await client.GetAsync(baseUri)) + { + response.EnsureSuccessStatusCode(); + Trace(TraceEventType.Information, "RemoveAllWorkers, 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 67ee56ab..3a4fe65e 100644 --- a/Kudu.Core/Infrastructure/FunctionAppHelper.cs +++ b/Kudu.Core/Infrastructure/FunctionAppHelper.cs @@ -1,4 +1,5 @@ -using System; +using Kudu.Contracts.Settings; +using System; using System.Linq; namespace Kudu.Core.Infrastructure @@ -10,6 +11,11 @@ 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.Core/Kudu.Core.csproj b/Kudu.Core/Kudu.Core.csproj index aaeeaedc..d75282c2 100644 --- a/Kudu.Core/Kudu.Core.csproj +++ b/Kudu.Core/Kudu.Core.csproj @@ -36,6 +36,7 @@ + diff --git a/Kudu.Services/Deployment/PushDeploymentController.cs b/Kudu.Services/Deployment/PushDeploymentController.cs index d503017c..82a916ca 100644 --- a/Kudu.Services/Deployment/PushDeploymentController.cs +++ b/Kudu.Services/Deployment/PushDeploymentController.cs @@ -214,8 +214,8 @@ private async Task PushDeployAsync(ZipDeploymentInfo deploymentIn { using (_tracer.Step("Writing zip file to {0}", zipFilePath)) { - if (context.Request.ContentType.Contains("multipart/form-data", - StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(context.Request.ContentType) && + context.Request.ContentType.Contains("multipart/form-data", StringComparison.OrdinalIgnoreCase)) { FormValueProvider formModel; using (_tracer.Step("Writing zip file to {0}", zipFilePath)) diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddleware.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddleware.cs index 1099fef9..432a15d4 100644 --- a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddleware.cs +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddleware.cs @@ -20,7 +20,6 @@ public class LinuxConsumptionRouteMiddleware { private static readonly HashSet Whitelist = new HashSet { - HomePageRoute, "/api/zipdeploy", "/admin/instance", "/deployments", @@ -66,7 +65,8 @@ public async Task Invoke(HttpContext context, IAuthorizationService authorizatio } else { - context.Request.Host = new HostString(SanitizeScmUrl(context.Request.Headers[HostHeader][0])); + context.Request.Host = new HostString(SanitizeScmUrl( + context.Request.Headers[HostHeader].FirstOrDefault())); } if (context.Request.Headers.TryGetValue(ForwardedProtocolHeader, out value)) @@ -74,17 +74,17 @@ public async Task Invoke(HttpContext context, IAuthorizationService authorizatio context.Request.Scheme = value; } - // Step 2: check if the request endpoint is enabled in Linux Consumption - if (!IsRouteWhitelisted(context.Request.Path)) + // Step 2: check if it is homepage route, always return 200 + if (IsHomePageRoute(context.Request.Path)) { - context.Response.StatusCode = 404; + context.Response.StatusCode = 200; return; } - // Step 3: check if it is homepage route, always return 200 - if (IsHomePageRoute(context.Request.Path)) + // Step 3: check if the request endpoint is enabled in Linux Consumption + if (!IsRouteWhitelisted(context.Request.Path)) { - context.Response.StatusCode = 200; + context.Response.StatusCode = 404; return; } @@ -116,7 +116,7 @@ private bool IsRouteWhitelisted(PathString routePath) private bool IsHomePageRoute(PathString routePath) { - return routePath.ToString() == "/"; + return routePath.ToString() == HomePageRoute; } private static string SanitizeScmUrl(string malformedUrl) diff --git a/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs new file mode 100644 index 00000000..82ca3d31 --- /dev/null +++ b/Kudu.Tests/Core/Deployment/Oryx/OryxArgumentsFactoryTests.cs @@ -0,0 +1,147 @@ +using Kudu.Core.Deployment; +using Kudu.Core.Deployment.Oryx; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Kudu.Tests.Core.Deployment.Oryx +{ + public class OryxArgumentsFactoryTests + { + [Fact] + public void OryxArgumentShouldBeAppService() + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + Assert.IsType(args); + } + + [Fact] + public void OryxArgumentShouldBeFunctionApp() + { + using (new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "PYTHON")) + using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + Assert.IsType(args); + } + } + + [Fact] + public void OryxArgumentShouldBeLinuxConsumption() + { + using (new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "PYTHON")) + using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) + using (new TestScopedEnvironmentVariable("SCM_RUN_FROM_PACKAGE", "http://microsoft.com")) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + Assert.IsType(args); + } + } + + [Theory] + [InlineData(false)] + [InlineData(false, "FUNCTIONS_EXTENSION_VERSION", "~2")] + [InlineData(false, "FUNCTIONS_EXTENSION_VERSION", "~2", "SCM_RUN_FROM_PACKAGE", "http://microsoft.com")] + [InlineData(true, "FUNCTIONS_EXTENSION_VERSION", "~2", "FUNCTIONS_WORKER_RUNTIME", "PYTHON")] + [InlineData(true, "FUNCTIONS_EXTENSION_VERSION", "~2", "SCM_RUN_FROM_PACKAGE", "http://microsoft.com", "FUNCTIONS_WORKER_RUNTIME", "PYTHON")] + public void OryxArgumentRunOryxBuild(bool expectedRunOryxBuild, params string[] varargs) + { + IDictionary env = new Dictionary(); + for (int i = 0; i < varargs.Length; i += 2) + { + env.Add(varargs[i], varargs[i + 1]); + } + + using (new TestScopedEnvironmentVariable(env)) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + Assert.Equal(expectedRunOryxBuild, args.RunOryxBuild); + } + } + + [Theory] + [InlineData(false)] + [InlineData(false, "FUNCTIONS_EXTENSION_VERSION", "~2")] + [InlineData(true, "FUNCTIONS_EXTENSION_VERSION", "~2", "SCM_RUN_FROM_PACKAGE", "http://microsoft.com")] + [InlineData(false, "FUNCTIONS_EXTENSION_VERSION", "~2", "FUNCTIONS_WORKER_RUNTIME", "PYTHON")] + [InlineData(true, "FUNCTIONS_EXTENSION_VERSION", "~2", "SCM_RUN_FROM_PACKAGE", "http://microsoft.com", "FUNCTIONS_WORKER_RUNTIME", "PYTHON")] + public void OryxArgumentSkipKuduSync(bool expectedSkipKuduSync, params string[] varargs) + { + IDictionary env = new Dictionary(); + for (int i = 0; i < varargs.Length; i += 2) + { + env.Add(varargs[i], varargs[i + 1]); + } + + using (new TestScopedEnvironmentVariable(env)) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + Assert.Equal(expectedSkipKuduSync, args.SkipKuduSync); + } + } + + [Fact] + public void BuildCommandForAppService() + { + DeploymentContext deploymentContext = new DeploymentContext() + { + OutputPath = "outputpath" + }; + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + string command = args.GenerateOryxBuildCommand(deploymentContext); + Assert.Equal(@"oryx build outputpath -o outputpath", command); + } + + [Fact] + public void BuildCommandForFunctionApp() + { + DeploymentContext deploymentContext = new DeploymentContext() + { + OutputPath = "outputpath", + BuildTempPath = "buildtemppath" + }; + + using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + string command = args.GenerateOryxBuildCommand(deploymentContext); + Assert.Equal(@"oryx build outputpath -o outputpath -i buildtemppath", command); + } + } + + [Fact] + public void BuildCommandForLinuxConsumptionFunctionApp() + { + DeploymentContext deploymentContext = new DeploymentContext() + { + RepositoryPath = "repositorypath" + }; + + using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) + using (new TestScopedEnvironmentVariable("SCM_RUN_FROM_PACKAGE", "http://microsoft.com")) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + string command = args.GenerateOryxBuildCommand(deploymentContext); + Assert.Equal(@"oryx build repositorypath -o repositorypath", command); + } + } + + [Fact] + public void BuildCommandForPythonFunctionApp() + { + DeploymentContext deploymentContext = new DeploymentContext() + { + OutputPath = "outputpath", + BuildTempPath = "buildtemppath" + }; + + using (new TestScopedEnvironmentVariable("FUNCTIONS_EXTENSION_VERSION", "~2")) + using (new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "python")) + { + IOryxArguments args = OryxArgumentsFactory.CreateOryxArguments(); + string command = args.GenerateOryxBuildCommand(deploymentContext); + Assert.Equal(@"oryx build outputpath -o outputpath -l python --language-version 3.6 -i buildtemppath -p packagedir=.python_packages\lib\python3.6\site-packages", command); + } + } + } +} diff --git a/Kudu.Tests/Core/Helpers/SimpleWebTokenTests.cs b/Kudu.Tests/Core/Helpers/SimpleWebTokenTests.cs index 3b9520dc..4a117e03 100644 --- a/Kudu.Tests/Core/Helpers/SimpleWebTokenTests.cs +++ b/Kudu.Tests/Core/Helpers/SimpleWebTokenTests.cs @@ -6,6 +6,8 @@ namespace Kudu.Tests.Core.Helpers { + // The following 'Collection' attribute is used for disabling parallel testing + // Some of the tests require changing the system environment variables, which is not thread safe. [Collection("MockedEnvironmentVariablesCollection")] public class SimpleWebTokenTests { diff --git a/Kudu.Tests/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddlewareTests.cs b/Kudu.Tests/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddlewareTests.cs index a4f07766..fec7c730 100644 --- a/Kudu.Tests/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddlewareTests.cs +++ b/Kudu.Tests/LinuxConsumptionInstanceAdmin/LinuxConsumptionRouteMiddlewareTests.cs @@ -30,6 +30,7 @@ public LinuxConsumptionRouteMiddlewareTests() _containerEncryptionKey = TestHelpers.GenerateKeyBytes(); _environmentVariables = new Dictionary { + { Constants.ContainerName, "linux_consumption_container_name" }, { SettingsKeys.AuthEncryptionKey, TestHelpers.GenerateKeyHexString(_websiteAuthEncryptionKey) }, { SettingsKeys.ContainerEncryptionKey, TestHelpers.GenerateKeyHexString(_containerEncryptionKey) } }; @@ -94,14 +95,14 @@ public void UnwhitelistedRouteNotFound() } [Fact] - public void HomepageRouteNotFound() + public void HomepageRouteFound() { using (new TestScopedEnvironmentVariable(_environmentVariables)) { HttpContext httpContext = GenerateHttpContext(DateTime.UtcNow.AddDays(1)); httpContext.Request.Path = "/"; _middleware.Invoke(httpContext).Wait(); - Assert.Equal(404, httpContext.Response.StatusCode); + Assert.Equal(200, httpContext.Response.StatusCode); } } diff --git a/Kudu.Tests/Services/Infrastructure/Authentication/ArmAuthenticationHandlerTests.cs b/Kudu.Tests/Services/Infrastructure/Authentication/ArmAuthenticationHandlerTests.cs index 598d82de..e3c0655a 100644 --- a/Kudu.Tests/Services/Infrastructure/Authentication/ArmAuthenticationHandlerTests.cs +++ b/Kudu.Tests/Services/Infrastructure/Authentication/ArmAuthenticationHandlerTests.cs @@ -13,6 +13,8 @@ namespace Kudu.Tests.Services.Infrastructure.Authentication { + // The following 'Collection' attribute is used for disabling parallel testing + // Some of the tests require changing the system environment variables, which is not thread safe. [Collection("MockedEnvironmentVariablesCollection")] public class ArmAuthenticationHandlerTests {