Example #1
0
        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());
                }