public Vec2 GetOffset(ModuleLocation modloc) { switch (modloc) { case ModuleLocation.Sight: return(new Vec2(5, -5)); case ModuleLocation.Barrel: return(DefBarrOffTl - _center + _extraOffset + new Vec2(-2f, -0.5f)); default: return(new Vec2()); } }
private async Task <ModuleLocation> FindExistingDependencyAsync(ModuleManifestDependency dependency) { try { var describe = await Settings.CfnClient.DescribeStacksAsync(new DescribeStacksRequest { StackName = ToStackName(dependency.ModuleFullName) }); var deployedOutputs = describe.Stacks.FirstOrDefault()?.Outputs; var deployedInfo = deployedOutputs?.FirstOrDefault(output => output.OutputKey == "Module")?.OutputValue; var success = deployedInfo.TryParseModuleDescriptor( out string deployedOwner, out string deployedName, out VersionInfo deployedVersion, out string deployedBucketName ); var deployed = new ModuleLocation(deployedOwner, deployedName, deployedVersion, deployedBucketName); if (!success) { LogError($"unable to retrieve information of the deployed dependent module"); return(deployed); } // confirm that the module name matches if (deployed.ModuleFullName != dependency.ModuleFullName) { LogError($"deployed dependent module name ({deployed.ModuleFullName}) does not match {dependency.ModuleFullName}"); return(deployed); } // confirm that the module version is in a valid range if ((dependency.MaxVersion != null) && (deployedVersion > dependency.MaxVersion)) { LogError($"deployed dependent module version (v{deployedVersion}) is newer than max version constraint v{dependency.MaxVersion}"); return(deployed); } if ((dependency.MinVersion != null) && (deployedVersion < dependency.MinVersion)) { LogError($"deployed dependent module version (v{deployedVersion}) is older than min version constraint v{dependency.MinVersion}"); return(deployed); } return(deployed); } catch (AmazonCloudFormationException) { // stack doesn't exist return(null); } }
//--- Methods --- public async Task <bool> DoAsync( DryRunLevel?dryRun, string moduleReference, string instanceName, bool allowDataLoos, bool protectStack, Dictionary <string, string> parameters, bool forceDeploy, bool promptAllParameters, XRayTracingLevel xRayTracingLevel, bool deployOnlyIfExists, bool allowDependencyUpgrades ) { Console.WriteLine($"Resolving module reference: {moduleReference}"); // determine location of cloudformation template from module key if (!ModuleInfo.TryParse(moduleReference, out var moduleInfo)) { LogError($"invalid module reference: {moduleReference}"); return(false); } var foundModuleLocation = await _loader.ResolveInfoToLocationAsync(moduleInfo, moduleInfo.Origin, ModuleManifestDependencyType.Root, allowImport : Settings.AllowImport, showError : !deployOnlyIfExists); if (foundModuleLocation == null) { // nothing to do; loader already emitted an error return(deployOnlyIfExists); } // download module manifest var(manifest, manifestErrorReason) = await _loader.LoadManifestFromLocationAsync(foundModuleLocation); if (manifest == null) { LogError(manifestErrorReason); return(false); } // deploy module if (dryRun == null) { var stackName = Settings.GetStackName(manifest.GetFullName(), instanceName); // check version of previously deployed module if (!deployOnlyIfExists) { Console.WriteLine("=> Validating module for deployment tier"); } var updateValidation = await IsValidModuleUpdateAsync(stackName, manifest, showError : !forceDeploy && !deployOnlyIfExists); if (!forceDeploy && !updateValidation.Success) { return(false); } // check if a previous deployment was found if (deployOnlyIfExists && (updateValidation.ExistingStack == null)) { // nothing to do return(true); } var existing = updateValidation.ExistingStack; // check if existing stack checksum matches template checksum if (!forceDeploy && !parameters.Any()) { var existingChecksum = existing?.Outputs.FirstOrDefault(output => output.OutputKey == "ModuleChecksum"); if (existingChecksum?.OutputValue == manifest.TemplateChecksum) { Console.WriteLine($"{Settings.LowContrastColor}=> No changes found to deploy{Settings.ResetColor}"); return(true); } } // prompt for missing parameters var deployParameters = PromptModuleParameters(manifest, existing, parameters, promptAllParameters); if (HasErrors) { return(false); } // check if module supports AWS X-Ray for tracing if ( manifest.GetAllParameters().Any(p => p.Name == "XRayTracing") && !deployParameters.Any(p => p.ParameterKey == "XRayTracing") ) { deployParameters.Add(new CloudFormationParameter { ParameterKey = "XRayTracing", ParameterValue = xRayTracingLevel.ToString() }); } // discover shared module dependencies and prompt for missing parameters var dependencies = (await _loader.DiscoverAllDependenciesAsync(manifest, checkExisting: true, allowImport: Settings.AllowImport, allowDependencyUpgrades: allowDependencyUpgrades)) .Where(dependency => dependency.Type == ModuleManifestDependencyType.Shared) .ToList(); if (HasErrors) { return(false); } var dependenciesParameters = dependencies .Select(dependency => new { ModuleFullName = dependency.Manifest.GetFullName(), Parameters = PromptModuleParameters( dependency.Manifest, promptAll: promptAllParameters ) }) .ToDictionary(t => t.ModuleFullName, t => t.Parameters); if (HasErrors) { return(false); } // deploy module dependencies foreach (var dependency in dependencies) { var dependencyLocation = new ModuleLocation(Settings.DeploymentBucketName, dependency.ModuleLocation.ModuleInfo, dependency.ModuleLocation.Hash); if (!await new ModelUpdater(Settings, SourceFilename).DeployChangeSetAsync( dependency.Manifest, await _loader.GetNameMappingsFromLocationAsync(dependencyLocation), dependencyLocation, Settings.GetStackName(dependency.Manifest.GetFullName()), allowDataLoos, protectStack, dependenciesParameters[dependency.Manifest.GetFullName()] )) { return(false); } } // deploy module var moduleLocation = new ModuleLocation(Settings.DeploymentBucketName, manifest.ModuleInfo, manifest.TemplateChecksum); return(await new ModelUpdater(Settings, moduleReference).DeployChangeSetAsync( manifest, await _loader.GetNameMappingsFromLocationAsync(moduleLocation), moduleLocation, stackName, allowDataLoos, protectStack, deployParameters )); } return(true); }
//--- Methods --- public async Task <bool> DeployChangeSetAsync( ModuleManifest manifest, ModuleNameMappings nameMappings, ModuleLocation moduleLocation, string stackName, bool allowDataLoss, bool protectStack, List <CloudFormationParameter> parameters ) { var now = DateTime.UtcNow; // check if cloudformation stack already exists and is in a final state Console.WriteLine(); Console.WriteLine($"Deploying stack: {stackName} [{moduleLocation.ModuleInfo}]"); var mostRecentStackEventId = await Settings.CfnClient.GetMostRecentStackEventIdAsync(stackName); // validate template (must have been copied to deployment bucket at this stage) if (moduleLocation.SourceBucketName != Settings.DeploymentBucketName) { LogError($"module source must match the deployment tier S3 bucket (EXPECTED: {Settings.DeploymentBucketName}, FOUND: {moduleLocation.SourceBucketName})"); return(false); } ValidateTemplateResponse validation; try { validation = await Settings.CfnClient.ValidateTemplateAsync(new ValidateTemplateRequest { TemplateURL = moduleLocation.ModuleTemplateUrl }); } catch (AmazonCloudFormationException e) { LogError($"{e.Message} (url: {moduleLocation.ModuleTemplateUrl})"); return(false); } // verify if we need to remove the old CloudFormation notification ARNs that were created by LambdaSharp config before v0.7 List <string> notificationARNs = null; ModuleNameMappings oldNameMappings = null; if (mostRecentStackEventId != null) { // fetch name mappings for current template; this is needed to properly map logical IDs to their original names when they get deleted oldNameMappings = await new ModelManifestLoader(Settings, "source").GetNameMappingsFromCloudFormationStackAsync(stackName); // NOTE (2019-09-19, bjorg): this is a HACK to remove the old notification ARNs, because doing it part // of the change set is not working for some reason. var deployedModule = await Settings.CfnClient.GetStackAsync(stackName, LogError); notificationARNs = deployedModule.Stack.NotificationARNs.Where(arn => !arn.Contains(":LambdaSharpTool-")).ToList(); if (notificationARNs.Count != deployedModule.Stack.NotificationARNs.Count) { Console.WriteLine("=> Removing legacy stack notification ARN"); var update = await Settings.CfnClient.UpdateStackAsync(new UpdateStackRequest { StackName = stackName, UsePreviousTemplate = true, Parameters = deployedModule.Stack.Parameters.Select(param => new CloudFormationParameter { ParameterKey = param.ParameterKey, UsePreviousValue = true }).ToList(), NotificationARNs = notificationARNs, Capabilities = validation.Capabilities }); var outcome = await Settings.CfnClient.TrackStackUpdateAsync(stackName, update.StackId, mostRecentStackEventId, nameMappings, oldNameMappings, LogError); if (!outcome.Success) { LogError("failed to remove legacy stack notification ARN; remove manually and try again"); return(false); } mostRecentStackEventId = await Settings.CfnClient.GetMostRecentStackEventIdAsync(stackName); Console.WriteLine("=> Legacy stack notification ARN has been removed"); } } // create change-set var success = false; var changeSetName = $"{moduleLocation.ModuleInfo.FullName.Replace(".", "-")}-{now:yyyy-MM-dd-hh-mm-ss}"; var updateOrCreate = (mostRecentStackEventId != null) ? "update" : "create"; var capabilities = validation.Capabilities.Any() ? "[" + string.Join(", ", validation.Capabilities) + "]" : ""; Console.WriteLine($"=> Stack {updateOrCreate} initiated for {Settings.InfoColor}{stackName}{Settings.ResetColor} {capabilities}"); CreateChangeSetResponse response; try { response = await Settings.CfnClient.CreateChangeSetAsync(new CreateChangeSetRequest { Capabilities = validation.Capabilities, ChangeSetName = changeSetName, ChangeSetType = (mostRecentStackEventId != null) ? ChangeSetType.UPDATE : ChangeSetType.CREATE, Description = $"Stack {updateOrCreate} {moduleLocation.ModuleInfo.FullName} (v{moduleLocation.ModuleInfo.Version})", Parameters = new List <CloudFormationParameter>(parameters) { new CloudFormationParameter { ParameterKey = "DeploymentPrefix", ParameterValue = Settings.TierPrefix }, new CloudFormationParameter { ParameterKey = "DeploymentPrefixLowercase", ParameterValue = Settings.TierPrefix.ToLowerInvariant() }, new CloudFormationParameter { ParameterKey = "DeploymentBucketName", ParameterValue = Settings.DeploymentBucketName }, new CloudFormationParameter { ParameterKey = "DeploymentChecksum", ParameterValue = manifest.TemplateChecksum } }, StackName = stackName, TemplateURL = moduleLocation.ModuleTemplateUrl, Tags = Settings.GetCloudFormationStackTags(moduleLocation.ModuleInfo.FullName, stackName) }); } catch (AmazonCloudFormationException e) { LogError($"cloudformation change-set failed: {e.Message}"); return(false); } try { var changes = await WaitForChangeSetAsync(response.Id); if (changes == null) { return(false); } if (!changes.Any()) { Console.WriteLine("=> No stack update required"); return(true); } // changes if (!allowDataLoss) { var lossyChanges = DetectLossyChanges(changes); if (lossyChanges.Any()) { Console.WriteLine(); Console.WriteLine($"{Settings.AlertColor}CAUTION:{Settings.ResetColor} detected potential replacement and data-loss in the following resources"); var maxResourceTypeWidth = lossyChanges.Select(change => change.ResourceChange.ResourceType.Length).Max(); foreach (var lossy in lossyChanges) { if (Settings.UseAnsiConsole) { if (lossy.ResourceChange.Replacement == Replacement.True) { Console.Write(AnsiTerminal.Red); Console.Write("ALWAYS "); } else { Console.Write(AnsiTerminal.Yellow); Console.Write("CONDITIONAL "); } Console.Write(AnsiTerminal.Reset); } else { Console.WriteLine((lossy.ResourceChange.Replacement == Replacement.True) ? "ALWAYS " : "CONDITIONAL " ); } Console.Write(lossy.ResourceChange.ResourceType); Console.Write("".PadRight(maxResourceTypeWidth - lossy.ResourceChange.ResourceType.Length + 4)); Console.Write(TranslateLogicalIdToFullName(lossy.ResourceChange.LogicalResourceId)); Console.WriteLine(); } if (!Settings.PromptYesNo("Proceed with potentially replacing/deleting resources?", false)) { return(false); } Console.WriteLine(); } } // execute change-set await Settings.CfnClient.ExecuteChangeSetAsync(new ExecuteChangeSetRequest { ChangeSetName = changeSetName, StackName = stackName }); var outcome = await Settings.CfnClient.TrackStackUpdateAsync( stackName, response.StackId, mostRecentStackEventId, nameMappings, oldNameMappings, LogError ); if (outcome.Success) { Console.WriteLine($"=> Stack {updateOrCreate} finished"); ShowStackResult(outcome.Stack); success = true; } else { Console.WriteLine($"=> Stack {updateOrCreate} FAILED"); } // optionally enable stack protection if (success) { // on success, protect the stack if requested if (protectStack) { await Settings.CfnClient.UpdateTerminationProtectionAsync(new UpdateTerminationProtectionRequest { EnableTerminationProtection = protectStack, StackName = stackName }); } } else if (mostRecentStackEventId == null) { // delete a new stack that failed to create try { await Settings.CfnClient.DeleteStackAsync(new DeleteStackRequest { StackName = stackName }); } catch { } } return(success); } finally { try { await Settings.CfnClient.DeleteChangeSetAsync(new DeleteChangeSetRequest { ChangeSetName = response.Id }); } catch { } } // local function string TranslateLogicalIdToFullName(string logicalId) { var fullName = logicalId; nameMappings?.ResourceNameMappings.TryGetValue(logicalId, out fullName); return(fullName ?? logicalId); } }
//--- Methods --- public async Task <bool> DeployChangeSetAsync( ModuleManifest manifest, ModuleLocation location, string stackName, bool allowDataLoss, bool protectStack, List <CloudFormationParameter> parameters ) { var now = DateTime.UtcNow; // check if cloudformation stack already exists and is in a final state Console.WriteLine(); Console.WriteLine($"Deploying stack: {stackName} [{location.ModuleFullName}:{location.ModuleVersion}]"); var mostRecentStackEventId = await Settings.CfnClient.GetMostRecentStackEventIdAsync(stackName); // set optional notification topics for cloudformation operations var notificationArns = new List <string>(); if (Settings.DeploymentNotificationsTopic != null) { notificationArns.Add(Settings.DeploymentNotificationsTopic); } // validate template var templateUrl = $"https://{location.ModuleBucketName}.s3.amazonaws.com/{location.TemplatePath}"; ValidateTemplateResponse validation; try { validation = await Settings.CfnClient.ValidateTemplateAsync(new ValidateTemplateRequest { TemplateURL = templateUrl }); } catch (AmazonCloudFormationException e) { LogError(e.Message); return(false); } // create change-set var success = false; var changeSetName = $"{location.ModuleFullName.Replace(".", "-")}-{now:yyyy-MM-dd-hh-mm-ss}"; var updateOrCreate = (mostRecentStackEventId != null) ? "update" : "create"; var capabilities = validation.Capabilities.Any() ? "[" + string.Join(", ", validation.Capabilities) + "]" : ""; Console.WriteLine($"=> Stack {updateOrCreate} initiated for {stackName} {capabilities}"); var response = await Settings.CfnClient.CreateChangeSetAsync(new CreateChangeSetRequest { Capabilities = validation.Capabilities, ChangeSetName = changeSetName, ChangeSetType = (mostRecentStackEventId != null) ? ChangeSetType.UPDATE : ChangeSetType.CREATE, Description = $"Stack {updateOrCreate} {location.ModuleFullName} (v{location.ModuleVersion})", NotificationARNs = notificationArns, Parameters = new List <CloudFormationParameter>(parameters) { new CloudFormationParameter { ParameterKey = "DeploymentPrefix", ParameterValue = string.IsNullOrEmpty(Settings.Tier) ? "" : (Settings.Tier + "-") }, new CloudFormationParameter { ParameterKey = "DeploymentPrefixLowercase", ParameterValue = string.IsNullOrEmpty(Settings.Tier) ? "" : (Settings.Tier.ToLowerInvariant() + "-") }, new CloudFormationParameter { ParameterKey = "DeploymentBucketName", ParameterValue = location.ModuleBucketName ?? "" } }, StackName = stackName, TemplateURL = templateUrl, Tags = new List <Tag> { new Tag { Key = "LambdaSharp:Tier", Value = Settings.Tier }, new Tag { Key = "LambdaSharp:Module", Value = location.ModuleFullName }, new Tag { Key = "LambdaSharp:RootStack", Value = stackName }, new Tag { Key = "LambdaSharp:DeployedBy", Value = Settings.AwsUserArn.Split(':').Last() } } }); try { var changes = await WaitForChangeSetAsync(response.Id); if (changes == null) { return(false); } if (!changes.Any()) { Console.WriteLine("=> No stack update required"); return(true); } // changes if (!allowDataLoss) { var lossyChanges = DetectLossyChanges(changes); if (lossyChanges.Any()) { LogError("one or more resources could be replaced or deleted; use --allow-data-loss to proceed"); Console.WriteLine("=> WARNING: detected potential replacement and data-loss in the following resources"); foreach (var lossy in lossyChanges) { Console.WriteLine($"{(lossy.ResourceChange.Replacement == Replacement.True ? "ALWAYS" : "CONDITIONAL"),-12} {lossy.ResourceChange.ResourceType,-55} {TranslateLogicalIdToFullName(lossy.ResourceChange.LogicalResourceId)}"); } return(false); } } // execute change-set await Settings.CfnClient.ExecuteChangeSetAsync(new ExecuteChangeSetRequest { ChangeSetName = changeSetName, StackName = stackName }); var outcome = await Settings.CfnClient.TrackStackUpdateAsync(stackName, mostRecentStackEventId, manifest.ResourceNameMappings, manifest.TypeNameMappings, logError : LogError); if (outcome.Success) { Console.WriteLine($"=> Stack {updateOrCreate} finished"); ShowStackResult(outcome.Stack); success = true; } else { Console.WriteLine($"=> Stack {updateOrCreate} FAILED"); } // optionally enable stack protection if (success) { // on success, protect the stack if requested if (protectStack) { await Settings.CfnClient.UpdateTerminationProtectionAsync(new UpdateTerminationProtectionRequest { EnableTerminationProtection = protectStack, StackName = stackName }); } } else if (mostRecentStackEventId == null) { // delete a new stack that failed to create try { await Settings.CfnClient.DeleteStackAsync(new DeleteStackRequest { StackName = stackName }); } catch { } } return(success); } finally { try { await Settings.CfnClient.DeleteChangeSetAsync(new DeleteChangeSetRequest { ChangeSetName = response.Id }); } catch { } } // local function string TranslateLogicalIdToFullName(string logicalId) { var fullName = logicalId; manifest.ResourceNameMappings?.TryGetValue(logicalId, out fullName); return(fullName ?? logicalId); } }
//--- Methods --- public async Task <bool> DeployChangeSetAsync( ModuleManifest manifest, ModuleNameMappings nameMappings, ModuleLocation moduleLocation, string stackName, bool allowDataLoss, bool protectStack, List <CloudFormationParameter> parameters ) { var now = DateTime.UtcNow; // check if cloudformation stack already exists and is in a final state Console.WriteLine(); Console.WriteLine($"Deploying stack: {stackName} [{moduleLocation.ModuleInfo}]"); var mostRecentStackEventId = await Settings.CfnClient.GetMostRecentStackEventIdAsync(stackName); // validate template (must have been copied to deployment bucket at this stage) if (moduleLocation.SourceBucketName != Settings.DeploymentBucketName) { LogError($"module source must match the deployment tier S3 bucket (EXPECTED: {Settings.DeploymentBucketName}, FOUND: {moduleLocation.SourceBucketName})"); return(false); } ValidateTemplateResponse validation; try { validation = await Settings.CfnClient.ValidateTemplateAsync(new ValidateTemplateRequest { TemplateURL = moduleLocation.ModuleTemplateUrl }); } catch (AmazonCloudFormationException e) { LogError($"{e.Message} (url: {moduleLocation.ModuleTemplateUrl})"); return(false); } // create change-set var success = false; var changeSetName = $"{moduleLocation.ModuleInfo.FullName.Replace(".", "-")}-{now:yyyy-MM-dd-hh-mm-ss}"; var updateOrCreate = (mostRecentStackEventId != null) ? "update" : "create"; var capabilities = validation.Capabilities.Any() ? "[" + string.Join(", ", validation.Capabilities) + "]" : ""; Console.WriteLine($"=> Stack {updateOrCreate} initiated for {stackName} {capabilities}"); var response = await Settings.CfnClient.CreateChangeSetAsync(new CreateChangeSetRequest { Capabilities = validation.Capabilities, ChangeSetName = changeSetName, ChangeSetType = (mostRecentStackEventId != null) ? ChangeSetType.UPDATE : ChangeSetType.CREATE, Description = $"Stack {updateOrCreate} {moduleLocation.ModuleInfo.FullName} (v{moduleLocation.ModuleInfo.Version})", Parameters = new List <CloudFormationParameter>(parameters) { new CloudFormationParameter { ParameterKey = "DeploymentPrefix", ParameterValue = Settings.TierPrefix }, new CloudFormationParameter { ParameterKey = "DeploymentPrefixLowercase", ParameterValue = Settings.TierPrefix.ToLowerInvariant() }, new CloudFormationParameter { ParameterKey = "DeploymentBucketName", ParameterValue = Settings.DeploymentBucketName } }, StackName = stackName, TemplateURL = moduleLocation.ModuleTemplateUrl, Tags = Settings.GetCloudFormationStackTags(moduleLocation.ModuleInfo.FullName, stackName) }); try { var changes = await WaitForChangeSetAsync(response.Id); if (changes == null) { return(false); } if (!changes.Any()) { Console.WriteLine("=> No stack update required"); return(true); } // changes if (!allowDataLoss) { var lossyChanges = DetectLossyChanges(changes); if (lossyChanges.Any()) { LogError("one or more resources could be replaced or deleted; use --allow-data-loss to proceed"); Console.WriteLine("=> WARNING: detected potential replacement and data-loss in the following resources"); foreach (var lossy in lossyChanges) { Console.WriteLine($"{(lossy.ResourceChange.Replacement == Replacement.True ? "ALWAYS" : "CONDITIONAL"),-12} {lossy.ResourceChange.ResourceType,-55} {TranslateLogicalIdToFullName(lossy.ResourceChange.LogicalResourceId)}"); } return(false); } } // execute change-set await Settings.CfnClient.ExecuteChangeSetAsync(new ExecuteChangeSetRequest { ChangeSetName = changeSetName, StackName = stackName }); var outcome = await Settings.CfnClient.TrackStackUpdateAsync(stackName, response.StackId, mostRecentStackEventId, nameMappings, LogError); if (outcome.Success) { Console.WriteLine($"=> Stack {updateOrCreate} finished"); ShowStackResult(outcome.Stack); success = true; } else { Console.WriteLine($"=> Stack {updateOrCreate} FAILED"); } // optionally enable stack protection if (success) { // on success, protect the stack if requested if (protectStack) { await Settings.CfnClient.UpdateTerminationProtectionAsync(new UpdateTerminationProtectionRequest { EnableTerminationProtection = protectStack, StackName = stackName }); } } else if (mostRecentStackEventId == null) { // delete a new stack that failed to create try { await Settings.CfnClient.DeleteStackAsync(new DeleteStackRequest { StackName = stackName }); } catch { } } return(success); } finally { try { await Settings.CfnClient.DeleteChangeSetAsync(new DeleteChangeSetRequest { ChangeSetName = response.Id }); } catch { } } // local function string TranslateLogicalIdToFullName(string logicalId) { var fullName = logicalId; nameMappings?.ResourceNameMappings.TryGetValue(logicalId, out fullName); return(fullName ?? logicalId); } }
protected ClassicModule(float priority, ModuleLocation modloc) : base(priority) { ModLoc = modloc; }