/// <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> public static async Task <Maybe <StackEvent> > GetLastStackEvent(this Func <IAmazonCloudFormation> clientFactory, StackArn stack, Func <StackEvent, bool> predicate = null) { try { var response = await clientFactory().DescribeStackEventsAsync(new DescribeStackEventsRequest { StackName = stack.Value }); return(response? .StackEvents.OrderByDescending(stackEvent => stackEvent.Timestamp) .FirstOrDefault(stackEvent => predicate == null || predicate(stackEvent)) .AsSome()); } 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 query the current state of the CloudFormation stack. " + "This step will complete without waiting for the stack to complete, and will not fail if the stack finishes in an error state.\n " + "Please ensure the current account has permission to perform action 'cloudformation:DescribeStackEvents'" + ex.Message); } catch (AmazonCloudFormationException) { return(Maybe <StackEvent> .None); } }
/// <summary> /// Wait for a given stack to complete by polling the stack events /// </summary> /// <param name="clientFactory">The client factory method to use</param> /// <param name="waitPeriod">The period to wait between events</param> /// <param name="stack">The stack name or id to query</param> /// param name="action">Callback for each event while waiting /// <param name="filter">The predicate for filtering the stack events</param> public static async Task WaitForStackToComplete(this Func <IAmazonCloudFormation> clientFactory, TimeSpan waitPeriod, StackArn stack, Action <Maybe <StackEvent> > action = null, Func <StackEvent, bool> filter = null) { Guard.NotNull(stack, "Stack should not be null"); Guard.NotNull(clientFactory, "Client factory should not be null"); var status = await clientFactory.StackExistsAsync(stack, StackStatus.DoesNotExist); if (status == StackStatus.DoesNotExist || status == StackStatus.Completed) { return; } do { await Task.Delay(waitPeriod); var @event = await clientFactory.GetLastStackEvent(stack, filter); action?.Invoke(@event); } while (await clientFactory.StackExistsAsync(stack, StackStatus.Completed) == StackStatus.InProgress); /* * The action here logs the event and throws an exception if the event indicates failure. There is a possibility * that between the calls to "clientFactory.GetLastStackEvent" and "clientFactory.StackExistsAsync" above * the stack completed but entered an error state that is not detected. Getting the last event after the stack is * in a completed state ensures we catch any errors. */ var lastStackEvent = await clientFactory.GetLastStackEvent(stack, filter); action?.Invoke(lastStackEvent); }
public static Task <DescribeChangeSetResponse> DescribeChangeSetAsync(this Func <IAmazonCloudFormation> factory, StackArn stack, ChangeSetArn changeSet) { return(factory().DescribeChangeSetAsync(new DescribeChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value })); }
public static DescribeChangeSetResponse DescribeChangeSet(this Func <IAmazonCloudFormation> factory, StackArn stack, ChangeSetArn changeSet) { return(factory().Map(client => client.DescribeChangeSet(new DescribeChangeSetRequest { ChangeSetName = changeSet.Value, StackName = stack.Value }))); }
/// <summary> /// Wait for a given stack to complete by polling the stack events /// </summary> /// <param name="clientFactory">The client factory method to use</param> /// <param name="waitPeriod">The period to wait between events</param> /// <param name="stack">The stack name or id to query</param> /// param name="action">Callback for each event while waiting /// <param name="filter">The predicate for filtering the stack events</param> public static void WaitForStackToComplete(this Func <IAmazonCloudFormation> clientFactory, TimeSpan waitPeriod, StackArn stack, Action <Maybe <StackEvent> > action = null, Func <StackEvent, bool> filter = null) { Guard.NotNull(stack, "Stack should not be null"); Guard.NotNull(clientFactory, "Client factory should not be null"); var status = clientFactory.StackExists(stack, StackStatus.DoesNotExist); if (status == StackStatus.DoesNotExist || status == StackStatus.Completed) { return; } do { Thread.Sleep(waitPeriod); var @event = clientFactory.GetLastStackEvent(stack, filter); action?.Invoke(@event); } while (clientFactory.StackExists(stack, StackStatus.Completed) == StackStatus.InProgress); }
public static Task <DeleteStackResponse> DeleteStackAsync(this Func <IAmazonCloudFormation> clientFactory, StackArn stack) { Guard.NotNull(clientFactory, "clientFactory should not be null"); Guard.NotNull(stack, "Stack should not be null"); try { return(clientFactory().DeleteStackAsync(new DeleteStackRequest { StackName = stack.Value })); } 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 delete the stack.\n" + "Please ensure the current account has permission to perform action 'cloudformation:DeleteStack'.\n" + ex.Message ); } }
/// <summary> /// Check to see if the stack name exists. /// </summary> /// <param name="clientFactory">The client factory method</param> /// <param name="stackArn">The stack name or id</param> /// <param name="defaultValue">The default value to return given no permission to query the stack</param> /// <returns>The current status of the stack</returns> public static async Task <StackStatus> StackExistsAsync(this Func <IAmazonCloudFormation> clientFactory, StackArn stackArn, StackStatus defaultValue) { try { var result = await clientFactory.DescribeStackAsync(stackArn); if (result == null) { return(StackStatus.DoesNotExist); } if (result.StackStatus == null || result.StackStatus.Value.EndsWith("_COMPLETE") || result.StackStatus.Value.EndsWith("_FAILED")) { return(StackStatus.Completed); } return(StackStatus.InProgress); } catch (AmazonCloudFormationException ex) when(ex.ErrorCode == "AccessDenied") { return(defaultValue); } catch (AmazonCloudFormationException ex) when(ex.ErrorCode == "ValidationError") { return(StackStatus.DoesNotExist); } }
/// <summary> /// Describe the stack /// </summary> /// <param name="clientFactory"></param> /// <param name="stack"></param> /// <returns></returns> public static async Task <Stack> DescribeStackAsync(this Func <IAmazonCloudFormation> clientFactory, StackArn stack) { var response = await clientFactory().DescribeStacksAsync(new DescribeStacksRequest { StackName = stack.Value }); return(response.Stacks.FirstOrDefault()); }
public RunningChangeSet(StackArn stack, ChangeSetArn changeSet) { Stack = stack; ChangeSet = changeSet; }
public static DeleteStackResponse DeleteStack(this Func <IAmazonCloudFormation> clientFactory, StackArn stack) { Guard.NotNull(clientFactory, "clientFactory should not be null"); Guard.NotNull(stack, "Stack should not be null"); try { return(clientFactory().Map(client => client.DeleteStack(new DeleteStackRequest { StackName = stack.Value }))); } catch (AmazonCloudFormationException ex) { if (ex.ErrorCode == "AccessDenied") { throw new PermissionException( "AWS-CLOUDFORMATION-ERROR-0009: The AWS account used to perform the operation does not have " + "the required permissions to delete the stack.\n" + ex.Message + "\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0009"); } throw new UnknownException( "AWS-CLOUDFORMATION-ERROR-0010: An unrecognised exception was thrown while deleting a CloudFormation stack.\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0010", ex); } }
/// <summary> /// Check to see if the stack name exists. /// </summary> /// <param name="clientFactory">The client factory method</param> /// <param name="stackArn">The stack name or id</param> /// <param name="defaultValue">The default value to return given no permission to query the stack</param> /// <returns>The current status of the stack</returns> public static StackStatus StackExists(this Func <IAmazonCloudFormation> clientFactory, StackArn stackArn, StackStatus defaultValue) { try { return(clientFactory.DescribeStack(stackArn) // Does the status indicate that processing has finished? ?.Map(stack => (stack.StackStatus?.Value.EndsWith("_COMPLETE") ?? true) || stack.StackStatus.Value.EndsWith("_FAILED")) .Map(completed => completed ? StackStatus.Completed : StackStatus.InProgress) ?? StackStatus.DoesNotExist); } catch (AmazonCloudFormationException ex) { if (ex.ErrorCode == "AccessDenied") { return(defaultValue); } // This is OK, we just return the fact that the stack does not exist. // While calling describe stacks and catching exceptions seems dirty, // this is how the stack-exists command on the CLI works: // https://docs.aws.amazon.com/cli/latest/reference/cloudformation/wait/stack-exists.html if (ex.ErrorCode == "ValidationError") { return(StackStatus.DoesNotExist); } throw new UnknownException( "AWS-CLOUDFORMATION-ERROR-0006: An unrecognised exception was thrown while checking to see if the CloudFormation stack exists.\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0006", ex); } }
/// <summary> /// Describe the stack /// </summary> /// <param name="clientFactory"></param> /// <param name="stack"></param> /// <returns></returns> public static Stack DescribeStack(this Func <IAmazonCloudFormation> clientFactory, StackArn stack) { return(clientFactory() // The client becomes the result of the API call .Map(client => client.DescribeStacks(new DescribeStacksRequest { StackName = stack.Value })) // Get the first stack .Map(response => response.Stacks.FirstOrDefault())); }
/// <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> public static Maybe <StackEvent> GetLastStackEvent(this Func <IAmazonCloudFormation> clientFactory, StackArn stack, Func <StackEvent, bool> predicate = null) { try { return(clientFactory() .DescribeStackEvents(new DescribeStackEventsRequest { StackName = stack.Value }) .Map(response => response?.StackEvents .OrderByDescending(stackEvent => stackEvent.Timestamp) .FirstOrDefault(stackEvent => predicate == null || predicate(stackEvent)) ).AsSome()); } catch (AmazonCloudFormationException ex) { if (ex.ErrorCode == "AccessDenied") { throw new PermissionException( "AWS-CLOUDFORMATION-ERROR-0002: The AWS account used to perform the operation does not have " + "the required permissions to query the current state of the CloudFormation stack. " + "This step will complete without waiting for the stack to complete, and will not fail if the " + "stack finishes in an error state.\n" + ex.Message + "\n" + "For more information visit https://g.octopushq.com/AwsCloudFormationDeploy#aws-cloudformation-error-0002"); } return(Maybe <StackEvent> .None); } }
/// <summary> /// Gets all stack events, optionally filtered by a predicate /// </summary> /// <param name="predicate">The optional predicate used to filter events</param> /// <returns>The stack events</returns> public static async Task <List <Maybe <StackEvent> > > GetStackEvents(this Func <IAmazonCloudFormation> clientFactory, StackArn stack, Func <StackEvent, bool> predicate = null) { try { var currentStackEvents = new List <StackEvent>(); var nextToken = (string)null; while (true) { var response = await clientFactory().DescribeStackEventsAsync(new DescribeStackEventsRequest { StackName = stack.Value, NextToken = nextToken }); var stackEvents = response? .StackEvents.Where(stackEvent => predicate == null || predicate(stackEvent)) .ToList(); currentStackEvents.AddRange(stackEvents); if (!string.IsNullOrEmpty(response.NextToken)) { nextToken = response.NextToken; // Get the next page of results } else { break; } } var results = new List <Maybe <StackEvent> >(); var nestedStackIds = currentStackEvents .Where(s => s.ResourceType == "AWS::CloudFormation::Stack" && !string.IsNullOrEmpty(s.PhysicalResourceId) && s.PhysicalResourceId != s.StackId) .Select(s => s.PhysicalResourceId) .Distinct() .ToList(); foreach (var nestedStackId in nestedStackIds) { var nestedStackEvents = await GetStackEvents(clientFactory, new StackArn(nestedStackId), predicate); if (nestedStackEvents.Any()) { results.AddRange(nestedStackEvents); } } results.AddRange(currentStackEvents.Select(s => s.AsSome())); return(results.OrderBy(s => s.SelectValueOr(e => e.Timestamp, DateTime.MinValue)).ToList()); } 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 query the current state of the CloudFormation stack. " + "This step will complete without waiting for the stack to complete, and will not fail if the stack finishes in an error state. " + "Please ensure the current account has permission to perform action 'cloudformation:DescribeStackEvents'" + ex.Message); } catch (AmazonCloudFormationException ex) when(ex.ErrorCode == "ExpiredToken") { throw new PermissionException(ex.Message + ". Please increase the session duration and/or check that the system date and time are set correctly." + ex.Message); } catch (AmazonCloudFormationException) { return(new List <Maybe <StackEvent> > { Maybe <StackEvent> .None }); } }