package tw.com;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.cli.MissingArgumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.services.cloudformation.model.CreateStackRequest;
import com.amazonaws.services.cloudformation.model.StackStatus;
import com.amazonaws.services.cloudformation.model.UpdateStackRequest;
import tw.com.entity.DeletionPending;
import tw.com.entity.DeletionsPending;
import tw.com.entity.StackNameAndId;
import tw.com.entity.StackNotification;
import tw.com.exceptions.CfnAssistException;
import tw.com.exceptions.NotReadyException;
import tw.com.exceptions.WrongNumberOfStacksException;
import tw.com.exceptions.WrongStackStatus;
import tw.com.repository.CheckStackExists;
public class SNSMonitor extends StackMonitor {
private static final Logger logger = LoggerFactory.getLogger(SNSMonitor.class);
private static final int LIMIT = 50; // total delay = LIMIT * SNSEventSource.QUEUE_READ_TIMEOUT_SECS
private static final String STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
private List<String> deleteAborts = Arrays.asList(DELETE_ABORTS);
private NotificationProvider notifProvider;
private CheckStackExists checkStackExists;
public SNSMonitor(NotificationProvider eventSource, CheckStackExists checkStackExists) {
this.notifProvider = eventSource;
this.checkStackExists = checkStackExists;
}
@Override
public String waitForCreateFinished(StackNameAndId stackId)
throws WrongNumberOfStacksException, InterruptedException,
NotReadyException, WrongStackStatus {
guardForInit();
return waitForStatus(stackId, StackStatus.CREATE_COMPLETE.toString(), Arrays.asList(CREATE_ABORTS));
}
@Override
public String waitForDeleteFinished(StackNameAndId stackId)
throws WrongNumberOfStacksException, InterruptedException, NotReadyException, WrongStackStatus {
guardForInit();
if (!checkStackExists.stackExists(stackId.getStackName())) {
return StackStatus.DELETE_COMPLETE.toString(); // assume already gone
}
return waitForStatus(stackId, StackStatus.DELETE_COMPLETE.toString(), deleteAborts);
}
@Override
public String waitForUpdateFinished(StackNameAndId stackId) throws WrongNumberOfStacksException, InterruptedException,
WrongStackStatus, NotReadyException {
guardForInit();
return waitForStatus(stackId, StackStatus.UPDATE_COMPLETE.toString(), Arrays.asList(UPDATE_ABORTS));
}
@Override
public String waitForRollbackComplete(StackNameAndId id) throws NotReadyException, InterruptedException, WrongStackStatus {
guardForInit();
return waitForStatus(id, StackStatus.ROLLBACK_COMPLETE.toString(), Arrays.asList(ROLLBACK_ABORTS));
}
@Override
public List<String> waitForDeleteFinished(DeletionsPending pending, SetsDeltaIndex setsDeltaIndex) throws CfnAssistException {
guardForInit();
logger.info("Waiting for delete notifications");
for(DeletionPending item : pending) {
if (!checkStackExists.stackExists(item.getStackId().getStackName())) {
logger.warn(String.format("Stack %s does not exist, assume already deleted", item.getStackId()));
pending.markIdAsDeleted(item.getStackId().getStackId());
}
}
int retryCount = 0;
while ((retryCount<LIMIT) && (pending.hasMore())) {
List<StackNotification> notifications = notifProvider.receiveNotifications();
if (notifications.size()==0) {
logger.info("No messages received within timeout, increment try counter");
retryCount++;
} else {
retryCount = 0; // reset retries
processNotificationsWithPendingDeletions(pending, notifications, setsDeltaIndex);
notifications.clear();
}
}
pending.updateDeltaIndex(setsDeltaIndex);
return pending.getNamesOfDeleted();
}
private void processNotificationsWithPendingDeletions(DeletionsPending pending, List<StackNotification> notifications,
SetsDeltaIndex setsDeltaIndex) throws WrongStackStatus {
String deleteStatus = StackStatus.DELETE_COMPLETE.toString();
for(StackNotification notification : notifications) {
String resourceType = notification.getResourceType();
if (resourceType.equals(STACK_RESOURCE_TYPE)) {
String status = notification.getStatus();
String stackName = notification.getStackName();
String stackId = notification.getStackId();
if (status.equals(deleteStatus)) {
logger.info(String.format("Delete complete for stack name %s and id %s", stackName, stackId));
pending.markIdAsDeleted(stackId);
} else if (deleteAborts.contains(status)) {
logger.error(String.format("Detected delete has failed for stackid %s name %s status was %s", stackId, stackName, status));
throw new WrongStackStatus(new StackNameAndId(stackName, stackId), deleteStatus, status);
}
else {
logger.info(String.format("Delete not yet complete for stack %s status is %s",stackName, notification.getStatus()));
}
} else {
logger.info(String.format("Got notification for resource type %s, status is %s", resourceType, notification.getStatus()));
}
}
}
private String waitForStatus(StackNameAndId stackId, String requiredStatus, List<String> aborts) throws InterruptedException, WrongStackStatus, NotReadyException {
logger.info(String.format("Waiting for stack %s to change to status %s", stackId, requiredStatus));
int retryCount = 0;
while (retryCount<LIMIT) {
List<StackNotification> notifications = notifProvider.receiveNotifications(); // blocks and then times out if no messages received
if (notifications.size()==0) {
logger.info("No notifications received within timeout, increment try counter");
retryCount++;
} else {
retryCount = 0;
String status = processNotification(stackId, requiredStatus, aborts,notifications);
if (!status.isEmpty()) {
return status;
}
notifications.clear();
}
}
logger.error("Timed out waiting for status to change");
throw new WrongStackStatus(stackId, requiredStatus, "timed out");
}
private String processNotification(StackNameAndId stackId, String requiredStatus, List<String> aborts, List<StackNotification> notifications)
throws WrongStackStatus {
for(StackNotification notification : notifications) {
if (isMatchingStackNotif(notification, stackId)) {
String status = notification.getStatus();
if (status.equals(requiredStatus)) {
return status;
}
if (aborts.contains(status)) {
logger.error(String.format("Got an failure status %s while waiting for status %s", status, requiredStatus));
throw new WrongStackStatus(stackId, requiredStatus, status);
}
}
}
return "";
}
private void guardForInit() throws NotReadyException {
if (!notifProvider.isInit()) {
logger.error("Not initialised");
throw new NotReadyException("SNSMonitor not initialised");
}
}
private boolean isMatchingStackNotif(StackNotification notification, StackNameAndId stackId) {
if (notification.getStackId().equals(stackId.getStackId())) {
logger.info(String.format("Received notification for %s status was %s", notification.getResourceType(), notification.getStatus()));
if (notification.getStatus().equals(StackStatus.CREATE_FAILED.toString())) {
logger.warn(String.format("Failed to create resource of type %s reason was %s",notification.getResourceType(),notification.getStatusReason()));
}
return notification.getResourceType().equals(STACK_RESOURCE_TYPE);
}
logger.info(String.format("Notification did not match stackId, expected: %s was: %s", stackId.getStackId(), notification.getStackId()));
return false;
}
@Override
public void init() throws MissingArgumentException, CfnAssistException, InterruptedException {
notifProvider.init();
}
@Override
public void addMonitoringTo(CreateStackRequest createStackRequest) throws NotReadyException {
Collection<String> arns = getArns();
createStackRequest.setNotificationARNs(arns);
}
@Override
public void addMonitoringTo(UpdateStackRequest updateStackRequest) throws NotReadyException {
Collection<String> arns = getArns();
updateStackRequest.setNotificationARNs(arns);
}
private Collection<String> getArns() throws NotReadyException {
String arn = notifProvider.getSNSArn();
logger.info("Setting arn for sns events to " + arn);
Collection<String> arns = new LinkedList<String>();
arns.add(arn);
return arns;
}
}