// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using AWS.Deploy.Common;
using AWS.Deploy.Common.DeploymentManifest;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Common.Recipes;
using AWS.Deploy.Orchestration;
using AWS.Deploy.Orchestration.Utilities;
using AWS.Deploy.Recipes;
using Newtonsoft.Json;
namespace AWS.Deploy.CLI.Commands
{
///
/// The class supports the functionality to create a new CDK project and save it at a customer
/// specified directory location.
///
public class GenerateDeploymentProjectCommand
{
private const int DEFAULT_PERSISTED_RECIPE_PRIORITY = 1000;
private readonly IToolInteractiveService _toolInteractiveService;
private readonly IConsoleUtilities _consoleUtilities;
private readonly ICdkProjectHandler _cdkProjectHandler;
private readonly ICommandLineWrapper _commandLineWrapper;
private readonly IDirectoryManager _directoryManager;
private readonly IFileManager _fileManager;
private readonly OrchestratorSession _session;
private readonly IDeploymentManifestEngine _deploymentManifestEngine;
private readonly IRecipeHandler _recipeHandler;
private readonly string _targetApplicationFullPath;
public GenerateDeploymentProjectCommand(
IToolInteractiveService toolInteractiveService,
IConsoleUtilities consoleUtilities,
ICdkProjectHandler cdkProjectHandler,
ICommandLineWrapper commandLineWrapper,
IDirectoryManager directoryManager,
IFileManager fileManager,
OrchestratorSession session,
IDeploymentManifestEngine deploymentManifestEngine,
IRecipeHandler recipeHandler,
string targetApplicationFullPath)
{
_toolInteractiveService = toolInteractiveService;
_consoleUtilities = consoleUtilities;
_cdkProjectHandler = cdkProjectHandler;
_commandLineWrapper = commandLineWrapper;
_directoryManager = directoryManager;
_fileManager = fileManager;
_session = session;
_deploymentManifestEngine = deploymentManifestEngine;
_recipeHandler = recipeHandler;
_targetApplicationFullPath = targetApplicationFullPath;
}
///
/// This method takes a user specified directory path and generates the CDK deployment project at this location.
/// If the provided directory path is an empty string, then a default directory is created to save the CDK deployment project.
///
/// An absolute or a relative path provided by the user.
/// The name of the deployment project that will be displayed in the list of available deployment options.
///
public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDisplayName)
{
var orchestrator = new Orchestrator(_session, _recipeHandler);
var recommendations = await GenerateRecommendationsToSaveDeploymentProject(orchestrator);
var selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations);
if (string.IsNullOrEmpty(saveCdkDirectoryPath))
saveCdkDirectoryPath = GenerateDefaultSaveDirectoryPath();
var newDirectoryCreated = CreateSaveCdkDirectory(saveCdkDirectoryPath);
var (isValid, errorMessage) = ValidateSaveCdkDirectory(saveCdkDirectoryPath);
if (!isValid)
{
if (newDirectoryCreated)
_directoryManager.Delete(saveCdkDirectoryPath);
errorMessage = $"Failed to generate deployment project.{Environment.NewLine}{errorMessage}";
throw new InvalidSaveDirectoryForCdkProject(DeployToolErrorCode.InvalidSaveDirectoryForCdkProject, errorMessage.Trim());
}
var directoryUnderSourceControl = await IsDirectoryUnderSourceControl(saveCdkDirectoryPath);
if (!directoryUnderSourceControl)
{
var userPrompt = "Warning: The target directory is not being tracked by source control. If the saved deployment " +
"project is used for deployment it is important that the deployment project is retained to allow " +
"future redeployments to previously deployed applications. " + Environment.NewLine + Environment.NewLine +
"Do you still want to continue?";
_toolInteractiveService.WriteLine();
var yesNoResult = _consoleUtilities.AskYesNoQuestion(userPrompt, YesNo.Yes);
if (yesNoResult == YesNo.No)
{
if (newDirectoryCreated)
_directoryManager.Delete(saveCdkDirectoryPath);
return;
}
}
_cdkProjectHandler.CreateCdkProject(selectedRecommendation, _session, saveCdkDirectoryPath);
await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveCdkDirectoryPath, projectDisplayName);
var saveCdkDirectoryFullPath = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).FullName;
_toolInteractiveService.WriteLine();
_toolInteractiveService.WriteLine($"Saving AWS CDK deployment project to: {saveCdkDirectoryFullPath}");
await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, _targetApplicationFullPath);
}
///
/// This method generates the appropriate recommendations for the target deployment project.
///
///
/// A list of
private async Task> GenerateRecommendationsToSaveDeploymentProject(Orchestrator orchestrator)
{
var recommendations = await orchestrator.GenerateRecommendationsToSaveDeploymentProject();
if (recommendations.Count == 0)
{
throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.DeploymentProjectNotSupported, "The project you are trying to deploy is currently not supported.");
}
return recommendations;
}
///
/// This method takes the path to the target deployment project and creates
/// a default save directory inside the parent folder of the current directory.
/// For example:
/// Target project directory - C:\Codebase\MyWebApp
/// Generated default save directory - C:\Codebase\MyWebApp.Deployment If the save directory already exists, then a suffix number is attached.
///
/// The default save directory path.
private string GenerateDefaultSaveDirectoryPath()
{
var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
if(targetApplicationDi.Parent == null)
{
throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
}
var applicatonDirectoryFullPath = targetApplicationDi.Parent.FullName;
var saveCdkDirectoryFullPath = applicatonDirectoryFullPath + ".Deployment";
var suffixNumber = 0;
while (_directoryManager.Exists(saveCdkDirectoryFullPath))
saveCdkDirectoryFullPath = applicatonDirectoryFullPath + $".Deployment{++suffixNumber}";
return saveCdkDirectoryFullPath;
}
///
/// This method takes a path and creates a new directory at this path if one does not already exist.
///
/// Relative or absolute path of the directory at which the CDK deployment project will be saved.
/// A boolean to indicate if a new directory was created.
private bool CreateSaveCdkDirectory(string saveCdkDirectoryPath)
{
var newDirectoryCreated = false;
if (!_directoryManager.Exists(saveCdkDirectoryPath))
{
_directoryManager.CreateDirectory(saveCdkDirectoryPath);
newDirectoryCreated = true;
}
return newDirectoryCreated;
}
///
/// This method takes the path to the intended location of the CDK deployment project and performs validations on it.
///
/// Relative or absolute path of the directory at which the CDK deployment project will be saved.
/// A tuple containing a boolean that indicates if the directory is valid and a corresponding string error message.
private Tuple ValidateSaveCdkDirectory(string saveCdkDirectoryPath)
{
var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
if (targetApplicationDi.Parent == null)
{
throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
}
var errorMessage = string.Empty;
var isValid = true;
var targetApplicationDirectoryFullPath = targetApplicationDi.Parent.FullName;
if (!_directoryManager.IsEmpty(saveCdkDirectoryPath))
{
errorMessage += "The directory specified for saving the CDK project is non-empty. " +
"Please provide an empty directory path and try again." + Environment.NewLine;
isValid = false;
}
if (_directoryManager.ExistsInsideDirectory(targetApplicationDirectoryFullPath, saveCdkDirectoryPath))
{
errorMessage += "The directory used to save the CDK deployment project is contained inside of " +
"the target application project directory. Please specify a different directory and try again.";
isValid = false;
}
return new Tuple(isValid, errorMessage.Trim());
}
///
/// Checks if the location of the saved CDK deployment project is monitored by a source control system.
///
/// Relative or absolute path of the directory at which the CDK deployment project will be saved.
///
private async Task IsDirectoryUnderSourceControl(string saveCdkDirectoryPath)
{
var gitStatusResult = await _commandLineWrapper.TryRunWithResult("git status", saveCdkDirectoryPath);
var svnStatusResult = await _commandLineWrapper.TryRunWithResult("svn status", saveCdkDirectoryPath);
return gitStatusResult.Success || svnStatusResult.Success;
}
///
/// Generates a snapshot of the deployment recipe inside the location at which the CDK deployment project is saved.
///
///
/// Relative or absolute path of the directory at which the CDK deployment project will be saved.
/// The name of the deployment project that will be displayed in the list of available deployment options.
private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendation, string saveCdkDirectoryPath, string projectDisplayName)
{
var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
if (targetApplicationDi.Parent == null)
{
throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
}
var targetApplicationDirectoryName = targetApplicationDi.Name;
var recipeSnapshotFileName = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).Name + ".recipe";
var recipeSnapshotFilePath = Path.Combine(saveCdkDirectoryPath, recipeSnapshotFileName);
var recipePath = recommendation.Recipe.RecipePath;
if (string.IsNullOrEmpty(recipePath))
throw new InvalidOperationException("The recipe path cannot be null or empty as part " +
$"of the {nameof(recommendation.Recipe)} object");
var recipeBody = await _fileManager.ReadAllTextAsync(recipePath);
var recipe = JsonConvert.DeserializeObject(recipeBody);
if (recipe == null)
throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentProjectRecipe, "Failed to deserialize deployment project recipe");
var recipeName = string.IsNullOrEmpty(projectDisplayName) ?
$"Deployment project for {targetApplicationDirectoryName} to {recommendation.Recipe.TargetService}"
: projectDisplayName;
recipe.Id = Guid.NewGuid().ToString();
recipe.Name = recipeName;
recipe.CdkProjectTemplateId = null;
recipe.CdkProjectTemplate = null;
recipe.PersistedDeploymentProject = true;
recipe.RecipePriority = DEFAULT_PERSISTED_RECIPE_PRIORITY;
recipe.BaseRecipeId = recommendation.Recipe.Id;
var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new SerializeModelContractResolver()
});
await _fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody);
}
}
}