diff --git a/Common/Constants.cs b/Common/Constants.cs index abd1c63d..b2443bfa 100644 --- a/Common/Constants.cs +++ b/Common/Constants.cs @@ -22,8 +22,8 @@ public static class Constants public const string WebRoot = "wwwroot"; public const string MappedSite = "/_app"; public const string RepositoryPath = "repository"; - public const string ZipTempPath = "zipdeploy"; - public const string ZipExtractPath = "extracted"; + public const string ZipTempDirectoryName = "zipdeploy"; + public const string ArtifactStagingDirectoryName = "extracted"; public const string LockPath = "locks"; public const string DeploymentLockFile = "deployments.lock"; @@ -100,6 +100,15 @@ public static TimeSpan MaxAllowedExecutionTime public const string LogicAppUrlKey = "LOGICAPP_URL"; public const string RestartApiPath = "/api/app/restart"; + public const string UpdateDeployStatusPath = "/api/app/updatedeploystatus"; + + // Deployment status API constants + public const string BuildRequestReceived = "BuildRequestReceived"; + public const string BuildPending = "BuildPending"; + public const string BuildInProgress = "BuildInProgress"; + public const string BuildSuccessful = "BuildSuccessful"; + public const string PostBuildRestartRequired = "PostBuildRestartRequired"; + public const string BuildFailed = "BuildFailed"; public const string SiteExtensionProvisioningStateCreated = "Created"; public const string SiteExtensionProvisioningStateAccepted = "Accepted"; @@ -115,7 +124,7 @@ public static TimeSpan MaxAllowedExecutionTime public const string FreeSKU = "Free"; public const string BasicSKU = "Basic"; - //Setting for VC++ for node builds + // Setting for VC++ for node builds public const string VCVersion = "2015"; public const string WebsiteSiteName = "WEBSITE_SITE_NAME"; @@ -165,5 +174,9 @@ public static TimeSpan MaxAllowedExecutionTime public const string KuduFileShareMountPath = "/kudu-mnt"; public const string KuduFileSharePrefix = "kudu-mnt"; public const string EnablePersistentStorage = "ENABLE_KUDU_PERSISTENT_STORAGE"; + + public const string OneDeploy = "OneDeploy"; + + public const string ScmDeploymentIdHeader = "SCM-DEPLOYMENT-ID"; } } diff --git a/Kudu.Contracts/Deployment/ArtifactType.cs b/Kudu.Contracts/Deployment/ArtifactType.cs new file mode 100644 index 00000000..5e14c9bb --- /dev/null +++ b/Kudu.Contracts/Deployment/ArtifactType.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kudu.Contracts.Deployment +{ + public enum ArtifactType + { + Unknown, + War, + Jar, + Ear, + Lib, + Static, + Startup, + Script, + Zip, + } +} diff --git a/Kudu.Contracts/Deployment/DeployStatusApiResult.cs b/Kudu.Contracts/Deployment/DeployStatusApiResult.cs new file mode 100644 index 00000000..8088b3ce --- /dev/null +++ b/Kudu.Contracts/Deployment/DeployStatusApiResult.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kudu.Contracts.Deployment +{ + public class DeployStatusApiResult + { + public DeployStatusApiResult(string DeploymentStatus, string DeploymentId) + { + this.DeploymentStatus = DeploymentStatus; + this.DeploymentId = DeploymentId; + } + + public string DeploymentStatus { get; set; } + + public string DeploymentId { get; set; } + } +} diff --git a/Kudu.Contracts/Deployment/DeploymentInfoBase.cs b/Kudu.Contracts/Deployment/DeploymentInfoBase.cs index e851a5a3..4cf84772 100644 --- a/Kudu.Contracts/Deployment/DeploymentInfoBase.cs +++ b/Kudu.Contracts/Deployment/DeploymentInfoBase.cs @@ -4,7 +4,8 @@ using System.Threading.Tasks; using System.Collections; using System.Collections.Generic; - +using Kudu.Contracts.Deployment; + namespace Kudu.Core.Deployment { public abstract class DeploymentInfoBase @@ -16,6 +17,9 @@ protected DeploymentInfoBase() IsReusable = true; AllowDeferredDeployment = true; DoFullBuildByDefault = true; + WatchedFileEnabled = true; + RestartAllowed = true; + ArtifactType = ArtifactType.Unknown; } public RepositoryType RepositoryType { get; set; } @@ -26,16 +30,32 @@ protected DeploymentInfoBase() // Allow deferred deployment via marker file mechanism. public bool AllowDeferredDeployment { get; set; } // indicating that this is a CI triggered by SCM provider - public bool IsContinuous { get; set; } + public bool IsContinuous { get; set; } + + // NOTE: Do not access the request stream in the Fetch handler as it may have been closed during asynchronous scenarios public FetchDelegate Fetch { get; set; } public bool AllowDeploymentWhileScmDisabled { get; set; } public IDictionary repositorySymlinks { get; set; } + // Optional. + // By default, TargetSubDirectoryRelativePath specifies the directory to deploy to relative to /home/site/wwwroot. + // This property can be used to change the root from wwwroot to something else. + public string TargetRootPath { get; set; } + // Optional. // Path of the directory to be deployed to. The path should be relative to the wwwroot directory. // Example: "webapps/ROOT" - public string TargetPath { get; set; } + public string TargetSubDirectoryRelativePath { get; set; } + + // Optional. + // Specifies the name of the deployed artifact. + // Example: When deploying startup files, OneDeploy will set this to startup.cmd (or startup.sh) + public string TargetFileName { get; set; } + + // Optional. + // Type of artifact being deployed. + public ArtifactType ArtifactType { get; set; } // Optional. // Path of the file that is watched for changes by the web server. @@ -71,11 +91,22 @@ public bool IsValid() // This is used in Run-From-Zip deployments where the content of wwwroot // 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; + public string SyncFunctionsTriggersPath { get; set; } = null; + + // Specifies whether to touch the watched file (example web.config, web.xml, etc) after the deployment + public bool WatchedFileEnabled { get; set; } + // Used to allow / disallow 'restart' on a per deployment basis, if needed. + // For example: OneDeploy allows clients to enable / disable 'restart'. + public bool RestartAllowed { get; set; } + // If DoSyncTriggers is set to true, the after Linux Consumption function app deployment, // will initiate a POST request to http://appname.azurewebsites.net/admin/host/synctriggers public bool DoSyncTriggers { get; set; } + + // Allows the use of a caller-provided GUID for the deployment, rather than + // a commit hash or a randomly-generated identifier. + public string ExternalDeploymentId { get; set; } = null; // This config is for Linux Consumption function app only // Allow artifact generation even when WEBSITE_RUN_FROM_PACKAGE is set to a Url (RunFromRemoteZip) diff --git a/Kudu.Contracts/IEnvironment.cs b/Kudu.Contracts/IEnvironment.cs index a10c22f8..12a0b26b 100644 --- a/Kudu.Contracts/IEnvironment.cs +++ b/Kudu.Contracts/IEnvironment.cs @@ -31,6 +31,6 @@ public interface IEnvironment string RequestId { get; } // e.g. x-arr-log-id or x-ms-request-id header value string KuduConsoleFullPath { get; } // e.g. KuduConsole/kudu.dll string SitePackagesPath { get; } // e.g. /data/SitePackages - bool IsOnLinuxConsumption { get; } // e.g. True on Linux Consumption. False on App Service. + bool IsOnLinuxConsumption { get; } // Check if the application is a Linux Consumption function app } } diff --git a/Kudu.Core/Deployment/ZipDeploymentInfo.cs b/Kudu.Core/Deployment/ArtifactDeploymentInfo.cs similarity index 52% rename from Kudu.Core/Deployment/ZipDeploymentInfo.cs rename to Kudu.Core/Deployment/ArtifactDeploymentInfo.cs index e37eee8d..ff66ea97 100644 --- a/Kudu.Core/Deployment/ZipDeploymentInfo.cs +++ b/Kudu.Core/Deployment/ArtifactDeploymentInfo.cs @@ -5,12 +5,12 @@ namespace Kudu.Core.Deployment { - public class ZipDeploymentInfo : DeploymentInfoBase + public class ArtifactDeploymentInfo : DeploymentInfoBase { private readonly IEnvironment _environment; private readonly ITraceFactory _traceFactory; - public ZipDeploymentInfo(IEnvironment environment, ITraceFactory traceFactory) + public ArtifactDeploymentInfo(IEnvironment environment, ITraceFactory traceFactory) { _environment = environment; _traceFactory = traceFactory; @@ -18,22 +18,22 @@ public ZipDeploymentInfo(IEnvironment environment, ITraceFactory traceFactory) public override IRepository GetRepository() { - // Zip "repository" does not conflict with other types, including NoRepository, + // Artifact "repository" does not conflict with other types, including NoRepository, // so there's no call to EnsureRepository - var path = Path.Combine(_environment.ZipTempPath, Constants.ZipExtractPath); - return new NullRepository(path, _traceFactory); + var path = Path.Combine(_environment.ZipTempPath, Constants.ArtifactStagingDirectoryName); + return new NullRepository(path, _traceFactory, ExternalDeploymentId); } public string Author { get; set; } public string AuthorEmail { get; set; } - public string Message { get; set; } - - // This is used if the deployment is Run-From-Zip - public string ZipName { get; set; } + public string Message { get; set; } + + // Optional file name. Used by certain features like run-from-zip. + public string ArtifactFileName { get; set; } - // This is used when getting the zipfile from the zipURL - public string ZipURL { get; set; } + // This is used when getting the artifact file from the remote URL + public string RemoteURL { get; set; } } } diff --git a/Kudu.Core/Deployment/DeploymentManager.cs b/Kudu.Core/Deployment/DeploymentManager.cs index e1486f86..92cf6969 100644 --- a/Kudu.Core/Deployment/DeploymentManager.cs +++ b/Kudu.Core/Deployment/DeploymentManager.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Kudu.Contracts.Deployment; using Kudu.Contracts.Infrastructure; using Kudu.Contracts.Settings; using Kudu.Contracts.Tracing; @@ -195,8 +196,8 @@ public async Task DeployAsync( "The current deployment branch is '{0}', but nothing has been pushed to it", targetBranch)); } - } - + } + string id = changeSet.Id; IDeploymentStatusFile statusFile = null; try @@ -293,12 +294,24 @@ public async Task DeployAsync( } } - public async Task RestartMainSiteIfNeeded(ITracer tracer, ILogger logger) + public async Task RestartMainSiteIfNeeded(ITracer tracer, ILogger logger, DeploymentInfoBase deploymentInfo) { // If post-deployment restart is disabled, do nothing. if (!_settings.RestartAppOnGitDeploy()) { return; + } + + // Proceed only if 'restart' is allowed for this deployment + if (deploymentInfo != null && !deploymentInfo.RestartAllowed) + { + return; + } + + if (deploymentInfo != null && deploymentInfo.Deployer == Constants.OneDeploy) + { + await PostDeploymentHelper.RestartMainSiteAsync(_environment.RequestId, new PostDeploymentTraceListener(tracer, logger)); + return; } if (_settings.RecylePreviewEnabled()) @@ -632,7 +645,7 @@ private async Task Build( { using (tracer.Step("Determining deployment builder")) { - builder = _builderFactory.CreateBuilder(tracer, innerLogger, perDeploymentSettings, repository); + builder = _builderFactory.CreateBuilder(tracer, innerLogger, perDeploymentSettings, repository, deploymentInfo); deploymentAnalytics.ProjectType = builder.ProjectType; tracer.Trace("Builder is {0}", builder.GetType().Name); } @@ -703,8 +716,7 @@ private async Task Build( { await builder.Build(context); builder.PostBuild(context); - - await RestartMainSiteIfNeeded(tracer, logger); + await RestartMainSiteIfNeeded(tracer, logger, deploymentInfo); if (FunctionAppHelper.LooksLikeFunctionApp() && _environment.IsOnLinuxConsumption) { @@ -725,14 +737,11 @@ await PostDeploymentHelper.SyncFunctionsTriggers( new PostDeploymentTraceListener(tracer, logger), deploymentInfo?.SyncFunctionsTriggersPath); - if (_settings.TouchWatchedFileAfterDeployment()) - { - TryTouchWatchedFile(context, deploymentInfo); - } - - if (_settings.RunFromLocalZip() && deploymentInfo is ZipDeploymentInfo) + TouchWatchedFileIfNeeded(_settings, deploymentInfo, context); + + if (_settings.RunFromLocalZip() && deploymentInfo is ArtifactDeploymentInfo) { - await PostDeploymentHelper.UpdatePackageName(deploymentInfo as ZipDeploymentInfo, _environment, logger); + await PostDeploymentHelper.UpdatePackageName(deploymentInfo as ArtifactDeploymentInfo, _environment, logger); } FinishDeployment(id, deployStep); @@ -761,6 +770,19 @@ await PostDeploymentHelper.SyncFunctionsTriggers( } } + private static void TouchWatchedFileIfNeeded(IDeploymentSettingsManager settings, DeploymentInfoBase deploymentInfo, DeploymentContext context) + { + if (deploymentInfo != null && !deploymentInfo.WatchedFileEnabled) + { + return; + } + + if (settings.TouchWatchedFileAfterDeployment()) + { + TryTouchWatchedFile(context, deploymentInfo); + } + } + private void PreDeployment(ITracer tracer) { if (Environment.IsAzureEnvironment() @@ -813,18 +835,23 @@ private static void FailDeployment(ITracer tracer, IDisposable deployStep, Deplo private static string GetOutputPath(DeploymentInfoBase deploymentInfo, IEnvironment environment, IDeploymentSettingsManager perDeploymentSettings) { - string targetPath = perDeploymentSettings.GetTargetPath(); + string targetSubDirectoryRelativePath = perDeploymentSettings.GetTargetPath(); - if (string.IsNullOrWhiteSpace(targetPath)) + if (string.IsNullOrWhiteSpace(targetSubDirectoryRelativePath)) { - targetPath = deploymentInfo?.TargetPath; - } - - if (!string.IsNullOrWhiteSpace(targetPath)) + targetSubDirectoryRelativePath = deploymentInfo?.TargetSubDirectoryRelativePath; + } + + if (deploymentInfo?.Deployer == Constants.OneDeploy) + { + return string.IsNullOrWhiteSpace(deploymentInfo?.TargetRootPath) ? environment.WebRootPath : deploymentInfo.TargetRootPath; + } + + if (!string.IsNullOrWhiteSpace(targetSubDirectoryRelativePath)) { - targetPath = targetPath.Trim('\\', '/'); - return Path.GetFullPath(Path.Combine(environment.WebRootPath, targetPath)); - } + targetSubDirectoryRelativePath = targetSubDirectoryRelativePath.Trim('\\', '/'); + return Path.GetFullPath(Path.Combine(environment.WebRootPath, targetSubDirectoryRelativePath)); + } return environment.WebRootPath; } diff --git a/Kudu.Core/Deployment/FetchDeploymentManager.cs b/Kudu.Core/Deployment/FetchDeploymentManager.cs index b43ace09..83e64e6f 100644 --- a/Kudu.Core/Deployment/FetchDeploymentManager.cs +++ b/Kudu.Core/Deployment/FetchDeploymentManager.cs @@ -7,6 +7,7 @@ using Kudu.Contracts.Infrastructure; using Kudu.Contracts.Settings; using Kudu.Contracts.Tracing; +using Kudu.Contracts.Deployment; using Kudu.Core.Deployment.Generator; using Kudu.Core.Helpers; using Kudu.Core.Hooks; @@ -14,6 +15,7 @@ using Kudu.Core.LinuxConsumption; using Kudu.Core.SourceControl; using Kudu.Core.Tracing; +using Newtonsoft.Json; namespace Kudu.Core.Deployment { @@ -85,7 +87,7 @@ public async Task FetchDeploy( { return FetchDeploymentRequestResult.ForbiddenScmDisabled; } - + // Else if this app is configured with a url in WEBSITE_USE_ZIP, then fail the deployment // since this is a RunFromZip site and the deployment has no chance of succeeding. // However, if this is a Linux Consumption function app, we allow KuduLite to change @@ -154,8 +156,8 @@ public async Task FetchDeploy( } } - public async Task PerformDeployment(DeploymentInfoBase deploymentInfo, - IDisposable tempDeployment = null, + public async Task PerformDeployment(DeploymentInfoBase deploymentInfo, + IDisposable tempDeployment = null, ChangeSet tempChangeSet = null) { DateTime currentMarkerFileUTC; @@ -181,6 +183,7 @@ public async Task PerformDeployment(DeploymentInfoBase deploymentInfo, deploymentInfo.Deployer); ILogger innerLogger = null; + DeployStatusApiResult updateStatusObj = null; try { ILogger logger = _deploymentManager.GetLogger(tempChangeSet.Id); @@ -222,6 +225,11 @@ public async Task PerformDeployment(DeploymentInfoBase deploymentInfo, // Here, we don't need to update the working files, since we know Fetch left them in the correct state // unless for GenericHandler where specific commitId is specified bool deploySpecificCommitId = !String.IsNullOrEmpty(deploymentInfo.CommitId); + if (PostDeploymentHelper.IsAzureEnvironment() && deploymentInfo.ExternalDeploymentId != null) + { + updateStatusObj = new DeployStatusApiResult(Constants.BuildInProgress, deploymentInfo.ExternalDeploymentId); + await SendDeployStatusUpdate(updateStatusObj); + } await _deploymentManager.DeployAsync( repository, @@ -231,6 +239,13 @@ await _deploymentManager.DeployAsync( deploymentInfo: deploymentInfo, needFileUpdate: deploySpecificCommitId, fullBuildByDefault: deploymentInfo.DoFullBuildByDefault); + + if (updateStatusObj != null) + { + updateStatusObj.DeploymentStatus = Constants.BuildSuccessful; + await SendDeployStatusUpdate(updateStatusObj); + } + } } catch (Exception ex) @@ -250,6 +265,13 @@ await _deploymentManager.DeployAsync( } } + if (updateStatusObj != null) + { + // Set deployment status as failure if exception is thrown + updateStatusObj.DeploymentStatus = Constants.BuildFailed; + await SendDeployStatusUpdate(updateStatusObj); + } + throw; } @@ -269,13 +291,40 @@ await _deploymentManager.DeployAsync( // if last change is not null and finish successfully, mean there was at least one deployoment happened // since deployment is now done, trigger swap if enabled await PostDeploymentHelper.PerformAutoSwap( - _environment.RequestId, - new PostDeploymentTraceListener(_tracer, + _environment.RequestId, + new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(lastChange.Id))); } } } + /// + /// This method tries to send the deployment status update through frontend to be saved to db + /// Since frontend throttling is in place, we retry 3 times with 5 sec gaps in between + /// + /// Obj containing status to save to DB + /// + private async Task SendDeployStatusUpdate(DeployStatusApiResult updateStatusObj) + { + int attemptCount = 0; + try + { + await OperationManager.AttemptAsync(async () => + { + attemptCount++; + + _tracer.Trace($" PostAsync - Trying to send {updateStatusObj.DeploymentStatus} deployment status to {Constants.UpdateDeployStatusPath}"); + await PostDeploymentHelper.PostAsync(Constants.UpdateDeployStatusPath, _environment.RequestId, JsonConvert.SerializeObject(updateStatusObj)); + + }, 3, 5*1000); + } + catch (Exception ex) + { + _tracer.TraceError($"Failed to request a post deployment status. Number of attempts: {attemptCount}. Exception: {ex}"); + throw; + } + } + // For continuous integration, we will only build/deploy if fetch new changes // The immediate goal is to address duplicated /deploy requests from Bitbucket (retry if taken > 20s) private bool ShouldDeploy(IRepository repository, DeploymentInfoBase deploymentInfo, string targetBranch) @@ -314,7 +363,7 @@ public static async Task PerformBackgroundDeployment( // Needed for deployments where deferred deployment is not allowed. Will be set to false if // lock contention occurs and AllowDeferredDeployment is false, otherwise true. var deploymentWillOccurTcs = new TaskCompletionSource(); - + // This task will be let out of scope intentionally var deploymentTask = Task.Run(() => { diff --git a/Kudu.Core/Deployment/Generator/OneDeployBuilder.cs b/Kudu.Core/Deployment/Generator/OneDeployBuilder.cs new file mode 100644 index 00000000..4ef8894a --- /dev/null +++ b/Kudu.Core/Deployment/Generator/OneDeployBuilder.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kudu.Contracts.Settings; +using Kudu.Core.Infrastructure; + +namespace Kudu.Core.Deployment.Generator +{ + // This is the site builder used for OneDeploy scenarios + public class OneDeployBuilder : BasicBuilder + { + private DeploymentInfoBase _deploymentInfo; + private string _repositoryPath; + + public OneDeployBuilder(IEnvironment environment, IDeploymentSettingsManager settings, IBuildPropertyProvider propertyProvider, string repositoryPath, string projectPath, DeploymentInfoBase deploymentInfo) + : base(environment, settings, propertyProvider, repositoryPath, projectPath) + { + _deploymentInfo = deploymentInfo; + _repositoryPath = repositoryPath; + } + + public override string ProjectType + { + get { return Constants.OneDeploy; } + } + + public override Task Build(DeploymentContext context) + { + context.Logger.Log($"Running build. Project type: {ProjectType}"); + + // Start by copying the manifest as-is so that + // manifest based deployments (Example: ZipDeploy) are unaffected + context.Logger.Log($"Copying the manifest"); + FileSystemHelpers.CopyFile(context.PreviousManifestFilePath, context.NextManifestFilePath); + + // If we want to clean up the target directory before copying + // the new files, use kudusync so that only unnecessary files are + // deleted. This has two benefits: + // 1. This is faster than deleting the target directory before copying the source dir. + // 2. Minimizes chances of failure in deleting a directory due to open handles. + // This is especially useful when a target directory is present in the source and + // need not be deleted. + if (_deploymentInfo.CleanupTargetDirectory) + { + context.Logger.Log($"Clean deploying to {context.OutputPath}"); + + // We do not want to use the manifest for OneDeploy. Use an empty manifest file. + // This way we don't interfere with manifest based deployments. + string tempManifestPath = null; + try + { + tempManifestPath = Path.GetTempFileName(); + context.PreviousManifestFilePath = context.NextManifestFilePath = tempManifestPath; + base.Build(context); + } + finally + { + if (!string.IsNullOrWhiteSpace(tempManifestPath)) + { + FileSystemHelpers.DeleteFileSafe(tempManifestPath); + } + } + } + else + { + context.Logger.Log($"Incrementally deploying to {context.OutputPath}"); + FileSystemHelpers.CopyDirectoryRecursive(_repositoryPath, context.OutputPath); + } + + context.Logger.Log($"Build completed succesfully."); + + return Task.CompletedTask; + } + } +} diff --git a/Kudu.Core/Deployment/Generator/SiteBuilderFactory.cs b/Kudu.Core/Deployment/Generator/SiteBuilderFactory.cs index f54b39d7..8771c1ea 100644 --- a/Kudu.Core/Deployment/Generator/SiteBuilderFactory.cs +++ b/Kudu.Core/Deployment/Generator/SiteBuilderFactory.cs @@ -23,7 +23,7 @@ public SiteBuilderFactory(IBuildPropertyProvider propertyProvider, IEnvironment _environment = environment; } - public ISiteBuilder CreateBuilder(ITracer tracer, ILogger logger, IDeploymentSettingsManager settings, IRepository repository) + public ISiteBuilder CreateBuilder(ITracer tracer, ILogger logger, IDeploymentSettingsManager settings, IRepository repository, DeploymentInfoBase deploymentInfo) { string repositoryRoot = repository.RepositoryPath; @@ -53,6 +53,12 @@ public ISiteBuilder CreateBuilder(ITracer tracer, ILogger logger, IDeploymentSet targetProjectPath = Path.GetFullPath(Path.Combine(repositoryRoot, targetProjectPath.TrimStart('/', '\\'))); } + if (deploymentInfo != null && deploymentInfo.Deployer == Constants.OneDeploy) + { + var projectPath = !String.IsNullOrEmpty(targetProjectPath) ? targetProjectPath : repositoryRoot; + return new OneDeployBuilder(_environment, settings, _propertyProvider, repositoryRoot, projectPath, deploymentInfo); + } + if (settings.RunFromLocalZip()) { return new RunFromZipSiteBuilder(); diff --git a/Kudu.Core/Deployment/ISiteBuilderFactory.cs b/Kudu.Core/Deployment/ISiteBuilderFactory.cs index 3c421819..609849c1 100644 --- a/Kudu.Core/Deployment/ISiteBuilderFactory.cs +++ b/Kudu.Core/Deployment/ISiteBuilderFactory.cs @@ -6,6 +6,6 @@ namespace Kudu.Core.Deployment { public interface ISiteBuilderFactory { - ISiteBuilder CreateBuilder(ITracer tracer, ILogger logger, IDeploymentSettingsManager settings, IRepository fileFinder); + ISiteBuilder CreateBuilder(ITracer tracer, ILogger logger, IDeploymentSettingsManager settings, IRepository fileFinder, DeploymentInfoBase deploymentInfo); } } diff --git a/Kudu.Core/Environment.cs b/Kudu.Core/Environment.cs index b18cfe5b..d396a0ef 100644 --- a/Kudu.Core/Environment.cs +++ b/Kudu.Core/Environment.cs @@ -29,7 +29,7 @@ public class Environment : IEnvironment private readonly string _locksPath; private readonly string _sshKeyPath; private readonly string _tempPath; - private readonly string _zipTempPath; + private readonly string _zipTempDirectoryPath; private readonly string _scriptPath; private readonly string _nodeModulesPath; private string _repositoryPath; @@ -49,7 +49,7 @@ public Environment( string rootPath, string siteRootPath, string tempPath, - string zipTempPath, + string zipTempDirectoryPath, string repositoryPath, string webRootPath, string deploymentsPath, @@ -73,7 +73,7 @@ public Environment( SiteRootPath = siteRootPath; _tempPath = tempPath; _repositoryPath = repositoryPath; - _zipTempPath = zipTempPath; + _zipTempDirectoryPath = zipTempDirectoryPath; _webRootPath = webRootPath; _deploymentsPath = deploymentsPath; _deploymentToolsPath = Path.Combine(_deploymentsPath, Constants.DeploymentToolsPath); @@ -118,7 +118,7 @@ public Environment( _tempPath = Path.GetTempPath(); _repositoryPath = repositoryPath; - _zipTempPath = Path.Combine(_tempPath, Constants.ZipTempPath); + _zipTempDirectoryPath = Path.Combine(_tempPath, Constants.ZipTempDirectoryName); _webRootPath = Path.Combine(SiteRootPath, Constants.WebRoot); _deploymentsPath = Path.Combine(SiteRootPath, Constants.DeploymentCachePath); _artifactsPath = Path.Combine(SiteRootPath, Constants.ArtifactsPath); @@ -270,7 +270,7 @@ public string ZipTempPath { get { - return FileSystemHelpers.EnsureDirectory(_zipTempPath); + return FileSystemHelpers.EnsureDirectory(_zipTempDirectoryPath); } } diff --git a/Kudu.Core/Helpers/EnvironmentHelper.cs b/Kudu.Core/Helpers/EnvironmentHelper.cs index c56d9311..94377ecd 100644 --- a/Kudu.Core/Helpers/EnvironmentHelper.cs +++ b/Kudu.Core/Helpers/EnvironmentHelper.cs @@ -42,5 +42,14 @@ public static bool IsWindowsContainers() } return isXenon; } + + // Check if an app is a Linux Consumption function app + // This method is similar to + public static bool IsOnLinuxConsumption() + { + bool isOnAppService = !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(Constants.AzureWebsiteInstanceId)); + bool isOnLinuxContainer = !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(Constants.ContainerName)); + return isOnLinuxContainer && !isOnAppService; + } } } diff --git a/Kudu.Core/Helpers/PostDeploymentHelper.cs b/Kudu.Core/Helpers/PostDeploymentHelper.cs index 4166fd78..d6fc978f 100644 --- a/Kudu.Core/Helpers/PostDeploymentHelper.cs +++ b/Kudu.Core/Helpers/PostDeploymentHelper.cs @@ -97,13 +97,13 @@ private static string WebSiteElasticScaleEnabled { get { return System.Environment.GetEnvironmentVariable(Constants.WebSiteElasticScaleEnabled); } } - + // WEBSITE_INSTANCE_ID not null or empty public static bool IsAzureEnvironment() { return !String.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(Constants.AzureWebsiteInstanceId)); } - + // WEBSITE_HOME_STAMPNAME = waws-prod-bay-001 private static string HomeStamp { @@ -120,7 +120,7 @@ private static bool IsLocalHost /// It is written to require least dependencies but framework assemblies. /// Caller is responsible for synchronization. /// - [SuppressMessage("Microsoft.Usage", "CA1801:Parameter 'siteRestrictedJwt' is never used", + [SuppressMessage("Microsoft.Usage", "CA1801:Parameter 'siteRestrictedJwt' is never used", Justification = "Method signature has to be the same because it's called via reflections from web-deploy")] public static async Task Run(string requestId, string siteRestrictedJwt, TraceListener tracer) { @@ -152,8 +152,8 @@ public static async Task SyncFunctionsTriggers(string requestId, TraceListener t functionsPath = !string.IsNullOrEmpty(functionsPath) ? functionsPath : System.Environment.ExpandEnvironmentVariables(@"%HOME%\site\wwwroot"); - - // Read host.json + + // Read host.json // Get HubName property for Durable Functions Dictionary durableConfig = null; string hostJson = Path.Combine(functionsPath, Constants.FunctionsHostConfigFile); @@ -176,7 +176,7 @@ public static async Task SyncFunctionsTriggers(string requestId, TraceListener t routing["type"] = "routingTrigger"; triggers.Add(routing); } - + // Add hubName, connection, to each Durable Functions trigger if (durableConfig != null) { @@ -223,12 +223,12 @@ public static async Task SyncFunctionsTriggers(string requestId, TraceListener t // this couples with sync function triggers await SyncLogicAppJson(requestId, tracer); } - + private static void ReadDurableConfig(string hostConfigPath, out Dictionary config) { config = new Dictionary(); var json = JObject.Parse(File.ReadAllText(hostConfigPath)); - + JToken durableTaskValue; // we will allow case insensitivity given it is likely user hand edited // see https://github.com/Azure/azure-functions-durable-extension/issues/111 @@ -240,7 +240,7 @@ private static void ReadDurableConfig(string hostConfigPath, out Dictionary @@ -590,12 +590,13 @@ private static void WriteAutoSwapOngoing() } // Throws on failure - private static async Task PostAsync(string path, string requestId, string content = null) + public static async Task PostAsync(string path, string requestId, string content = null) { var hostOrAuthority = IsLocalHost ? HttpAuthority : HttpHost; var scheme = IsLocalHost ? "http" : "https"; var ipAddress = await GetAlternativeIPAddress(hostOrAuthority); var statusCode = default(HttpStatusCode); + string resContent = ""; try { using (var client = HttpClientFactory()) @@ -620,10 +621,38 @@ private static async Task PostAsync(string path, string requestId, string conten using (var response = await client.PostAsync(path, payload)) { statusCode = response.StatusCode; + if (response.Content != null) + { + resContent = response.Content.ReadAsStringAsync().Result; + } + response.EnsureSuccessStatusCode(); } + + if (path.Equals(Constants.UpdateDeployStatusPath) && resContent.Contains("Excessive SCM Site operation requests. Retry after 5 seconds")) + { + // Request was throttled throw an exception + // If max retries aren't reached, this request will be retried + Trace(TraceEventType.Information, $"Call to {path} was throttled. Setting statusCode to {HttpStatusCode.NotAcceptable}"); + + statusCode = HttpStatusCode.NotAcceptable; + throw new HttpRequestException(); + } } } + catch (HttpRequestException ex) + { + if (path.Equals(Constants.UpdateDeployStatusPath, StringComparison.OrdinalIgnoreCase) && statusCode == HttpStatusCode.NotFound) + { + // Fail silently if 404 is encountered. + // This will only happen transiently during a platform upgrade if new bits aren't on the FrontEnd yet. + Trace(TraceEventType.Warning, $"Call to {path} ended in 404. {ex}"); + } + else + { + throw; + } + } finally { Trace(TraceEventType.Verbose, "End HttpPost, status: {0}", statusCode); @@ -858,12 +887,12 @@ private static void Trace(TraceListener tracer, TraceEventType eventType, string tracer.TraceEvent(null, "PostDeployment", eventType, (int)eventType, format, args); } } - - public static async Task UpdatePackageName(ZipDeploymentInfo deploymentInfo, IEnvironment environment, ILogger logger) + + public static async Task UpdatePackageName(ArtifactDeploymentInfo deploymentInfo, IEnvironment environment, ILogger logger) { var packageNamePath = Path.Combine(environment.SitePackagesPath, Constants.PackageNameTxt); - logger.Log($"Updating {packageNamePath} with deployment {deploymentInfo.ZipName}"); - await FileSystemHelpers.WriteAllTextToFileAsync(packageNamePath, deploymentInfo.ZipName); + logger.Log($"Updating {packageNamePath} with deployment {deploymentInfo.ArtifactFileName}"); + await FileSystemHelpers.WriteAllTextToFileAsync(packageNamePath, deploymentInfo.ArtifactFileName); } } } diff --git a/Kudu.Core/SourceControl/NullRepository.cs b/Kudu.Core/SourceControl/NullRepository.cs index 34224853..baf95b82 100644 --- a/Kudu.Core/SourceControl/NullRepository.cs +++ b/Kudu.Core/SourceControl/NullRepository.cs @@ -20,11 +20,13 @@ public class NullRepository : IRepository private readonly string _path; private readonly ITraceFactory _traceFactory; + private readonly string _commitId; - public NullRepository(string path, ITraceFactory traceFactory) + public NullRepository(string path, ITraceFactory traceFactory, string commitId = null) { _path = path; _traceFactory = traceFactory; + _commitId = commitId; } public string CurrentId @@ -82,7 +84,7 @@ public bool Commit(string message, string authorName, string emailAddress) var tracer = _traceFactory.GetTracer(); using (tracer.Step("None repository commit")) { - var changeSet = new ChangeSet(Guid.NewGuid().ToString("N"), + var changeSet = new ChangeSet(!string.IsNullOrEmpty(_commitId) ? _commitId : Guid.NewGuid().ToString("N"), !string.IsNullOrEmpty(authorName) ? authorName : Resources.Deployment_UnknownValue, !string.IsNullOrEmpty(emailAddress) ? emailAddress : Resources.Deployment_UnknownValue, message ?? Resources.Deployment_UnknownValue, diff --git a/Kudu.Services.Web/Kudu.Services.Web.csproj b/Kudu.Services.Web/Kudu.Services.Web.csproj index 8a3dc344..97f2684d 100644 --- a/Kudu.Services.Web/Kudu.Services.Web.csproj +++ b/Kudu.Services.Web/Kudu.Services.Web.csproj @@ -29,6 +29,7 @@ + @@ -44,7 +45,6 @@ - diff --git a/Kudu.Services.Web/Pages/Index.cshtml b/Kudu.Services.Web/Pages/Index.cshtml index 1cf11845..93af17d1 100644 --- a/Kudu.Services.Web/Pages/Index.cshtml +++ b/Kudu.Services.Web/Pages/Index.cshtml @@ -58,14 +58,15 @@ Build
- @version + @version - @if (!string.IsNullOrEmpty(sha)) - { - - (@sha.Substring(0, 10)) - - } + @if (!string.IsNullOrEmpty(sha)) + { + string githubProjectRoot = Kudu.Core.Helpers.OSDetector.IsOnWindows() ? "ProjectKudu/kudu" : "Azure-App-Service/KuduLite"; + + (@sha.Substring(0, 10)) + + }
@@ -92,32 +93,7 @@ @System.IO.Path.GetTempPath()
-

