public async Task PerformDeployment(DeploymentInfoBase deploymentInfo, IDisposable tempDeployment = null, ChangeSet tempChangeSet = null) { DateTime currentMarkerFileUTC; DateTime nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); ChangeSet lastChange = null; do { // save the current marker currentMarkerFileUTC = nextMarkerFileUTC; string targetBranch = _settings.GetBranch(); using (_tracer.Step("Performing fetch based deployment")) { // create temporary deployment before the actual deployment item started // this allows portal ui to readily display on-going deployment (not having to wait for fetch to complete). // in addition, it captures any failure that may occur before the actual deployment item started tempDeployment = tempDeployment ?? _deploymentManager.CreateTemporaryDeployment( Resources.ReceivingChanges, out tempChangeSet, deploymentInfo.TargetChangeset, deploymentInfo.Deployer); ILogger innerLogger = null; DeployStatusApiResult updateStatusObj = null; try { ILogger logger = _deploymentManager.GetLogger(tempChangeSet.Id); // Fetch changes from the repository innerLogger = logger.Log(Resources.FetchingChanges); IRepository repository = deploymentInfo.GetRepository(); try { await deploymentInfo.Fetch(repository, deploymentInfo, targetBranch, innerLogger, _tracer); } catch (BranchNotFoundException) { // mark no deployment is needed deploymentInfo.TargetChangeset = null; } // set to null as Deploy() below takes over logging innerLogger = null; // The branch or commit id to deploy string deployBranch = !String.IsNullOrEmpty(deploymentInfo.CommitId) ? deploymentInfo.CommitId : targetBranch; try { _tracer.Trace($"Before sending {Constants.BuildRequestReceived} status to /api/updatedeploystatus"); if (PostDeploymentHelper.IsAzureEnvironment()) { if (deploymentInfo != null && !string.IsNullOrEmpty(deploymentInfo.DeploymentTrackingId)) { // Only send an updatedeploystatus request if DeploymentTrackingId is non null // This signifies the client has opted in for these deployment updates for this deploy request updateStatusObj = new DeployStatusApiResult(Constants.BuildRequestReceived, deploymentInfo.DeploymentTrackingId); await _deploymentManager.SendDeployStatusUpdate(updateStatusObj); } } } catch (Exception e) { _tracer.TraceError($"Exception while sending {Constants.BuildRequestReceived} status to /api/updatedeploystatus. " + $"Entry in the operations table for the deployment status may not have been created. {e}"); } // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null && ShouldDeploy(repository, deploymentInfo, deployBranch)) { // Perform the actual deployment var changeSet = repository.GetChangeSet(deployBranch); if (changeSet == null && !String.IsNullOrEmpty(deploymentInfo.CommitId)) { throw new InvalidOperationException(String.Format("Invalid revision '{0}'!", deploymentInfo.CommitId)); } lastChange = changeSet; // 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 (updateStatusObj != null) { updateStatusObj.DeploymentStatus = Constants.BuildInProgress; await _deploymentManager.SendDeployStatusUpdate(updateStatusObj); } await _deploymentManager.DeployAsync( repository, changeSet, deploymentInfo.Deployer, clean : false, deploymentInfo : deploymentInfo, needFileUpdate : deploySpecificCommitId, fullBuildByDefault : deploymentInfo.DoFullBuildByDefault); if (updateStatusObj != null && !deploymentInfo.RestartAllowed) { // If restart is disallowed, send BuildSuccessful here as PostBuildRestartRequired was not sent // during the DeployAsync flow. updateStatusObj.DeploymentStatus = Constants.BuildSuccessful; await _deploymentManager.SendDeployStatusUpdate(updateStatusObj); } } } catch (Exception ex) { if (innerLogger != null) { innerLogger.Log(ex); } // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null) { IDeploymentStatusFile statusFile = _status.Open(deploymentInfo.TargetChangeset.Id); if (statusFile != null) { statusFile.MarkFailed(); } } try { if (updateStatusObj != null) { // Set deployment status as failure if exception is thrown updateStatusObj.DeploymentStatus = Constants.BuildFailed; await _deploymentManager.SendDeployStatusUpdate(updateStatusObj); } } catch { // no-op } throw; } // only clean up temp deployment if successful tempDeployment.Dispose(); } // check marker file and, if changed (meaning new /deploy request), redeploy. nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); } while (deploymentInfo.IsReusable && currentMarkerFileUTC != nextMarkerFileUTC); if (lastChange != null && PostDeploymentHelper.IsAutoSwapEnabled()) { IDeploymentStatusFile statusFile = _status.Open(lastChange.Id); if (statusFile.Status == DeployStatus.Success) { // if last change is not null and finish successfully, mean there was at least one deployment happened // since deployment is now done, trigger swap if enabled await PostDeploymentHelper.PerformAutoSwap(_environment.RequestId, new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(lastChange.Id))); } } }
public async Task PerformDeployment(DeploymentInfo deploymentInfo, IDisposable tempDeployment = null, ChangeSet tempChangeSet = null) { DateTime currentMarkerFileUTC; DateTime nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); ChangeSet lastChange = null; do { // save the current marker currentMarkerFileUTC = nextMarkerFileUTC; string targetBranch = _settings.GetBranch(); using (_tracer.Step("Performing fetch based deployment")) { // create temporary deployment before the actual deployment item started // this allows portal ui to readily display on-going deployment (not having to wait for fetch to complete). // in addition, it captures any failure that may occur before the actual deployment item started tempDeployment = tempDeployment ?? _deploymentManager.CreateTemporaryDeployment( Resources.ReceivingChanges, out tempChangeSet, deploymentInfo.TargetChangeset, deploymentInfo.Deployer); ILogger innerLogger = null; try { ILogger logger = _deploymentManager.GetLogger(tempChangeSet.Id); // Fetch changes from the repository innerLogger = logger.Log(Resources.FetchingChanges); IRepository repository = _repositoryFactory.EnsureRepository(deploymentInfo.RepositoryType); try { await deploymentInfo.Handler.Fetch(repository, deploymentInfo, targetBranch, innerLogger, _tracer); } catch (BranchNotFoundException) { // mark no deployment is needed deploymentInfo.TargetChangeset = null; } // set to null as Deploy() below takes over logging innerLogger = null; // The branch or commit id to deploy string deployBranch = !String.IsNullOrEmpty(deploymentInfo.CommitId) ? deploymentInfo.CommitId : targetBranch; // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null && ShouldDeploy(repository, deploymentInfo, deployBranch)) { // Perform the actual deployment var changeSet = repository.GetChangeSet(deployBranch); if (changeSet == null && !String.IsNullOrEmpty(deploymentInfo.CommitId)) { throw new InvalidOperationException(String.Format("Invalid revision '{0}'!", deploymentInfo.CommitId)); } lastChange = changeSet; // 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); await _deploymentManager.DeployAsync(repository, changeSet, deploymentInfo.Deployer, clean : false, needFileUpdate : deploySpecificCommitId); } } catch (Exception ex) { if (innerLogger != null) { innerLogger.Log(ex); } // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null) { IDeploymentStatusFile statusFile = _status.Open(deploymentInfo.TargetChangeset.Id); if (statusFile != null) { statusFile.MarkFailed(); } } throw; } // only clean up temp deployment if successful tempDeployment.Dispose(); } // check marker file and, if changed (meaning new /deploy request), redeploy. nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); } while (deploymentInfo.IsReusable && currentMarkerFileUTC != nextMarkerFileUTC); if (lastChange != null && PostDeploymentHelper.IsAutoSwapEnabled()) { IDeploymentStatusFile statusFile = _status.Open(lastChange.Id); if (statusFile.Status == DeployStatus.Success) { // 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, _environment.SiteRestrictedJwt, new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(lastChange.Id))); } } }
public override async Task ProcessRequestAsync(HttpContext context) { using (_tracer.Step("FetchHandler")) { // Redirect GET /deploy requests to the Kudu root for convenience when using URL from Azure portal if (String.Equals(context.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect("~/"); context.ApplicationInstance.CompleteRequest(); return; } if (!String.Equals(context.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase)) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; context.ApplicationInstance.CompleteRequest(); return; } context.Response.TrySkipIisCustomErrors = true; DeploymentInfo deployInfo = null; // We are going to assume that the branch details are already set by the time it gets here. This is particularly important in the mercurial case, // since Settings hardcodes the default value for Branch to be "master". Consequently, Kudu will NoOp requests for Mercurial commits. string targetBranch = _settings.GetBranch(); try { var request = new HttpRequestWrapper(context.Request); JObject payload = GetPayload(request); DeployAction action = GetRepositoryInfo(request, payload, targetBranch, out deployInfo); if (action == DeployAction.NoOp) { return; } // If Scm is not enabled, we will reject all but one payload for GenericHandler // This is to block the unintended CI with Scm providers like GitHub // Since Generic payload can only be done by user action, we loosely allow // that and assume users know what they are doing. Same applies to git // push/clone endpoint. if (!_settings.IsScmEnabled() && !(deployInfo.Handler is GenericHandler || deployInfo.Handler is DropboxHandler)) { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.ApplicationInstance.CompleteRequest(); return; } } catch (FormatException ex) { _tracer.TraceError(ex); context.Response.StatusCode = 400; context.Response.Write(ex.Message); context.ApplicationInstance.CompleteRequest(); return; } // for CI payload, we will return Accepted and do the task in the BG // if isAsync is defined, we will return Accepted and do the task in the BG // since autoSwap relies on the response header, deployment has to be synchronously. bool isAsync = String.Equals(context.Request.QueryString["isAsync"], "true", StringComparison.OrdinalIgnoreCase); bool isBackground = (isAsync || deployInfo.IsContinuous) && !_autoSwapHandler.IsAutoSwapEnabled(); if (isBackground) { using (_tracer.Step("Start deployment in the background")) { ChangeSet tempChangeSet = null; IDisposable tempDeployment = null; if (isAsync) { // create temporary deployment before the actual deployment item started // this allows portal ui to readily display on-going deployment (not having to wait for fetch to complete). // in addition, it captures any failure that may occur before the actual deployment item started tempDeployment = _deploymentManager.CreateTemporaryDeployment( Resources.ReceivingChanges, out tempChangeSet, deployInfo.TargetChangeset, deployInfo.Deployer); } PerformBackgroundDeployment(deployInfo, _environment, _settings, _tracer.TraceLevel, context.Request.Url, tempDeployment, tempChangeSet); } // to avoid regression, only set location header if isAsync if (isAsync) { // latest deployment keyword reserved to poll till deployment done context.Response.Headers["Location"] = new Uri(context.Request.Url, String.Format("/api/deployments/{0}?deployer={1}&time={2}", Constants.LatestDeployment, deployInfo.Deployer, DateTime.UtcNow.ToString("yyy-MM-dd_HH-mm-ssZ"))).ToString(); } context.Response.StatusCode = (int)HttpStatusCode.Accepted; context.ApplicationInstance.CompleteRequest(); return; } _tracer.Trace("Attempting to fetch target branch {0}", targetBranch); bool acquired = await _deploymentLock.TryLockOperationAsync(async() => { if (_autoSwapHandler.IsAutoSwapOngoing()) { context.Response.StatusCode = (int)HttpStatusCode.Conflict; context.Response.Write(Resources.Error_AutoSwapDeploymentOngoing); context.ApplicationInstance.CompleteRequest(); return; } await PerformDeployment(deployInfo); _autoSwapHandler.HandleAutoSwap(); }, TimeSpan.Zero); if (!acquired) { // Create a marker file that indicates if there's another deployment to pull // because there was a deployment in progress. using (_tracer.Step("Update pending deployment marker file")) { // REVIEW: This makes the assumption that the repository url is the same. // If it isn't the result would be buggy either way. FileSystemHelpers.SetLastWriteTimeUtc(_markerFilePath, DateTime.UtcNow); } // Return a http 202: the request has been accepted for processing, but the processing has not been completed. context.Response.StatusCode = (int)HttpStatusCode.Accepted; context.ApplicationInstance.CompleteRequest(); } } }
public async Task PerformDeployment(DeploymentInfo deploymentInfo) { DateTime currentMarkerFileUTC; DateTime nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); do { // save the current marker currentMarkerFileUTC = nextMarkerFileUTC; string targetBranch = _settings.GetBranch(); using (_tracer.Step("Performing fetch based deployment")) { // create temporary deployment before the actual deployment item started // this allows portal ui to readily display on-going deployment (not having to wait for fetch to complete). // in addition, it captures any failure that may occur before the actual deployment item started ChangeSet tempChangeSet; IDisposable tempDeployment = _deploymentManager.CreateTemporaryDeployment( Resources.ReceivingChanges, out tempChangeSet, deploymentInfo.TargetChangeset, deploymentInfo.Deployer); ILogger innerLogger = null; try { ILogger logger = _deploymentManager.GetLogger(tempChangeSet.Id); // Fetch changes from the repository innerLogger = logger.Log(Resources.FetchingChanges); IRepository repository = _repositoryFactory.EnsureRepository(deploymentInfo.RepositoryType); try { await deploymentInfo.Handler.Fetch(repository, deploymentInfo, targetBranch, innerLogger); } catch (BranchNotFoundException) { // mark no deployment is needed deploymentInfo.TargetChangeset = null; } // set to null as Deploy() below takes over logging innerLogger = null; // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null && ShouldDeploy(repository, deploymentInfo, targetBranch)) { // Perform the actual deployment var changeSet = repository.GetChangeSet(targetBranch); // Here, we don't need to update the working files, since we know Fetch left them in the correct state await _deploymentManager.DeployAsync(repository, changeSet, deploymentInfo.Deployer, clean : false, needFileUpdate : false); } } catch (Exception ex) { if (innerLogger != null) { innerLogger.Log(ex); } // In case the commit or perhaps fetch do no-op. if (deploymentInfo.TargetChangeset != null) { IDeploymentStatusFile statusFile = _status.Open(deploymentInfo.TargetChangeset.Id); if (statusFile != null) { statusFile.MarkFailed(); } } throw; } // only clean up temp deployment if successful tempDeployment.Dispose(); } // check marker file and, if changed (meaning new /deploy request), redeploy. nextMarkerFileUTC = FileSystemHelpers.GetLastWriteTimeUtc(_markerFilePath); } while (deploymentInfo.IsReusable && currentMarkerFileUTC != nextMarkerFileUTC); }
public async Task Invoke( HttpContext context, ITracer tracer, IGitServer gitServer, IDictionary <string, IOperationLock> namedLocks, IDeploymentManager deploymentManager, IRepositoryFactory repositoryFactory, IEnvironment environment) { //Get the deployment lock from the locks dictionary var deploymentLock = namedLocks["deployment"]; using (tracer.Step("RpcService.ReceivePack")) { // Ensure that the target directory does not have a non-Git repository. IRepository repository = repositoryFactory.GetRepository(); if (repository != null && repository.RepositoryType != RepositoryType.Git) { context.Response.StatusCode = StatusCodes.Status400BadRequest; return; } try { await deploymentLock.LockOperationAsync(() => { context.Response.ContentType = "application/x-git-receive-pack-result"; if (PostDeploymentHelper.IsAutoSwapOngoing()) { context.Response.StatusCode = StatusCodes.Status409Conflict; var msg = Encoding.UTF8.GetBytes(Resources.Error_AutoSwapDeploymentOngoing); return(context.Response.Body.WriteAsync(msg, 0, msg.Length)); } string username = null; if (AuthUtility.TryExtractBasicAuthUser(context.Request, out username)) { gitServer.SetDeployer(username); } UpdateNoCacheForResponse(context.Response); // This temporary deployment is for ui purposes only, it will always be deleted via finally. ChangeSet tempChangeSet; using (deploymentManager.CreateTemporaryDeployment(Resources.ReceivingChanges, out tempChangeSet)) { // to pass to kudu.exe post receive hook System.Environment.SetEnvironmentVariable(Constants.RequestIdHeader, environment.RequestId); try { gitServer.Receive(context.Request.Body, context.Response.Body); } finally { System.Environment.SetEnvironmentVariable(Constants.RequestIdHeader, null); } } return(Task.CompletedTask); }, "Handling git receive pack", TimeSpan.Zero); } catch (LockOperationException ex) { context.Response.StatusCode = StatusCodes.Status409Conflict; var msg = Encoding.UTF8.GetBytes(ex.Message); await context.Response.Body.WriteAsync(msg, 0, msg.Length); } } }