// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Amazon.CloudFormation;
using Amazon.CloudFormation.Model;
using AWS.Deploy.CLI.Extensions;
using AWS.Deploy.Common;
namespace AWS.Deploy.CLI.CloudFormation
{
///
/// Monitors a CloudFormation Stack event activities independently
/// It uses stdout and displays the current status of the CloudFormation stack by polling periodically
///
internal class StackEventMonitor
{
private const int TIMESTAMP_WIDTH = 18;
private const int RESOURCE_STATUS_WIDTH = 20;
private const int RESOURCE_TYPE_WIDTH = 40;
private const int LOGICAL_RESOURCE_WIDTH = 40;
private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5);
private readonly string _stackName;
private bool _isActive;
private DateTime _startTime;
private readonly IAmazonCloudFormation _cloudFormationClient;
private readonly HashSet _processedEventIds = new HashSet();
private readonly IConsoleUtilities _consoleUtilities;
private readonly IToolInteractiveService _interactiveService;
public StackEventMonitor(string stackName, IAWSClientFactory awsClientFactory, IConsoleUtilities consoleUtilities, IToolInteractiveService interactiveService)
{
_stackName = stackName;
_consoleUtilities = consoleUtilities;
_interactiveService = interactiveService;
_cloudFormationClient = awsClientFactory.GetAWSClient();
}
///
/// Starts monitoring the CloudFormation Stack events since now
///
public async Task StartAsync()
{
// CloudFormation API returns timestamps for events based on the system time
_startTime = DateTime.Now;
_isActive = true;
await PollEventsAsync();
}
///
/// Stops monitoring the CloudFormation Stack events
///
public void Stop()
{
_isActive = false;
}
private async Task PollEventsAsync()
{
while (_isActive)
{
await ReadNewEventsAsync();
await Task.Delay(s_pollingPeriod);
}
}
private async Task ReadNewEventsAsync()
{
var stackEvents = new List();
var describeStackEventsRequest = new DescribeStackEventsRequest { StackName = _stackName };
var listStacksPaginator = _cloudFormationClient.Paginators.DescribeStackEvents(describeStackEventsRequest);
try
{
var breakPaginator = false;
await foreach (var response in listStacksPaginator.Responses)
{
foreach (var stackEvent in response?.StackEvents ?? new List())
{
// Event from before we are interested in
if (stackEvent.Timestamp < _startTime)
{
breakPaginator = true;
break;
}
// Already processed event
if (_processedEventIds.Contains(stackEvent.EventId))
{
breakPaginator = true;
break;
}
// New event, save it
_processedEventIds.Add(stackEvent.EventId);
stackEvents.Add(stackEvent);
}
if (breakPaginator)
{
break;
}
}
}
catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack [{_stackName}] does not exist"))
{
// Stack is deleted, there could be some missed events between the last poll timestamp and DELETE_COMPLETE
_interactiveService.WriteDebugLine(exception.PrettyPrint());
}
catch (AmazonCloudFormationException exception)
{
// Other AmazonCloudFormationException
_interactiveService.WriteDebugLine(exception.PrettyPrint());
}
foreach (var stackEvent in stackEvents.OrderBy(e => e.Timestamp))
{
var row = new[]
{
(stackEvent.Timestamp.ToString(CultureInfo.InvariantCulture), TIMESTAMP_WIDTH),
(stackEvent.ResourceStatus.ToString(), RESOURCE_STATUS_WIDTH),
(stackEvent.ResourceType.Truncate(RESOURCE_TYPE_WIDTH, true), RESOURCE_TYPE_WIDTH),
(stackEvent.LogicalResourceId, LOGICAL_RESOURCE_WIDTH),
};
_consoleUtilities.DisplayRow(row);
}
}
}
}