/// <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());
        }
Exemple #9
0
 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
                });
            }
        }