public static (string source, DirectoryInfo deployTargetDirectory, TempDirectory tempTargetDir) CopyTestData( ILogger logger) { string testDataPath = Path.Combine(VcsTestPathHelper.FindVcsRootPath(), "tests", "Milou.Deployer.Tests.Integration", "TestData", "AppDataTest"); string source = Path.Combine(testDataPath, "Source"); string target = Path.Combine(testDataPath, "Target"); var tempTargetDir = TempDirectory.CreateTempDirectory(); var deployTargetDirectory = tempTargetDir.Directory; deployTargetDirectory.Refresh(); RecursiveIO.RecursiveDelete(deployTargetDirectory, logger); deployTargetDirectory.EnsureExists(); var testTargetDirectory = new DirectoryInfo(target); RecursiveIO.RecursiveCopy(testTargetDirectory, deployTargetDirectory, logger, ImmutableArray <string> .Empty); deployTargetDirectory.Refresh(); var filesBefore = deployTargetDirectory.GetFiles(); foreach (var fileInfo in filesBefore) { logger.Debug("Existing file before deploy: {File}", fileInfo.Name); } Assert.Contains(filesBefore, file => file.Name.Equals("DeleteMe.txt", StringComparison.OrdinalIgnoreCase)); return(source, deployTargetDirectory, tempTargetDir); }
private async Task <ExitCode> InternalDeployAsync( ImmutableArray <DeploymentExecutionDefinition> deploymentExecutionDefinitions, SemanticVersion explicitVersion, CancellationToken cancellationToken = default) { var tempDirectoriesToClean = new List <DirectoryInfo>(); var tempFilesToClean = new List <string>(); try { _logger.Verbose("Executing deployment execution definitions [{Length}]: {Executions}", deploymentExecutionDefinitions.Length, string.Join($"{Environment.NewLine}\t", deploymentExecutionDefinitions.Select(_ => $"'{_}'"))); foreach (DeploymentExecutionDefinition deploymentExecutionDefinition in deploymentExecutionDefinitions) { if (string.IsNullOrWhiteSpace(deploymentExecutionDefinition.TargetDirectoryPath) && string.IsNullOrWhiteSpace(deploymentExecutionDefinition.PublishSettingsFile)) { throw new InvalidOperationException($"{nameof(deploymentExecutionDefinition.TargetDirectoryPath)} and {nameof(deploymentExecutionDefinition.PublishSettingsFile)} are both not set"); } string asJson = JsonConvert.SerializeObject(deploymentExecutionDefinition, Formatting.Indented); _logger.Information("Executing deployment execution definition: '{DeploymentExecutionDefinition}'", asJson); const string TempPrefix = "MD-"; string uniqueSuffix = DateTime.Now.ToString("MMddHHmmssfff", CultureInfo.InvariantCulture); string tempPath = Path.Combine( Path.GetTempPath(), $"{TempPrefix}{uniqueSuffix}{Guid.NewGuid().ToString().Substring(0, 6)}"); var tempWorkingDirectory = new DirectoryInfo(tempPath); DirectoryInfo packageInstallTempDirectory = tempWorkingDirectory; tempDirectoriesToClean.Add(packageInstallTempDirectory); MayBe <InstalledPackage> installedMainPackage = await _packageInstaller.InstallPackageAsync( deploymentExecutionDefinition, packageInstallTempDirectory, false, explicitVersion, cancellationToken).ConfigureAwait(false); if (!installedMainPackage.HasValue) { _logger.Error( "Could not install package defined in deployment execution definition {DeploymentExecutionDefinition}", deploymentExecutionDefinition); return(ExitCode.Failure); } InstalledPackage installedPackage = installedMainPackage.Value; _logger.Information( "Successfully installed NuGet package '{PackageId}' version '{Version}' to path '{NugetPackageFullPath}'", installedPackage.PackageId, installedPackage.Version.ToNormalizedString(), installedPackage.NugetPackageFullPath); tempWorkingDirectory.Refresh(); DirectoryInfo[] packagesDirectory = tempWorkingDirectory.GetDirectories(); DirectoryInfo packageDirectory = packagesDirectory.Single(directory => directory.Name.Equals(installedPackage.PackageId, StringComparison.OrdinalIgnoreCase)); SemanticVersion version = explicitVersion ?? GetSemanticVersionFromDefinition( deploymentExecutionDefinition, packageDirectory, installedPackage.Version); _logger.Verbose("Package version is {Version}", version.ToNormalizedString()); var possibleXmlTransformations = new List <FileMatch>(); var replaceFiles = new List <FileMatch>(); var environmentPackageResult = new EnvironmentPackageResult(true); var contentDirectory = new DirectoryInfo(Path.Combine(packageDirectory.FullName, "Content")); if (!contentDirectory.Exists) { _logger.Error("Content directory '{FullName}' does not exist", contentDirectory.FullName); return(ExitCode.Failure); } FileInfo contentFilesJson = packageDirectory.GetFiles("contentFiles.json").SingleOrDefault(); if (contentFilesJson?.Exists == true) { ExitCode exitCode = VerifyFiles(contentFilesJson.FullName, contentDirectory); if (!exitCode.IsSuccess) { return(exitCode); } } else { _logger.Debug("No file contentFiles.json was found in package directory {PackageDirectory}", packageDirectory.FullName); } if (!string.IsNullOrWhiteSpace(deploymentExecutionDefinition.EnvironmentConfig)) { _logger.Information( "Fetching environment packages for package {Package} and environment {Environment}", deploymentExecutionDefinition.PackageId, deploymentExecutionDefinition.EnvironmentConfig); environmentPackageResult = await AddEnvironmentPackageAsync(deploymentExecutionDefinition, packageInstallTempDirectory, possibleXmlTransformations, replaceFiles, tempDirectoriesToClean, version, cancellationToken).ConfigureAwait(false); if (!environmentPackageResult.IsSuccess) { return(ExitCode.Failure); } if (environmentPackageResult.Version != null) { _logger.Information("Installed environment package version {Version}", environmentPackageResult.Version.ToNormalizedString()); } else { _logger.Information("No environment package was installed"); } } else { _logger.Debug("Definition has no environment configuration specified"); } if (possibleXmlTransformations.Any()) { _logger.Debug("Possible Xml transformation files {V}", string.Join(", ", possibleXmlTransformations.Select(fileMatch => $"'{fileMatch.TargetName}' replaced by --> '{fileMatch.ActionFile.FullName}'"))); } var xmlTransformedFiles = new List <string>(); foreach (FileMatch possibleXmlTransformation in possibleXmlTransformations) { TransformationResult result = _xmlTransformer.TransformMatch(possibleXmlTransformation, contentDirectory); if (!result.IsSuccess) { return(ExitCode.Failure); } xmlTransformedFiles.AddRange(result.TransformedFiles); } if (replaceFiles.Any()) { _logger.Debug("Possible replacing files {Files}", string.Join(", ", replaceFiles.Select(fileMatch => $"'{fileMatch.TargetName}' replaced by --> '{fileMatch.ActionFile.FullName}'"))); } var replacedFiles = new List <string>(); foreach (FileMatch replacement in replaceFiles) { ReplaceResult result = ReplaceFileIfMatchingFiles(replacement, contentDirectory); if (!result.IsSuccess) { return(ExitCode.Failure); } replacedFiles.AddRange(result.ReplacedFiles); } if (!string.IsNullOrWhiteSpace(deploymentExecutionDefinition.WebConfigTransformFile)) { DeploymentTransformation.Transform(deploymentExecutionDefinition, contentDirectory, _logger); } string uniqueTargetTempSuffix = DateTime.Now.ToString("MMddHHmmssfff", CultureInfo.InvariantCulture); string uniqueTargetTempPath = Path.Combine( Path.GetTempPath(), $"{TempPrefix}t{uniqueTargetTempSuffix}{Guid.NewGuid().ToString().Substring(0, 6)}"); var targetTempDirectoryInfo = new DirectoryInfo(uniqueTargetTempPath); if (!targetTempDirectoryInfo.Exists) { _logger.Debug("Creating temp target directory '{FullName}'", packageInstallTempDirectory.FullName); targetTempDirectoryInfo.Create(); } string wwwrootPath = Path.Combine(contentDirectory.FullName, "wwwroot"); var wwwRootDirectory = new DirectoryInfo(wwwrootPath); DirectoryInfo applicationMetadataTargetDirectory = wwwRootDirectory.Exists ? wwwRootDirectory : contentDirectory; string versionFile = ApplicationMetadataCreator.SetVersionFile( installedMainPackage.Value, applicationMetadataTargetDirectory, deploymentExecutionDefinition, xmlTransformedFiles, replacedFiles, environmentPackageResult, _logger); _logger.Information("Successfully wrote metadata file {Path}", versionFile); _logger.Verbose("Copying content files to '{FullName}'", targetTempDirectoryInfo.FullName); bool usePublishSettingsFile = !string.IsNullOrWhiteSpace(deploymentExecutionDefinition.PublishSettingsFile); var targetAppOffline = new FileInfo(Path.Combine(targetTempDirectoryInfo.FullName, DeploymentConstants.AppOfflineHtm)); var ruleConfiguration = RuleConfiguration.Get(deploymentExecutionDefinition, DeployerConfiguration, _logger); if (ruleConfiguration.AppOfflineEnabled && usePublishSettingsFile) { string sourceAppOffline = Path.Combine(contentDirectory.FullName, DeploymentConstants.AppOfflineHtm); if (!File.Exists(sourceAppOffline) && !targetAppOffline.Exists) { using var _ = File.Create(targetAppOffline.FullName); _logger.Debug("Created offline file '{File}'", targetAppOffline.FullName); if (DeployerConfiguration.DefaultWaitTimeAfterAppOffline > TimeSpan.Zero) { await Task.Delay(DeployerConfiguration.DefaultWaitTimeAfterAppOffline, cancellationToken) .ConfigureAwait(false); } tempFilesToClean.Add(targetAppOffline.FullName); } } RecursiveIO.RecursiveCopy(contentDirectory, targetTempDirectoryInfo, _logger, deploymentExecutionDefinition.ExcludedFilePatterns); tempDirectoriesToClean.Add(targetTempDirectoryInfo); _logger.Debug("Copied content files from '{ContentDirectory}' to '{FullName}'", contentDirectory, targetTempDirectoryInfo.FullName); tempDirectoriesToClean.Add(packageInstallTempDirectory); bool hasPublishSettingsFile = !string.IsNullOrWhiteSpace(deploymentExecutionDefinition.PublishSettingsFile) && File.Exists(deploymentExecutionDefinition.PublishSettingsFile); if (hasPublishSettingsFile) { _logger.Debug("The publish settings file '{PublishSettingsFile}' exists", deploymentExecutionDefinition.PublishSettingsFile); } else { _logger.Debug("The deployment definition has no publish setting file"); } if (deploymentExecutionDefinition.PublishType == PublishType.WebDeploy) { _webDeployHelper.DeploymentTraceEventHandler += (sender, args) => { if (string.IsNullOrWhiteSpace(args.Message)) { return; } if (args.EventLevel == TraceLevel.Verbose) { _logger.Verbose("{Message}", args.Message); return; } _logger.Information("{Message}", args.Message); }; } bool hasIisSiteName = deploymentExecutionDefinition.IisSiteName.HasValue(); IDeploymentChangeSummary summary; try { IIisManager?manager = default; if (!string.IsNullOrWhiteSpace(deploymentExecutionDefinition.IisSiteName)) { manager = _iisManager(deploymentExecutionDefinition); } if (hasIisSiteName && manager is {}) { bool stopped = manager.StopSiteIfApplicable(); if (!stopped) { _logger.Error( "Could not stop IIS site for deployment execution definition {DeploymentExecutionDefinition}", deploymentExecutionDefinition); return(ExitCode.Failure); } } try { if (deploymentExecutionDefinition.PublishType == PublishType.WebDeploy) { _logger.Information("Deploying {Target} with WebDeploy", deploymentExecutionDefinition.TargetDirectoryPath); summary = await _webDeployHelper.DeployContentToOneSiteAsync( targetTempDirectoryInfo.FullName, deploymentExecutionDefinition.PublishSettingsFile, DeployerConfiguration.DefaultWaitTimeAfterAppOffline, doNotDelete : ruleConfiguration.DoNotDeleteEnabled, appOfflineEnabled : ruleConfiguration.AppOfflineEnabled, useChecksum : ruleConfiguration.UseChecksumEnabled, whatIf : ruleConfiguration.WhatIfEnabled, traceLevel : TraceLevel.Verbose, appDataSkipDirectiveEnabled : ruleConfiguration.AppDataSkipDirectiveEnabled, applicationInsightsProfiler2SkipDirectiveEnabled : ruleConfiguration.ApplicationInsightsProfiler2SkipDirectiveEnabled, logAction : message => _logger.Debug("{Message}", message), targetPath : hasPublishSettingsFile ?string.Empty : deploymentExecutionDefinition.TargetDirectoryPath ).ConfigureAwait(false); } else if (deploymentExecutionDefinition.PublishType.IsAnyFtpType) { var basePath = deploymentExecutionDefinition.FtpPath; bool isSecure = deploymentExecutionDefinition.PublishType == PublishType.Ftps; var ftpSettings = new FtpSettings(basePath, isSecure); _logger.Information("Deploying {Target} with {PublishType}", deploymentExecutionDefinition.FtpPath?.Path, deploymentExecutionDefinition.PublishType); string publishSettingsFile = deploymentExecutionDefinition.PublishSettingsFile; if (string.IsNullOrWhiteSpace(publishSettingsFile)) { _logger.Error( "Deployment target type is set to {Type} but no publish file is set", deploymentExecutionDefinition.PublishTypeValue); return(ExitCode.Failure); } using IFtpHandler ftpHandler = await _ftpHandlerFactory.CreateWithPublishSettings( publishSettingsFile, ftpSettings, _logger, cancellationToken); _logger.Verbose("Created FTP handler, starting publish"); summary = await ftpHandler.PublishAsync( ruleConfiguration, targetTempDirectoryInfo, cancellationToken); } else { throw new InvalidOperationException( $"Publish type {deploymentExecutionDefinition.PublishType} is not supported"); } } catch (Exception ex) when(!ex.IsFatal()) { _logger.Error(ex, "Could not deploy site {DeploymentExecutionDefinition}", deploymentExecutionDefinition); return(ExitCode.Failure); } finally { manager?.Dispose(); } } catch (Exception ex) when(!ex.IsFatal()) { _logger.Error(ex, "Could not handle start/stop for iis site {Site}", deploymentExecutionDefinition.IisSiteName); return(ExitCode.Failure); } _logger.Information("Summary: {Summary}", summary.ToDisplayValue()); }