diff --git a/aws-cloudformation-stackset/.rpdk-config b/aws-cloudformation-stackset/.rpdk-config
index 3ea66f5..f09ba84 100644
--- a/aws-cloudformation-stackset/.rpdk-config
+++ b/aws-cloudformation-stackset/.rpdk-config
@@ -10,6 +10,7 @@
"amazon",
"cloudformation",
"stackset"
- ]
+ ],
+ "codegen_template_path": "guided_aws"
}
}
diff --git a/aws-cloudformation-stackset/README.md b/aws-cloudformation-stackset/README.md
index 8b812e8..74ef28c 100644
--- a/aws-cloudformation-stackset/README.md
+++ b/aws-cloudformation-stackset/README.md
@@ -3,15 +3,10 @@
Congratulations on starting development! Next steps:
1. Write the JSON schema describing your resource, `aws-cloudformation-stackset.json`
-2. The RPDK will automatically generate the correct resource model from the
- schema whenever the project is built via Maven. You can also do this manually
- with the following command: `cfn generate`
-3. Implement your resource handlers
+1. Implement your resource handlers.
+The RPDK will automatically generate the correct resource model from the schema whenever the project is built via Maven. You can also do this manually with the following command: `cfn generate`.
-Please don't modify files under `target/generated-sources/rpdk`, as they will be
-automatically overwritten.
+> Please don't modify files under `target/generated-sources/rpdk`, as they will be automatically overwritten.
-The code use [Lombok](https://projectlombok.org/), and [you may have to install
-IDE integrations](https://projectlombok.org/) to enable auto-complete for
-Lombok-annotated classes.
+The code uses [Lombok](https://projectlombok.org/), and [you may have to install IDE integrations](https://projectlombok.org/) to enable auto-complete for Lombok-annotated classes.
diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json
index 06427d8..4148b8c 100644
--- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json
+++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json
@@ -74,6 +74,58 @@
}
},
"additionalProperties": false
+ },
+ "StackInstances": {
+ "description": "Stack instances in some specific accounts and Regions.",
+ "type": "object",
+ "properties": {
+ "DeploymentTargets": {
+ "description": " The AWS OrganizationalUnitIds or Accounts for which to create stack instances in the specified Regions.",
+ "type": "object",
+ "properties": {
+ "Accounts": {
+ "description": "AWS accounts that you want to create stack instances in the specified Region(s) for.",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/Account"
+ }
+ },
+ "OrganizationalUnitIds": {
+ "description": "The organization root ID or organizational unit (OU) IDs to which StackSets deploys.",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/OrganizationalUnitId"
+ }
+ }
+ }
+ },
+ "Regions": {
+ "description": "The names of one or more Regions where you want to create stack instances using the specified AWS account(s).",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/Region"
+ }
+ },
+ "ParameterOverrides": {
+ "description": "A list of stack set parameters whose values you want to override in the selected stack instances.",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/Parameter"
+ }
+ }
+ },
+ "required": [
+ "DeploymentTargets",
+ "Regions"
+ ]
}
},
"properties": {
@@ -100,30 +152,6 @@
"$ref": "#/definitions/Capability"
}
},
- "DeploymentTargets": {
- "description": "",
- "type": "object",
- "properties": {
- "Accounts" : {
- "description": "AWS accounts that you want to create stack instances in the specified Region(s) for.",
- "type": "array",
- "uniqueItems": true,
- "insertionOrder": false,
- "items": {
- "$ref": "#/definitions/Account"
- }
- },
- "OrganizationalUnitIds": {
- "description": "The organization root ID or organizational unit (OU) IDs to which StackSets deploys.",
- "type": "array",
- "uniqueItems": true,
- "insertionOrder": false,
- "items": {
- "$ref": "#/definitions/OrganizationalUnitId"
- }
- }
- }
- },
"Description": {
"description": "A description of the stack set. You can use the description to identify the stack set's purpose or other important information.",
"type": "string",
@@ -166,6 +194,15 @@
}
}
},
+ "StackInstancesGroup": {
+ "description": "",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/StackInstances"
+ }
+ },
"Parameters": {
"description": "The input parameters for the stack set template.",
"type": "array",
@@ -183,15 +220,6 @@
"SELF_MANAGED"
]
},
- "Regions": {
- "description": "The names of one or more Regions where you want to create stack instances using the specified AWS account(s).",
- "type": "array",
- "uniqueItems": true,
- "insertionOrder": false,
- "items": {
- "$ref": "#/definitions/Region"
- }
- },
"Tags": {
"description": "The key-value pairs to associate with this stack set and the stacks created from it. AWS CloudFormation also propagates these tags to supported resources that are created in the stacks. A maximum number of 50 tags can be specified.",
"type": "array",
@@ -216,8 +244,7 @@
}
},
"required": [
- "PermissionModel",
- "Regions"
+ "PermissionModel"
],
"additionalProperties": false,
"createOnlyProperties": [
diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml
index 7ed012a..cd22281 100644
--- a/aws-cloudformation-stackset/pom.xml
+++ b/aws-cloudformation-stackset/pom.xml
@@ -1,8 +1,8 @@
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0
software.amazon.cloudformation.stackset
@@ -30,7 +30,7 @@
software.amazon.awssdk
bom
- 2.11.12
+ 2.10.70
pom
import
@@ -191,6 +191,7 @@
**/Configuration*
**/util/AwsCredentialsExtractor*
+ **/util/ClientBuilder*
**/BaseConfiguration*
**/BaseHandler*
**/HandlerWrapper*
@@ -246,13 +247,5 @@
-
-
- src/test/
-
- **/resources/*
-
-
-
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/BaseHandlerStd.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/BaseHandlerStd.java
new file mode 100644
index 0000000..0d4060a
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/BaseHandlerStd.java
@@ -0,0 +1,218 @@
+package software.amazon.cloudformation.stackset;
+
+import com.google.common.annotations.VisibleForTesting;
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
+import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesRequest;
+import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesResponse;
+import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationResponse;
+import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetResponse;
+import software.amazon.awssdk.services.cloudformation.model.LimitExceededException;
+import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException;
+import software.amazon.awssdk.services.cloudformation.model.StackSet;
+import software.amazon.awssdk.services.cloudformation.model.StackSetNotEmptyException;
+import software.amazon.awssdk.services.cloudformation.model.StackSetOperationStatus;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.cloudformation.proxy.delay.MultipleOf;
+import software.amazon.cloudformation.stackset.util.ClientBuilder;
+
+import java.time.Duration;
+import java.util.function.BiFunction;
+
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackInstancesRequest;
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackInstancesRequest;
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetOperationRequest;
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetRequest;
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackInstancesRequest;
+
+/**
+ * Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers
+ */
+public abstract class BaseHandlerStd extends BaseHandler {
+
+
+ protected static final int NO_CALLBACK_DELAY = 0;
+
+ protected static final MultipleOf MULTIPLE_OF = MultipleOf.multipleOf()
+ .multiple(2)
+ .timeout(Duration.ofHours(24L))
+ .delay(Duration.ofSeconds(2L))
+ .build();
+
+ protected static final BiFunction, ResourceModel>
+ EMPTY_CALL = (model, proxyClient) -> model;
+
+ @Override
+ public final ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+
+ return handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ proxy.newProxy(ClientBuilder::getClient),
+ logger
+ );
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger);
+
+ protected boolean filterException(AwsRequest request,
+ Exception e,
+ ProxyClient client,
+ ResourceModel model,
+ CallbackContext context) {
+ return e instanceof OperationInProgressException | e instanceof StackSetNotEmptyException;
+ }
+
+ protected ProgressEvent createStackInstances(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient client,
+ final ProgressEvent progress,
+ final Logger logger) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ callbackContext.getCreateStacksList().forEach(stackInstances -> proxy
+ .initiate("AWS-CloudFormation-StackSet::CreateStackInstances", client, model, callbackContext)
+ .request(modelRequest -> createStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances))
+ .retry(MULTIPLE_OF)
+ .call((modelRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::createStackInstances))
+ .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger))
+ .exceptFilter(this::filterException)
+ .progress());
+
+ return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model);
+ }
+
+ protected ProgressEvent deleteStackInstances(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient client,
+ final ProgressEvent progress,
+ final Logger logger) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ callbackContext.getDeleteStacksList().forEach(stackInstances -> proxy
+ .initiate("AWS-CloudFormation-StackSet::DeleteStackInstances", client, model, callbackContext)
+ .request(modelRequest -> deleteStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances))
+ .retry(MULTIPLE_OF)
+ .call((modelRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::deleteStackInstances))
+ .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger))
+ .exceptFilter(this::filterException)
+ .progress());
+
+ return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model);
+ }
+
+ protected ProgressEvent updateStackInstances(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient client,
+ final ProgressEvent progress,
+ final Logger logger) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ callbackContext.getUpdateStacksList().forEach(stackInstances -> proxy
+ .initiate("AWS-CloudFormation-StackSet::UpdateStackInstances", client, model, callbackContext)
+ .request(modelRequest -> updateStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances))
+ .retry(MULTIPLE_OF)
+ .call((modelRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackInstances))
+ .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger))
+ .exceptFilter(this::filterException)
+ .progress());
+
+ return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model);
+ }
+
+ /**
+ * Get {@link StackSet} from service client using stackSetId
+ * @param stackSetId StackSet Id
+ * @return {@link StackSet}
+ */
+ protected StackSet describeStackSet(
+ final ProxyClient proxyClient,
+ final String stackSetId) {
+
+ final DescribeStackSetResponse stackSetResponse = proxyClient.injectCredentialsAndInvokeV2(
+ describeStackSetRequest(stackSetId), proxyClient.client()::describeStackSet);
+ return stackSetResponse.stackSet();
+ }
+
+ /**
+ * Checks if the operation is stabilized using OperationId to interact with
+ * {@link DescribeStackSetOperationResponse}
+ * @param model {@link ResourceModel}
+ * @param operationId OperationId from operation response
+ * @param logger Logger
+ * @return A boolean value indicates if operation is complete
+ */
+ protected boolean isOperationStabilized(final ProxyClient proxyClient,
+ final ResourceModel model,
+ final String operationId,
+ final Logger logger) {
+
+ final String stackSetId = model.getStackSetId();
+ final StackSetOperationStatus status = getStackSetOperationStatus(proxyClient, stackSetId, operationId);
+ return isStackSetOperationDone(status, operationId, logger);
+ }
+
+
+ /**
+ * Retrieves the {@link StackSetOperationStatus} from {@link DescribeStackSetOperationResponse}
+ * @param stackSetId {@link ResourceModel#getStackSetId()}
+ * @param operationId Operation ID
+ * @return {@link StackSetOperationStatus}
+ */
+ private static StackSetOperationStatus getStackSetOperationStatus(
+ final ProxyClient proxyClient,
+ final String stackSetId,
+ final String operationId) {
+
+ final DescribeStackSetOperationResponse response = proxyClient.injectCredentialsAndInvokeV2(
+ describeStackSetOperationRequest(stackSetId, operationId),
+ proxyClient.client()::describeStackSetOperation);
+ return response.stackSetOperation().status();
+ }
+
+ /**
+ * Compares {@link StackSetOperationStatus} with specific statuses
+ * @param status {@link StackSetOperationStatus}
+ * @param operationId Operation ID
+ * @return boolean
+ */
+ @VisibleForTesting
+ protected static boolean isStackSetOperationDone(
+ final StackSetOperationStatus status, final String operationId, final Logger logger) {
+
+ switch (status) {
+ case SUCCEEDED:
+ logger.log(String.format("%s has been successfully stabilized.", operationId));
+ return true;
+ case RUNNING:
+ case QUEUED:
+ return false;
+ default:
+ logger.log(String.format("StackInstanceOperation [%s] unexpected status [%s]", operationId, status));
+ throw new CfnServiceInternalErrorException(
+ String.format("Stack set operation [%s] was unexpectedly stopped or failed", operationId));
+ }
+ }
+
+}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java
index 9fa4130..1fd2336 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java
@@ -1,79 +1,23 @@
package software.amazon.cloudformation.stackset;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-import lombok.Builder;
-import lombok.Data;
-import software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations;
+import software.amazon.cloudformation.proxy.StdCallbackContext;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.LinkedList;
+import java.util.List;
-@Data
-@Builder
-@JsonDeserialize(builder = CallbackContext.CallbackContextBuilder.class)
-public class CallbackContext {
+@lombok.Getter
+@lombok.Setter
+@lombok.ToString
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext {
- // Operation Id to verify stabilization for StackSet operation.
- private String operationId;
+ // List to keep track on the complete status for creating
+ private List createStacksList = new LinkedList<>();
- // Elapsed counts of retries on specific exceptions.
- private int retries;
+ // List to keep track on stack instances for deleting
+ private List deleteStacksList = new LinkedList<>();
- // Indicates initiation of resource stabilization.
- private boolean stabilizationStarted;
+ // List to keep track on stack instances for update
+ private List updateStacksList = new LinkedList<>();
- // Indicates initiation of stack instances creation.
- private boolean addStacksByRegionsStarted;
-
- // Indicates initiation of stack instances creation.
- private boolean addStacksByTargetsStarted;
-
- // Indicates initiation of stack instances delete.
- private boolean deleteStacksByRegionsStarted;
-
- // Indicates initiation of stack instances delete.
- private boolean deleteStacksByTargetsStarted;
-
- // Indicates initiation of stack set update.
- private boolean updateStackSetStarted;
-
- // Indicates initiation of stack instances update.
- private boolean updateStackInstancesStarted;
-
- // Total running time
- @Builder.Default
- private int elapsedTime = 0;
-
- /**
- * Default as 0, will be {@link software.amazon.cloudformation.stackset.util.Stabilizer#BASE_CALLBACK_DELAY_SECONDS}
- * When it enters the first IN_PROGRESS callback
- */
- @Builder.Default private int currentDelaySeconds = 0;
-
- // Map to keep track on the complete status for operations in Update
- @Builder.Default
- private Map operationsStabilizationMap = Arrays.stream(UpdateOperations.values())
- .collect(Collectors.toMap(e -> e, e -> false));
-
- @JsonIgnore
- public void incrementRetryCounter() {
- retries++;
- }
-
- /**
- * Increments {@link CallbackContext#elapsedTime} and returns the total elapsed time
- * @return {@link CallbackContext#getElapsedTime()} after incrementing
- */
- @JsonIgnore
- public int incrementElapsedTime() {
- elapsedTime = elapsedTime + currentDelaySeconds;
- return elapsedTime;
- }
-
- @JsonPOJOBuilder(withPrefix = "")
- public static class CallbackContextBuilder {
- }
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java
index 99648e0..1432145 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java
@@ -1,32 +1,8 @@
package software.amazon.cloudformation.stackset;
-import org.json.JSONObject;
-import org.json.JSONTokener;
-import software.amazon.awssdk.utils.CollectionUtils;
-
-import java.util.Map;
-import java.util.stream.Collectors;
-
class Configuration extends BaseConfiguration {
public Configuration() {
super("aws-cloudformation-stackset.json");
}
-
- public JSONObject resourceSchemaJSONObject() {
- return new JSONObject(new JSONTokener(this.getClass().getClassLoader().getResourceAsStream(schemaFilename)));
- }
-
- /**
- * Providers should implement this method if their resource has a 'Tags' property to define resource-level tags
- * @param resourceModel The request resource model with user defined tags.
- * @return A map of key/value pairs representing tags from the request resource model.
- */
- @Override
- public Map resourceDefinedTags(final ResourceModel resourceModel) {
- if (CollectionUtils.isNullOrEmpty(resourceModel.getTags())) return null;
- return resourceModel.getTags()
- .stream()
- .collect(Collectors.toMap(Tag::getKey, Tag::getValue));
- }
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java
index 6f7b049..f0142e8 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java
@@ -4,114 +4,82 @@
import lombok.Builder;
import lombok.NoArgsConstructor;
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
-import software.amazon.awssdk.services.cloudformation.model.AlreadyExistsException;
-import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesResponse;
+import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest;
import software.amazon.awssdk.services.cloudformation.model.CreateStackSetResponse;
import software.amazon.awssdk.services.cloudformation.model.InsufficientCapabilitiesException;
-import software.amazon.awssdk.services.cloudformation.model.LimitExceededException;
-import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException;
-import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException;
-import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
-import software.amazon.cloudformation.exceptions.CfnNotFoundException;
-import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
-import software.amazon.cloudformation.stackset.util.ClientBuilder;
+import software.amazon.cloudformation.stackset.util.InstancesAnalyzer;
import software.amazon.cloudformation.stackset.util.PhysicalIdGenerator;
-import software.amazon.cloudformation.stackset.util.Stabilizer;
import software.amazon.cloudformation.stackset.util.Validator;
-import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackInstancesRequest;
import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackSetRequest;
-import static software.amazon.cloudformation.stackset.util.Stabilizer.getDelaySeconds;
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class CreateHandler extends BaseHandler {
+public class CreateHandler extends BaseHandlerStd {
- private AmazonWebServicesClientProxy proxy;
- private ResourceModel model;
- private CloudFormationClient client;
- private CallbackContext context;
private Logger logger;
- private Stabilizer stabilizer;
- @Builder.Default
- private Validator validator = new Validator();
-
- @Override
- public ProgressEvent handleRequest(
+ protected ProgressEvent handleRequest(
final AmazonWebServicesClientProxy proxy,
final ResourceHandlerRequest request,
final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
final Logger logger) {
- this.context = callbackContext == null ? CallbackContext.builder().build() : callbackContext;
- this.model = request.getDesiredResourceState();
this.logger = logger;
- this.proxy = proxy;
- this.client = ClientBuilder.getClient();
- this.stabilizer = Stabilizer.builder().proxy(proxy).client(client).logger(logger).build();
-
- // Create a resource when a creation has not initialed
- if (!context.isStabilizationStarted()) {
- validator.validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger);
- final String stackSetName = PhysicalIdGenerator.generatePhysicalId(request);
- createStackSet(stackSetName, request.getClientRequestToken());
-
- } else if (stabilizer.isStabilized(model, context)) {
- return ProgressEvent.defaultSuccessHandler(model);
- }
-
- return ProgressEvent.defaultInProgressHandler(
- context,
- getDelaySeconds(context),
- model);
+ final ResourceModel model = request.getDesiredResourceState();
+ final String stackSetName = PhysicalIdGenerator.generatePhysicalId(request);
+ analyzeTemplate(proxy, model, callbackContext);
+
+ return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext)
+ .request(resourceModel -> createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken()))
+ .call((modelRequest, proxyInvocation) -> createResource(modelRequest, proxyClient, model))
+ .progress()
+ .then(progress -> createStackInstances(proxy, proxyClient, progress, logger))
+ .then(progress -> ProgressEvent.defaultSuccessHandler(model));
}
- private void createStackSet(final String stackSetName, final String requestToken) {
+ /**
+ * Implement client invocation of the create request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ * @param awsRequest the aws service request to create a resource
+ * @param proxyClient the aws service client to make the call
+ * @return awsResponse create resource response
+ */
+ private CreateStackSetResponse createResource(
+ final CreateStackSetRequest awsRequest,
+ final ProxyClient proxyClient,
+ final ResourceModel model) {
+
+ CreateStackSetResponse response;
try {
- final CreateStackSetResponse response = proxy.injectCredentialsAndInvokeV2(
- createStackSetRequest(model, stackSetName, requestToken), client::createStackSet);
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createStackSet);
model.setStackSetId(response.stackSetId());
- logger.log(String.format("%s [%s] StackSet creation succeeded", ResourceModel.TYPE_NAME, stackSetName));
-
- createStackInstances(stackSetName);
-
- } catch (final AlreadyExistsException e) {
- throw new CfnAlreadyExistsException(e);
-
- } catch (final LimitExceededException e) {
- throw new CfnServiceLimitExceededException(e);
-
} catch (final InsufficientCapabilitiesException e) {
throw new CfnInvalidRequestException(e);
}
- }
-
- private void createStackInstances(final String stackSetName) {
- try {
- final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2(
- createStackInstancesRequest(stackSetName, model.getOperationPreferences(),
- model.getDeploymentTargets(), model.getRegions()),
- client::createStackInstances);
- logger.log(String.format("%s [%s] stack instances creation initiated",
- ResourceModel.TYPE_NAME, stackSetName));
-
- context.setStabilizationStarted(true);
- context.setOperationId(response.operationId());
-
- } catch (final StackSetNotFoundException e) {
- throw new CfnNotFoundException(e);
+ logger.log(String.format("%s [%s] StackSet creation succeeded", ResourceModel.TYPE_NAME, model.getStackSetId()));
+ return response;
+ }
- } catch (final OperationInProgressException e) {
- context.incrementRetryCounter();
- }
+ /**
+ * Analyzes/validates template and StackInstancesGroup
+ * @param proxy {@link AmazonWebServicesClientProxy}
+ * @param model {@link ResourceModel}
+ * @param context {@link CallbackContext}
+ */
+ private void analyzeTemplate(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceModel model,
+ final CallbackContext context) {
+
+ new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger);
+ InstancesAnalyzer.builder().desiredModel(model).build().analyzeForCreate(context);
}
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java
index 1e6c3d9..8b3fd5b 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java
@@ -1,92 +1,74 @@
package software.amazon.cloudformation.stackset;
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
-import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesResponse;
-import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException;
+import software.amazon.awssdk.services.cloudformation.model.DeleteStackSetResponse;
import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException;
import software.amazon.cloudformation.exceptions.CfnNotFoundException;
-import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
-import software.amazon.cloudformation.stackset.util.ClientBuilder;
-import software.amazon.cloudformation.stackset.util.Stabilizer;
-import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackInstancesRequest;
-import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackSetRequest;
-import static software.amazon.cloudformation.stackset.util.Stabilizer.getDelaySeconds;
-
-public class DeleteHandler extends BaseHandler {
-
- @Override
- public ProgressEvent handleRequest(
- final AmazonWebServicesClientProxy proxy,
- final ResourceHandlerRequest request,
- final CallbackContext callbackContext,
- final Logger logger) {
-
- final CallbackContext context = callbackContext == null ? CallbackContext.builder().build() : callbackContext;
- final ResourceModel model = request.getDesiredResourceState();
- final CloudFormationClient client = ClientBuilder.getClient();
-
- final Stabilizer stabilizer = Stabilizer.builder().proxy(proxy).client(client).logger(logger).build();
-
- // Delete resource
- if (!context.isStabilizationStarted()) {
- deleteStackInstances(proxy, model, logger, client, context);
+import java.util.ArrayList;
+import java.util.function.Function;
- } else if (stabilizer.isStabilized(model, context)){
- deleteStackSet(proxy, model.getStackSetId(), logger, client);
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackSetRequest;
- return ProgressEvent.defaultSuccessHandler(model);
- }
+public class DeleteHandler extends BaseHandlerStd {
- return ProgressEvent.defaultInProgressHandler(
- context,
- getDelaySeconds(context),
- model);
- }
+ private Logger logger;
- private void deleteStackSet(
+ protected ProgressEvent handleRequest(
final AmazonWebServicesClientProxy proxy,
- final String stackSetName,
- final Logger logger,
- final CloudFormationClient client) {
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
- try {
- proxy.injectCredentialsAndInvokeV2(deleteStackSetRequest(stackSetName), client::deleteStackSet);
- logger.log(String.format("%s [%s] StackSet deletion succeeded", ResourceModel.TYPE_NAME, stackSetName));
-
- } catch (final StackSetNotFoundException e) {
- throw new CfnNotFoundException(e);
- }
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+ // Add all stack instances into delete list
+ callbackContext.setDeleteStacksList(new ArrayList<>(model.getStackInstancesGroup()));
+
+ return proxy.initiate("AWS-CloudFormation-StackSet::Delete", proxyClient, model, callbackContext)
+ .request(Function.identity())
+ .retry(MULTIPLE_OF)
+ .call(EMPTY_CALL)
+ .progress()
+ // delete/stabilize progress chain - delete all associated stack instances
+ .then(progress -> deleteStackInstances(proxy, proxyClient, progress, logger))
+ .then(progress -> deleteStackSet(proxy, proxyClient, progress));
}
- private void deleteStackInstances(
+ /**
+ * Implement client invocation of the delete request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ *
+ * @param proxy Amazon webservice proxy to inject credentials correctly.
+ * @param client the aws service client to make the call
+ * @param progress event of the previous state indicating success, in progress with delay callback or failed state
+ * @return delete resource response
+ */
+ protected ProgressEvent deleteStackSet(
final AmazonWebServicesClientProxy proxy,
- final ResourceModel model,
- final Logger logger,
- final CloudFormationClient client,
- final CallbackContext context) {
+ final ProxyClient client,
+ final ProgressEvent progress) {
- try {
- final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2(
- deleteStackInstancesRequest(model.getStackSetId(),
- model.getOperationPreferences(), model.getDeploymentTargets(), model.getRegions()),
- client::deleteStackInstances);
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
- logger.log(String.format("%s [%s] stack instances deletion initiated",
- ResourceModel.TYPE_NAME, model.getStackSetId()));
-
- context.setOperationId(response.operationId());
- context.setStabilizationStarted(true);
-
- } catch (final StackSetNotFoundException e) {
- throw new CfnNotFoundException(e);
+ return proxy.initiate("AWS-CloudFormation-StackSet::DeleteStackSet", client, model, callbackContext)
+ .request(modelRequest -> deleteStackSetRequest(modelRequest.getStackSetId()))
+ .call((modelRequest, proxyInvocation) -> deleteStackSet(model.getStackSetId(), proxyInvocation))
+ .success();
+ }
- } catch (final OperationInProgressException e) {
- context.incrementRetryCounter();
- }
+ private DeleteStackSetResponse deleteStackSet(final String stackSetId, final ProxyClient proxyClient) {
+ DeleteStackSetResponse response;
+ response = proxyClient.injectCredentialsAndInvokeV2(
+ deleteStackSetRequest(stackSetId), proxyClient.client()::deleteStackSet);
+ logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME));
+ return response;
}
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java
index 4f99067..a83126f 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java
@@ -1,46 +1,39 @@
package software.amazon.cloudformation.stackset;
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
-import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetResponse;
import software.amazon.awssdk.services.cloudformation.model.ListStackSetsResponse;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
-import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
-import software.amazon.cloudformation.stackset.util.ClientBuilder;
-import software.amazon.cloudformation.stackset.util.OperationOperator;
import software.amazon.cloudformation.stackset.util.ResourceModelBuilder;
-import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetRequest;
import static software.amazon.cloudformation.stackset.translator.RequestTranslator.listStackSetsRequest;
-public class ListHandler extends BaseHandler {
+public class ListHandler extends BaseHandlerStd {
@Override
- public ProgressEvent handleRequest(
- final AmazonWebServicesClientProxy proxy,
- final ResourceHandlerRequest request,
- final CallbackContext callbackContext,
- final Logger logger) {
-
- final CloudFormationClient client = ClientBuilder.getClient();
- final OperationOperator operator = OperationOperator.builder().proxy(proxy).client(client).build();
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
- final ListStackSetsResponse response = proxy.injectCredentialsAndInvokeV2(
- listStackSetsRequest(request.getNextToken()), client::listStackSets);
+ final ListStackSetsResponse response = proxyClient.injectCredentialsAndInvokeV2(
+ listStackSetsRequest(request.getNextToken()), proxyClient.client()::listStackSets);
final List models = response
.summaries()
.stream()
.map(stackSetSummary -> ResourceModelBuilder.builder()
- .proxy(proxy)
- .client(client)
- .stackSet(operator.getStackSet(stackSetSummary.stackSetId()))
+ .proxyClient(proxyClient)
+ .stackSet(describeStackSet(proxyClient, stackSetSummary.stackSetId()))
.build().buildModel())
.collect(Collectors.toList());
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java
index b32a7af..76dca3a 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java
@@ -5,29 +5,28 @@
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.OperationStatus;
import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
-import software.amazon.cloudformation.stackset.util.ClientBuilder;
-import software.amazon.cloudformation.stackset.util.OperationOperator;
import software.amazon.cloudformation.stackset.util.ResourceModelBuilder;
-public class ReadHandler extends BaseHandler {
+public class ReadHandler extends BaseHandlerStd {
- @Override
- public ProgressEvent handleRequest(
- final AmazonWebServicesClientProxy proxy,
- final ResourceHandlerRequest request,
- final CallbackContext callbackContext,
- final Logger logger) {
+ private Logger logger;
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
final ResourceModel model = request.getDesiredResourceState();
- final CloudFormationClient client = ClientBuilder.getClient();
- final OperationOperator operator = OperationOperator.builder().proxy(proxy).client(client).build();
return ProgressEvent.builder()
.resourceModel(ResourceModelBuilder.builder()
- .proxy(proxy)
- .client(client)
- .stackSet(operator.getStackSet(model.getStackSetId()))
+ .proxyClient(proxyClient)
+ .stackSet(describeStackSet(proxyClient, model.getStackSetId()))
.build().buildModel())
.status(OperationStatus.SUCCESS)
.build();
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java
index 12f40f0..4145c48 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java
@@ -4,127 +4,77 @@
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
-import software.amazon.cloudformation.stackset.util.ClientBuilder;
-import software.amazon.cloudformation.stackset.util.OperationOperator;
-import software.amazon.cloudformation.stackset.util.Stabilizer;
-import software.amazon.cloudformation.stackset.util.UpdatePlaceholder;
+import software.amazon.cloudformation.stackset.util.InstancesAnalyzer;
import software.amazon.cloudformation.stackset.util.Validator;
-import java.util.Set;
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest;
-import static software.amazon.cloudformation.stackset.util.Comparator.isAddingStackInstances;
-import static software.amazon.cloudformation.stackset.util.Comparator.isDeletingStackInstances;
-import static software.amazon.cloudformation.stackset.util.Comparator.isStackSetConfigEquals;
-import static software.amazon.cloudformation.stackset.util.Comparator.isUpdatingStackInstances;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.ADD_INSTANCES_BY_REGIONS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.ADD_INSTANCES_BY_TARGETS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.DELETE_INSTANCES_BY_REGIONS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.DELETE_INSTANCES_BY_TARGETS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.STACK_SET_CONFIGS;
-import static software.amazon.cloudformation.stackset.util.Stabilizer.getDelaySeconds;
-import static software.amazon.cloudformation.stackset.util.Stabilizer.isPreviousOperationDone;
-import static software.amazon.cloudformation.stackset.util.Stabilizer.isUpdateStabilized;
+public class UpdateHandler extends BaseHandlerStd {
+ private Logger logger;
-public class UpdateHandler extends BaseHandler {
-
- private Validator validator;
-
- public UpdateHandler() {
- this.validator = new Validator();
- }
-
- public UpdateHandler(Validator validator) {
- this.validator = validator;
- }
-
- @Override
- public ProgressEvent handleRequest(
+ protected ProgressEvent handleRequest(
final AmazonWebServicesClientProxy proxy,
final ResourceHandlerRequest request,
final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
final Logger logger) {
- final CallbackContext context = callbackContext == null ? CallbackContext.builder().build() : callbackContext;
- final CloudFormationClient client = ClientBuilder.getClient();
- final ResourceModel previousModel = request.getPreviousResourceState();
- final ResourceModel desiredModel = request.getDesiredResourceState();
- final Stabilizer stabilizer = Stabilizer.builder().proxy(proxy).client(client).logger(logger).build();
- final OperationOperator operator = OperationOperator.builder()
- .client(client).desiredModel(desiredModel).previousModel(previousModel)
- .logger(logger).proxy(proxy).context(context)
- .build();
-
- final boolean isStackSetUpdating = !isStackSetConfigEquals(previousModel, desiredModel);
- final boolean isPerformingStackSetUpdate = stabilizer.isPerformingOperation(isStackSetUpdating,
- context.isUpdateStackSetStarted(), null, STACK_SET_CONFIGS, desiredModel, context);
-
- if (isPerformingStackSetUpdate) {
- if (previousModel.getTemplateURL() != desiredModel.getTemplateURL()) {
- validator.validateTemplate(
- proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger);
- }
- operator.updateStackSet(STACK_SET_CONFIGS,null, null);
- }
-
- final boolean isPerformingStackInstancesUpdate = isPreviousOperationDone(context, STACK_SET_CONFIGS) &&
- isUpdatingStackInstances(previousModel, desiredModel, context);
-
- if (isPerformingStackInstancesUpdate) {
-
- final UpdatePlaceholder updateTable = new UpdatePlaceholder(previousModel, desiredModel);
- final Set regionsToAdd = updateTable.getRegionsToAdd();
- final Set targetsToAdd = updateTable.getTargetsToAdd();
- final Set regionsToDelete = updateTable.getRegionsToDelete();
- final Set targetsToDelete = updateTable.getTargetsToDelete();
-
- if (isDeletingStackInstances(regionsToDelete, targetsToDelete, context)) {
-
- if (stabilizer.isPerformingOperation(
- !regionsToDelete.isEmpty(), context.isDeleteStacksByRegionsStarted(),
- STACK_SET_CONFIGS, DELETE_INSTANCES_BY_REGIONS, desiredModel, context)) {
+ this.logger = logger;
- operator.updateStackSet(DELETE_INSTANCES_BY_REGIONS, regionsToDelete, null);
- }
-
- if (stabilizer.isPerformingOperation(
- !targetsToDelete.isEmpty(), context.isDeleteStacksByTargetsStarted(),
- DELETE_INSTANCES_BY_REGIONS, DELETE_INSTANCES_BY_TARGETS, desiredModel, context)) {
-
- operator.updateStackSet(DELETE_INSTANCES_BY_TARGETS, regionsToDelete, targetsToDelete);
- }
- }
-
- if (isAddingStackInstances(regionsToAdd, targetsToAdd, context)) {
-
- if (stabilizer.isPerformingOperation(
- !regionsToAdd.isEmpty(), context.isAddStacksByRegionsStarted(),
- DELETE_INSTANCES_BY_TARGETS, ADD_INSTANCES_BY_REGIONS, desiredModel, context)) {
-
- operator.updateStackSet(ADD_INSTANCES_BY_REGIONS, regionsToAdd, null);
- }
-
- if (stabilizer.isPerformingOperation(
- !targetsToAdd.isEmpty(), context.isAddStacksByTargetsStarted(),
- ADD_INSTANCES_BY_REGIONS, ADD_INSTANCES_BY_TARGETS, desiredModel, context)) {
+ final ResourceModel model = request.getDesiredResourceState();
+ final ResourceModel previousModel = request.getPreviousResourceState();
+ analyzeTemplate(proxy, previousModel, model, callbackContext);
- operator.updateStackSet(ADD_INSTANCES_BY_TARGETS, regionsToAdd, targetsToAdd);
- }
- }
- }
+ return updateStackSet(proxy, proxyClient, model, callbackContext)
+ .then(progress -> deleteStackInstances(proxy, proxyClient, progress, logger))
+ .then(progress -> createStackInstances(proxy, proxyClient, progress, logger))
+ .then(progress -> updateStackInstances(proxy, proxyClient, progress, logger));
+ }
- if (isUpdateStabilized(context)) {
- return ProgressEvent.defaultSuccessHandler(desiredModel);
+ /**
+ * Implement client invocation of the update request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ *
+ * @param proxy {@link AmazonWebServicesClientProxy} to initiate proxy chain
+ * @param client the aws service client {@link ProxyClient} to make the call
+ * @param model {@link ResourceModel}
+ * @param callbackContext {@link CallbackContext}
+ * @return progressEvent indicating success, in progress with delay callback or failed state
+ */
+ protected ProgressEvent updateStackSet(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient client,
+ final ResourceModel model,
+ final CallbackContext callbackContext) {
+
+ return proxy.initiate("AWS-CloudFormation-StackSet::UpdateStackSet", client, model, callbackContext)
+ .request(modelRequest -> updateStackSetRequest(modelRequest))
+ .retry(MULTIPLE_OF)
+ .call((modelRequest, proxyInvocation) ->
+ proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackSet))
+ .stabilize((request, response, proxyInvocation, resourceModel, context) ->
+ isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger))
+ .exceptFilter(this::filterException)
+ .progress();
+ }
- } else {
- return ProgressEvent.defaultInProgressHandler(
- context,
- getDelaySeconds(context),
- desiredModel);
- }
+ /**
+ * Analyzes/validates template and StackInstancesGroup
+ * @param proxy {@link AmazonWebServicesClientProxy}
+ * @param previousModel previous {@link ResourceModel}
+ * @param model {@link ResourceModel}
+ * @param context {@link CallbackContext}
+ */
+ private void analyzeTemplate(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceModel previousModel,
+ final ResourceModel model,
+ final CallbackContext context) {
+ new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger);
+ InstancesAnalyzer.builder().desiredModel(model).previousModel(previousModel).build().analyzeForUpdate(context);
}
-
}
-
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java
index e244f6c..bbde220 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java
@@ -3,10 +3,12 @@
import software.amazon.awssdk.services.cloudformation.model.AutoDeployment;
import software.amazon.awssdk.services.cloudformation.model.DeploymentTargets;
import software.amazon.awssdk.services.cloudformation.model.Parameter;
+import software.amazon.awssdk.services.cloudformation.model.StackInstanceSummary;
import software.amazon.awssdk.services.cloudformation.model.StackSetOperationPreferences;
import software.amazon.awssdk.services.cloudformation.model.Tag;
import software.amazon.awssdk.utils.CollectionUtils;
import software.amazon.cloudformation.stackset.OperationPreferences;
+import software.amazon.cloudformation.stackset.util.StackInstance;
import java.util.Collection;
import java.util.List;
@@ -79,7 +81,7 @@ static List translateToSdkParameters(
*/
public static Set translateFromSdkParameters(
final Collection parameters) {
- if (parameters == null) return null;
+ if (CollectionUtils.isNullOrEmpty(parameters)) return null;
return parameters.stream()
.map(parameter -> software.amazon.cloudformation.stackset.Parameter.builder()
.parameterKey(parameter.parameterKey())
@@ -133,4 +135,26 @@ public static Set translateFromSdkT
.build())
.collect(Collectors.toSet());
}
+
+ /**
+ * Converts {@link StackInstanceSummary} to {@link StackInstance} utility placeholder
+ * @param isSelfManaged if PermissionModel is SELF_MANAGED
+ * @param summary {@link StackInstanceSummary}
+ * @return {@link StackInstance}
+ */
+ public static StackInstance translateToStackInstance(
+ final boolean isSelfManaged,
+ final StackInstanceSummary summary,
+ final Collection parameters) {
+
+ final StackInstance stackInstance = StackInstance.builder()
+ .region(summary.region())
+ .parameters(translateFromSdkParameters(parameters))
+ .build();
+
+ // Currently OrganizationalUnitId is Reserved for internal use. No data returned from this API
+ // TODO: Once OrganizationalUnitId is added back, we need to change to set organizationalUnitId to DeploymentTarget if SERVICE_MANAGED
+ stackInstance.setDeploymentTarget(summary.account());
+ return stackInstance;
+ }
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java
index 7e3b02a..eac24cb 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java
@@ -4,17 +4,17 @@
import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest;
import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesRequest;
import software.amazon.awssdk.services.cloudformation.model.DeleteStackSetRequest;
+import software.amazon.awssdk.services.cloudformation.model.DescribeStackInstanceRequest;
import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationRequest;
import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetRequest;
import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest;
import software.amazon.awssdk.services.cloudformation.model.ListStackSetsRequest;
+import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesRequest;
import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
-import software.amazon.cloudformation.stackset.DeploymentTargets;
import software.amazon.cloudformation.stackset.OperationPreferences;
import software.amazon.cloudformation.stackset.ResourceModel;
-
-import java.util.Set;
+import software.amazon.cloudformation.stackset.StackInstances;
import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkAutoDeployment;
import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkDeploymentTargets;
@@ -47,13 +47,26 @@ public static CreateStackSetRequest createStackSetRequest(
public static CreateStackInstancesRequest createStackInstancesRequest(
final String stackSetName,
final OperationPreferences operationPreferences,
- final DeploymentTargets deploymentTargets,
- final Set regions) {
+ final StackInstances stackInstances) {
return CreateStackInstancesRequest.builder()
.stackSetName(stackSetName)
- .regions(regions)
+ .regions(stackInstances.getRegions())
+ .operationPreferences(translateToSdkOperationPreferences(operationPreferences))
+ .deploymentTargets(translateToSdkDeploymentTargets(stackInstances.getDeploymentTargets()))
+ .parameterOverrides(translateToSdkParameters(stackInstances.getParameterOverrides()))
+ .build();
+ }
+
+ public static UpdateStackInstancesRequest updateStackInstancesRequest(
+ final String stackSetName,
+ final OperationPreferences operationPreferences,
+ final StackInstances stackInstances) {
+ return UpdateStackInstancesRequest.builder()
+ .stackSetName(stackSetName)
+ .regions(stackInstances.getRegions())
.operationPreferences(translateToSdkOperationPreferences(operationPreferences))
- .deploymentTargets(translateToSdkDeploymentTargets(deploymentTargets))
+ .deploymentTargets(translateToSdkDeploymentTargets(stackInstances.getDeploymentTargets()))
+ .parameterOverrides(translateToSdkParameters(stackInstances.getParameterOverrides()))
.build();
}
@@ -66,13 +79,12 @@ public static DeleteStackSetRequest deleteStackSetRequest(final String stackSetN
public static DeleteStackInstancesRequest deleteStackInstancesRequest(
final String stackSetName,
final OperationPreferences operationPreferences,
- final DeploymentTargets deploymentTargets,
- final Set regions) {
+ final StackInstances stackInstances) {
return DeleteStackInstancesRequest.builder()
.stackSetName(stackSetName)
- .regions(regions)
+ .regions(stackInstances.getRegions())
.operationPreferences(translateToSdkOperationPreferences(operationPreferences))
- .deploymentTargets(translateToSdkDeploymentTargets(deploymentTargets))
+ .deploymentTargets(translateToSdkDeploymentTargets(stackInstances.getDeploymentTargets()))
.build();
}
@@ -113,6 +125,17 @@ public static DescribeStackSetRequest describeStackSetRequest(final String stack
.build();
}
+ public static DescribeStackInstanceRequest describeStackInstanceRequest(
+ final String account,
+ final String region,
+ final String stackSetId) {
+ return DescribeStackInstanceRequest.builder()
+ .stackInstanceAccount(account)
+ .stackInstanceRegion(region)
+ .stackSetName(stackSetId)
+ .build();
+ }
+
public static DescribeStackSetOperationRequest describeStackSetOperationRequest(
final String stackSetName, final String operationId) {
return DescribeStackSetOperationRequest.builder()
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java
index 56e2286..ef7f870 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java
@@ -1,6 +1,5 @@
package software.amazon.cloudformation.stackset.util;
-import lombok.Builder;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.awscore.AwsRequest;
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
@@ -246,4 +245,4 @@ public static GetAwsCredentialsResponseMetadata create(AwsResponseMetadata respo
return new GetAwsCredentialsResponseMetadata(responseMetadata);
}
}
-}
\ No newline at end of file
+}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java
index ab4af02..3bfc9df 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java
@@ -13,12 +13,16 @@ private ClientBuilder() {}
* Get CloudFormationClient for requests to interact with StackSet client
* @return {@link CloudFormationClient}
*/
- public static CloudFormationClient getClient() {
- return CloudFormationClient.builder()
+ private static class LazyHolder {
+ public static CloudFormationClient SERVICE_CLIENT = CloudFormationClient.builder()
.httpClient(LambdaWrapper.HTTP_CLIENT)
.build();
}
+ public static CloudFormationClient getClient() {
+ return LazyHolder.SERVICE_CLIENT;
+ }
+
/**
* Gets S3 client for requests to interact with getting/validating template content
* if {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL()} is passed in
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java
index cfca487..012ef5e 100644
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java
@@ -1,122 +1,16 @@
package software.amazon.cloudformation.stackset.util;
import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
-import software.amazon.cloudformation.stackset.CallbackContext;
+import software.amazon.awssdk.services.cloudformation.model.PermissionModels;
import software.amazon.cloudformation.stackset.ResourceModel;
import java.util.Collection;
-import java.util.Set;
-
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.ADD_INSTANCES_BY_REGIONS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.ADD_INSTANCES_BY_TARGETS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.DELETE_INSTANCES_BY_REGIONS;
-import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations.DELETE_INSTANCES_BY_TARGETS;
/**
* Utility class to help comparing previous model and desire model
*/
public class Comparator {
- /**
- * Compares if desired model uses the same stack set configs other than stack instances
- * when it comes to updating the resource
- * @param previousModel previous {@link ResourceModel}
- * @param desiredModel desired {@link ResourceModel}
- * @return
- */
- public static boolean isStackSetConfigEquals(
- final ResourceModel previousModel, final ResourceModel desiredModel) {
-
- if (!isEquals(previousModel.getTags(), desiredModel.getTags()))
- return false;
-
- if (StringUtils.compare(previousModel.getAdministrationRoleARN(),
- desiredModel.getAdministrationRoleARN()) != 0)
- return false;
-
- if (StringUtils.compare(previousModel.getDescription(), desiredModel.getDescription()) != 0)
- return false;
-
- if (StringUtils.compare(previousModel.getExecutionRoleName(), desiredModel.getExecutionRoleName()) != 0)
- return false;
-
- if (StringUtils.compare(previousModel.getTemplateURL(), desiredModel.getTemplateURL()) != 0)
- return false;
-
- if (StringUtils.compare(previousModel.getTemplateBody(), desiredModel.getTemplateBody()) != 0)
- return false;
-
- return true;
- }
-
- /**
- * Checks if stack instances need to be updated
- * @param previousModel previous {@link ResourceModel}
- * @param desiredModel desired {@link ResourceModel}
- * @param context {@link CallbackContext}
- * @return
- */
- public static boolean isUpdatingStackInstances(
- final ResourceModel previousModel,
- final ResourceModel desiredModel,
- final CallbackContext context) {
-
- // if updating stack instances is unnecessary, mark all instances operation as complete
- if (CollectionUtils.isEqualCollection(previousModel.getRegions(), desiredModel.getRegions()) &&
- previousModel.getDeploymentTargets().equals(desiredModel.getDeploymentTargets())) {
-
- context.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true);
- context.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true);
- context.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_REGIONS, true);
- context.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_TARGETS, true);
- return false;
- }
- return true;
- }
-
- /**
- * Checks if there is any stack instances need to be delete during the update
- * @param regionsToDelete regions to delete
- * @param targetsToDelete targets (accounts or OUIDs) to delete
- * @param context {@link CallbackContext}
- * @return
- */
- public static boolean isDeletingStackInstances(
- final Set regionsToDelete,
- final Set targetsToDelete,
- final CallbackContext context) {
-
- // If no stack instances need to be deleted, mark DELETE_INSTANCES operations as done.
- if (regionsToDelete.isEmpty() && targetsToDelete.isEmpty()) {
- context.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true);
- context.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true);
- return false;
- }
- return true;
- }
-
- /**
- * Checks if new stack instances need to be added
- * @param regionsToAdd regions to add
- * @param targetsToAdd targets to add
- * @param context {@link CallbackContext}
- * @return
- */
- public static boolean isAddingStackInstances(
- final Set regionsToAdd,
- final Set targetsToAdd,
- final CallbackContext context) {
-
- // If no stack instances need to be added, mark ADD_INSTANCES operations as done.
- if (regionsToAdd.isEmpty() && targetsToAdd.isEmpty()) {
- context.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_REGIONS, true);
- context.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_TARGETS, true);
- return false;
- }
- return true;
- }
-
/**
* Compares if two collections equal in a null-safe way.
* @param collection1
@@ -127,4 +21,8 @@ public static boolean isEquals(final Collection> collection1, final Collection
if (collection1 == null) return collection2 == null ? true : false;
return CollectionUtils.isEqualCollection(collection1, collection2);
}
+
+ public static boolean isSelfManaged(final ResourceModel model) {
+ return PermissionModels.fromValue(model.getPermissionModel()).equals(PermissionModels.SELF_MANAGED);
+ }
}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java
deleted file mode 100644
index a02b5ff..0000000
--- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package software.amazon.cloudformation.stackset.util;
-
-public class EnumUtils {
-
- /**
- * Operations that need to complete during update
- */
- public enum UpdateOperations {
- STACK_SET_CONFIGS, ADD_INSTANCES_BY_REGIONS, ADD_INSTANCES_BY_TARGETS,
- DELETE_INSTANCES_BY_REGIONS,DELETE_INSTANCES_BY_TARGETS
- }
-
-}
diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java
new file mode 100644
index 0000000..2d901c3
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java
@@ -0,0 +1,216 @@
+package software.amazon.cloudformation.stackset.util;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+import software.amazon.awssdk.services.cloudformation.model.PermissionModels;
+import software.amazon.cloudformation.stackset.CallbackContext;
+import software.amazon.cloudformation.stackset.DeploymentTargets;
+import software.amazon.cloudformation.stackset.Parameter;
+import software.amazon.cloudformation.stackset.ResourceModel;
+import software.amazon.cloudformation.stackset.StackInstances;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static software.amazon.cloudformation.stackset.util.Comparator.isSelfManaged;
+
+/**
+ * Utility class to hold {@link StackInstances} that need to be modified during the update
+ */
+@Builder
+@Data
+public class InstancesAnalyzer {
+
+ private ResourceModel previousModel;
+
+ private ResourceModel desiredModel;
+
+ /**
+ * Analyzes {@link StackInstances} that need to be modified during the update
+ * @param context {@link CallbackContext}
+ */
+ public void analyzeForUpdate(final CallbackContext context) {
+ final boolean isSelfManaged = isSelfManaged(desiredModel);
+
+ final Set previousStackInstances =
+ flattenStackInstancesGroup(previousModel.getStackInstancesGroup(), isSelfManaged);
+ final Set desiredStackInstances =
+ flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged);
+
+ // Calculates all necessary differences that we need to take actions
+ final Set stacksToAdd = new HashSet<>(desiredStackInstances);
+ stacksToAdd.removeAll(previousStackInstances);
+ final Set stacksToDelete = new HashSet<>(previousStackInstances);
+ stacksToDelete.removeAll(desiredStackInstances);
+ final Set stacksToCompare = new HashSet<>(desiredStackInstances);
+ stacksToCompare.retainAll(previousStackInstances);
+
+ final Set stackInstancesGroupToAdd = aggregateStackInstances(stacksToAdd, isSelfManaged);
+ final Set stackInstancesGroupToDelete = aggregateStackInstances(stacksToDelete, isSelfManaged);
+
+ // Since StackInstance.parameters is excluded for @EqualsAndHashCode,
+ // we needs to construct a key value map to keep track on previous StackInstance objects
+ final Set stacksToUpdate = getUpdatingStackInstances(
+ stacksToCompare, previousStackInstances.stream().collect(Collectors.toMap(s -> s, s -> s)));
+ final Set stackInstancesGroupToUpdate = aggregateStackInstances(stacksToUpdate, isSelfManaged);
+
+ // Update the stack lists that need to write of callbackContext holder
+ context.setCreateStacksList(new ArrayList<>(stackInstancesGroupToAdd));
+ context.setDeleteStacksList(new ArrayList<>(stackInstancesGroupToDelete));
+ context.setUpdateStacksList(new ArrayList<>(stackInstancesGroupToUpdate));
+ }
+
+ /**
+ * Analyzes {@link StackInstances} that need to be modified during the update
+ * Updates callbackContext with the stack list to create
+ * @param context {@link CallbackContext}
+ */
+ public void analyzeForCreate(final CallbackContext context) {
+ if (desiredModel.getStackInstancesGroup() == null) return;
+ if (desiredModel.getStackInstancesGroup().size() == 1) {
+ context.setCreateStacksList(new ArrayList<>(desiredModel.getStackInstancesGroup()));
+ }
+ final boolean isSelfManaged = isSelfManaged(desiredModel);
+
+ final Set desiredStackInstances =
+ flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged);
+
+ final Set stackInstancesGroupToAdd = aggregateStackInstances(desiredStackInstances, isSelfManaged);
+ context.setCreateStacksList(new ArrayList<>(stackInstancesGroupToAdd));
+ }
+
+ /**
+ * Aggregates flat {@link StackInstance} to a group of {@link StackInstances} to call
+ * corresponding StackSet APIs
+ * @param flatStackInstances {@link StackInstance}
+ * @return {@link StackInstances} set
+ */
+ public static Set aggregateStackInstances(
+ @NonNull final Set flatStackInstances, final boolean isSelfManaged) {
+ final Set groupedStacks = groupInstancesByTargets(flatStackInstances, isSelfManaged);
+ return aggregateInstancesByRegions(groupedStacks, isSelfManaged);
+ }
+
+ /**
+ * Group regions by {@link DeploymentTargets} and {@link StackInstance#getParameters()}
+ * @return {@link StackInstances}
+ */
+ public static Set groupInstancesByTargets(
+ @NonNull final Set flatStackInstances, final boolean isSelfManaged) {
+
+ final Map, StackInstances> groupedStacksMap = new HashMap<>();
+ for (final StackInstance stackInstance : flatStackInstances) {
+ final String target = stackInstance.getDeploymentTarget();
+ final String region = stackInstance.getRegion();
+ final Set parameterSet = stackInstance.getParameters();
+ final List