REST API (works best when using a JSON viewer extension)

- -

Browse Directory

- + +

More information about Kudu can be found on the wiki.

diff --git a/Kudu.Services.Web/Pages/Index_BrowseHistory.cshtml b/Kudu.Services.Web/Pages/Index_BrowseHistory.cshtml new file mode 100644 index 00000000..98687235 --- /dev/null +++ b/Kudu.Services.Web/Pages/Index_BrowseHistory.cshtml @@ -0,0 +1,12 @@ +@if (!Kudu.Core.Helpers.EnvironmentHelper.IsOnLinuxConsumption()) +{ +

Browse Directory

+ +} diff --git a/Kudu.Services.Web/Pages/Index_RestApi.cshtml b/Kudu.Services.Web/Pages/Index_RestApi.cshtml new file mode 100644 index 00000000..9a6e1762 --- /dev/null +++ b/Kudu.Services.Web/Pages/Index_RestApi.cshtml @@ -0,0 +1,21 @@ +

REST API (works best when using a JSON viewer extension)

+ \ No newline at end of file diff --git a/Kudu.Services.Web/Pages/NewUI/Index.cshtml b/Kudu.Services.Web/Pages/NewUI/Index.cshtml index 33996770..ec9176ef 100644 --- a/Kudu.Services.Web/Pages/NewUI/Index.cshtml +++ b/Kudu.Services.Web/Pages/NewUI/Index.cshtml @@ -143,9 +143,6 @@
  • App Settings
  • -
  • - Browse WWWRoot -
  • System Configuration
  • @@ -153,7 +150,7 @@ Virtual File System API
  • - Browse Logs Dir + File Manager
  • Scan file system diff --git a/Kudu.Services.Web/Pages/NewUI/_Layout.cshtml b/Kudu.Services.Web/Pages/NewUI/_Layout.cshtml index 09a07e64..f5730b22 100644 --- a/Kudu.Services.Web/Pages/NewUI/_Layout.cshtml +++ b/Kudu.Services.Web/Pages/NewUI/_Layout.cshtml @@ -69,49 +69,54 @@