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 compositeKey = Arrays.asList(target, parameterSet); + + if (groupedStacksMap.containsKey(compositeKey)) { + groupedStacksMap.get(compositeKey).getRegions().add(stackInstance.getRegion()); + } else { + final DeploymentTargets targets = DeploymentTargets.builder().build(); + if (isSelfManaged) { + targets.setAccounts(new HashSet<>(Arrays.asList(target))); + } else { + targets.setOrganizationalUnitIds(new HashSet<>(Arrays.asList(target))); + } + + final StackInstances stackInstances = StackInstances.builder() + .regions(new HashSet<>(Arrays.asList(region))) + .deploymentTargets(targets) + .parameterOverrides(parameterSet) + .build(); + groupedStacksMap.put(compositeKey, stackInstances); + } + } + return new HashSet<>(groupedStacksMap.values()); + } + + /** + * Aggregates instances with similar {@link StackInstances#getRegions()} + * @param groupedStacks {@link StackInstances} set + * @return Aggregated {@link StackInstances} set + */ + private static Set aggregateInstancesByRegions( + final Set groupedStacks, + final boolean isSelfManaged) { + + final Map, StackInstances> groupedStacksMap = new HashMap<>(); + for (final StackInstances stackInstances : groupedStacks) { + final DeploymentTargets target = stackInstances.getDeploymentTargets(); + final Set parameterSet = stackInstances.getParameterOverrides(); + final List compositeKey = Arrays.asList(stackInstances.getRegions(), parameterSet); + if (groupedStacksMap.containsKey(compositeKey)) { + if (isSelfManaged) { + groupedStacksMap.get(compositeKey).getDeploymentTargets() + .getAccounts().addAll(target.getAccounts()); + } else { + groupedStacksMap.get(compositeKey).getDeploymentTargets() + .getOrganizationalUnitIds().addAll(target.getOrganizationalUnitIds()); + } + } else { + groupedStacksMap.put(compositeKey, stackInstances); + } + } + return new HashSet<>(groupedStacksMap.values()); + } + + /** + * Compares {@link StackInstance#getParameters()} with previous {@link StackInstance#getParameters()} + * Gets the StackInstances need to update + * @param intersection {@link StackInstance} retaining desired stack instances + * @param previousStackMap Map contains previous stack instances + * @return {@link StackInstance} to update + */ + private static Set getUpdatingStackInstances( + final Set intersection, + final Map previousStackMap) { + + return intersection.stream() + .filter(stackInstance -> !Comparator.isEquals( + previousStackMap.get(stackInstance).getParameters(), stackInstance.getParameters())) + .collect(Collectors.toSet()); + } + + /** + * Since Stack instances are defined across accounts and regions with(out) parameters, + * We are expanding all before we tack actions + * @param stackInstancesGroup {@link ResourceModel#getStackInstancesGroup()} + * @return {@link StackInstance} set + */ + private static Set flattenStackInstancesGroup( + final Collection stackInstancesGroup, final boolean isSelfManaged) { + + final Set flatStacks = new HashSet<>(); + + for (final StackInstances stackInstances : stackInstancesGroup) { + for (final String region : stackInstances.getRegions()) { + + final Set targets = isSelfManaged ? stackInstances.getDeploymentTargets().getAccounts() + : stackInstances.getDeploymentTargets().getOrganizationalUnitIds(); + + for (final String target : targets) { + final StackInstance stackInstance = StackInstance.builder() + .region(region).deploymentTarget(target).parameters(stackInstances.getParameterOverrides()) + .build(); + + if (flatStacks.contains(stackInstance)) { + throw new ParseException(String.format("Stack instance [%s,%s] is duplicated", target, region)); + } + + flatStacks.add(stackInstance); + } + } + } + return flatStacks; + } +} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java deleted file mode 100644 index 876757c..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java +++ /dev/null @@ -1,214 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesResponse; -import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesResponse; -import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetResponse; -import software.amazon.awssdk.services.cloudformation.model.InvalidOperationException; -import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException; -import software.amazon.awssdk.services.cloudformation.model.PermissionModels; -import software.amazon.awssdk.services.cloudformation.model.StackSet; -import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException; -import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.stackset.CallbackContext; -import software.amazon.cloudformation.stackset.DeploymentTargets; -import software.amazon.cloudformation.stackset.ResourceModel; - -import java.util.HashSet; -import java.util.Set; - -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.describeStackSetRequest; -import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest; - -/** - * Helper class to perform operations that we need to interact with service client from the requests - */ -@AllArgsConstructor -@Builder -public class OperationOperator { - - private AmazonWebServicesClientProxy proxy; - private CloudFormationClient client; - private ResourceModel previousModel; - private ResourceModel desiredModel; - private Logger logger; - private CallbackContext context; - - private static String OPERATION_IN_PROGRESS_MSG = "StackSet Operation retrying due to prior operation incomplete"; - - /** - * Performs to update stack set configs - * @return {@link UpdateStackSetResponse#operationId()} - */ - private String updateStackSetConfig() { - final UpdateStackSetResponse response = proxy.injectCredentialsAndInvokeV2( - updateStackSetRequest(desiredModel), client::updateStackSet); - - context.setUpdateStackSetStarted(true); - return response.operationId(); - } - - /** - * Performs to delete stack instances based on the new removed regions - * with all targets including new removed targets - * @param regionsToDelete Region to delete - * @return {@link DeleteStackInstancesResponse#operationId()} - */ - private String deleteStackInstancesByRegions(final Set regionsToDelete) { - final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( - deleteStackInstancesRequest(previousModel.getStackSetId(), desiredModel.getOperationPreferences(), - previousModel.getDeploymentTargets(), regionsToDelete), client::deleteStackInstances); - - context.setDeleteStacksByRegionsStarted(true); - return response.operationId(); - } - - /** - * Performs to delete stack instances based on the newly removed targets - * @param regionsDeleted Region have been delete in {@link OperationOperator#deleteStackInstancesByRegions} - * @param targetsToDelete Targets to delete - * @return {@link DeleteStackInstancesResponse#operationId()} - */ - private String deleteStackInstancesByTargets(final Set regionsDeleted, final Set targetsToDelete) { - // Constructing deploymentTargets which need to be deleted - final boolean isSelfManaged = PermissionModels.SELF_MANAGED - .equals(PermissionModels.fromValue(previousModel.getPermissionModel())); - final DeploymentTargets deploymentTargets = DeploymentTargets.builder().build(); - - if (isSelfManaged) { - deploymentTargets.setAccounts(targetsToDelete); - } else { - deploymentTargets.setOrganizationalUnitIds(targetsToDelete); - } - - final Set regionsToDelete = new HashSet<>(previousModel.getRegions()); - - // Avoid to delete regions that were already deleted above - if (!regionsDeleted.isEmpty()) regionsToDelete.removeAll(regionsDeleted); - - final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( - deleteStackInstancesRequest(previousModel.getStackSetId(), desiredModel.getOperationPreferences(), - deploymentTargets, regionsToDelete), client::deleteStackInstances); - - context.setDeleteStacksByTargetsStarted(true); - return response.operationId(); - } - - /** - * Performs to create stack instances based on the new added regions - * with all targets including new added targets - * @param regionsToAdd Region to add - * @return {@link CreateStackInstancesResponse#operationId()} - */ - private String addStackInstancesByRegions(final Set regionsToAdd) { - final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( - createStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), - desiredModel.getDeploymentTargets(), regionsToAdd), - client::createStackInstances); - - context.setAddStacksByRegionsStarted(true); - return response.operationId(); - } - - /** - * Performs to create stack instances based on the new added targets - * @param regionsAdded Region have been added in {@link OperationOperator#addStackInstancesByRegions} - * @param targetsToAdd Targets to add - * @return {@link CreateStackInstancesResponse#operationId()} - */ - private String addStackInstancesByTargets(final Set regionsAdded, final Set targetsToAdd) { - // Constructing deploymentTargets which need to be added - final boolean isSelfManaged = PermissionModels.SELF_MANAGED - .equals(PermissionModels.fromValue(desiredModel.getPermissionModel())); - final DeploymentTargets deploymentTargets = DeploymentTargets.builder().build(); - - if (isSelfManaged) { - deploymentTargets.setAccounts(targetsToAdd); - } else { - deploymentTargets.setOrganizationalUnitIds(targetsToAdd); - } - - final Set regionsToAdd = new HashSet<>(desiredModel.getRegions()); - /** - * Avoid to create instances in regions that have already created in - * {@link OperationOperator#addStackInstancesByRegions} - */ - if (!regionsAdded.isEmpty()) regionsToAdd.removeAll(regionsAdded); - - final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2(createStackInstancesRequest( - desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), deploymentTargets, regionsToAdd), - client::createStackInstances); - - context.setAddStacksByTargetsStarted(true); - return response.operationId(); - } - - /** - * Get {@link StackSet} from service client using stackSetId - * @param stackSetId StackSet Id - * @return {@link StackSet} - */ - public StackSet getStackSet(final String stackSetId) { - try { - final DescribeStackSetResponse stackSetResponse = proxy.injectCredentialsAndInvokeV2( - describeStackSetRequest(stackSetId), client::describeStackSet); - return stackSetResponse.stackSet(); - } catch (final StackSetNotFoundException e) { - throw new CfnNotFoundException(e); - } - } - - /** - * Update the StackSet with the {@link EnumUtils.UpdateOperations} passed in - * @param operation {@link EnumUtils.UpdateOperations} - * @param regions Regions to add or delete - * @param targets Targets to add or delete - */ - public void updateStackSet( - final EnumUtils.UpdateOperations operation, - final Set regions, - final Set targets) { - - try { - String operationId = null; - switch (operation) { - case STACK_SET_CONFIGS: - operationId = updateStackSetConfig(); - break; - case DELETE_INSTANCES_BY_REGIONS: - operationId = deleteStackInstancesByRegions(regions); - break; - case DELETE_INSTANCES_BY_TARGETS: - operationId = deleteStackInstancesByTargets(regions, targets); - break; - case ADD_INSTANCES_BY_REGIONS: - operationId = addStackInstancesByRegions(regions); - break; - case ADD_INSTANCES_BY_TARGETS: - operationId = addStackInstancesByTargets(regions, targets); - } - - logger.log(String.format("%s [%s] %s update initiated", - ResourceModel.TYPE_NAME, desiredModel.getStackSetId(), operation)); - context.setOperationId(operationId); - - } catch (final InvalidOperationException e) { - throw new CfnInvalidRequestException(e); - - } catch (final StackSetNotFoundException e) { - throw new CfnNotFoundException(e); - - } catch (final OperationInProgressException e) { - logger.log(OPERATION_IN_PROGRESS_MSG); - context.incrementRetryCounter(); - } - } -} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java index c6f2e69..53d1d08 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java @@ -5,7 +5,7 @@ import software.amazon.cloudformation.stackset.ResourceModel; /** - * Utility class to generate Physical Resource Id from {@link ResourceHandlerRequest}. + * Utility class to generate Physical Resource Id from {@link ResourceHandlerRequest< ResourceModel >}. */ public class PhysicalIdGenerator { diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java index 8d6e789..8583df0 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java @@ -3,23 +3,27 @@ import lombok.AllArgsConstructor; import lombok.Builder; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackInstanceResponse; import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesResponse; +import software.amazon.awssdk.services.cloudformation.model.Parameter; import software.amazon.awssdk.services.cloudformation.model.PermissionModels; import software.amazon.awssdk.services.cloudformation.model.StackInstanceSummary; import software.amazon.awssdk.services.cloudformation.model.StackSet; -import software.amazon.cloudformation.exceptions.CfnInternalFailureException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.stackset.CallbackContext; -import software.amazon.cloudformation.stackset.DeploymentTargets; +import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.stackset.ResourceModel; +import software.amazon.cloudformation.stackset.StackInstances; import java.util.HashSet; +import java.util.List; +import java.util.Set; import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateFromSdkAutoDeployment; import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateFromSdkParameters; import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateFromSdkTags; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToStackInstance; +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackInstanceRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.listStackInstancesRequest; +import static software.amazon.cloudformation.stackset.util.InstancesAnalyzer.aggregateStackInstances; /** * Utility class to construct {@link ResourceModel} for Read/List request based on {@link StackSet} @@ -29,17 +33,15 @@ @Builder public class ResourceModelBuilder { - private AmazonWebServicesClientProxy proxy; - private CloudFormationClient client; + private ProxyClient proxyClient; private StackSet stackSet; - private PermissionModels permissionModel; + private boolean isSelfManaged; /** * Returns the model we construct from StackSet service client using PrimaryIdentifier StackSetId * @return {@link ResourceModel} */ public ResourceModel buildModel() { - permissionModel = stackSet.permissionModel(); final String stackSetId = stackSet.stackSetId(); @@ -51,71 +53,59 @@ public ResourceModel buildModel() { .permissionModel(stackSet.permissionModelAsString()) .capabilities(new HashSet<>(stackSet.capabilitiesAsStrings())) .tags(translateFromSdkTags(stackSet.tags())) - .regions(new HashSet<>()) .parameters(translateFromSdkParameters(stackSet.parameters())) .templateBody(stackSet.templateBody()) - .deploymentTargets(DeploymentTargets.builder().build()) .build(); - if (PermissionModels.SELF_MANAGED.equals(permissionModel)) { + isSelfManaged = stackSet.permissionModel().equals(PermissionModels.SELF_MANAGED); + + if (isSelfManaged) { model.setAdministrationRoleARN(stackSet.administrationRoleARN()); model.setExecutionRoleName(stackSet.executionRoleName()); } String token = null; + final Set stackInstanceSet = new HashSet<>(); // Retrieves all Stack Instances associated with the StackSet, // Attaches regions and deploymentTargets to the constructing model do { - putRegionsAndDeploymentTargets(stackSetId, model, token); + attachStackInstances(stackSetId, isSelfManaged, stackInstanceSet, token); } while (token != null); + if (!stackInstanceSet.isEmpty()) { + final Set stackInstancesGroup = aggregateStackInstances(stackInstanceSet, isSelfManaged); + model.setStackInstancesGroup(stackInstancesGroup); + } + return model; } /** * Loop through all stack instance details and attach to the constructing model * @param stackSetId {@link ResourceModel#getStackSetId()} - * @param model {@link ResourceModel} + * @param isSelfManaged if permission model is SELF_MANAGED * @param token {@link ListStackInstancesResponse#nextToken()} */ - private void putRegionsAndDeploymentTargets( + private void attachStackInstances( final String stackSetId, - final ResourceModel model, + final boolean isSelfManaged, + final Set stackInstanceSet, String token) { - final ListStackInstancesResponse listStackInstancesResponse = proxy.injectCredentialsAndInvokeV2( - listStackInstancesRequest(token, stackSetId), client::listStackInstances); + final ListStackInstancesResponse listStackInstancesResponse = proxyClient.injectCredentialsAndInvokeV2( + listStackInstancesRequest(token, stackSetId), proxyClient.client()::listStackInstances); token = listStackInstancesResponse.nextToken(); - listStackInstancesResponse.summaries().forEach(member -> putRegionsAndDeploymentTargets(member, model)); + listStackInstancesResponse.summaries().forEach(member -> { + final List parameters = getStackInstance(member); + stackInstanceSet.add(translateToStackInstance(isSelfManaged, member, parameters)); + }); } - /** - * Helper method to attach StackInstance details to the constructing model - * @param instance {@link StackInstanceSummary} - * @param model {@link ResourceModel} - */ - private void putRegionsAndDeploymentTargets(final StackInstanceSummary instance, final ResourceModel model) { - model.getRegions().add(instance.region()); - - if (model.getRegions() == null) model.setRegions(new HashSet<>()); - - // If using SELF_MANAGED, getting accounts - if (PermissionModels.SELF_MANAGED.equals(permissionModel)) { - if (model.getDeploymentTargets().getAccounts() == null) { - model.getDeploymentTargets().setAccounts(new HashSet<>()); - } - model.getDeploymentTargets().getAccounts().add(instance.account()); - - } else if (PermissionModels.SERVICE_MANAGED.equals(permissionModel)) { - // If using SERVICE_MANAGED, getting OUIds - if (model.getDeploymentTargets().getOrganizationalUnitIds() == null) { - model.getDeploymentTargets().setOrganizationalUnitIds(new HashSet<>()); - } - model.getDeploymentTargets().getOrganizationalUnitIds().add(instance.organizationalUnitId()); - - } else { - throw new CfnServiceInternalErrorException( - String.format("%s is not valid PermissionModels", permissionModel)); - } + private List getStackInstance(final StackInstanceSummary summary) { + final DescribeStackInstanceResponse describeStackInstanceResponse = proxyClient.injectCredentialsAndInvokeV2( + describeStackInstanceRequest(summary.account(), summary.region(), summary.stackSetId()), + proxyClient.client()::describeStackInstance); + return describeStackInstanceResponse.stackInstance().parameterOverrides(); } + } diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java deleted file mode 100644 index 73ec5d0..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java +++ /dev/null @@ -1,197 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationResponse; -import software.amazon.awssdk.services.cloudformation.model.StackSetOperationStatus; -import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.stackset.CallbackContext; -import software.amazon.cloudformation.stackset.ResourceModel; -import software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations; - -import java.util.Map; - -import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetOperationRequest; - -/** - * Utility class to help keeping track on stabilization status - */ -@AllArgsConstructor -@Builder -public class Stabilizer { - - private static final String INTERNAL_FAILURE = "Internal Failure"; - private static final int ONE_DAY_IN_SECONDS = 24 * 60 * 60; - - public static final Double RATE = 1.1; - public static final int MAX_RETRIES = 60; - public static final int BASE_CALLBACK_DELAY_SECONDS = 3; - public static final int MAX_CALLBACK_DELAY_SECONDS = 30; - public static final int EXECUTION_TIMEOUT_SECONDS = ONE_DAY_IN_SECONDS; - - private AmazonWebServicesClientProxy proxy; - private CloudFormationClient client; - private Logger logger; - - /** - * Gets new exponential delay seconds based on {@link CallbackContext#getCurrentDelaySeconds}, - * However, the delay seconds will not exceed {@link Stabilizer#MAX_CALLBACK_DELAY_SECONDS} - * @param context {@link CallbackContext} - * @return New exponential delay seconds - */ - public static int getDelaySeconds(final CallbackContext context) { - final int currentDelaySeconds = context.getCurrentDelaySeconds(); - final int exponentialDelay = getExponentialDelay(currentDelaySeconds); - context.setCurrentDelaySeconds(Math.min(MAX_CALLBACK_DELAY_SECONDS, exponentialDelay)); - return context.getCurrentDelaySeconds(); - } - - /** - * Helper to get exponential delay seconds - * @param delaySeconds current delay seconds - * @return New exponential delay seconds - */ - private static int getExponentialDelay(final int delaySeconds) { - if (delaySeconds == 0) return BASE_CALLBACK_DELAY_SECONDS; - final int exponentialDelay = (int) (delaySeconds * RATE); - return delaySeconds == exponentialDelay ? delaySeconds + 1 : exponentialDelay; - } - - /** - * Checks if the operation is stabilized using {@link CallbackContext#getOperationId()} to interact with - * {@link DescribeStackSetOperationResponse} - * @param model {@link ResourceModel} - * @param context {@link CallbackContext} - * @return A boolean value indicates if operation is complete - */ - public boolean isStabilized(final ResourceModel model, final CallbackContext context) { - final String operationId = context.getOperationId(); - - // If no stabilizing operation was run. - if (operationId == null) return true; - - final String stackSetId = model.getStackSetId(); - final StackSetOperationStatus status = getStackSetOperationStatus(stackSetId, operationId); - - try { - // If it exceeds max stabilization times - if (context.incrementElapsedTime() > EXECUTION_TIMEOUT_SECONDS) { - logger.log(String.format("StackSet stabilization [%s] time out", stackSetId)); - throw new CfnServiceInternalErrorException(ResourceModel.TYPE_NAME); - } - - // If it exceeds max retries - if (context.getRetries() > MAX_RETRIES) { - logger.log(String.format("StackSet stabilization [%s] reaches max retries", stackSetId)); - throw new CfnServiceInternalErrorException(ResourceModel.TYPE_NAME); - } - return isStackSetOperationDone(status, operationId); - - } catch (final CfnServiceInternalErrorException e) { - throw new CfnNotStabilizedException(e); - } - } - - /** - * Retrieves the {@link StackSetOperationStatus} from {@link DescribeStackSetOperationResponse} - * @param stackSetId {@link ResourceModel#getStackSetId()} - * @param operationId {@link CallbackContext#getOperationId()} - * @return {@link StackSetOperationStatus} - */ - private StackSetOperationStatus getStackSetOperationStatus(final String stackSetId, final String operationId) { - final DescribeStackSetOperationResponse response = proxy.injectCredentialsAndInvokeV2( - describeStackSetOperationRequest(stackSetId, operationId), - client::describeStackSetOperation); - return response.stackSetOperation().status(); - } - - /** - * Compares {@link StackSetOperationStatus} with specific statuses - * @param status {@link StackSetOperationStatus} - * @param operationId {@link CallbackContext#getOperationId()} - * @return Boolean - */ - private Boolean isStackSetOperationDone(final StackSetOperationStatus status, final String operationId) { - switch (status) { - case SUCCEEDED: - 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)); - } - } - - /** - * Checks if this operation {@link UpdateOperations} needs to run at this stabilization runtime - * @param isRequiredToRun If the operation is necessary to operate - * @param isStabilizedStarted If the operation has been initialed - * @param previousOperation Previous {@link UpdateOperations} - * @param operation {@link UpdateOperations} - * @param model {@link ResourceModel} - * @param context {@link CallbackContext} - * @return boolean - */ - public boolean isPerformingOperation( - final boolean isRequiredToRun, - final boolean isStabilizedStarted, - final UpdateOperations previousOperation, - final UpdateOperations operation, - final ResourceModel model, - final CallbackContext context) { - - final Map operationsCompletionMap = context.getOperationsStabilizationMap(); - - // if previousOperation is not done or this operation has completed - if (!isPreviousOperationDone(context, previousOperation) || operationsCompletionMap.get(operation)) { - return false; - } - - // if it is not required to run, mark as complete - if (!isRequiredToRun) { - operationsCompletionMap.put(operation, true); - return false; - } - - // if this operation has not started yet - if (!isStabilizedStarted) return true; - - // if it is running check if it is stabilized, if so mark as complete - if (isStabilized(model, context)) operationsCompletionMap.put(operation, true); - return false; - } - - /** - * Checks if the update request is complete by retrieving the operation statuses in - * {@link CallbackContext#getOperationsStabilizationMap()} - * @param context {@link CallbackContext} - * @return boolean indicates whether the update is done - */ - public static boolean isUpdateStabilized(final CallbackContext context) { - for (Map.Entry entry : context.getOperationsStabilizationMap().entrySet()) { - if (!entry.getValue()) return false; - } - return true; - } - - /** - * Checks if previous {@link UpdateOperations} is complete - * to avoid running other operations until previous operation is done - * @param context {@link CallbackContext} - * @param previousOperation {@link UpdateOperations} - * @return boolean indicates whether the previous operation is done - */ - public static boolean isPreviousOperationDone(final CallbackContext context, - final UpdateOperations previousOperation) { - // Checks if previous operation is done. If no previous operation is running, mark as done - return previousOperation == null ? - true : context.getOperationsStabilizationMap().get(previousOperation); - } -} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstance.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstance.java new file mode 100644 index 0000000..72d0933 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstance.java @@ -0,0 +1,24 @@ +package software.amazon.cloudformation.stackset.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import software.amazon.cloudformation.stackset.Parameter; + +import java.util.Set; + +@Data +@Builder +@EqualsAndHashCode +public class StackInstance { + + @JsonProperty("Region") + private String region; + + @JsonProperty("DeploymentTarget") + private String deploymentTarget; + + @EqualsAndHashCode.Exclude + private Set parameters; +} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java index 6a61420..372b313 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java @@ -114,7 +114,7 @@ protected static Map deserializeYaml(final String templateString @SuppressWarnings("unchecked") @VisibleForTesting protected static Map deserializeJson(final String templateString) { - Map template = null; + Map template; try { JsonParser parser = new MappingJsonFactory().createParser(templateString); template = OBJECT_MAPPER.readValue(parser, Map.class); diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java deleted file mode 100644 index 4fdc417..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java +++ /dev/null @@ -1,62 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import software.amazon.awssdk.services.cloudformation.model.PermissionModels; -import software.amazon.cloudformation.stackset.ResourceModel; - -import java.util.HashSet; -import java.util.Set; - -/** - * Utility class to hold regions and targets that need to be modified during the update - */ -@Data -public class UpdatePlaceholder { - - @JsonProperty("RegionsToAdd") - private Set regionsToAdd; - - @JsonProperty("TargetsToAdd") - private Set targetsToAdd; - - @JsonProperty("RegionsToDelete") - private Set regionsToDelete; - - @JsonProperty("TargetsToDelete") - private Set targetsToDelete; - - /** - * Analyzes regions and targets that need to be modified during the update - * @param previousModel Previous {@link ResourceModel} - * @param desiredModel Desired {@link ResourceModel} - */ - public UpdatePlaceholder(final ResourceModel previousModel, final ResourceModel desiredModel) { - final Set previousRegions = previousModel.getRegions(); - final Set desiredRegion = desiredModel.getRegions(); - - Set previousTargets; - Set desiredTargets; - - if (PermissionModels.SELF_MANAGED.equals(PermissionModels.fromValue(desiredModel.getPermissionModel()))) { - previousTargets = previousModel.getDeploymentTargets().getAccounts(); - desiredTargets = desiredModel.getDeploymentTargets().getAccounts(); - } else { - previousTargets = previousModel.getDeploymentTargets().getOrganizationalUnitIds(); - desiredTargets = desiredModel.getDeploymentTargets().getOrganizationalUnitIds(); - } - - // Calculates all necessary differences that we need to take actions - regionsToAdd = new HashSet<>(desiredRegion); - regionsToAdd.removeAll(previousRegions); - targetsToAdd = new HashSet<>(desiredTargets); - targetsToAdd.removeAll(previousTargets); - - regionsToDelete = new HashSet<>(previousRegions); - regionsToDelete.removeAll(desiredRegion); - targetsToDelete = new HashSet<>(previousTargets); - targetsToDelete.removeAll(desiredTargets); - - } - -} diff --git a/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json b/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json deleted file mode 100644 index 8cfdd86..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Resources": { - "MyStack" : { - "Type" : "AWS::CloudFormation::Stack", - "Properties" : { - "TemplateURL" : "test.url" - }, - } -} \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml b/aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml deleted file mode 100644 index 2706e91..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml +++ /dev/null @@ -1,6 +0,0 @@ -Resources: - DNS: - Type: Test::Test::Example - Properties: - Name: "test.com" -Error \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/nested_stack.json b/aws-cloudformation-stackset/src/test/java/resources/nested_stack.json deleted file mode 100644 index 9e8c3b1..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/nested_stack.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Resources": { - "MyStack" : { - "Type" : "AWS::CloudFormation::Stack", - "Properties" : { - "TemplateURL" : "test.url" - } - } - } -} \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json b/aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json deleted file mode 100644 index 4571fe7..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Resources": { - "MyStack" : { - "Type" : "AWS::CloudFormation::StackSet", - "Properties" : { - "TemplateURL" : "test.url" - } - } - } -} \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/text_null.json b/aws-cloudformation-stackset/src/test/java/resources/text_null.json deleted file mode 100644 index ec747fa..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/text_null.json +++ /dev/null @@ -1 +0,0 @@ -null \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/valid.json b/aws-cloudformation-stackset/src/test/java/resources/valid.json deleted file mode 100644 index 0340a5b..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/valid.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "Parameters": { - "DomainName": { - "Type": "String", - "Default": "myexample.com" - } - }, - "Resources": { - "BasicHealthCheck": { - "Type": "AWS::Route53::HealthCheck", - "Properties": { - "HealthCheckConfig": { - "RequestInterval": 10, - "FullyQualifiedDomainName": { - "Ref": "DomainName" - }, - "IPAddress": "98.139.180.149", - "Port": "88", - "ResourcePath": "/docs/route-53-health-check.html", - "Type": "HTTP" - }, - "HealthCheckTags": [ - { - "Key": "A", - "Value": "1" - }, - { - "Key": "B", - "Value": "1" - }, - { - "Key": "C", - "Value": "1" - } - ] - } - } - }, - "Outputs": { - "HealthCheckId": { - "Value": { - "Ref": "BasicHealthCheck" - } - } - } -} \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/resources/valid.yaml b/aws-cloudformation-stackset/src/test/java/resources/valid.yaml deleted file mode 100644 index da653dd..0000000 --- a/aws-cloudformation-stackset/src/test/java/resources/valid.yaml +++ /dev/null @@ -1,27 +0,0 @@ -Parameters: - DomainName: - Type: String - Default: myexample.com -Resources: - BasicHealthCheck: - Type: AWS::Route53::HealthCheck - Properties: - HealthCheckConfig: - RequestInterval: 10 - FullyQualifiedDomainName: - Ref: DomainName - IPAddress: 98.139.180.149 - Port: "88" - ResourcePath: /docs/route-53-health-check.html - Type: HTTP - HealthCheckTags: - - Key: A - Value: "1" - - Key: B - Value: "1" - - Key: C - Value: "1" -Outputs: - HealthCheckId: - Value: - Ref: BasicHealthCheck \ No newline at end of file diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java new file mode 100644 index 0000000..2787c26 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java @@ -0,0 +1,47 @@ +package software.amazon.cloudformation.stackset; + +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Credentials; +import software.amazon.cloudformation.proxy.LoggerProxy; +import software.amazon.cloudformation.proxy.ProxyClient; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class AbstractTestBase { + protected static final Credentials MOCK_CREDENTIALS; + protected static final LoggerProxy logger; + + static { + MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); + logger = new LoggerProxy(); + } + + static ProxyClient MOCK_PROXY( + final AmazonWebServicesClientProxy proxy, + final CloudFormationClient sdkClient) { + return new ProxyClient() { + + @Override + public ResponseT + injectCredentialsAndInvokeV2(RequestT request, Function requestFunction) { + return proxy.injectCredentialsAndInvokeV2(request, requestFunction); + } + + @Override + public CompletableFuture + injectCredentialsAndInvokeV2Aync( + RequestT request, Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public CloudFormationClient client() { + return sdkClient; + } + }; + } +} diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java index bcfb432..90cd7a7 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java @@ -5,317 +5,140 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import software.amazon.awssdk.services.cloudformation.model.AlreadyExistsException; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetRequest; 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.awssdk.services.cloudformation.model.ListStackInstancesRequest; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; -import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -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.Validator; +import java.time.Duration; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static software.amazon.cloudformation.stackset.util.Stabilizer.BASE_CALLBACK_DELAY_SECONDS; -import static software.amazon.cloudformation.stackset.util.Stabilizer.EXECUTION_TIMEOUT_SECONDS; -import static software.amazon.cloudformation.stackset.util.Stabilizer.MAX_CALLBACK_DELAY_SECONDS; -import static software.amazon.cloudformation.stackset.util.Stabilizer.MAX_RETRIES; +import static org.mockito.Mockito.verify; import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SERVICE_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LOGICAL_ID; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_ID_1; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_RUNNING_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_STOPPED_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_SUCCEED_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.REQUEST_TOKEN; -import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_MODEL; -import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_TEMPLATE_BODY_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; @ExtendWith(MockitoExtension.class) -public class CreateHandlerTest { +public class CreateHandlerTest extends AbstractTestBase { private CreateHandler handler; private ResourceHandlerRequest request; @Mock - private Validator validator; + private AmazonWebServicesClientProxy proxy; @Mock - private AmazonWebServicesClientProxy proxy; + private ProxyClient proxyClient; @Mock - private Logger logger; + CloudFormationClient sdkClient; @BeforeEach public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); - validator = mock(Validator.class); - handler = CreateHandler.builder().validator(validator).build(); - request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_MODEL) - .logicalResourceIdentifier(LOGICAL_ID) - .clientRequestToken(REQUEST_TOKEN) - .build(); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudFormationClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + handler = new CreateHandler(); } @Test - public void handleRequest_SimpleSuccess() { + public void handleRequest_ServiceManagedSS_SimpleSuccess() { - doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) + request = ResourceHandlerRequest.builder() + .desiredResourceState(SERVICE_MANAGED_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) .build(); + doReturn(CREATE_STACK_SET_RESPONSE).when(proxyClient.client()) + .createStackSet(any(CreateStackSetRequest.class)); + doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .createStackInstances(any(CreateStackInstancesRequest.class)); + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxyClient.client()) + .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } @Test - public void handleRequest_TemplateUrl_CreateNotYetStarted_InProgress() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doReturn(CREATE_STACK_SET_RESPONSE, - CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_TemplateBody_CreateNotYetStarted_InProgress() { + public void handleRequest_SelfManagedSS_SimpleSuccess() { request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_TEMPLATE_BODY_MODEL) + .desiredResourceState(SELF_MANAGED_MODEL) .logicalResourceIdentifier(LOGICAL_ID) .clientRequestToken(REQUEST_TOKEN) .build(); - doReturn(CREATE_STACK_SET_RESPONSE, - CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - - @Test - public void handleRequest_CreateNotYetStabilized_InProgress() { - - doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) - .build(); + doReturn(CREATE_STACK_SET_RESPONSE).when(proxyClient.client()) + .createStackSet(any(CreateStackSetRequest.class)); + doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .createStackInstances(any(CreateStackInstancesRequest.class)); + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxyClient.client()) + .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } - @Test - public void handleRequest_OperationStopped_CfnNotStabilizedException() { - - doReturn(OPERATION_STOPPED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - assertThrows(CfnNotStabilizedException.class, - () -> handler.handleRequest(proxy, request, inputContext, logger)); - } - - @Test - public void handleRequest_OperationTimesOut_CfnNotStabilizedException() { - - doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .elapsedTime(EXECUTION_TIMEOUT_SECONDS) - .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) - .build(); - - assertThrows(CfnNotStabilizedException.class, - () -> handler.handleRequest(proxy, request, inputContext, logger)); - } - - @Test - public void handleRequest_OperationMaxRetries_CfnNotStabilizedException() { - - doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .retries(MAX_RETRIES + 1) - .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) - .build(); - - assertThrows(CfnNotStabilizedException.class, - () -> handler.handleRequest(proxy, request, inputContext, logger)); - } - - @Test - public void handlerRequest_AlreadyExistsException() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doThrow(AlreadyExistsException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); - - assertThrows(CfnAlreadyExistsException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_LimitExceededException() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doThrow(LimitExceededException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); - - assertThrows(CfnServiceLimitExceededException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - @Test public void handlerRequest_InsufficientCapabilitiesException() { - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doThrow(InsufficientCapabilitiesException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); - - assertThrows(CfnInvalidRequestException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_StackSetNotFoundException() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doReturn(CREATE_STACK_SET_RESPONSE).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); - - doThrow(StackSetNotFoundException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackInstancesRequest.class), any()); - - assertThrows(CfnNotFoundException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_OperationInProgressException() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doReturn(CREATE_STACK_SET_RESPONSE).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); - - doThrow(OperationInProgressException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(CreateStackInstancesRequest.class), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .retries(1) + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) .build(); + doThrow(InsufficientCapabilitiesException.class).when(proxyClient.client()) + .createStackSet(any(CreateStackSetRequest.class)); + final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getErrorCode()).isNotNull(); } } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java index dacdb25..e1b8f4a 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java @@ -1,35 +1,44 @@ package software.amazon.cloudformation.stackset; -import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesRequest; -import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException; -import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.proxy.OperationStatus; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; +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.DeleteStackSetResponse; +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.cloudformation.proxy.AmazonWebServicesClientProxy; +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 java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_ID_1; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_RUNNING_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SERVICE_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LOGICAL_ID; import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_SUCCEED_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_MODEL; -import static software.amazon.cloudformation.stackset.util.Stabilizer.BASE_CALLBACK_DELAY_SECONDS; +import static software.amazon.cloudformation.stackset.util.TestUtils.REQUEST_TOKEN; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; @ExtendWith(MockitoExtension.class) -public class DeleteHandlerTest { +public class DeleteHandlerTest extends AbstractTestBase { private DeleteHandler handler; @@ -39,143 +48,43 @@ public class DeleteHandlerTest { private AmazonWebServicesClientProxy proxy; @Mock - private Logger logger; + private ProxyClient proxyClient; + + @Mock + CloudFormationClient sdkClient; @BeforeEach public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); handler = new DeleteHandler(); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudFormationClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_MODEL) + .desiredResourceState(SERVICE_MANAGED_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) .build(); } @Test public void handleRequest_SimpleSuccess() { - doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .deleteStackInstances(any(DeleteStackInstancesRequest.class)); + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxyClient.client()) + .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + doReturn(DELETE_STACK_SET_RESPONSE).when(proxyClient.client()) + .deleteStackSet(any(DeleteStackSetRequest.class)); - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } - - @Test - public void handleRequest_DeleteNotYetStarted_InProgress() { - - doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_DeleteNotYetStabilized_InProgress() { - - doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) - .operationId(OPERATION_ID_1) - .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handlerRequest_DeleteStackSet_StackSetNotFoundException() { - - doThrow(StackSetNotFoundException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - assertThrows(CfnNotFoundException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_DeleteInstances_StackSetNotFoundException() { - - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) - .build(); - - doThrow(StackSetNotFoundException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - assertThrows(CfnNotFoundException.class, - () -> handler.handleRequest(proxy, request, inputContext, logger)); - - } - - @Test - public void handlerRequest_OperationInProgressException() { - - doThrow(OperationInProgressException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(DeleteStackInstancesRequest.class), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .retries(1) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java index 3b2f502..635d20b 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java @@ -1,9 +1,16 @@ package software.amazon.cloudformation.stackset; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackInstanceRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetRequest; +import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesResponse; +import software.amazon.awssdk.services.cloudformation.model.ListStackSetsRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; 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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,55 +18,77 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.Duration; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_1; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_2; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_3; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_4; +import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SERVICE_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_STACK_SETS_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.READ_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_FOR_READ; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; @ExtendWith(MockitoExtension.class) -public class ListHandlerTest { +public class ListHandlerTest extends AbstractTestBase { + + private ListHandler handler; + + private ResourceHandlerRequest request; @Mock private AmazonWebServicesClientProxy proxy; @Mock - private Logger logger; + private ProxyClient proxyClient; + + @Mock + CloudFormationClient sdkClient; @BeforeEach public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); + handler = new ListHandler(); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudFormationClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + request = ResourceHandlerRequest.builder() + .desiredResourceState(READ_MODEL) + .build(); } @Test - public void handleRequest_SimpleSuccess() { - final ListHandler handler = new ListHandler(); - - final ResourceModel model = ResourceModel.builder().build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); + public void handleRequest_SelfManagedSS_Success() { - doReturn(LIST_STACK_SETS_RESPONSE, - DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE, - LIST_SERVICE_MANAGED_STACK_SET_RESPONSE) - .when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); + doReturn(LIST_STACK_SETS_RESPONSE).when(proxyClient.client()) + .listStackSets(any(ListStackSetsRequest.class)); + doReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE).when(proxyClient.client()) + .describeStackSet(any(DescribeStackSetRequest.class)); + doReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE).when(proxyClient.client()) + .listStackInstances(any(ListStackInstancesRequest.class)); + doReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4).when(proxyClient.client()) + .describeStackInstance(any(DescribeStackInstanceRequest.class)); - final ProgressEvent response = - handler.handleRequest(proxy, request, null, logger); + final ProgressEvent response + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isNull(); - assertThat(response.getResourceModels()).containsExactly(SERVICE_MANAGED_MODEL); + assertThat(response.getResourceModels()).containsExactly(SELF_MANAGED_MODEL_FOR_READ); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java index a656d0c..23efedb 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java @@ -1,13 +1,15 @@ package software.amazon.cloudformation.stackset; -import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; -import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; -import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackInstanceRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetRequest; +import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,23 +18,24 @@ import org.mockito.junit.jupiter.MockitoExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_1; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_2; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_3; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_4; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SERVICE_MANAGED_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.READ_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_FOR_READ; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; @ExtendWith(MockitoExtension.class) -public class ReadHandlerTest { +public class ReadHandlerTest extends AbstractTestBase { private ReadHandler handler; @@ -42,68 +45,45 @@ public class ReadHandlerTest { private AmazonWebServicesClientProxy proxy; @Mock - private Logger logger; + private ProxyClient proxyClient; + + @Mock + CloudFormationClient sdkClient; @BeforeEach public void setup() { handler = new ReadHandler(); - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudFormationClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); request = ResourceHandlerRequest.builder() .desiredResourceState(READ_MODEL) .build(); } - @Test - public void handleRequest_ServiceManagedSS_Success() { - - doReturn(DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE, - LIST_SERVICE_MANAGED_STACK_SET_RESPONSE) - .when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(SERVICE_MANAGED_MODEL); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - @Test public void handleRequest_SelfManagedSS_Success() { - doReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE, - LIST_SELF_MANAGED_STACK_SET_RESPONSE) - .when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); + doReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE).when(proxyClient.client()) + .describeStackSet(any(DescribeStackSetRequest.class)); + doReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE).when(proxyClient.client()) + .listStackInstances(any(ListStackInstancesRequest.class)); + doReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4).when(proxyClient.client()) + .describeStackInstance(any(DescribeStackInstanceRequest.class)); final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_FOR_READ); assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } - - @Test - public void handlerRequest_StackSetNotFoundException() { - - doThrow(StackSetNotFoundException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - assertThrows(CfnNotFoundException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java index 5e886f8..241cbfa 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java @@ -1,436 +1,93 @@ package software.amazon.cloudformation.stackset; +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; +import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationRequest; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetRequest; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import software.amazon.awssdk.services.cloudformation.model.InvalidOperationException; -import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException; -import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.proxy.OperationStatus; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.stackset.util.Validator; - -import java.util.EnumMap; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_ID_1; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_ID_2; -import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_RUNNING_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LOGICAL_ID; +import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_SUCCEED_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.REQUEST_TOKEN; import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL; -import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_MODEL; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_SELF_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATE_STACK_INSTANCES_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATE_STACK_SET_RESPONSE; -import static software.amazon.cloudformation.stackset.util.EnumUtils.UpdateOperations; -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.BASE_CALLBACK_DELAY_SECONDS; @ExtendWith(MockitoExtension.class) -public class UpdateHandlerTest { +public class UpdateHandlerTest extends AbstractTestBase { private UpdateHandler handler; private ResourceHandlerRequest request; @Mock - private Validator validator; + private AmazonWebServicesClientProxy proxy; @Mock - private AmazonWebServicesClientProxy proxy; + private ProxyClient proxyClient; @Mock - private Logger logger; + CloudFormationClient sdkClient; @BeforeEach public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); - validator = mock(Validator.class); - handler = new UpdateHandler(validator); - request = ResourceHandlerRequest.builder() - .desiredResourceState(UPDATED_MODEL) - .previousResourceState(SIMPLE_MODEL) - .build(); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudFormationClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + handler = new UpdateHandler(); } @Test - public void handleRequest_NotUpdatable_Success() { + public void handleRequest_SelfManagedSS_SimpleSuccess() { request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_MODEL) - .previousResourceState(SIMPLE_MODEL) + .desiredResourceState(UPDATED_SELF_MANAGED_MODEL) + .previousResourceState(SELF_MANAGED_MODEL) .build(); - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_AllUpdatesStabilized_Success() { - - final Map updateOperationsMap = new EnumMap<>(UpdateOperations.class); - updateOperationsMap.put(STACK_SET_CONFIGS, true); - updateOperationsMap.put(DELETE_INSTANCES_BY_REGIONS, true); - updateOperationsMap.put(DELETE_INSTANCES_BY_TARGETS, true); - updateOperationsMap.put(ADD_INSTANCES_BY_REGIONS, true); - updateOperationsMap.put(ADD_INSTANCES_BY_TARGETS, true); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByTargetsStarted(true) - .deleteStacksByRegionsStarted(true) - .addStacksByRegionsStarted(true) - .addStacksByTargetsStarted(true) - .operationId(OPERATION_ID_1) - .operationsStabilizationMap(updateOperationsMap) - .build(); + doReturn(UPDATE_STACK_SET_RESPONSE).when(proxyClient.client()) + .updateStackSet(any(UpdateStackSetRequest.class)); + doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .createStackInstances(any(CreateStackInstancesRequest.class)); + doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .deleteStackInstances(any(DeleteStackInstancesRequest.class)); + doReturn(UPDATE_STACK_INSTANCES_RESPONSE).when(proxyClient.client()) + .updateStackInstances(any(UpdateStackInstancesRequest.class)); + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxyClient.client()) + .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_UpdateStackSetNotStarted_InProgress() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doReturn(UPDATE_STACK_SET_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_UpdateStackSetNotStabilized_InProgress() { - - doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) - .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_DeleteStacksRegionsNotStarted_InProgress() { - - doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .operationId(OPERATION_ID_2) - .build(); - - inputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - outputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_SelfManaged_DeleteStacksRegionsNotStarted_InProgress() { - request = ResourceHandlerRequest.builder() - .desiredResourceState(UPDATED_SELF_MANAGED_MODEL) - .previousResourceState(SELF_MANAGED_MODEL) - .build(); - - doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .operationId(OPERATION_ID_2) - .build(); - - inputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - outputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_DeleteStacksTargetsNotStarted_InProgress() { - - doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .operationId(OPERATION_ID_2) - .build(); - - inputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - outputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_AddStacksRegionsNotStarted_InProgress() { - - doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .operationId(OPERATION_ID_2) - .build(); - - inputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - outputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true); - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_AddStacksTargetsNotStarted_InProgress() { - - doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext inputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) - .operationId(OPERATION_ID_2) - .build(); - - inputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true); - inputContext.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_REGIONS, true); - - final CallbackContext outputContext = CallbackContext.builder() - .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) - .addStacksByTargetsStarted(true) - .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .build(); - - outputContext.getOperationsStabilizationMap().put(STACK_SET_CONFIGS, true); - outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_REGIONS, true); - outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES_BY_TARGETS, true); - outputContext.getOperationsStabilizationMap().put(ADD_INSTANCES_BY_REGIONS, true); - - final ProgressEvent response - = handler.handleRequest(proxy, request, inputContext, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - - @Test - public void handlerRequest_InvalidOperationException() { - - doThrow(InvalidOperationException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - assertThrows(CfnInvalidRequestException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_StackSetNotFoundException() { - - doThrow(StackSetNotFoundException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - assertThrows(CfnNotFoundException.class, - () -> handler.handleRequest(proxy, request, null, logger)); - - } - - @Test - public void handlerRequest_OperationInProgressException() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - - doThrow(OperationInProgressException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); - - final CallbackContext outputContext = CallbackContext.builder() - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .retries(1) - .build(); - - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); - assertThat(response.getCallbackContext()).isEqualTo(outputContext); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(outputContext.getCurrentDelaySeconds()); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java index 0e4eaf2..5486ffb 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java @@ -10,17 +10,17 @@ public class PropertyTranslatorTest { @Test - public void testNull_translateFromSdkParameters_isNull() { + public void test_translateFromSdkParameters_IfIsNull() { assertThat(translateFromSdkParameters(null)).isNull(); } @Test - public void test_translateToSdkTags_isNull() { + public void test_translateToSdkTags_IfIsNull() { assertThat(translateToSdkTags(null)).isNull(); } @Test - public void test_translateFromSdkTags_isNull() { + public void test_translateFromSdkTags_IfIsNull() { assertThat(translateFromSdkTags(null)).isNull(); } } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java index e409a87..5a2bf2e 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java @@ -1,94 +1,13 @@ package software.amazon.cloudformation.stackset.util; import org.junit.jupiter.api.Test; -import software.amazon.cloudformation.stackset.CallbackContext; -import software.amazon.cloudformation.stackset.ResourceModel; - -import java.util.HashSet; import static org.assertj.core.api.Assertions.assertThat; -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.isEquals; -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.TestUtils.ADMINISTRATION_ROLE_ARN; -import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIPTION; -import static software.amazon.cloudformation.stackset.util.TestUtils.EXECUTION_ROLE_NAME; -import static software.amazon.cloudformation.stackset.util.TestUtils.REGIONS; import static software.amazon.cloudformation.stackset.util.TestUtils.TAGS; -import static software.amazon.cloudformation.stackset.util.TestUtils.TAGS_TO_UPDATE; -import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_BODY; -import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_URL; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_ADMINISTRATION_ROLE_ARN; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_DESCRIPTION; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_EXECUTION_ROLE_NAME; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_TEMPLATE_BODY; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_TEMPLATE_URL; public class ComparatorTest { - @Test - public void testIsStackSetConfigEquals() { - - final ResourceModel testPreviousModel = ResourceModel.builder().tags(TAGS).build(); - final ResourceModel testDesiredModel = ResourceModel.builder().tags(TAGS_TO_UPDATE).build(); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - - testDesiredModel.setTags(TAGS); - testDesiredModel.setAdministrationRoleARN(UPDATED_ADMINISTRATION_ROLE_ARN); - testPreviousModel.setAdministrationRoleARN(ADMINISTRATION_ROLE_ARN); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - - testDesiredModel.setAdministrationRoleARN(ADMINISTRATION_ROLE_ARN); - testDesiredModel.setDescription(UPDATED_DESCRIPTION); - testPreviousModel.setDescription(DESCRIPTION); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - - testDesiredModel.setDescription(DESCRIPTION); - testDesiredModel.setExecutionRoleName(UPDATED_EXECUTION_ROLE_NAME); - testPreviousModel.setExecutionRoleName(EXECUTION_ROLE_NAME); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - - testDesiredModel.setExecutionRoleName(EXECUTION_ROLE_NAME); - testDesiredModel.setTemplateURL(UPDATED_TEMPLATE_URL); - testPreviousModel.setTemplateURL(TEMPLATE_URL); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - - testDesiredModel.setTemplateURL(null); - testPreviousModel.setTemplateURL(null); - - testDesiredModel.setTemplateBody(UPDATED_TEMPLATE_BODY); - testPreviousModel.setTemplateBody(TEMPLATE_BODY); - - assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); - } - - @Test - public void testIsDeletingStackInstances() { - // Both are empty - assertThat(isDeletingStackInstances(new HashSet<>(), new HashSet<>(), CallbackContext.builder().build())) - .isFalse(); - // targetsToDelete is empty - assertThat(isDeletingStackInstances(REGIONS, new HashSet<>(), CallbackContext.builder().build())) - .isTrue(); - } - - @Test - public void testisAddingStackInstances() { - // Both are empty - assertThat(isAddingStackInstances(new HashSet<>(), new HashSet<>(), CallbackContext.builder().build())) - .isFalse(); - // targetsToDelete is empty - assertThat(isAddingStackInstances(REGIONS, new HashSet<>(), CallbackContext.builder().build())) - .isTrue(); - } - @Test public void testIsEquals() { assertThat(isEquals(null, TAGS)).isFalse(); diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java index 801004e..1759beb 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java @@ -4,12 +4,15 @@ import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesResponse; import software.amazon.awssdk.services.cloudformation.model.CreateStackSetResponse; import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesResponse; +import software.amazon.awssdk.services.cloudformation.model.DeleteStackSetResponse; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackInstanceResponse; import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationResponse; import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetResponse; import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesResponse; import software.amazon.awssdk.services.cloudformation.model.ListStackSetsResponse; import software.amazon.awssdk.services.cloudformation.model.Parameter; import software.amazon.awssdk.services.cloudformation.model.PermissionModels; +import software.amazon.awssdk.services.cloudformation.model.StackInstance; import software.amazon.awssdk.services.cloudformation.model.StackInstanceSummary; import software.amazon.awssdk.services.cloudformation.model.StackSet; import software.amazon.awssdk.services.cloudformation.model.StackSetOperation; @@ -22,6 +25,7 @@ import software.amazon.cloudformation.stackset.DeploymentTargets; import software.amazon.cloudformation.stackset.OperationPreferences; import software.amazon.cloudformation.stackset.ResourceModel; +import software.amazon.cloudformation.stackset.StackInstances; import java.util.Arrays; import java.util.HashSet; @@ -88,6 +92,12 @@ public class TestUtils { public final static String US_EAST_2 = "us-east-2"; public final static String US_WEST_2 = "us-west-2"; + public final static String EU_EAST_1 = "eu-east-1"; + public final static String EU_EAST_2 = "eu-east-2"; + public final static String EU_EAST_3 = "eu-east-3"; + public final static String EU_CENTRAL_1 = "eu-central-1"; + public final static String EU_NORTH_1 = "eu-north-1"; + public final static String ORGANIZATION_UNIT_ID_1 = "ou-example-1"; public final static String ORGANIZATION_UNIT_ID_2 = "ou-example-2"; public final static String ORGANIZATION_UNIT_ID_3 = "ou-example-3"; @@ -150,8 +160,11 @@ public class TestUtils { public final static Map NEW_RESOURCE_TAGS = ImmutableMap.of( "key1", "val1", "key2updated", "val2updated", "key3", "val3"); - public final static Set REGIONS = new HashSet<>(Arrays.asList(US_WEST_1, US_EAST_1)); - public final static Set UPDATED_REGIONS = new HashSet<>(Arrays.asList(US_WEST_2, US_EAST_2)); + public final static Set REGIONS_1 = new HashSet<>(Arrays.asList(US_WEST_1, US_EAST_1)); + public final static Set UPDATED_REGIONS_1 = new HashSet<>(Arrays.asList(US_WEST_1, US_EAST_2)); + + public final static Set REGIONS_2 = new HashSet<>(Arrays.asList(EU_EAST_1, EU_EAST_2)); + public final static Set UPDATED_REGIONS_2 = new HashSet<>(Arrays.asList(EU_EAST_3, EU_CENTRAL_1)); public final static DeploymentTargets SERVICE_MANAGED_TARGETS = DeploymentTargets.builder() .organizationalUnitIds(new HashSet<>(Arrays.asList( @@ -165,12 +178,12 @@ public class TestUtils { public final static DeploymentTargets SELF_MANAGED_TARGETS = DeploymentTargets.builder() .accounts(new HashSet<>(Arrays.asList( - ACCOUNT_ID_1, ACCOUNT_ID_2))) + ACCOUNT_ID_1))) .build(); public final static DeploymentTargets UPDATED_SELF_MANAGED_TARGETS = DeploymentTargets.builder() .accounts(new HashSet<>(Arrays.asList( - ACCOUNT_ID_3, ACCOUNT_ID_4))) + ACCOUNT_ID_2))) .build(); public final static Set CAPABILITIES = new HashSet<>(Arrays.asList( @@ -181,7 +194,6 @@ public class TestUtils { .maxConcurrentCount(1) .build(); - public final static Set TAGS = new HashSet<>(Arrays.asList( new software.amazon.cloudformation.stackset.Tag("key1", "val1"), new software.amazon.cloudformation.stackset.Tag("key2", "val2"), @@ -239,14 +251,55 @@ public class TestUtils { public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_7 = StackInstanceSummary.builder() .account(ACCOUNT_ID_2) - .region(US_EAST_1) + .region(EU_EAST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_8 = StackInstanceSummary.builder() .account(ACCOUNT_ID_2) + .region(EU_EAST_2) + .build(); + + public final static StackInstance STACK_INSTANCE_1 = StackInstance.builder() + .account(ACCOUNT_ID_1) + .region(US_EAST_1) + .parameterOverrides(SDK_PARAMETER_1) + .build(); + + public final static DescribeStackInstanceResponse DESCRIBE_STACK_INSTANCE_RESPONSE_1 = + DescribeStackInstanceResponse.builder() + .stackInstance(STACK_INSTANCE_1) + .build(); + + public final static StackInstance STACK_INSTANCE_2 = StackInstance.builder() + .account(ACCOUNT_ID_1) .region(US_WEST_1) + .parameterOverrides(SDK_PARAMETER_1) .build(); + public final static DescribeStackInstanceResponse DESCRIBE_STACK_INSTANCE_RESPONSE_2 = + DescribeStackInstanceResponse.builder() + .stackInstance(STACK_INSTANCE_2) + .build(); + + public final static StackInstance STACK_INSTANCE_3 = StackInstance.builder() + .account(ACCOUNT_ID_2) + .region(EU_EAST_1) + .build(); + + public final static DescribeStackInstanceResponse DESCRIBE_STACK_INSTANCE_RESPONSE_3 = + DescribeStackInstanceResponse.builder() + .stackInstance(STACK_INSTANCE_3) + .build(); + + public final static StackInstance STACK_INSTANCE_4 = StackInstance.builder() + .account(ACCOUNT_ID_2) + .region(EU_EAST_2) + .build(); + + public final static DescribeStackInstanceResponse DESCRIBE_STACK_INSTANCE_RESPONSE_4 = + DescribeStackInstanceResponse.builder() + .stackInstance(STACK_INSTANCE_4) + .build(); public final static List SERVICE_MANAGED_STACK_INSTANCE_SUMMARIES = Arrays.asList( STACK_INSTANCE_SUMMARY_1, STACK_INSTANCE_SUMMARY_2, STACK_INSTANCE_SUMMARY_3, STACK_INSTANCE_SUMMARY_4); @@ -260,15 +313,46 @@ public class TestUtils { .enabled(true) .build(); + public final static StackInstances STACK_INSTANCES_1 = StackInstances.builder() + .regions(REGIONS_1) + .deploymentTargets(SERVICE_MANAGED_TARGETS) + .build(); + + public final static StackInstances STACK_INSTANCES_2 = StackInstances.builder() + .regions(REGIONS_2) + .deploymentTargets(UPDATED_SERVICE_MANAGED_TARGETS) + .build(); + + public final static StackInstances SELF_MANAGED_STACK_INSTANCES_1 = StackInstances.builder() + .regions(REGIONS_1) + .deploymentTargets(SELF_MANAGED_TARGETS) + .parameterOverrides(new HashSet<>(Arrays.asList(PARAMETER_1))) + .build(); + + public final static StackInstances SELF_MANAGED_STACK_INSTANCES_2 = StackInstances.builder() + .regions(REGIONS_2) + .deploymentTargets(UPDATED_SELF_MANAGED_TARGETS) + .build(); + + public final static StackInstances SELF_MANAGED_STACK_INSTANCES_3 = StackInstances.builder() + .regions(UPDATED_REGIONS_1) + .deploymentTargets(SELF_MANAGED_TARGETS) + .parameterOverrides(new HashSet<>(Arrays.asList(PARAMETER_2))) + .build(); + + public final static StackInstances SELF_MANAGED_STACK_INSTANCES_4 = StackInstances.builder() + .regions(REGIONS_2) + .deploymentTargets(UPDATED_SELF_MANAGED_TARGETS) + .parameterOverrides(new HashSet<>(Arrays.asList(PARAMETER_1))) + .build(); + public final static StackSetSummary STACK_SET_SUMMARY_1 = StackSetSummary.builder() - .autoDeployment(SDK_AUTO_DEPLOYMENT) .description(DESCRIPTION) - .permissionModel(PermissionModels.SERVICE_MANAGED) + .permissionModel(PermissionModels.SELF_MANAGED) .stackSetId(STACK_SET_ID) .stackSetName(STACK_SET_NAME) .build(); - public final static StackSet SERVICE_MANAGED_STACK_SET = StackSet.builder() .stackSetId(STACK_SET_ID) .stackSetName(STACK_SET_NAME) @@ -287,53 +371,67 @@ public class TestUtils { .capabilitiesWithStrings(CAPABILITIES) .description(DESCRIPTION) .parameters(SDK_PARAMETER_1, SDK_PARAMETER_2) + .templateBody(TEMPLATE_BODY) .permissionModel(PermissionModels.SELF_MANAGED) .tags(TAGGED_RESOURCES) .build(); public final static ResourceModel SERVICE_MANAGED_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SERVICE_MANAGED_TARGETS) .permissionModel(SERVICE_MANAGED) .capabilities(CAPABILITIES) .description(DESCRIPTION) .autoDeployment(AUTO_DEPLOYMENT) - .regions(REGIONS) + .templateBody(TEMPLATE_BODY) + .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) .tags(TAGS) .build(); public final static ResourceModel SELF_MANAGED_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SELF_MANAGED_TARGETS) .permissionModel(SELF_MANAGED) .capabilities(CAPABILITIES) + .templateBody(TEMPLATE_BODY) .description(DESCRIPTION) - .regions(REGIONS) + .stackInstancesGroup( + new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_1, SELF_MANAGED_STACK_INSTANCES_2))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) .tags(TAGS) .build(); public final static ResourceModel UPDATED_SELF_MANAGED_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(UPDATED_SELF_MANAGED_TARGETS) .permissionModel(SELF_MANAGED) .capabilities(CAPABILITIES) - .regions(UPDATED_REGIONS) + .templateBody(TEMPLATE_BODY) + .stackInstancesGroup( + new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_3, SELF_MANAGED_STACK_INSTANCES_4))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_3))) .tags(TAGS) .build(); + public final static ResourceModel SELF_MANAGED_MODEL_FOR_READ = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SELF_MANAGED) + .capabilities(CAPABILITIES) + .templateBody(TEMPLATE_BODY) + .description(DESCRIPTION) + .stackInstancesGroup( + new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_1, SELF_MANAGED_STACK_INSTANCES_2))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + public final static ResourceModel READ_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) .build(); public final static ResourceModel SIMPLE_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SERVICE_MANAGED_TARGETS) - .permissionModel(SERVICE_MANAGED) - .autoDeployment(AUTO_DEPLOYMENT) - .regions(REGIONS) + .permissionModel(SELF_MANAGED) + .stackInstancesGroup( + new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_1, SELF_MANAGED_STACK_INSTANCES_2))) .templateURL(TEMPLATE_URL) .tags(TAGS) .operationPreferences(OPERATION_PREFERENCES) @@ -341,10 +439,9 @@ public class TestUtils { public final static ResourceModel SIMPLE_TEMPLATE_BODY_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SERVICE_MANAGED_TARGETS) + .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) .permissionModel(SERVICE_MANAGED) .autoDeployment(AUTO_DEPLOYMENT) - .regions(REGIONS) .templateBody(TEMPLATE_BODY) .tags(TAGS) .operationPreferences(OPERATION_PREFERENCES) @@ -353,10 +450,9 @@ public class TestUtils { public final static ResourceModel UPDATED_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(UPDATED_SERVICE_MANAGED_TARGETS) .permissionModel(SERVICE_MANAGED) .autoDeployment(AUTO_DEPLOYMENT) - .regions(UPDATED_REGIONS) + .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) .templateURL(UPDATED_TEMPLATE_URL) .tags(TAGS_TO_UPDATE) .build(); @@ -392,6 +488,9 @@ public class TestUtils { .operationId(OPERATION_ID_1) .build(); + public final static DeleteStackSetResponse DELETE_STACK_SET_RESPONSE = + DeleteStackSetResponse.builder().build(); + public final static UpdateStackSetResponse UPDATE_STACK_SET_RESPONSE = UpdateStackSetResponse.builder() .operationId(OPERATION_ID_1) diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java index e4885d1..9be9856 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java @@ -1,6 +1,5 @@ package software.amazon.cloudformation.stackset.util; -import com.amazonaws.util.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,14 +10,11 @@ import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; -import java.io.IOException; import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_BODY; @@ -27,15 +23,6 @@ @ExtendWith(MockitoExtension.class) public class ValidatorTest { - private static final String TEMPLATES_PATH_PREFIX = "/java/resources/"; - - private static final List INVALID_TEMPLATE_FILENAMES = Arrays.asList( - "nested_stack.json", "nested_stackset.json", "invalid_format.json", - "invalid_format.yaml"); - - private static final List VALID_TEMPLATE_FILENAMES = Arrays.asList( - "valid.json", "valid.yaml"); - private static final List INVALID_S3_URLS = Arrays.asList( "http://s3-us-west-2.amazonaws.com//object.json", "nhttp://s3-us-west-2.amazonaws.com/test/", "invalid_url", "http://s3-us-west-2.amazonaws.com"); @@ -56,23 +43,6 @@ public void setup() { validator = spy(Validator.class); } - @Test - public void testValidateTemplate_InvalidFormatError() { - for (final String filename : INVALID_TEMPLATE_FILENAMES) { - doReturn(read(TEMPLATES_PATH_PREFIX + filename)).when(validator).getUrlContent(any(), any()); - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, null, TEMPLATE_URL, logger)); - } - } - - @Test - public void testValidateTemplate_ValidS3Format() { - for (final String filename : VALID_TEMPLATE_FILENAMES) { - doReturn(read(TEMPLATES_PATH_PREFIX + filename)).when(validator).getUrlContent(any(), any()); - assertDoesNotThrow(() -> validator.validateTemplate(proxy, null, TEMPLATE_URL, logger)); - } - } - @Test public void testValidateTemplate_InvalidUri() { for (final String invalidS3Url : INVALID_S3_URLS) { @@ -97,12 +67,4 @@ public void testValidateTemplate_BothBodyAndUriNotExist() { public void testValidateTemplate_ValidTemplateBody() { assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); } - - public String read(final String fileName) { - try { - return IOUtils.toString(this.getClass().getResourceAsStream(fileName)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } }