public async Task DeployAsync( IRepository repository, ChangeSet changeSet, string deployer, bool clean, DeploymentInfoBase deploymentInfo = null, bool needFileUpdate = true, bool fullBuildByDefault = true) { using (var deploymentAnalytics = new DeploymentAnalytics(_analytics, _settings)) { Exception exception = null; ITracer tracer = _traceFactory.GetTracer(); IDisposable deployStep = null; ILogger innerLogger = null; string targetBranch = null; // If we don't get a changeset, find out what branch we should be deploying and get the commit ID from it if (changeSet == null) { targetBranch = _settings.GetBranch(); changeSet = repository.GetChangeSet(targetBranch); if (changeSet == null) { throw new InvalidOperationException(String.Format("The current deployment branch is '{0}', but nothing has been pushed to it", targetBranch)); } } string id = changeSet.Id; IDeploymentStatusFile statusFile = null; try { deployStep = tracer.Step($"DeploymentManager.Deploy(id:{id})"); // Remove the old log file for this deployment id string logPath = GetLogPath(id); FileSystemHelpers.DeleteFileSafe(logPath); statusFile = GetOrCreateStatusFile(changeSet, tracer, deployer); statusFile.MarkPending(); ILogger logger = GetLogger(changeSet.Id); if (needFileUpdate) { using (tracer.Step("Updating to specific changeset")) { innerLogger = logger.Log(Resources.Log_UpdatingBranch, targetBranch ?? id); using (var writer = new ProgressWriter()) { // Update to the specific changeset or branch repository.Update(targetBranch ?? id); } } } if (_settings.ShouldUpdateSubmodules()) { using (tracer.Step("Updating submodules")) { innerLogger = logger.Log(Resources.Log_UpdatingSubmodules); repository.UpdateSubmodules(); } } if (clean) { tracer.Trace("Cleaning {0} repository", repository.RepositoryType); innerLogger = logger.Log(Resources.Log_CleaningRepository, repository.RepositoryType); repository.Clean(); } // set to null as Build() below takes over logging innerLogger = null; // Perform the build deployment of this changeset await Build(changeSet, tracer, deployStep, repository, deploymentInfo, deploymentAnalytics, fullBuildByDefault); if (!(OSDetector.IsOnWindows() && !EnvironmentHelper.IsWindowsContainers()) && _settings.RestartAppContainerOnGitDeploy()) { logger.Log(Resources.Log_TriggeringContainerRestart); DockerContainerRestartTrigger.RequestContainerRestart(_environment, RestartTriggerReason); } } catch (Exception ex) { exception = ex; if (innerLogger != null) { innerLogger.Log(ex); } if (statusFile != null) { MarkStatusComplete(statusFile, success: false); } tracer.TraceError(ex); deploymentAnalytics.Error = ex.ToString(); if (deployStep != null) { deployStep.Dispose(); } } // Reload status file with latest updates statusFile = _status.Open(id); if (statusFile != null) { await _hooksManager.PublishEventAsync(HookEventTypes.PostDeployment, statusFile); } if (exception != null) { throw new DeploymentFailedException(exception); } if (statusFile != null && statusFile.Status == DeployStatus.Success && _settings.RunFromLocalZip()) { var zipDeploymentInfo = deploymentInfo as ZipDeploymentInfo; if (zipDeploymentInfo != null) { await PostDeploymentHelper.UpdateSiteVersion(zipDeploymentInfo, _environment, tracer); } } } }
/// <summary> /// Builds and deploys a particular changeset. Puts all build artifacts in a deployments/{id} /// </summary> private async Task Build( ChangeSet changeSet, ITracer tracer, IDisposable deployStep, IRepository repository, DeploymentInfoBase deploymentInfo, DeploymentAnalytics deploymentAnalytics, bool fullBuildByDefault) { if (changeSet == null || String.IsNullOrEmpty(changeSet.Id)) { throw new ArgumentException("The changeSet.Id parameter is null or empty", "changeSet.Id"); } ILogger logger = null; IDeploymentStatusFile currentStatus = null; string buildTempPath = null; string id = changeSet.Id; try { logger = GetLogger(id); ILogger innerLogger = logger.Log(Resources.Log_PreparingDeployment, TrimId(id)); currentStatus = _status.Open(id); currentStatus.Complete = false; currentStatus.StartTime = DateTime.UtcNow; currentStatus.Status = DeployStatus.Building; currentStatus.StatusText = String.Format(CultureInfo.CurrentCulture, Resources.Status_BuildingAndDeploying, id); currentStatus.Save(); ISiteBuilder builder = null; // Add in per-deploy default settings values based on the details of this deployment var perDeploymentDefaults = new Dictionary <string, string> { { SettingsKeys.DoBuildDuringDeployment, fullBuildByDefault.ToString() } }; var settingsProviders = _settings.SettingsProviders.Concat( new[] { new BasicSettingsProvider(perDeploymentDefaults, SettingsProvidersPriority.PerDeploymentDefault) }); var perDeploymentSettings = DeploymentSettingsManager.BuildPerDeploymentSettingsManager(repository.RepositoryPath, settingsProviders); string delayMaxInStr = perDeploymentSettings.GetValue(SettingsKeys.MaxRandomDelayInSec); if (!String.IsNullOrEmpty(delayMaxInStr)) { int maxDelay; if (!Int32.TryParse(delayMaxInStr, out maxDelay) || maxDelay < 0) { tracer.Trace("Invalid {0} value, expect a positive integer, received {1}", SettingsKeys.MaxRandomDelayInSec, delayMaxInStr); } else { tracer.Trace("{0} is set to {1}s", SettingsKeys.MaxRandomDelayInSec, maxDelay); int gap = _random.Next(maxDelay); using (tracer.Step("Randomization applied to {0}, Start sleeping for {1}s", maxDelay, gap)) { logger.Log(Resources.Log_DelayingBeforeDeployment, gap); await Task.Delay(TimeSpan.FromSeconds(gap)); } } } try { using (tracer.Step("Determining deployment builder")) { builder = _builderFactory.CreateBuilder(tracer, innerLogger, perDeploymentSettings, repository); deploymentAnalytics.ProjectType = builder.ProjectType; tracer.Trace("Builder is {0}", builder.GetType().Name); } } catch (Exception ex) { // If we get a TargetInvocationException, use the inner exception instead to avoid // useless 'Exception has been thrown by the target of an invocation' messages var targetInvocationException = ex as System.Reflection.TargetInvocationException; if (targetInvocationException != null) { ex = targetInvocationException.InnerException; } _globalLogger.Log(ex); innerLogger.Log(ex); MarkStatusComplete(currentStatus, success: false); FailDeployment(tracer, deployStep, deploymentAnalytics, ex); return; } // Create a directory for the script output temporary artifacts // Use tick count (in hex) instead of guid to keep the path for getting to long buildTempPath = Path.Combine(_environment.TempPath, DateTime.UtcNow.Ticks.ToString("x")); FileSystemHelpers.EnsureDirectory(buildTempPath); var context = new DeploymentContext { NextManifestFilePath = GetDeploymentManifestPath(id), PreviousManifestFilePath = GetActiveDeploymentManifestPath(), IgnoreManifest = deploymentInfo != null && deploymentInfo.CleanupTargetDirectory, // Ignoring the manifest will cause kudusync to delete sub-directories / files // in the destination directory that are not present in the source directory, // without checking the manifest to see if the file was copied over to the destination // during a previous kudusync operation. This effectively performs a clean deployment // from the source to the destination directory Tracer = tracer, Logger = logger, GlobalLogger = _globalLogger, OutputPath = GetOutputPath(deploymentInfo, _environment, perDeploymentSettings), BuildTempPath = buildTempPath, CommitId = id, Message = changeSet.Message }; if (context.PreviousManifestFilePath == null) { // this file (/site/firstDeploymentManifest) capture the last active deployment when disconnecting SCM context.PreviousManifestFilePath = Path.Combine(_environment.SiteRootPath, Constants.FirstDeploymentManifestFileName); if (!FileSystemHelpers.FileExists(context.PreviousManifestFilePath)) { // In the first deployment we want the wwwroot directory to be cleaned, we do that using a manifest file // That has the expected content of a clean deployment (only one file: hostingstart.html) // This will result in KuduSync cleaning this file. context.PreviousManifestFilePath = Path.Combine(_environment.ScriptPath, Constants.FirstDeploymentManifestFileName); } } PreDeployment(tracer); using (tracer.Step("Building")) { try { await builder.Build(context); builder.PostBuild(context); await PostDeploymentHelper.SyncFunctionsTriggers( _environment.RequestId, new PostDeploymentTraceListener(tracer, logger), deploymentInfo?.SyncFunctionsTriggersPath); if (_settings.TouchWatchedFileAfterDeployment()) { TryTouchWatchedFile(context, deploymentInfo); } if (_settings.RunFromLocalZip() && deploymentInfo is ZipDeploymentInfo) { await PostDeploymentHelper.UpdatePackageName(deploymentInfo as ZipDeploymentInfo, _environment, logger); } FinishDeployment(id, deployStep); deploymentAnalytics.VsProjectId = TryGetVsProjectId(context); deploymentAnalytics.Result = DeployStatus.Success.ToString(); } catch (Exception ex) { MarkStatusComplete(currentStatus, success: false); FailDeployment(tracer, deployStep, deploymentAnalytics, ex); return; } } } catch (Exception ex) { FailDeployment(tracer, deployStep, deploymentAnalytics, ex); } finally { // Clean the temp folder up CleanBuild(tracer, buildTempPath); } }
public async Task <FetchDeploymentRequestResult> FetchDeploy( DeploymentInfoBase deployInfo, bool asyncRequested, Uri requestUri, string targetBranch) { // 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 and zip deployment. if (!(_settings.IsScmEnabled() || deployInfo.AllowDeploymentWhileScmDisabled)) { 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. else if (_settings.RunFromRemoteZip()) { return(FetchDeploymentRequestResult.ConflictRunFromRemoteZipConfigured); } // 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 isBackground = asyncRequested || deployInfo.IsContinuous; if (isBackground) { using (_tracer.Step("Start deployment in the background")) { var waitForTempDeploymentCreation = asyncRequested; var successfullyRequested = await PerformBackgroundDeployment( deployInfo, _environment, _settings, _tracer.TraceLevel, requestUri, waitForTempDeploymentCreation); return(successfullyRequested ? FetchDeploymentRequestResult.RunningAynschronously : FetchDeploymentRequestResult.ConflictDeploymentInProgress); } } _tracer.Trace("Attempting to fetch target branch {0}", targetBranch); try { return(await _deploymentLock.LockOperation(async() => { if (PostDeploymentHelper.IsAutoSwapOngoing()) { return FetchDeploymentRequestResult.ConflictAutoSwapOngoing; } await PerformDeployment(deployInfo); return FetchDeploymentRequestResult.RanSynchronously; }, "Performing continuous deployment", TimeSpan.Zero)); } catch (LockOperationException) { if (deployInfo.AllowDeferredDeployment) { // 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(FetchDeploymentRequestResult.Pending); } else { return(FetchDeploymentRequestResult.ConflictDeploymentInProgress); } } }
private static int PerformDeploy( string appRoot, string wapTargets, string deployer, string lockPath, IEnvironment env, IDeploymentSettingsManager settingsManager, TraceLevel level, ITracer tracer, ITraceFactory traceFactory, IOperationLock deploymentLock) { System.Environment.SetEnvironmentVariable("GIT_DIR", null, System.EnvironmentVariableTarget.Process); // Skip SSL Certificate Validate if (System.Environment.GetEnvironmentVariable(SettingsKeys.SkipSslValidation) == "1") { ServicePointManager.ServerCertificateValidationCallback = delegate { return(true); }; } // Adjust repo path env.RepositoryPath = Path.Combine(env.SiteRootPath, settingsManager.GetRepositoryPath()); string statusLockPath = Path.Combine(lockPath, Constants.StatusLockFile); string hooksLockPath = Path.Combine(lockPath, Constants.HooksLockFile); IOperationLock statusLock = new LockFile(statusLockPath, traceFactory); IOperationLock hooksLock = new LockFile(hooksLockPath, traceFactory); IBuildPropertyProvider buildPropertyProvider = new BuildPropertyProvider(); ISiteBuilderFactory builderFactory = new SiteBuilderFactory(buildPropertyProvider, env); var logger = new ConsoleLogger(); IRepository gitRepository; if (settingsManager.UseLibGit2SharpRepository()) { gitRepository = new LibGit2SharpRepository(env, settingsManager, traceFactory); } else { gitRepository = new GitExeRepository(env, settingsManager, traceFactory); } IServerConfiguration serverConfiguration = new ServerConfiguration(); IAnalytics analytics = new Analytics(settingsManager, serverConfiguration, traceFactory); IWebHooksManager hooksManager = new WebHooksManager(tracer, env, hooksLock); IDeploymentStatusManager deploymentStatusManager = new DeploymentStatusManager(env, analytics, statusLock); IDeploymentManager deploymentManager = new DeploymentManager(builderFactory, env, traceFactory, analytics, settingsManager, deploymentStatusManager, deploymentLock, GetLogger(env, level, logger), hooksManager); var step = tracer.Step(XmlTracer.ExecutingExternalProcessTrace, new Dictionary <string, string> { { "type", "process" }, { "path", "kudu.exe" }, { "arguments", appRoot + " " + wapTargets } }); using (step) { try { // although the api is called DeployAsync, most expensive works are done synchronously. // need to launch separate task to go async explicitly (consistent with FetchDeploymentManager) var deploymentTask = Task.Run(async() => await deploymentManager.DeployAsync(gitRepository, changeSet: null, deployer: deployer, clean: false)); #pragma warning disable 4014 // Track pending task PostDeploymentHelper.TrackPendingOperation(deploymentTask, TimeSpan.Zero); #pragma warning restore 4014 deploymentTask.Wait(); if (PostDeploymentHelper.IsAutoSwapEnabled()) { string branch = settingsManager.GetBranch(); ChangeSet changeSet = gitRepository.GetChangeSet(branch); IDeploymentStatusFile statusFile = deploymentStatusManager.Open(changeSet.Id); if (statusFile != null && statusFile.Status == DeployStatus.Success) { PostDeploymentHelper.PerformAutoSwap(env.RequestId, env.SiteRestrictedJwt, new PostDeploymentTraceListener(tracer, deploymentManager.GetLogger(changeSet.Id))).Wait(); } } } catch (Exception e) { tracer.TraceError(e); System.Console.Error.WriteLine(e.GetBaseException().Message); System.Console.Error.WriteLine(Resources.Log_DeploymentError); return(1); } } if (logger.HasErrors) { System.Console.Error.WriteLine(Resources.Log_DeploymentError); return(1); } return(0); }
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; 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; // 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, deploymentInfo : deploymentInfo, needFileUpdate : deploySpecificCommitId, fullBuildByDefault : deploymentInfo.DoFullBuildByDefault); } } 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, new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(lastChange.Id))); } } }
// key goal is to create background tracer that is independent of request. public static async Task <bool> PerformBackgroundDeployment( DeploymentInfoBase deployInfo, IEnvironment environment, IDeploymentSettingsManager settings, TraceLevel traceLevel, Uri uri, bool waitForTempDeploymentCreation) { var tracer = traceLevel <= TraceLevel.Off ? NullTracer.Instance : new CascadeTracer(new XmlTracer(environment.TracePath, traceLevel), new ETWTracer(environment.RequestId, "POST")); var traceFactory = new TracerFactory(() => tracer); var backgroundTrace = tracer.Step(XmlTracer.BackgroundTrace, new Dictionary <string, string> { { "url", uri.AbsolutePath }, { "method", "POST" } }); // For waiting on creation of temp deployment var tempDeploymentCreatedTcs = new TaskCompletionSource <object>(); // For determining whether or not we failed to create the deployment due to lock contention. // 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 <bool>(); // This task will be let out of scope intentionally var deploymentTask = Task.Run(() => { try { // lock related string lockPath = Path.Combine(environment.SiteRootPath, Constants.LockPath); string deploymentLockPath = Path.Combine(lockPath, Constants.DeploymentLockFile); string statusLockPath = Path.Combine(lockPath, Constants.StatusLockFile); string hooksLockPath = Path.Combine(lockPath, Constants.HooksLockFile); var statusLock = new LockFile(statusLockPath, traceFactory); var hooksLock = new LockFile(hooksLockPath, traceFactory); var deploymentLock = DeploymentLockFile.GetInstance(deploymentLockPath, traceFactory); var analytics = new Analytics(settings, new ServerConfiguration(), traceFactory); var deploymentStatusManager = new DeploymentStatusManager(environment, analytics, statusLock); var siteBuilderFactory = new SiteBuilderFactory(new BuildPropertyProvider(), environment); var webHooksManager = new WebHooksManager(tracer, environment, hooksLock); var deploymentManager = new DeploymentManager(siteBuilderFactory, environment, traceFactory, analytics, settings, deploymentStatusManager, deploymentLock, NullLogger.Instance, webHooksManager); var fetchDeploymentManager = new FetchDeploymentManager(settings, environment, tracer, deploymentLock, deploymentManager, deploymentStatusManager); IDisposable tempDeployment = null; try { // Perform deployment deploymentLock.LockOperation(() => { deploymentWillOccurTcs.TrySetResult(true); ChangeSet tempChangeSet = null; if (waitForTempDeploymentCreation) { // 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); tempDeploymentCreatedTcs.TrySetResult(null); } fetchDeploymentManager.PerformDeployment(deployInfo, tempDeployment, tempChangeSet).Wait(); }, "Performing continuous deployment", TimeSpan.Zero); } catch (LockOperationException) { if (tempDeployment != null) { tempDeployment.Dispose(); } if (deployInfo.AllowDeferredDeployment) { deploymentWillOccurTcs.TrySetResult(true); 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(fetchDeploymentManager._markerFilePath, DateTime.UtcNow); } } } } catch (Exception ex) { tracer.TraceError(ex); } finally { // Will no-op if already set deploymentWillOccurTcs.TrySetResult(false); backgroundTrace.Dispose(); } }); #pragma warning disable 4014 // Run on BG task (Task.Run) to avoid ASP.NET Request thread terminated with request completion and // it doesn't get chance to clean up the pending marker. Task.Run(() => PostDeploymentHelper.TrackPendingOperation(deploymentTask, TimeSpan.Zero)); #pragma warning restore 4014 // When the frontend/ARM calls /deploy with isAsync=true, it starts polling // the deployment status immediately, so it's important that the temp deployment // is created before we return. if (waitForTempDeploymentCreation) { // deploymentTask may return withoout creating the temp deployment (lock contention, // other exception), in which case just continue. await Task.WhenAny(tempDeploymentCreatedTcs.Task, deploymentTask); } // If deferred deployment is not permitted, we need to know whether or not the deployment was // successfully requested. Otherwise, to preserve existing behavior, we assume it was. if (!deployInfo.AllowDeferredDeployment) { return(await deploymentWillOccurTcs.Task); } else { return(true); } }
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()) { // Parse the changesetId into a GUID // The FE hook allows only GUID as a deployment id // If the id is already in GUID format nothing will happen // If it doesn't have the necessary format for a GUID, and exception will be thrown var changeSet = repository.GetChangeSet(deployBranch); updateStatusObj = new DeployStatusApiResult(Constants.BuildRequestReceived, Guid.Parse(changeSet.Id).ToString()); await 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 SendDeployStatusUpdate(updateStatusObj); } await _deploymentManager.DeployAsync( repository, changeSet, deploymentInfo.Deployer, clean : false, deploymentInfo : deploymentInfo, needFileUpdate : deploySpecificCommitId, fullBuildByDefault : deploymentInfo.DoFullBuildByDefault); if (updateStatusObj != null) { updateStatusObj.DeploymentStatus = Constants.BuildSuccessful; await 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) { _tracer.Trace("Marking deployment as failed"); statusFile.MarkFailed(); } else { _tracer.Trace("Could not find status file to mark the deployment failed"); } } if (updateStatusObj != null) { // Set deployment status as failure if exception is thrown updateStatusObj.DeploymentStatus = Constants.BuildFailed; await SendDeployStatusUpdate(updateStatusObj); } throw; } _tracer.Trace("Cleaning up temporary deployment - fetch deployment was successful"); // 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, new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(lastChange.Id))); } } }
public async Task <IActionResult> Deploy(string id = null) { JObject jsonContent = GetJsonContent(); // Just block here to read the json payload from the body using (_tracer.Step("DeploymentService.Deploy(id)")) { IActionResult result = Ok(); try { await _deploymentLock.LockOperationAsync(async() => { try { if (PostDeploymentHelper.IsAutoSwapOngoing()) { result = StatusCode(StatusCodes.Status409Conflict, Resources.Error_AutoSwapDeploymentOngoing); return; } DeployResult deployResult; if (TryParseDeployResult(id, jsonContent, out deployResult)) { using (_tracer.Step("DeploymentService.Create(id)")) { CreateDeployment(deployResult, jsonContent.Value <string>("details")); // e.g if final url is "https://kudutry.scm.azurewebsites.net/api/deployments/ef52ec67fc9574e726955a9cbaf7bcba791e4e95/log" // deploymentUri should be "https://kudutry.scm.azurewebsites.net/api/deployments/ef52ec67fc9574e726955a9cbaf7bcba791e4e95" Uri deploymentUri = kUriHelper.MakeRelative(kUriHelper.GetBaseUri(Request), new Uri(Request.GetDisplayUrl()).AbsolutePath); deployResult.Url = deploymentUri; deployResult.LogUrl = kUriHelper.MakeRelative(deploymentUri, "log"); // response = Request.CreateResponse(HttpStatusCode.OK, ArmUtils.AddEnvelopeOnArmRequest(deployResult, Request)); result = Ok(ArmUtils.AddEnvelopeOnArmRequest(deployResult, Request)); return; } } bool clean = false; bool needFileUpdate = true; if (jsonContent != null) { clean = jsonContent.Value <bool>("clean"); JToken needFileUpdateToken; if (jsonContent.TryGetValue("needFileUpdate", out needFileUpdateToken)) { needFileUpdate = needFileUpdateToken.Value <bool>(); } } string username = null; AuthUtility.TryExtractBasicAuthUser(Request, out username); IRepository repository = _repositoryFactory.GetRepository(); if (repository == null) { result = NotFound(Resources.Error_RepositoryNotFound); return; } ChangeSet changeSet = null; if (!String.IsNullOrEmpty(id)) { changeSet = repository.GetChangeSet(id); if (changeSet == null) { string message = String.Format(CultureInfo.CurrentCulture, Resources.Error_DeploymentNotFound, id); result = NotFound(message); return; } } try { await _deploymentManager.DeployAsync(repository, changeSet, username, clean, deploymentInfo: null, needFileUpdate: needFileUpdate); } catch (DeploymentFailedException ex) { if (!ArmUtils.IsArmRequest(Request)) { throw; } // if requests comes thru ARM, we adjust the error code from 500 -> 400 result = BadRequest(ex.ToString()); return; } // auto-swap if (PostDeploymentHelper.IsAutoSwapEnabled()) { if (changeSet == null) { var targetBranch = _settings.GetBranch(); changeSet = repository.GetChangeSet(targetBranch); } IDeploymentStatusFile statusFile = _status.Open(changeSet.Id); if (statusFile != null && statusFile.Status == DeployStatus.Success) { await PostDeploymentHelper.PerformAutoSwap( _environment.RequestId, new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(changeSet.Id))); } } } catch (FileNotFoundException ex) { result = NotFound(ex); } catch (InvalidStatusException) { result = BadRequest("Only successful status can be active!"); } }, "Performing deployment", TimeSpan.Zero); } catch (LockOperationException ex) { return(StatusCode(StatusCodes.Status409Conflict, ex.Message)); } return(result); } }
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); } } }
public async Task HandleAutoSwapTests() { var homePath = System.Environment.GetEnvironmentVariable("HOME"); var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { System.Environment.SetEnvironmentVariable(Constants.SiteRestrictedJWT, null); System.Environment.SetEnvironmentVariable(Constants.HttpHost, null); System.Environment.SetEnvironmentVariable("HOME", tempPath); System.Environment.SetEnvironmentVariable(Constants.WebSiteSwapSlotName, null); var autoSwapLockFile = Path.Combine(tempPath, @"site\locks", PostDeploymentHelper.AutoSwapLockFile); string deploymentId = Guid.Empty.ToString(); var tracerMock = new Mock <ITracer>(); var traceListener = new PostDeploymentTraceListener(tracerMock.Object, Mock.Of <ILogger>()); TestTracer.Trace("Autoswap will not happen, since it is not enabled."); await PostDeploymentHelper.PerformAutoSwap(traceListener); tracerMock.Verify(l => l.Trace("AutoSwap is not enabled", It.IsAny <IDictionary <string, string> >()), Times.Once); TestTracer.Trace("Autoswap will not happen, since there is no JWT token."); System.Environment.SetEnvironmentVariable(Constants.WebSiteSwapSlotName, "someslot"); var exception = await Assert.ThrowsAsync <InvalidOperationException>(() => PostDeploymentHelper.PerformAutoSwap(traceListener)); Assert.Equal("Missing X-MS-SITE-RESTRICTED-JWT env!", exception.Message); string jwtToken = Guid.NewGuid().ToString(); System.Environment.SetEnvironmentVariable(Constants.SiteRestrictedJWT, jwtToken); exception = await Assert.ThrowsAsync <InvalidOperationException>(() => PostDeploymentHelper.PerformAutoSwap(traceListener)); Assert.Equal("Missing HTTP_HOST env!", exception.Message); string hostName = "foo.scm.bar"; System.Environment.SetEnvironmentVariable(Constants.HttpHost, hostName); TestTracer.Trace("Autoswap will be triggered"); string newDeploymentId = Guid.NewGuid().ToString(); string autoSwapRequestUrl = null; string bearerToken = null; PostDeploymentHelper.HttpClientFactory = () => new HttpClient(new TestMessageHandler((HttpRequestMessage requestMessage) => { autoSwapRequestUrl = requestMessage.RequestUri.AbsoluteUri; bearerToken = requestMessage.Headers.GetValues("Authorization").First(); return(new HttpResponseMessage(HttpStatusCode.OK)); })); Assert.True(!File.Exists(autoSwapLockFile), string.Format("File {0} should not exist.", autoSwapLockFile)); await PostDeploymentHelper.PerformAutoSwap(traceListener); Assert.True(File.Exists(autoSwapLockFile), string.Format("File {0} should exist.", autoSwapLockFile)); Assert.NotNull(autoSwapRequestUrl); Assert.True(autoSwapRequestUrl.StartsWith("https://foo.scm.bar/operations/autoswap?slot=someslot&operationId=AUTOSWAP")); Assert.NotNull(bearerToken); Assert.Equal("Bearer " + jwtToken, bearerToken); } finally { System.Environment.SetEnvironmentVariable(Constants.SiteRestrictedJWT, null); System.Environment.SetEnvironmentVariable(Constants.HttpHost, null); System.Environment.SetEnvironmentVariable("HOME", homePath); System.Environment.SetEnvironmentVariable(Constants.WebSiteSwapSlotName, null); PostDeploymentHelper.HttpClientFactory = null; if (Directory.Exists(tempPath)) { Directory.Delete(tempPath, recursive: true); } } }
public async Task <HttpResponseMessage> Deploy(string id = null) { JObject result = GetJsonContent(); // Just block here to read the json payload from the body using (_tracer.Step("DeploymentService.Deploy(id)")) { HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK); await _deploymentLock.LockHttpOperationAsync(async() => { try { if (PostDeploymentHelper.IsAutoSwapOngoing()) { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.Conflict, Resources.Error_AutoSwapDeploymentOngoing)); } DeployResult deployResult; if (TryParseDeployResult(id, result, out deployResult)) { using (_tracer.Step("DeploymentService.Create(id)")) { CreateDeployment(deployResult, result.Value <string>("details")); // e.g if final url is "https://kudutry.scm.azurewebsites.net/api/deployments/ef52ec67fc9574e726955a9cbaf7bcba791e4e95/log" // deploymentUri should be "https://kudutry.scm.azurewebsites.net/api/deployments/ef52ec67fc9574e726955a9cbaf7bcba791e4e95" Uri deploymentUri = UriHelper.MakeRelative(UriHelper.GetBaseUri(Request), Request.RequestUri.AbsolutePath); deployResult.Url = deploymentUri; deployResult.LogUrl = UriHelper.MakeRelative(deploymentUri, "log"); response = Request.CreateResponse(HttpStatusCode.OK, ArmUtils.AddEnvelopeOnArmRequest(deployResult, Request)); return; } } bool clean = false; bool needFileUpdate = true; if (result != null) { clean = result.Value <bool>("clean"); JToken needFileUpdateToken; if (result.TryGetValue("needFileUpdate", out needFileUpdateToken)) { needFileUpdate = needFileUpdateToken.Value <bool>(); } } string username = null; AuthUtility.TryExtractBasicAuthUser(Request, out username); IRepository repository = _repositoryFactory.GetRepository(); if (repository == null) { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, Resources.Error_RepositoryNotFound)); } ChangeSet changeSet = null; if (!String.IsNullOrEmpty(id)) { changeSet = repository.GetChangeSet(id); if (changeSet == null) { string message = String.Format(CultureInfo.CurrentCulture, Resources.Error_DeploymentNotFound, id); throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, message)); } } await _deploymentManager.DeployAsync(repository, changeSet, username, clean, needFileUpdate); // auto-swap if (PostDeploymentHelper.IsAutoSwapEnabled()) { if (changeSet == null) { var targetBranch = _settings.GetBranch(); changeSet = repository.GetChangeSet(targetBranch); } IDeploymentStatusFile statusFile = _status.Open(changeSet.Id); if (statusFile != null && statusFile.Status == DeployStatus.Success) { await PostDeploymentHelper.PerformAutoSwap(new PostDeploymentTraceListener(_tracer, _deploymentManager.GetLogger(changeSet.Id))); } } } catch (FileNotFoundException ex) { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, ex)); } }, "Performing deployment"); return(response); } }
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) { _tracer.Trace("No-op for deployment."); 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(); _tracer.Trace("Scm is not enabled, reject all requests."); 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; if (isBackground) { using (_tracer.Step("Start deployment in the background")) { var waitForTempDeploymentCreation = isAsync; await PerformBackgroundDeployment( deployInfo, _environment, _settings, _tracer.TraceLevel, context.Request.Url, waitForTempDeploymentCreation); } // 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); try { await _deploymentLock.LockOperationAsync(async() => { if (PostDeploymentHelper.IsAutoSwapOngoing()) { context.Response.StatusCode = (int)HttpStatusCode.Conflict; context.Response.Write(Resources.Error_AutoSwapDeploymentOngoing); context.ApplicationInstance.CompleteRequest(); return; } await PerformDeployment(deployInfo); }, "Performing continuous deployment", TimeSpan.Zero); } catch (LockOperationException) { // 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 override void ProcessRequestBase(HttpContextBase context) { 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 = (int)HttpStatusCode.BadRequest; if (context.ApplicationInstance != null) { context.ApplicationInstance.CompleteRequest(); } return; } try { DeploymentLock.LockOperation(() => { context.Response.ContentType = "application/x-git-receive-pack-result"; if (PostDeploymentHelper.IsAutoSwapOngoing()) { context.Response.StatusCode = (int)HttpStatusCode.Conflict; context.Response.Write(Resources.Error_AutoSwapDeploymentOngoing); context.ApplicationInstance.CompleteRequest(); return; } 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); System.Environment.SetEnvironmentVariable(Constants.SiteRestrictedJWT, _environment.SiteRestrictedJwt); try { GitServer.Receive(context.Request.GetInputStream(), context.Response.OutputStream); } finally { System.Environment.SetEnvironmentVariable(Constants.RequestIdHeader, null); System.Environment.SetEnvironmentVariable(Constants.SiteRestrictedJWT, null); } } }, "Handling git receive pack", TimeSpan.Zero); } catch (LockOperationException ex) { context.Response.StatusCode = 409; context.Response.Write(ex.Message); context.ApplicationInstance.CompleteRequest(); } } }