private async Task <bool> ImportDependencies(ModuleManifest manifest, bool allowImport) { // discover module dependencies var dependencies = await _loader.DiscoverAllDependenciesAsync(manifest, checkExisting : false, allowImport, allowDependencyUpgrades : false); if (HasErrors) { return(false); } // copy all dependencies to deployment bucket that are missing or have a pre-release version foreach (var dependency in dependencies.Where(dependency => dependency.ModuleLocation.SourceBucketName != Settings.DeploymentBucketName)) { var imported = false; // copy check-summed module artifacts (guaranteed immutable) foreach (var artifact in dependency.Manifest.Artifacts) { imported = imported | await ImportArtifact(dependency.ModuleLocation.ModuleInfo.Origin, artifact); } // copy version manifest imported = imported | await ImportArtifact(dependency.ModuleLocation.ModuleInfo.Origin, dependency.ModuleLocation.ModuleInfo.VersionPath, replace : dependency.ModuleLocation.ModuleInfo.Version.IsPreRelease()); // show message if any artifacts were imported if (imported) { new ModelManifestLoader(Settings, manifest.GetFullName()).ResetCache(Settings.DeploymentBucketName, dependency.ModuleLocation.ModuleInfo); Console.WriteLine($"=> Imported {dependency.ModuleLocation.ModuleInfo}"); } } return(true); }
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 { } } } }
//--- Methods --- public async Task <string> PublishAsync(ModuleManifest manifest, bool forcePublish) { Console.WriteLine($"Publishing module: {manifest.GetFullName()}"); _forcePublish = forcePublish; _changesDetected = false; // verify that all files referenced by manifest exist (NOTE: source file was already checked) foreach (var file in manifest.Assets) { var filepath = Path.Combine(Settings.OutputDirectory, file); if (!File.Exists(filepath)) { LogError($"could not find: '{filepath}'"); } } if (Settings.HasErrors) { return(null); } // verify that manifest is either a pre-release or its version has not been published yet if (!manifest.Module.TryParseModuleDescriptor( out string moduleOwner, out string moduleName, out VersionInfo moduleVersion, out string _ )) { throw new ApplicationException("invalid module info"); } var destinationKey = $"{moduleOwner}/Modules/{moduleName}/Versions/{moduleVersion}/cloudformation.json"; // check if we want to always publish, regardless of version or detected changes if (!forcePublish) { // check if a manifest already exists for this version var existingManifest = await new ModelManifestLoader(Settings, "cloudformation.json").LoadFromS3Async(Settings.DeploymentBucketName, destinationKey, errorIfMissing: false); if (existingManifest != null) { if (existingManifest.Hash == manifest.Hash) { // manifest matches, nothing further to do Console.WriteLine($"=> No changes found to publish"); return($"s3://{Settings.DeploymentBucketName}/{destinationKey}"); } else if (!moduleVersion.IsPreRelease) { // don't allow publishing over an existing, stable version LogError($"{moduleOwner}.{moduleName} (v{moduleVersion}) is already published; use --force-publish to proceed anyway"); return(null); } } } // upload assets for (var i = 0; i < manifest.Assets.Count; ++i) { manifest.Assets[i] = await UploadPackageAsync(manifest, manifest.Assets[i], "asset"); } // upload CloudFormation template var template = await UploadTemplateFileAsync(manifest, "template"); // store copy of cloudformation template under version number await Settings.S3Client.CopyObjectAsync(new CopyObjectRequest { SourceBucket = Settings.DeploymentBucketName, SourceKey = template, DestinationBucket = Settings.DeploymentBucketName, DestinationKey = destinationKey, ContentType = "application/json" }); if (!_changesDetected) { // NOTE: this message should never appear since we already do a similar check earlier Console.WriteLine($"=> No changes found to upload"); } return($"s3://{Settings.DeploymentBucketName}/{destinationKey}"); }