/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.cloudformation.resources.standard.actions;
import com.amazonaws.services.simpleworkflow.flow.core.Promise;
import com.eucalyptus.cloudformation.CloudFormation;
import com.eucalyptus.cloudformation.CloudFormationService;
import com.eucalyptus.cloudformation.CreateStackResponseType;
import com.eucalyptus.cloudformation.CreateStackType;
import com.eucalyptus.cloudformation.DeleteStackResponseType;
import com.eucalyptus.cloudformation.DeleteStackType;
import com.eucalyptus.cloudformation.DescribeStacksResponseType;
import com.eucalyptus.cloudformation.DescribeStacksType;
import com.eucalyptus.cloudformation.Output;
import com.eucalyptus.cloudformation.Outputs;
import com.eucalyptus.cloudformation.Parameter;
import com.eucalyptus.cloudformation.Parameters;
import com.eucalyptus.cloudformation.ResourceList;
import com.eucalyptus.cloudformation.Tag;
import com.eucalyptus.cloudformation.Tags;
import com.eucalyptus.cloudformation.UpdateStackResponseType;
import com.eucalyptus.cloudformation.UpdateStackType;
import com.eucalyptus.cloudformation.ValidationErrorException;
import com.eucalyptus.cloudformation.entity.StackEntityHelper;
import com.eucalyptus.cloudformation.entity.StackUpdateInfoEntityManager;
import com.eucalyptus.cloudformation.entity.StacksWithNoUpdateToPerformEntityManager;
import com.eucalyptus.cloudformation.entity.Status;
import com.eucalyptus.cloudformation.resources.ResourceAction;
import com.eucalyptus.cloudformation.resources.ResourceInfo;
import com.eucalyptus.cloudformation.resources.ResourceProperties;
import com.eucalyptus.cloudformation.resources.standard.info.AWSCloudFormationStackResourceInfo;
import com.eucalyptus.cloudformation.resources.standard.propertytypes.AWSCloudFormationStackProperties;
import com.eucalyptus.cloudformation.resources.standard.propertytypes.CloudFormationResourceTag;
import com.eucalyptus.cloudformation.template.JsonHelper;
import com.eucalyptus.cloudformation.util.MessageHelper;
import com.eucalyptus.cloudformation.workflow.ResourceFailureException;
import com.eucalyptus.cloudformation.workflow.RetryAfterConditionCheckFailedException;
import com.eucalyptus.cloudformation.workflow.StackActivityClient;
import com.eucalyptus.cloudformation.workflow.UpdateStackPartsWorkflowKickOff;
import com.eucalyptus.cloudformation.workflow.steps.Step;
import com.eucalyptus.cloudformation.workflow.steps.StepBasedResourceAction;
import com.eucalyptus.cloudformation.workflow.steps.UpdateCleanupUpdateMultiStepPromise;
import com.eucalyptus.cloudformation.workflow.steps.UpdateRollbackCleanupUpdateMultiStepPromise;
import com.eucalyptus.cloudformation.workflow.steps.UpdateStep;
import com.eucalyptus.cloudformation.workflow.updateinfo.UpdateType;
import com.eucalyptus.cloudformation.workflow.updateinfo.UpdateTypeAndDirection;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.Topology;
import com.eucalyptus.util.async.AsyncExceptions;
import com.eucalyptus.util.async.AsyncRequests;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.netflix.glisten.WorkflowOperations;
import org.apache.log4j.Logger;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
/**
* Created by ethomas on 2/3/14.
*/
public class AWSCloudFormationStackResourceAction extends StepBasedResourceAction {
private static final Logger LOG = Logger.getLogger(AWSCloudFormationStackResourceAction.class);
private AWSCloudFormationStackProperties properties = new AWSCloudFormationStackProperties();
private AWSCloudFormationStackResourceInfo info = new AWSCloudFormationStackResourceInfo();
public AWSCloudFormationStackResourceAction() {
super(fromEnum(CreateSteps.class), fromEnum(DeleteSteps.class), fromUpdateEnum(UpdateNoInterruptionSteps.class), null);
setUpdateSteps(UpdateTypeAndDirection.UPDATE_ROLLBACK_NO_INTERRUPTION, fromUpdateEnum(UpdateRollbackNoInterruptionSteps.class));
clearAndPutIfNotNull(updateCleanupUpdateSteps, fromEnum(UpdateCleanupUpdateSteps.class));
clearAndPutIfNotNull(updateRollbackCleanupUpdateSteps, fromEnum(UpdateRollbackCleanupUpdateSteps.class));
}
protected Map<String, Step> updateCleanupUpdateSteps = Maps.newLinkedHashMap();
public final Step getUpdateCleanupUpdateStep(String stepId) {
return updateCleanupUpdateSteps.get(stepId);
}
protected Map<String, Step> updateRollbackCleanupUpdateSteps = Maps.newLinkedHashMap();
public final Step getUpdateRollbackCleanupUpdateStep(String stepId) {
return updateRollbackCleanupUpdateSteps.get(stepId);
}
@Override
public boolean mustCheckUpdateTypeEvenIfNoPropertiesChanged() {
return true;
}
@Override
public UpdateType getUpdateType(ResourceAction resourceAction, boolean stackTagsChanged) throws Exception {
AWSCloudFormationStackResourceAction otherAction = (AWSCloudFormationStackResourceAction) resourceAction;
// always no interruption
return UpdateType.NO_INTERRUPTION;
}
private enum CreateSteps implements Step {
CREATE_STACK {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
CreateStackType createStackType = MessageHelper.createMessage(CreateStackType.class, action.info.getEffectiveUserId());
String stackName = action.getDefaultPhysicalResourceId();
createStackType.setStackName(stackName);
if (action.properties.getTimeoutInMinutes() != null) {
createStackType.setTimeoutInMinutes(action.properties.getTimeoutInMinutes());
}
if (action.properties.getNotificationARNs() != null) {
ResourceList notificationARNs = new ResourceList();
notificationARNs.getMember().addAll(action.properties.getNotificationARNs());
createStackType.setNotificationARNs(notificationARNs);
}
if (action.properties.getTags() != null) {
Tags tags = new Tags();
for (CloudFormationResourceTag cloudFormationResourceTag: action.properties.getTags()) {
Tag tag = new Tag();
tag.setKey(cloudFormationResourceTag.getKey());
tag.setValue(cloudFormationResourceTag.getValue());
tags.getMember().add(tag);
}
ResourceList notificationARNs = new ResourceList();
notificationARNs.getMember().addAll(action.properties.getNotificationARNs());
createStackType.setTags(tags);
}
createStackType.setDisableRollback(true); // Rollback will be handled by outer stack
if (action.properties.getParameters() != null) {
Parameters parameters = new Parameters();
createStackType.setParameters(parameters);
if (!action.properties.getParameters().isObject()) {
throw new ValidationErrorException("Invalid Parameters value " + action.properties.getParameters());
}
for (String paramName : Lists.newArrayList(action.properties.getParameters().fieldNames())) {
JsonNode paramValue = action.properties.getParameters().get(paramName);
if (!paramValue.isValueNode()) {
throw new ValidationErrorException("All Parameters must have String values for nested stacks");
} else {
Parameter parameter = new Parameter();
parameter.setParameterKey(paramName);
parameter.setParameterValue(paramValue.asText());
parameters.getMember().add(parameter);
}
}
}
createStackType.setTemplateURL(action.properties.getTemplateURL());
// inherit outer stack capabilities
ResourceList capabilities = new ResourceList();
List<String> stackCapabilities = StackEntityHelper.jsonToCapabilities(action.getStackEntity().getCapabilitiesJson());
if (stackCapabilities != null) {
capabilities.getMember().addAll(stackCapabilities);
}
createStackType.setCapabilities(capabilities);
CreateStackResponseType createStackResponseType = AsyncRequests.<CreateStackType, CreateStackResponseType>sendSync(configuration, createStackType);
action.info.setPhysicalResourceId(createStackResponseType.getCreateStackResult().getStackId());
action.info.setCreatedEnoughToDelete(true);
return action;
}
},
WAIT_UNTIL_CREATE_COMPLETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, action.info.getEffectiveUserId());
describeStacksType.setStackName(action.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() != 1) {
throw new ResourceFailureException("Not exactly one stack returned for stack " + action.info.getPhysicalResourceId());
}
String status = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatus();
String statusReason = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatusReason();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + action.info.getPhysicalResourceId());
}
if (!status.startsWith("CREATE")) {
throw new ResourceFailureException("Stack " + action.info.getPhysicalResourceId() + " is no longer being created.");
}
if (status.equals(Status.CREATE_FAILED.toString())) {
throw new ResourceFailureException("Failed to create stack " + action.info.getPhysicalResourceId() + "." + statusReason);
}
if (status.equals(Status.CREATE_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being created.");
}
return action;
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
},
POPULATE_OUTPUTS {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, action.info.getEffectiveUserId());
describeStacksType.setStackName(action.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() != 1) {
throw new ResourceFailureException("Not exactly one stack returned for stack " + action.info.getPhysicalResourceId());
}
Outputs outputs = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getOutputs();
if (outputs != null && outputs.getMember() != null && !outputs.getMember().isEmpty()) {
for (Output output: outputs.getMember()) {
action.info.getOutputAttributes().put("Outputs." + output.getOutputKey(), JsonHelper.getStringFromJsonNode(new TextNode(output.getOutputValue())));
}
}
action.info.setReferenceValueJson(JsonHelper.getStringFromJsonNode(new TextNode(action.info.getPhysicalResourceId())));
return action;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
private enum DeleteSteps implements Step {
DEAL_WITH_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS_ON_DELETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
StatusAndReason statusAndReason = getStackStatusAndReason(action, configuration);
String status = statusAndReason.getStatus();
if (Status.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status) || Status.UPDATE_ROLLBACK_IN_PROGRESS.toString().equals(status)) {
action.info.getEucaAttributes().put(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS,
JsonHelper.getStringFromJsonNode(new TextNode("true")));
UpdateStackPartsWorkflowKickOff.kickOffUpdateRollbackStackWorkflow(action.info.getPhysicalResourceId(), action.info.getAccountId(),
action.getStackEntity().getStackId(), action.info.getEffectiveUserId());
} else {
action.info.getEucaAttributes().remove(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS);
}
return action;
}
},
WAIT_UNTIL_NOT_UPDATE_ROLLBACK_IN_PROGRESS {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
if (action.info.getEucaAttributes().containsKey(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS)) {
boolean deleteStatusUpdateCompleteCleanupInProgress = Boolean.valueOf(JsonHelper.getJsonNodeFromString(
action.info.getEucaAttributes().get(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS)).asText());
if (deleteStatusUpdateCompleteCleanupInProgress) {
StatusAndReason statusAndReason = getStackStatusAndReason(action, configuration);
String status = statusAndReason.getStatus();
if (Status.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status) || Status.UPDATE_ROLLBACK_IN_PROGRESS.toString().equals(status)) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being rolled back.");
}
}
action.info.getEucaAttributes().remove(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_COMPLETE_CLEANUP_IN_PROGRESS);
}
return action;
}
@Override
public Integer getTimeout() {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
},
DEAL_WITH_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS_ON_DELETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
StatusAndReason statusAndReason = getStackStatusAndReason(action, configuration);
String status = statusAndReason.getStatus();
if (Status.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status)) {
action.info.getEucaAttributes().put(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS,
JsonHelper.getStringFromJsonNode(new TextNode("true")));
UpdateStackPartsWorkflowKickOff.kickOffUpdateRollbackCleanupStackWorkflow(action.info.getPhysicalResourceId(),
action.info.getAccountId(), action.info.getEffectiveUserId());
} else {
action.info.getEucaAttributes().remove(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS);
}
return action;
}
},
WAIT_UNTIL_NOT_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
if (action.info.getEucaAttributes().containsKey(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS)) {
boolean deleteStatusUpdateCompleteCleanupInProgress = Boolean.valueOf(JsonHelper.getJsonNodeFromString(
action.info.getEucaAttributes().get(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS)).asText());
if (deleteStatusUpdateCompleteCleanupInProgress) {
StatusAndReason statusAndReason = getStackStatusAndReason(action, configuration);
String status = statusAndReason.getStatus();
if (Status.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status)) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being rolled back clean up.");
}
}
action.info.getEucaAttributes().remove(AWSCloudFormationStackResourceInfo.EUCA_DELETE_STATUS_UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS);
}
return action;
}
@Override
public Integer getTimeout() {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
},
DELETE_STACK {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
DeleteStackType deleteStackType = MessageHelper.createMessage(DeleteStackType.class, action.info.getEffectiveUserId());
deleteStackType.setStackName(action.info.getPhysicalResourceId()); // actually stack id
AsyncRequests.<DeleteStackType, DeleteStackResponseType>sendSync(configuration, deleteStackType);
return action;
}
},
WAIT_UNTIL_DELETE_COMPLETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (alreadyDeletedOrNeverCreated(action, configuration)) return action;
// First see if stack exists or has been deleted
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, action.info.getEffectiveUserId());
describeStacksType.setStackName(action.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().isEmpty()) {
return action;
}
if (describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() > 1) {
throw new ResourceFailureException("More than one stack returned for stack " + action.info.getPhysicalResourceId());
}
String status = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatus();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + action.info.getPhysicalResourceId());
}
if (status.equals(Status.DELETE_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being deleted.");
}
// TODO: consider this logic
if (status.endsWith("IN_PROGRESS")) {
throw new ResourceFailureException("Stack " + action.info.getPhysicalResourceId() + " is in the middle of " + status + ", not deleting");
}
if (status.equals(Status.DELETE_COMPLETE.toString())) {
return action;
}
if (status.equals(Status.DELETE_FAILED.toString())) {
throw new ResourceFailureException("Deleting stack " + action.info.getPhysicalResourceId() + " failed");
}
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " current status is " + status + ", maybe not yet started deleting?");
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
private static boolean alreadyDeletedOrNeverCreated(AWSCloudFormationStackResourceAction action, ServiceConfiguration configuration) throws Exception {
if (!Boolean.TRUE.equals(action.info.getCreatedEnoughToDelete())) return true;
// First see if stack exists or has been deleted
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, action.info.getEffectiveUserId());
describeStacksType.setStackName(action.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().isEmpty()) {
return true;
}
if (describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() > 1) {
throw new ResourceFailureException("More than one stack returned for stack " + action.info.getPhysicalResourceId());
}
String status = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatus();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + action.info.getPhysicalResourceId());
}
if (status.equals(Status.DELETE_COMPLETE.toString())) {
return true;
}
return false;
}
}
private enum UpdateNoInterruptionSteps implements UpdateStep {
UPDATE_STACK {
@Override
public ResourceAction perform(ResourceAction oldResourceAction, ResourceAction newResourceAction) throws Exception {
AWSCloudFormationStackResourceAction newAction = (AWSCloudFormationStackResourceAction) newResourceAction;
AWSCloudFormationStackResourceAction oldAction = (AWSCloudFormationStackResourceAction) oldResourceAction;
StacksWithNoUpdateToPerformEntityManager.deleteStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId());
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
UpdateStackType updateStackType = MessageHelper.createMessage(UpdateStackType.class, newAction.info.getEffectiveUserId());
String stackName = newAction.info.getPhysicalResourceId();
updateStackType.setStackName(stackName);
if (newAction.properties.getNotificationARNs() != null) {
ResourceList notificationARNs = new ResourceList();
notificationARNs.getMember().addAll(newAction.properties.getNotificationARNs());
updateStackType.setNotificationARNs(notificationARNs);
}
if (newAction.properties.getTags() != null) {
Tags tags = new Tags();
for (CloudFormationResourceTag cloudFormationResourceTag: newAction.properties.getTags()) {
Tag tag = new Tag();
tag.setKey(cloudFormationResourceTag.getKey());
tag.setValue(cloudFormationResourceTag.getValue());
tags.getMember().add(tag);
}
ResourceList notificationARNs = new ResourceList();
notificationARNs.getMember().addAll(newAction.properties.getNotificationARNs());
updateStackType.setTags(tags);
}
if (newAction.properties.getParameters() != null) {
Parameters parameters = new Parameters();
updateStackType.setParameters(parameters);
if (!newAction.properties.getParameters().isObject()) {
throw new ValidationErrorException("Invalid Parameters value " + newAction.properties.getParameters());
}
for (String paramName : Lists.newArrayList(newAction.properties.getParameters().fieldNames())) {
JsonNode paramValue = newAction.properties.getParameters().get(paramName);
if (!paramValue.isValueNode()) {
throw new ValidationErrorException("All Parameters must have String values for nested stacks");
} else {
Parameter parameter = new Parameter();
parameter.setParameterKey(paramName);
parameter.setParameterValue(paramValue.asText());
parameters.getMember().add(parameter);
}
}
}
updateStackType.setTemplateURL(newAction.properties.getTemplateURL());
// inherit outer stack capabilities
ResourceList capabilities = new ResourceList();
List<String> stackCapabilities = StackEntityHelper.jsonToCapabilities(newAction.getStackEntity().getCapabilitiesJson());
if (stackCapabilities != null) {
capabilities.getMember().addAll(stackCapabilities);
}
updateStackType.setCapabilities(capabilities);
try {
final UpdateStackResponseType updateStackResponseType = AsyncRequests.<UpdateStackType, UpdateStackResponseType>sendSync(configuration, updateStackType);
newAction.info.setPhysicalResourceId(updateStackResponseType.getUpdateStackResult().getStackId());
newAction.info.setCreatedEnoughToDelete(true);
newAction.info.setReferenceValueJson( JsonHelper.getStringFromJsonNode(new TextNode(newAction.info.getPhysicalResourceId())) );
} catch (final Exception e) {
final Optional<AsyncExceptions.AsyncWebServiceError> error = AsyncExceptions.asWebServiceError(e);
if (error.isPresent() && Strings.nullToEmpty(error.get().getMessage()).equals(CloudFormationService.NO_UPDATES_ARE_TO_BE_PERFORMED)) {
StacksWithNoUpdateToPerformEntityManager.addStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId());
} else {
throw e;
}
}
return newAction;
}
},
WAIT_UNTIL_UPDATE_COMPLETE {
@Override
public ResourceAction perform(ResourceAction oldResourceAction, ResourceAction newResourceAction) throws Exception {
AWSCloudFormationStackResourceAction newAction = (AWSCloudFormationStackResourceAction) newResourceAction;
AWSCloudFormationStackResourceAction oldAction = (AWSCloudFormationStackResourceAction) oldResourceAction;
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, newAction.info.getEffectiveUserId());
describeStacksType.setStackName(newAction.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() != 1) {
throw new ResourceFailureException("Not exactly one stack returned for stack " + newAction.info.getPhysicalResourceId());
}
String status = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatus();
String statusReason = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatusReason();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + newAction.info.getPhysicalResourceId());
}
if (!status.startsWith("UPDATE")) {
throw new ResourceFailureException("Stack " + newAction.info.getPhysicalResourceId() + " is no longer being updated.");
}
if (status.startsWith("UPDATE_ROLLBACK") || status.startsWith("UPDATE_FAILED")) {
throw new ResourceFailureException("Failed to update stack " + newAction.info.getPhysicalResourceId() + "." + statusReason);
}
if (status.equals(Status.UPDATE_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + newAction.info.getPhysicalResourceId() + " is still being updated.");
}
return newAction;
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
},
POPULATE_OUTPUTS {
@Override
public ResourceAction perform(ResourceAction oldResourceAction, ResourceAction newResourceAction) throws Exception {
AWSCloudFormationStackResourceAction newAction = (AWSCloudFormationStackResourceAction) newResourceAction;
AWSCloudFormationStackResourceAction oldAction = (AWSCloudFormationStackResourceAction) oldResourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, newAction.info.getEffectiveUserId());
describeStacksType.setStackName(newAction.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() != 1) {
throw new ResourceFailureException("Not exactly one stack returned for stack " + newAction.info.getPhysicalResourceId());
}
Outputs outputs = describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getOutputs();
if (outputs != null && outputs.getMember() != null && !outputs.getMember().isEmpty()) {
for (Output output: outputs.getMember()) {
newAction.info.getOutputAttributes().put("Outputs." + output.getOutputKey(), JsonHelper.getStringFromJsonNode(new TextNode(output.getOutputValue())));
}
}
newAction.info.setReferenceValueJson(JsonHelper.getStringFromJsonNode(new TextNode(newAction.info.getPhysicalResourceId())));
return newAction;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
private enum UpdateRollbackNoInterruptionSteps implements UpdateStep {
UPDATE_ROLLBACK_STACK {
@Override
public ResourceAction perform(ResourceAction oldResourceAction, ResourceAction newResourceAction) throws Exception {
AWSCloudFormationStackResourceAction newAction = (AWSCloudFormationStackResourceAction) newResourceAction;
AWSCloudFormationStackResourceAction oldAction = (AWSCloudFormationStackResourceAction) oldResourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
UpdateStackPartsWorkflowKickOff.kickOffUpdateRollbackStackWorkflow(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId(),
newAction.getStackEntity().getStackId(), newAction.info.getEffectiveUserId());
return newAction;
}
},
WAIT_UNTIL_UPDATE_ROLLBACK_COMPLETE {
@Override
public ResourceAction perform(ResourceAction oldResourceAction, ResourceAction newResourceAction) throws Exception {
AWSCloudFormationStackResourceAction newAction = (AWSCloudFormationStackResourceAction) newResourceAction;
AWSCloudFormationStackResourceAction oldAction = (AWSCloudFormationStackResourceAction) oldResourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(newAction.info.getPhysicalResourceId(), newAction.info.getAccountId())) {
return newAction;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
StatusAndReason statusAndReason = getStackStatusAndReason(newAction, configuration);
String status = statusAndReason.getStatus();
String statusReason = statusAndReason.getReason();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + newAction.info.getPhysicalResourceId());
}
if (!status.startsWith("UPDATE")) {
throw new ResourceFailureException("Stack " + newAction.info.getPhysicalResourceId() + " is no longer being updated.");
}
if (status.equals(Status.UPDATE_ROLLBACK_FAILED.toString())) {
throw new ResourceFailureException("Failed to update rollback " + newAction.info.getPhysicalResourceId() + "." + statusReason);
}
if (status.equals(Status.UPDATE_ROLLBACK_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + newAction.info.getPhysicalResourceId() + " is still being rolled back.");
}
return newAction;
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
private static StatusAndReason getStackStatusAndReason(AWSCloudFormationStackResourceAction action, ServiceConfiguration configuration) throws Exception {
DescribeStacksType describeStacksType = MessageHelper.createMessage(DescribeStacksType.class, action.info.getEffectiveUserId());
describeStacksType.setStackName(action.info.getPhysicalResourceId()); // actually the stack id...
DescribeStacksResponseType describeStacksResponseType = AsyncRequests.<DescribeStacksType, DescribeStacksResponseType>sendSync(configuration, describeStacksType);
if (describeStacksResponseType.getDescribeStacksResult() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember() == null ||
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().size() != 1) {
throw new ResourceFailureException("Not exactly one stack returned for stack " + action.info.getPhysicalResourceId());
}
return new StatusAndReason(describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatus(),
describeStacksResponseType.getDescribeStacksResult().getStacks().getMember().get(0).getStackStatusReason());
}
private static class StatusAndReason {
private String status;
private String reason;
private StatusAndReason(String status, String reason) {
this.status = status;
this.reason = reason;
}
public String getStatus() {
return status;
}
public String getReason() {
return reason;
}
}
private enum UpdateCleanupUpdateSteps implements Step {
UPDATE_CLEANUP_STACK {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
String status = getStackStatusAndReason(action, configuration).getStatus();
if (!Status.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status)) {
throw new ResourceFailureException("Update cleanup stack called when status is " + status + " for stack " + action.info.getPhysicalResourceId());
}
UpdateStackPartsWorkflowKickOff.kickOffUpdateCleanupStackWorkflow(action.info.getPhysicalResourceId(), action.info.getAccountId(), action.info.getEffectiveUserId());
return action;
}
},
WAIT_UNTIL_UPDATE_CLEANUP_COMPLETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
String status = getStackStatusAndReason(action, configuration).getStatus();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + action.info.getPhysicalResourceId());
}
if (!status.startsWith("UPDATE")) {
throw new ResourceFailureException("Stack " + action.info.getPhysicalResourceId() + " is no longer being updated.");
}
if (status.equals(Status.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being cleaned up.");
}
return action;
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
private enum UpdateRollbackCleanupUpdateSteps implements Step {
UPDATE_ROLLBACK_CLEANUP_STACK {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
String status = getStackStatusAndReason(action, configuration).getStatus();
if (!Status.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS.toString().equals(status)) {
throw new ResourceFailureException("Update rollback cleanup stack called when status is " + status + " for stack " + action.info.getPhysicalResourceId());
}
UpdateStackPartsWorkflowKickOff.kickOffUpdateRollbackCleanupStackWorkflow(action.info.getPhysicalResourceId(), action.info.getAccountId(), action.info.getEffectiveUserId());
return action;
}
},
WAIT_UNTIL_UPDATE_ROLLBACK_CLEANUP_COMPLETE {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationStackResourceAction action = (AWSCloudFormationStackResourceAction) resourceAction;
if (StacksWithNoUpdateToPerformEntityManager.isStackWithNoUpdateToPerform(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
if (StackUpdateInfoEntityManager.hasNoUpdateInfoRecord(action.info.getPhysicalResourceId(), action.info.getAccountId())) {
return action;
}
ServiceConfiguration configuration = Topology.lookup(CloudFormation.class);
String status = getStackStatusAndReason(action, configuration).getStatus();
if (status == null) {
throw new ResourceFailureException("Null status for stack " + action.info.getPhysicalResourceId());
}
if (!status.startsWith("UPDATE")) {
throw new ResourceFailureException("Stack " + action.info.getPhysicalResourceId() + " is no longer being updated.");
}
if (status.equals(Status.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS.toString())) {
throw new RetryAfterConditionCheckFailedException("Stack " + action.info.getPhysicalResourceId() + " is still being rolled back clean up.");
}
return action;
}
@Override
public Integer getTimeout( ) {
// Wait as long as necessary for stacks
return MAX_TIMEOUT;
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
@Override
public ResourceProperties getResourceProperties() {
return properties;
}
@Override
public void setResourceProperties(ResourceProperties resourceProperties) {
properties = (AWSCloudFormationStackProperties) resourceProperties;
}
@Override
public ResourceInfo getResourceInfo() {
return info;
}
@Override
public void setResourceInfo(ResourceInfo resourceInfo) {
info = (AWSCloudFormationStackResourceInfo) resourceInfo;
}
public Promise<String> getUpdateCleanupUpdatePromise(WorkflowOperations<StackActivityClient> workflowOperations, String resourceId, String stackId, String accountId, String effectiveUserId, int updatedResourceVersion) {
List<String> stepIds = Lists.newArrayList(updateCleanupUpdateSteps.keySet());
return new UpdateCleanupUpdateMultiStepPromise(workflowOperations, stepIds, this).getUpdateCleanupUpdatePromise(resourceId, stackId, accountId, effectiveUserId, updatedResourceVersion);
}
public Promise<String> getUpdateRollbackCleanupUpdatePromise(WorkflowOperations<StackActivityClient> workflowOperations, String resourceId, String stackId, String accountId, String effectiveUserId, int updatedResourceVersion) {
List<String> stepIds = Lists.newArrayList(updateRollbackCleanupUpdateSteps.keySet());
return new UpdateRollbackCleanupUpdateMultiStepPromise(workflowOperations, stepIds, this).getUpdateRollbackCleanupUpdatePromise(resourceId, stackId, accountId, effectiveUserId, updatedResourceVersion);
}
}