private async Task <(bool Success, CloudFormationStack ExistingStack)> IsValidModuleUpdateAsync(string stackName, ModuleManifest manifest) { try { // check if the module was already deployed var describe = await Settings.CfnClient.DescribeStacksAsync(new DescribeStacksRequest { StackName = stackName }); // make sure the stack is in a stable state (not updating and not failed) var existing = describe.Stacks.FirstOrDefault(); switch (existing?.StackStatus) { case null: case "CREATE_COMPLETE": case "ROLLBACK_COMPLETE": case "UPDATE_COMPLETE": case "UPDATE_ROLLBACK_COMPLETE": // we're good to go break; default: LogError($"deployed module is not in a valid state; module deployment must be complete and successful (Status: {existing?.StackStatus})"); return(false, existing); } // validate existing module deployment var deployedOutputs = existing?.Outputs; var deployed = deployedOutputs?.FirstOrDefault(output => output.OutputKey == "Module")?.OutputValue; if (!deployed.TryParseModuleDescriptor( out string deployedOwner, out string deployedName, out VersionInfo deployedVersion, out string _ )) { LogError("unable to determine the name of the deployed module; use --force-deploy to proceed anyway"); return(false, existing); } var deployedFullName = $"{deployedOwner}.{deployedName}"; if (deployedFullName != manifest.GetFullName()) { LogError($"deployed module name ({deployedFullName}) does not match {manifest.GetFullName()}; use --force-deploy to proceed anyway"); return(false, existing); } if (deployedVersion > manifest.GetVersion()) { LogError($"deployed module version (v{deployedVersion}) is newer than v{manifest.GetVersion()}; use --force-deploy to proceed anyway"); return(false, existing); } return(true, existing); } catch (AmazonCloudFormationException) { // stack doesn't exist } return(true, null); }
private async Task <(bool Success, CloudFormationStack ExistingStack)> IsValidModuleUpdateAsync(string stackName, ModuleManifest manifest, bool showError) { // check if the module was already deployed var existing = await Settings.CfnClient.GetStackAsync(stackName, LogError); if (existing.Stack == null) { return(existing.Success, existing.Stack); } // validate existing module deployment var deployed = existing.Stack?.GetModuleVersionText(); if (!ModuleInfo.TryParse(deployed, out var deployedModuleInfo)) { if (showError) { LogError("unable to determine the name of the deployed module; use --force-deploy to proceed anyway"); } return(false, existing.Stack); } if (deployedModuleInfo.FullName != manifest.GetFullName()) { if (showError) { LogError($"deployed module name ({deployedModuleInfo.FullName}) does not match {manifest.GetFullName()}; use --force-deploy to proceed anyway"); } return(false, existing.Stack); } var versionComparison = deployedModuleInfo.Version.CompareToVersion(manifest.GetVersion()); if (versionComparison > 0) { if (showError) { LogError($"deployed module version (v{deployedModuleInfo.Version}) is newer than v{manifest.GetVersion()}; use --force-deploy to proceed anyway"); } return(false, existing.Stack); } else if (versionComparison == null) { if (showError) { LogError($"deployed module version (v{deployedModuleInfo.Version}) is not compatible with v{manifest.GetVersion()}; use --force-deploy to proceed anyway"); } return(false, existing.Stack); } return(true, existing.Stack); }
private List <CloudFormationParameter> PromptModuleParameters( ModuleManifest manifest, CloudFormationStack existing = null, Dictionary <string, string> parameters = null, bool promptAll = false ) { var stackParameters = new Dictionary <string, CloudFormationParameter>(); // tentatively indicate to reuse previous parameter values if (existing != null) { foreach (var parameter in manifest.ParameterSections .SelectMany(section => section.Parameters) .Where(moduleParameter => existing.Parameters.Any(existingParameter => existingParameter.ParameterKey == moduleParameter.Name)) ) { stackParameters[parameter.Name] = new CloudFormationParameter { ParameterKey = parameter.Name, UsePreviousValue = true }; } } // deployment bucket must always be set to match deployment tier in case it changed because LambdaSharp.Core was recreated stackParameters["DeploymentBucketName"] = new CloudFormationParameter { ParameterKey = "DeploymentBucketName", ParameterValue = Settings.DeploymentBucketName }; // add all provided parameters if (parameters != null) { foreach (var parameter in parameters) { stackParameters[parameter.Key] = new CloudFormationParameter { ParameterKey = parameter.Key, ParameterValue = parameter.Value }; } } // check if module requires any prompts if (manifest.GetAllParameters().Any(RequiresPrompt)) { Console.WriteLine(); Console.WriteLine($"Configuration for {manifest.GetFullName()} (v{manifest.GetVersion()})"); // only list parameter sections that contain a parameter that requires a prompt foreach (var parameterGroup in manifest.ParameterSections.Where(group => group.Parameters.Any(RequiresPrompt))) { Console.WriteLine(); Settings.PromptLabel(parameterGroup.Title.ToUpper()); // only prompt for required parameters foreach (var parameter in parameterGroup.Parameters.Where(RequiresPrompt)) { // check if parameter is multiple choice string enteredValue; if (parameter.AllowedValues?.Any() ?? false) { var message = parameter.Name; if (parameter.Label != null) { message += $": {parameter.Label}"; } enteredValue = Settings.PromptChoice(message, parameter.AllowedValues); } else { var constraints = new StringBuilder(); if ((parameter.MinValue != null) || (parameter.MaxValue != null)) { // append value constraints constraints.Append(" ("); if ((parameter.MinValue != null) && (parameter.MaxValue != null)) { constraints.Append($"Range: {parameter.MinValue.Value}..{parameter.MaxValue.Value}"); } else if (parameter.MinValue != null) { constraints.Append($"Mininum: {parameter.MinValue.Value}"); } else if (parameter.MaxValue != null) { constraints.Append($"Maximum: {parameter.MaxValue.Value}"); } constraints.Append(")"); } else if ((parameter.MinLength != null) || (parameter.MaxLength != null)) { // append length constraints constraints.Append(" ("); if ((parameter.MinLength != null) || (parameter.MaxLength != null)) { constraints.Append($"Length: {parameter.MinValue.Value}..{parameter.MaxValue.Value}"); } else if (parameter.MinLength != null) { constraints.Append($"Mininum Length: {parameter.MinLength.Value}"); } else if (parameter.MaxLength != null) { constraints.Append($"Maximum Length: {parameter.MaxLength.Value}"); } constraints.Append(")"); } var message = $"{parameter.Name} [{parameter.Type}]{constraints}"; if (parameter.Label != null) { message += $": {parameter.Label}"; } enteredValue = Settings.PromptString(message, parameter.Default, parameter.AllowedPattern, parameter.ConstraintDescription) ?? ""; } stackParameters[parameter.Name] = new CloudFormationParameter { ParameterKey = parameter.Name, ParameterValue = enteredValue }; } } } // check if LambdaSharp.Core services should be enabled by default if ( (Settings.CoreServices == CoreServices.Enabled) && manifest.GetAllParameters().Any(p => p.Name == "LambdaSharpCoreServices") && !stackParameters.Any(p => p.Value.ParameterKey == "LambdaSharpCoreServices") ) { stackParameters.Add("LambdaSharpCoreServices", new CloudFormationParameter { ParameterKey = "LambdaSharpCoreServices", ParameterValue = Settings.CoreServices.ToString() }); } return(stackParameters.Values.ToList()); // local functions bool RequiresPrompt(ModuleManifestParameter parameter) { if (parameters?.ContainsKey(parameter.Name) == true) { // no prompt since parameter is provided explicitly return(false); } if (existing?.Parameters.Any(p => p.ParameterKey == parameter.Name) == true) { // no prompt since we can reuse the previous parameter value return(false); } if (!promptAll && (parameter.Default != null)) { // no prompt since parameter has a default value return(false); } if (Settings.PromptsAsErrors) { LogError($"{manifest.GetFullName()} requires value for parameter '{parameter.Name}'"); return(false); } return(true); } }
private List <CloudFormationParameter> PromptModuleParameters( ModuleManifest manifest, CloudFormationStack existing = null, Dictionary <string, string> parameters = null, bool promptAll = false, bool promptsAsErrors = false ) { var stackParameters = new Dictionary <string, CloudFormationParameter>(); // tentatively indicate to reuse previous parameter values if (existing != null) { foreach (var parameter in manifest.ParameterSections .SelectMany(section => section.Parameters) .Where(moduleParameter => existing.Parameters.Any(existingParameter => existingParameter.ParameterKey == moduleParameter.Name)) ) { stackParameters[parameter.Name] = new CloudFormationParameter { ParameterKey = parameter.Name, UsePreviousValue = true }; } } // add all provided parameters if (parameters != null) { foreach (var parameter in parameters) { stackParameters[parameter.Key] = new CloudFormationParameter { ParameterKey = parameter.Key, ParameterValue = parameter.Value }; } } // check if module requires any prompts if (manifest.GetAllParameters().Any(RequiresPrompt)) { Console.WriteLine(); Console.WriteLine($"Configuration for {manifest.GetFullName()} (v{manifest.GetVersion()})"); // only list parameter sections that contain a parameter that requires a prompt foreach (var parameterGroup in manifest.ParameterSections.Where(group => group.Parameters.Any(RequiresPrompt))) { Console.WriteLine(); Console.WriteLine($"*** {parameterGroup.Title.ToUpper()} ***"); // only prompt for required parameters foreach (var parameter in parameterGroup.Parameters.Where(RequiresPrompt)) { var enteredValue = PromptString(parameter, parameter.Default) ?? ""; stackParameters[parameter.Name] = new CloudFormationParameter { ParameterKey = parameter.Name, ParameterValue = enteredValue }; } } } return(stackParameters.Values.ToList()); // local functions bool RequiresPrompt(ModuleManifestParameter parameter) { if (parameters?.ContainsKey(parameter.Name) == true) { // no prompt since parameter is provided explicitly return(false); } if (existing?.Parameters.Any(p => p.ParameterKey == parameter.Name) == true) { // no prompt since we can reuse the previous parameter value return(false); } if (!promptAll && (parameter.Default != null)) { // no prompt since parameter has a default value return(false); } if (promptsAsErrors) { LogError($"{manifest.GetFullName()} requires value for parameter '{parameter.Name}'"); return(false); } return(true); } string PromptString(ModuleManifestParameter parameter, string defaultValue = null) { var prompt = $"|=> {parameter.Name} [{parameter.Type}]:"; if (parameter.Label != null) { prompt += $" {parameter.Label}:"; } if (!string.IsNullOrEmpty(defaultValue)) { prompt = $"{prompt} [{defaultValue}]"; } Console.Write(prompt); Console.Write(' '); SetCursorVisible(true); var resp = Console.ReadLine(); SetCursorVisible(false); if (!string.IsNullOrEmpty(resp)) { return(resp); } return(defaultValue); // local functions void SetCursorVisible(bool visible) { try { Console.CursorVisible = visible; } catch { } } } }