protected Stack QueryStack(Func <IAmazonCloudFormation> clientFactory, StackArn stack) { try { return(clientFactory.DescribeStack(stack)); } catch (AmazonServiceException ex) { if (ex.ErrorCode == "AccessDenied") { throw new PermissionException( "AWS-CLOUDFORMATION-ERROR-0004: The AWS account used to perform the operation does not have " + "the required permissions to describe the CloudFormation stack. " + "This means that the step is not able to generate any output variables.\n" + ex.Message + "\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0004", ex); } throw new UnknownException( "AWS-CLOUDFORMATION-ERROR-0005: An unrecognised exception was thrown while querying the CloudFormation stacks.\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0005", ex); } }
public async Task DescribeChangeset(StackArn stack, ChangeSetArn changeSet, IVariables variables) { Guard.NotNull(stack, "The provided stack identifer or name may not be null"); Guard.NotNull(changeSet, "The provided change set identifier or name may not be null"); Guard.NotNull(variables, "The variable dictionary may not be null"); try { var response = await clientFactory.DescribeChangeSetAsync(stack, changeSet); SetOutputVariable(variables, "ChangeCount", response.Changes.Count.ToString()); SetOutputVariable(variables, "Changes", JsonConvert.SerializeObject(response.Changes, Formatting.Indented)); } catch (AmazonCloudFormationException ex) when(ex.ErrorCode == "AccessDenied") { throw new PermissionException( "The AWS account used to perform the operation does not have the required permissions to describe the change set.\n" + "Please ensure the current account has permission to perfrom action 'cloudformation:DescribeChangeSet'." + ex.Message + "\n"); } catch (AmazonCloudFormationException ex) { throw new UnknownException("An unrecognized exception was thrown while describing the CloudFormation change set.", ex); } }
public void DescribeChangeset(StackArn stack, ChangeSetArn changeSet, CalamariVariableDictionary variables) { Guard.NotNull(stack, "The provided stack identifer or name may not be null"); Guard.NotNull(changeSet, "The provided change set identifier or name may not be null"); Guard.NotNull(variables, "The variable dictionary may not be null"); try { var response = clientFactory.DescribeChangeSet(stack, changeSet); SetOutputVariable(variables, "ChangeCount", response.Changes.Count.ToString()); SetOutputVariable(variables, "Changes", JsonConvert.SerializeObject(response.Changes, Formatting.Indented)); } catch (AmazonCloudFormationException ex) { if (ex.ErrorCode == "AccessDenied") { throw new PermissionException( @"AWS-CLOUDFORMATION-ERROR-0015: The AWS account used to perform the operation does not have " + "the required permissions to describe the change set.\n" + ex.Message + "\n" + "For more information visit the [octopus docs](https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0015)"); } throw new UnknownException( "AWS-CLOUDFORMATION-ERROR-0016: An unrecognised exception was thrown while describing the CloudFormation change set.\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0016", ex); } }
public static ICloudFormationRequestBuilder Create(ITemplateResolver templateResolver, string templateFile, string templateParameterFile, bool filesInPackage, ICalamariFileSystem fileSystem, IVariables variables, string stackName, List <string> capabilities, bool disableRollback, string roleArn, IEnumerable <KeyValuePair <string, string> > tags, StackArn stack, Func <IAmazonCloudFormation> clientFactory) { var resolvedTemplate = templateResolver.Resolve(templateFile, filesInPackage, variables); var resolvedParameters = templateResolver.MaybeResolve(templateParameterFile, filesInPackage, variables); if (!string.IsNullOrWhiteSpace(templateParameterFile) && !resolvedParameters.Some()) { throw new CommandException("Could not find template parameters file: " + templateParameterFile); } return(new CloudFormationTemplate(() => variables.Evaluate(fileSystem.ReadFile(resolvedTemplate.Value)), CloudFormationParametersFile.Create(resolvedParameters, fileSystem, variables), stackName, capabilities, disableRollback, roleArn, tags, stack, clientFactory, variables)); }
private void DeployCloudFormation(RunningDeployment deployment, StackArn stack, CloudFormationTemplate template) { Guard.NotNull(deployment, "deployment can not be null"); clientFactory.WaitForStackToComplete(CloudFormationDefaults.StatusWaitPeriod, stack, LogAndThrowRollbacks(clientFactory, stack, false)); DeployStack(deployment, stack, template); }
public BaseTemplate(IEnumerable <Parameter> inputs, string stackName, List <string> iamCapabilities, bool disableRollback, string roleArn, IEnumerable <KeyValuePair <string, string> > tags, StackArn stack, Func <IAmazonCloudFormation> clientFactory, IVariables variables) { Inputs = inputs; this.stackName = stackName; this.disableRollback = disableRollback; this.stack = stack; this.roleArn = roleArn; this.clientFactory = clientFactory; this.variables = variables; this.tags = tags?.Select(x => new Tag { Key = x.Key, Value = x.Value }).ToList(); var(validCapabilities, _) = ExcludeAndLogUnknownIamCapabilities(iamCapabilities); capabilities = validCapabilities.ToList(); }
/// <summary> /// Deletes the stack /// </summary> private void DeleteCloudFormation(StackArn stack) { WithAmazonServiceExceptionHandling(() => { clientFactory.DeleteStack(stack); Log.Info($"Deleted stack called {stackName} in region {awsEnvironmentGeneration.AwsRegion.SystemName}"); }); }
private Task DeleteCloudFormation(StackArn stack) { Guard.NotNull(stack, "Stack must not be null"); return(WithAmazonServiceExceptionHandling(async() => { await clientFactory.DeleteStackAsync(stack); })); }
/// <summary> /// Deletes the stack /// </summary> private Task DeleteCloudFormation(StackArn stack) { return(WithAmazonServiceExceptionHandling(async() => { await clientFactory.DeleteStackAsync(stack); Log.Info($"Deleted stack called {stackName} in region {awsEnvironmentGeneration.AwsRegion.SystemName}"); })); }
private async Task DeployCloudFormation(RunningDeployment deployment, StackArn stack, ICloudFormationRequestBuilder template) { Guard.NotNull(deployment, "deployment can not be null"); var deploymentStartTime = DateTime.Now; await clientFactory.WaitForStackToComplete(CloudFormationDefaults.StatusWaitPeriod, stack, LogAndThrowRollbacks(clientFactory, stack, false, filter : FilterStackEventsSince(deploymentStartTime))); await DeployStack(deployment, stack, template, deploymentStartTime); }
/// <summary> /// Updates the stack and returns the stack ID /// </summary> /// <param name="stack">The stack name or id</param> /// <param name="deployment">The current deployment</param> /// <param name="template">The CloudFormation template</param> /// <returns>stackId</returns> private async Task <string> UpdateCloudFormation( RunningDeployment deployment, StackArn stack, CloudFormationTemplate template) { Guard.NotNull(deployment, "deployment can not be null"); try { var result = await ClientHelpers.CreateCloudFormationClient(awsEnvironmentGeneration).UpdateStackAsync(new UpdateStackRequest { StackName = stackName, TemplateBody = template.Content, Parameters = template.Inputs.ToList(), Capabilities = capabilities, RoleARN = roleArnProvider(deployment) }); Log.Info( $"Updated stack with id {result.StackId} in region {awsEnvironmentGeneration.AwsRegion.SystemName}"); return(result.StackId); } catch (AmazonCloudFormationException ex) { // Some stack states indicate that we can delete the stack and start again. Otherwise we have some other // exception that needs to be dealt with. if (!(await StackMustBeDeleted(stack)).SelectValueOrDefault(x => x)) { // Is this an unrecoverable state, or just a stack that has nothing to update? if (DealWithUpdateException(ex)) { // There was nothing to update, but we return the id for consistency anyway var result = await QueryStackAsync(clientFactory, stack); return(result.StackId); } } // If the stack exists, is in a ROLLBACK_COMPLETE state, and was never successfully // created in the first place, we can end up here. In this case we try to create // the stack from scratch. await DeleteCloudFormation(stack); await clientFactory.WaitForStackToComplete(CloudFormationDefaults.StatusWaitPeriod, stack, LogAndThrowRollbacks(clientFactory, stack, false)); return(await CreateCloudFormation(deployment, template)); } catch (AmazonServiceException ex) { LogAmazonServiceException(ex); throw ex; } }
/// <summary> /// Check whether the stack must be deleted in order to recover. /// </summary> /// <param name="stack">The stack id or name</param> /// <returns>true if this status indicates that the stack has to be deleted, and false otherwise</returns> private async Task <Maybe <bool> > StackMustBeDeleted(StackArn stack) { try { return((await StackEvent(stack)).Select(x => x.StackIsUnrecoverable())); } catch (PermissionException) { // If we can't get the stack status, assume it is not in a state that we can recover from return(Maybe <bool> .None); } }
public static ICloudFormationRequestBuilder Create(string templateS3Url, string templateParameterS3Url, ICalamariFileSystem fileSystem, IVariables variables, ILog log, string stackName, List <string> capabilities, bool disableRollback, string roleArn, IEnumerable <KeyValuePair <string, string> > tags, StackArn stack, Func <IAmazonCloudFormation> clientFactory) { if (!string.IsNullOrWhiteSpace(templateParameterS3Url) && !templateParameterS3Url.StartsWith("http")) { throw new CommandException("Parameters file must start with http: " + templateParameterS3Url); } if (!string.IsNullOrWhiteSpace(templateS3Url) && !templateS3Url.StartsWith("http")) { throw new CommandException("Template file must start with http: " + templateS3Url); } var templatePath = string.IsNullOrWhiteSpace(templateParameterS3Url) ? Maybe <ResolvedTemplatePath> .None : new ResolvedTemplatePath(ParametersFile).AsSome(); if (templatePath.Some()) { DownloadS3(variables, log, templateParameterS3Url); } var parameters = CloudFormationParametersFile.Create(templatePath, fileSystem, variables); return(new CloudFormationS3Template(parameters, templateS3Url, stackName, capabilities, disableRollback, roleArn, tags, stack, clientFactory, variables)); }
/// <summary> /// Update or create the stack /// </summary> /// <param name="deployment">The current deployment</param> /// <param name="stack"></param> /// <param name="template"></param> private async Task DeployStack(RunningDeployment deployment, StackArn stack, CloudFormationTemplate template) { Guard.NotNull(deployment, "deployment can not be null"); var stackId = await template.Inputs // Use the parameters to either create or update the stack .Map(async parameters => await StackExists(stack, StackStatus.DoesNotExist) != StackStatus.DoesNotExist ?await UpdateCloudFormation(deployment, stack, template) : await CreateCloudFormation(deployment, template)); if (waitForComplete) { await clientFactory.WaitForStackToComplete(CloudFormationDefaults.StatusWaitPeriod, stack, LogAndThrowRollbacks(clientFactory, stack)); } // Take the stack ID returned by the create or update events, and save it as an output variable Log.SetOutputVariable("AwsOutputs[StackId]", stackId ?? "", deployment.Variables); Log.Info( $"Saving variable \"Octopus.Action[{deployment.Variables["Octopus.Action.Name"]}].Output.AwsOutputs[StackId]\""); }
public CloudFormationTemplate(Func <string> content, ITemplateInputs <Parameter> parameters, string stackName, List <string> iamCapabilities, bool disableRollback, string roleArn, IEnumerable <KeyValuePair <string, string> > tags, StackArn stack, Func <IAmazonCloudFormation> clientFactory, IVariables variables) : base(parameters.Inputs, stackName, iamCapabilities, disableRollback, roleArn, tags, stack, clientFactory, variables) { this.content = content; }
private async Task <Maybe <RunningChangeSet> > ExecuteChangeset(Func <IAmazonCloudFormation> factory, StackArn stack, ChangeSetArn changeSet) { try { var changes = await factory.WaitForChangeSetCompletion(CloudFormationDefaults.StatusWaitPeriod, new RunningChangeSet(stack, changeSet)); if (changes.Status == ChangeSetStatus.FAILED && string.Compare(changes.StatusReason, "No updates are to be performed.", StringComparison.InvariantCultureIgnoreCase) == 0) { //We don't need the failed changeset to hang around if there are no changes await factory().DeleteChangeSetAsync(new DeleteChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }); return(Maybe <RunningChangeSet> .None); } if (changes.Status == ChangeSetStatus.FAILED) { throw new UnknownException($"The changeset failed to create.\n{changes.StatusReason}"); } await factory().ExecuteChangeSetAsync(new ExecuteChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }); return(new RunningChangeSet(stack, changeSet).AsSome()); } catch (AmazonCloudFormationException exception) when(exception.ErrorCode == "AccessDenied") { throw new PermissionException( "The AWS account used to perform the operation does not have the required permission to execute the changeset.\n" + "Please ensure the current account has permission to perfrom action 'cloudformation:ExecuteChangeSet'.\n" + exception.Message + "\n"); } catch (AmazonServiceException exception) { LogAmazonServiceException(exception); throw; } }
protected Task <Stack> QueryStackAsync(Func <IAmazonCloudFormation> clientFactory, StackArn stack) { try { return(clientFactory.DescribeStackAsync(stack)); } catch (AmazonServiceException ex) when(ex.ErrorCode == "AccessDenied") { throw new PermissionException( "The AWS account used to perform the operation does not have the required permissions to describe the CloudFormation stack. " + "This means that the step is not able to generate any output variables.\n " + "Please ensure the current account has permission to perform action 'cloudformation:DescribeStacks'.\n" + ex.Message + "\n" + ex); } catch (AmazonServiceException ex) { throw new Exception("An unrecognised exception was thrown while querying the CloudFormation stacks.", ex); } }
private CreateChangeSetRequest CreateChangesetRequest(StackStatus status, string changesetName, StackArn stack, string roleArn, CloudFormationTemplate template, List <string> capabilities) { return(new CreateChangeSetRequest { StackName = stack.Value, TemplateBody = template.Content, Parameters = template.Inputs.ToList(), ChangeSetName = changesetName, ChangeSetType = status == StackStatus.DoesNotExist ? ChangeSetType.CREATE : ChangeSetType.UPDATE, Capabilities = capabilities, RoleARN = roleArn }); }
private Maybe <RunningChangeSet> ExecuteChangeset(Func <IAmazonCloudFormation> factory, StackArn stack, ChangeSetArn changeSet) { try { var changes = factory.WaitForChangeSetCompletion(CloudFormationDefaults.StatusWaitPeriod, new RunningChangeSet(stack, changeSet)); if (changes.Status == ChangeSetStatus.FAILED && string.Compare(changes.StatusReason, "No updates are to be performed.", StringComparison.InvariantCultureIgnoreCase) == 0) { //We don't need the failed changeset to hang around if there are no changes factory().DeleteChangeSet(new DeleteChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }); return(Maybe <RunningChangeSet> .None); } if (changes.Status == ChangeSetStatus.FAILED) { throw new UnknownException($"AWS-CLOUDFORMATION-ERROR-0019: The changeset failed to create.\n{changes.StatusReason}"); } factory().ExecuteChangeSet(new ExecuteChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }); return(new RunningChangeSet(stack, changeSet).AsSome()); } catch (AmazonCloudFormationException exception) { if (exception.ErrorCode == "AccessDenied") { throw new PermissionException( "AWS-CLOUDFORMATION-ERROR-0011: The AWS account used to perform the operation does not have " + "the required permissions to update the stack.\n" + exception.Message + "\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0011"); } throw new UnknownException( "AWS-CLOUDFORMATION-ERROR-0011: An unrecognised exception was thrown while updating a CloudFormation stack.\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0011", exception); } catch (AmazonServiceException exception) { HandleAmazonServiceException(exception); throw; } }
private async Task <RunningChangeSet> ExecuteChangeset(Func <IAmazonCloudFormation> factory, StackArn stack, ChangeSetArn changeSet) { try { var changes = await factory.WaitForChangeSetCompletion(CloudFormationDefaults.StatusWaitPeriod, new RunningChangeSet(stack, changeSet)); if (changes.Status == ChangeSetStatus.FAILED) { throw new UnknownException($"The changeset failed to create.\n{changes.StatusReason}"); } await factory().ExecuteChangeSetAsync(new ExecuteChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }); return(new RunningChangeSet(stack, changeSet)); } catch (AmazonCloudFormationException exception) when(exception.ErrorCode == "AccessDenied") { throw new PermissionException( "The AWS account used to perform the operation does not have the required permission to execute the changeset.\n" + "Please ensure the current account has permission to perfrom action 'cloudformation:ExecuteChangeSet'.\n" + exception.Message + "\n"); } catch (AmazonServiceException exception) { LogAmazonServiceException(exception); throw; } }
/// <summary> /// Check to see if the stack name exists. /// </summary> /// <param name="defaultValue">The return value when the user does not have the permissions to query the stacks</param> /// <returns>The current status of the stack</returns> async Task <StackStatus> StackExists(StackArn stack, StackStatus defaultValue) { return(await WithAmazonServiceExceptionHandling(async() => await clientFactory.StackExistsAsync(stack, defaultValue))); }
/// <summary> /// Creates a handler which will log stack events and throw on common rollback events /// </summary> /// <param name="clientFactory">The client factory</param> /// <param name="stack">The stack to query</param> /// <param name="filter">The filter for stack events</param> /// <param name="expectSuccess">Whether we expected a success</param> /// <param name="missingIsFailure"></param> /// <returns>Stack event handler</returns> protected Action <Maybe <StackEvent> > LogAndThrowRollbacks(Func <IAmazonCloudFormation> clientFactory, StackArn stack, bool expectSuccess = true, bool missingIsFailure = true, Func <StackEvent, bool> filter = null) { return(@event => { try { Logger.Log(@event); Logger.LogRollbackError(@event, x => WithAmazonServiceExceptionHandling(() => clientFactory.GetLastStackEvent(stack, x)), expectSuccess, missingIsFailure); } catch (PermissionException exception) { Log.Warn(exception.Message); } }); }
/// <summary> /// Check to see if the stack name exists. /// </summary> /// <param name="defaultValue">The return value when the user does not have the permissions to query the stacks</param> /// <returns>The current status of the stack</returns> private Task <StackStatus> StackExists(StackArn stack, StackStatus defaultValue) { return(WithAmazonServiceExceptionHandling(() => clientFactory.StackExistsAsync(stack, defaultValue))); }
/// <summary> /// Gets the last stack event by timestamp, optionally filtered by a predicate /// </summary> /// <param name="predicate">The optional predicate used to filter events</param> /// <returns>The stack event</returns> private Task <Maybe <StackEvent> > StackEvent(StackArn stack, Func <StackEvent, bool> predicate = null) { return(WithAmazonServiceExceptionHandling( async() => await clientFactory.GetLastStackEvent(stack, predicate))); }
private void DeleteCloudFormation(StackArn stack) { Guard.NotNull(stack, "Stack must not be null"); WithAmazonServiceExceptionHandling(() => clientFactory.DeleteStack(stack)); }