From 574d4eeec7da31c26c74cb705ee6183a57ec93d1 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Wed, 15 Apr 2020 11:11:49 -0700 Subject: [PATCH 01/19] Add StackSet resource with all implementations --- aws-cloudformation-stackset/.gitignore | 20 + aws-cloudformation-stackset/.rpdk-config | 15 + aws-cloudformation-stackset/README.md | 17 + .../aws-cloudformation-stackset.json | 274 +++++++++++ aws-cloudformation-stackset/lombok.config | 1 + aws-cloudformation-stackset/pom.xml | 258 +++++++++++ .../resource-role.yaml | 40 ++ .../stackset/CallbackContext.java | 79 ++++ .../stackset/Configuration.java | 32 ++ .../stackset/CreateHandler.java | 117 +++++ .../stackset/DeleteHandler.java | 92 ++++ .../cloudformation/stackset/ListHandler.java | 52 +++ .../cloudformation/stackset/ReadHandler.java | 35 ++ .../stackset/UpdateHandler.java | 130 ++++++ .../translator/PropertyTranslator.java | 136 ++++++ .../translator/RequestTranslator.java | 131 ++++++ .../util/AwsCredentialsExtractor.java | 249 ++++++++++ .../stackset/util/ClientBuilder.java | 34 ++ .../stackset/util/Comparator.java | 130 ++++++ .../stackset/util/EnumUtils.java | 13 + .../stackset/util/OperationOperator.java | 214 +++++++++ .../stackset/util/ParseException.java | 12 + .../stackset/util/PhysicalIdGenerator.java | 25 + .../stackset/util/ResourceModelBuilder.java | 121 +++++ .../stackset/util/Stabilizer.java | 197 ++++++++ .../stackset/util/TemplateParser.java | 164 +++++++ .../stackset/util/UpdatePlaceholder.java | 62 +++ .../stackset/util/Validator.java | 157 +++++++ .../test/java/resources/invalid_format.json | 10 + .../test/java/resources/invalid_format.yaml | 6 + .../src/test/java/resources/nested_stack.json | 11 + .../test/java/resources/nested_stackset.json | 11 + .../src/test/java/resources/text_null.json | 1 + .../src/test/java/resources/valid.json | 46 ++ .../src/test/java/resources/valid.yaml | 27 ++ .../stackset/CreateHandlerTest.java | 321 +++++++++++++ .../stackset/DeleteHandlerTest.java | 181 ++++++++ .../stackset/ListHandlerTest.java | 66 +++ .../stackset/ReadHandlerTest.java | 109 +++++ .../stackset/UpdateHandlerTest.java | 438 ++++++++++++++++++ .../translator/PropertyTranslatorTest.java | 26 ++ .../stackset/util/ComparatorTest.java | 97 ++++ .../stackset/util/TemplateParserTest.java | 36 ++ .../stackset/util/TestUtils.java | 435 +++++++++++++++++ .../stackset/util/ValidatorTest.java | 108 +++++ aws-cloudformation-stackset/template.yml | 23 + 46 files changed, 4759 insertions(+) create mode 100644 aws-cloudformation-stackset/.gitignore create mode 100644 aws-cloudformation-stackset/.rpdk-config create mode 100644 aws-cloudformation-stackset/README.md create mode 100644 aws-cloudformation-stackset/aws-cloudformation-stackset.json create mode 100644 aws-cloudformation-stackset/lombok.config create mode 100644 aws-cloudformation-stackset/pom.xml create mode 100644 aws-cloudformation-stackset/resource-role.yaml create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java create mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.json create mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml create mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stack.json create mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json create mode 100644 aws-cloudformation-stackset/src/test/java/resources/text_null.json create mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.json create mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.yaml create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java create mode 100644 aws-cloudformation-stackset/template.yml diff --git a/aws-cloudformation-stackset/.gitignore b/aws-cloudformation-stackset/.gitignore new file mode 100644 index 0000000..9b36fbc --- /dev/null +++ b/aws-cloudformation-stackset/.gitignore @@ -0,0 +1,20 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath + +# IntelliJ +*.iml +.idea +out.java +out/ +.settings +.project + +# auto-generated files +target/ + +# our logs +rpdk.log diff --git a/aws-cloudformation-stackset/.rpdk-config b/aws-cloudformation-stackset/.rpdk-config new file mode 100644 index 0000000..3ea66f5 --- /dev/null +++ b/aws-cloudformation-stackset/.rpdk-config @@ -0,0 +1,15 @@ +{ + "typeName": "AWS::CloudFormation::StackSet", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.cloudformation.stackset.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.cloudformation.stackset.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "cloudformation", + "stackset" + ] + } +} diff --git a/aws-cloudformation-stackset/README.md b/aws-cloudformation-stackset/README.md new file mode 100644 index 0000000..8b812e8 --- /dev/null +++ b/aws-cloudformation-stackset/README.md @@ -0,0 +1,17 @@ +# AWS::CloudFormation::StackSet + +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 + + +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. diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json new file mode 100644 index 0000000..06427d8 --- /dev/null +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -0,0 +1,274 @@ +{ + "typeName": "AWS::CloudFormation::StackSet", + "description": "Resource Type definition for AWS::CloudFormation::StackSet", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-cloudformation.git", + "definitions": { + "Tag": { + "description": "Tag type enables you to specify a key-value pair that can be used to store information about an AWS CloudFormation StackSet.", + "type": "object", + "properties": { + "Key": { + "description": "A string used to identify this tag. You can specify a maximum of 128 characters for a tag key.", + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$" + }, + "Value": { + "description": "A string containing the value for this tag. You can specify a maximum of 256 characters for a tag value.", + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "Value", + "Key" + ] + }, + "AutoDeployment": { + "type": "object", + "properties": { + "Enabled": { + "description": "If set to true, StackSets automatically deploys additional stack instances to AWS Organizations accounts that are added to a target organization or organizational unit (OU) in the specified Regions. If an account is removed from a target organization or OU, StackSets deletes stack instances from the account in the specified Regions.", + "type": "boolean" + }, + "RetainStacksOnAccountRemoval": { + "description": "If set to true, stack resources are retained when an account is removed from a target organization or OU. If set to false, stack resources are deleted. Specify only if Enabled is set to True.", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Account": { + "description": "AWS account that you want to create stack instances in the specified Region(s) for.", + "type": "string", + "pattern": "^[0-9]{12}$" + }, + "Region": { + "type": "string", + "pattern": "^[a-zA-Z0-9-]{1,128}$" + }, + "OrganizationalUnitId": { + "type": "string", + "pattern": "^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})$" + }, + "Capability": { + "type": "string", + "enum": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ] + }, + "Parameter": { + "type": "object", + "properties": { + "ParameterKey": { + "description": "The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template.", + "type": "string" + }, + "ParameterValue": { + "description": "The input value associated with the parameter.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "properties": { + "StackSetId": { + "description": "The ID of the stack set that you're creating.", + "type": "string" + }, + "AdministrationRoleARN": { + "description": "The Amazon Resource Number (ARN) of the IAM role to use to create this stack set. Specify an IAM role only if you are using customized administrator roles to control which users or groups can manage specific stack sets within the same administrator account.", + "type": "string", + "minLength": 20, + "maxLength": 2048 + }, + "AutoDeployment": { + "description": "Describes whether StackSets automatically deploys to AWS Organizations accounts that are added to the target organization or organizational unit (OU). Specify only if PermissionModel is SERVICE_MANAGED.", + "$ref": "#/definitions/AutoDeployment" + }, + "Capabilities": { + "description": "In some cases, you must explicitly acknowledge that your stack set template contains certain capabilities in order for AWS CloudFormation to create the stack set and related stack instances.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$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", + "minLength": 1, + "maxLength": 1024 + }, + "ExecutionRoleName": { + "description": "The name of the IAM execution role to use to create the stack set. If you do not specify an execution role, AWS CloudFormation uses the AWSCloudFormationStackSetExecutionRole role for the stack set operation.", + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "OperationPreferences": { + "description": "The user-specified preferences for how AWS CloudFormation performs a stack set operation.", + "type": "object", + "properties": { + "FailureToleranceCount": { + "type": "integer", + "minimum": 0 + }, + "FailureTolerancePercentage": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "MaxConcurrentCount": { + "type": "integer", + "minimum": 1 + }, + "MaxConcurrentPercentage": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "RegionOrder": { + "type": "array", + "items": { + "$ref": "#/definitions/Region" + } + } + } + }, + "Parameters": { + "description": "The input parameters for the stack set template.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$ref": "#/definitions/Parameter" + } + }, + "PermissionModel": { + "description": "Describes how the IAM roles required for stack set operations are created. By default, SELF-MANAGED is specified.", + "type": "string", + "enum": [ + "SERVICE_MANAGED", + "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", + "uniqueItems": true, + "insertionOrder": false, + "maxItems": 50, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "TemplateBody": { + "description": "The structure that contains the template body, with a minimum length of 1 byte and a maximum length of 51,200 bytes.", + "type": "string", + "minLength": 1, + "maxLength": 51200 + }, + "TemplateURL": { + "description": "Location of file containing the template body. The URL must point to a template (max size: 460,800 bytes) that is located in an Amazon S3 bucket.", + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "PermissionModel", + "Regions" + ], + "additionalProperties": false, + "createOnlyProperties": [ + "/properties/PermissionModel" + ], + "writeOnlyProperties": [ + "/properties/TemplateURL", + "/properties/OperationPreferences" + ], + "readOnlyProperties": [ + "/properties/StackSetId" + ], + "primaryIdentifier": [ + "/properties/StackSetId" + ], + "handlers": { + "create": { + "permissions": [ + "s3:GetObject", + "cloudformation:CreateStackSet", + "cloudformation:CreateStackInstances", + "cloudformation:DescribeStackSetOperation" + ] + }, + "read": { + "permissions": [ + "cloudformation:DescribeStackSet", + "cloudformation:CreateStackInstances", + "cloudformation:DescribeStackInstances" + ] + }, + "update": { + "permissions": [ + "s3:GetObject", + "cloudformation:UpdateStackSet", + "cloudformation:CreateStackInstances", + "cloudformation:DeleteStackInstances", + "cloudformation:DescribeStackSetOperation" + ] + }, + "delete": { + "permissions": [ + "cloudformation:DeleteStackSet", + "cloudformation:DeleteStackInstances", + "cloudformation:DescribeStackSetOperation" + ] + }, + "list": { + "permissions": [ + "cloudformation:ListStackSets" + ] + } + } +} diff --git a/aws-cloudformation-stackset/lombok.config b/aws-cloudformation-stackset/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-cloudformation-stackset/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml new file mode 100644 index 0000000..7ed012a --- /dev/null +++ b/aws-cloudformation-stackset/pom.xml @@ -0,0 +1,258 @@ + + + 4.0.0 + + software.amazon.cloudformation.stackset + aws-cloudformation-stackset-handler + aws-cloudformation-stackset-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + central + https://repo1.maven.org/maven2/ + + + + + + + software.amazon.awssdk + bom + 2.11.12 + pom + import + + + + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + 1.0.2 + + + + software.amazon.awssdk + cloudformation + + + + org.yaml + snakeyaml + 1.26 + + + + software.amazon.awssdk + s3 + + + + com.amazonaws + aws-java-sdk-s3 + 1.11.759 + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + + org.mockito + mockito-core + 2.26.0 + test + + + + org.mockito + mockito-junit-jupiter + 2.26.0 + test + + + org.apache.commons + commons-collections4 + 4.4 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + maven-surefire-plugin + 3.0.0-M3 + + + org.jacoco + jacoco-maven-plugin + 0.8.4 + + + **/Configuration* + **/util/AwsCredentialsExtractor* + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-cloudformation-stackset.json + + + + + + src/test/ + + **/resources/* + + + + + diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml new file mode 100644 index 0000000..dda6531 --- /dev/null +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -0,0 +1,40 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "cloudformation:CreateStackInstances" + - "cloudformation:CreateStackSet" + - "cloudformation:DeleteStackInstances" + - "cloudformation:DeleteStackSet" + - "cloudformation:DescribeStackInstances" + - "cloudformation:DescribeStackSet" + - "cloudformation:DescribeStackSetOperation" + - "cloudformation:ListStackSets" + - "cloudformation:UpdateStackSet" + - "s3:GetObject" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn 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 new file mode 100644 index 0000000..9fa4130 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java @@ -0,0 +1,79 @@ +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 java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@Data +@Builder +@JsonDeserialize(builder = CallbackContext.CallbackContextBuilder.class) +public class CallbackContext { + + // Operation Id to verify stabilization for StackSet operation. + private String operationId; + + // Elapsed counts of retries on specific exceptions. + private int retries; + + // Indicates initiation of resource stabilization. + private boolean stabilizationStarted; + + // 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 new file mode 100644 index 0000000..99648e0 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..6f7b049 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java @@ -0,0 +1,117 @@ +package software.amazon.cloudformation.stackset; + +import lombok.AllArgsConstructor; +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.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.ResourceHandlerRequest; +import software.amazon.cloudformation.stackset.util.ClientBuilder; +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 { + + 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( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + 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); + } + + private void createStackSet(final String stackSetName, final String requestToken) { + try { + final CreateStackSetResponse response = proxy.injectCredentialsAndInvokeV2( + createStackSetRequest(model, stackSetName, requestToken), 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); + + } catch (final OperationInProgressException e) { + context.incrementRetryCounter(); + } + } +} 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 new file mode 100644 index 0000000..1e6c3d9 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java @@ -0,0 +1,92 @@ +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.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.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); + + } else if (stabilizer.isStabilized(model, context)){ + deleteStackSet(proxy, model.getStackSetId(), logger, client); + + return ProgressEvent.defaultSuccessHandler(model); + } + + return ProgressEvent.defaultInProgressHandler( + context, + getDelaySeconds(context), + model); + } + + private void deleteStackSet( + final AmazonWebServicesClientProxy proxy, + final String stackSetName, + final Logger logger, + final CloudFormationClient client) { + + 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); + } + } + + private void deleteStackInstances( + final AmazonWebServicesClientProxy proxy, + final ResourceModel model, + final Logger logger, + final CloudFormationClient client, + final CallbackContext context) { + + try { + final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( + deleteStackInstancesRequest(model.getStackSetId(), + model.getOperationPreferences(), model.getDeploymentTargets(), model.getRegions()), + client::deleteStackInstances); + + 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); + + } catch (final OperationInProgressException e) { + context.incrementRetryCounter(); + } + } +} 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 new file mode 100644 index 0000000..4f99067 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java @@ -0,0 +1,52 @@ +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.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 { + + @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(); + + final ListStackSetsResponse response = proxy.injectCredentialsAndInvokeV2( + listStackSetsRequest(request.getNextToken()), client::listStackSets); + + final List models = response + .summaries() + .stream() + .map(stackSetSummary -> ResourceModelBuilder.builder() + .proxy(proxy) + .client(client) + .stackSet(operator.getStackSet(stackSetSummary.stackSetId())) + .build().buildModel()) + .collect(Collectors.toList()); + + return ProgressEvent.builder() + .resourceModels(models) + .status(OperationStatus.SUCCESS) + .build(); + } +} 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 new file mode 100644 index 0000000..b32a7af --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java @@ -0,0 +1,35 @@ +package software.amazon.cloudformation.stackset; + +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +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.ClientBuilder; +import software.amazon.cloudformation.stackset.util.OperationOperator; +import software.amazon.cloudformation.stackset.util.ResourceModelBuilder; + +public class ReadHandler extends BaseHandler { + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final 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())) + .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 new file mode 100644 index 0000000..12f40f0 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java @@ -0,0 +1,130 @@ +package software.amazon.cloudformation.stackset; + +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +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.Validator; + +import java.util.Set; + +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 BaseHandler { + + private Validator validator; + + public UpdateHandler() { + this.validator = new Validator(); + } + + public UpdateHandler(Validator validator) { + this.validator = validator; + } + + @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 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)) { + + 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)) { + + operator.updateStackSet(ADD_INSTANCES_BY_TARGETS, regionsToAdd, targetsToAdd); + } + } + } + + if (isUpdateStabilized(context)) { + return ProgressEvent.defaultSuccessHandler(desiredModel); + + } else { + return ProgressEvent.defaultInProgressHandler( + context, + getDelaySeconds(context), + desiredModel); + } + + } + +} + 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 new file mode 100644 index 0000000..e244f6c --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java @@ -0,0 +1,136 @@ +package software.amazon.cloudformation.stackset.translator; + +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.StackSetOperationPreferences; +import software.amazon.awssdk.services.cloudformation.model.Tag; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.cloudformation.stackset.OperationPreferences; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class PropertyTranslator { + + /** + * Converts AutoDeployment (from StackSet SDK) to AutoDeployment (from CFN resource model) + * @param autoDeployment SDK AutoDeployment + * @return Resource model AutoDeployment + */ + public static software.amazon.cloudformation.stackset.AutoDeployment translateFromSdkAutoDeployment( + final AutoDeployment autoDeployment) { + if (autoDeployment == null) return null; + return software.amazon.cloudformation.stackset.AutoDeployment.builder() + .enabled(autoDeployment.enabled()) + .retainStacksOnAccountRemoval(autoDeployment.retainStacksOnAccountRemoval()) + .build(); + } + + /** + * Converts AutoDeployment (from CFN resource model) to AutoDeployment (from StackSet SDK) + * @param autoDeployment AutoDeployment from resource model + * @return SDK AutoDeployment + */ + public static AutoDeployment translateToSdkAutoDeployment( + final software.amazon.cloudformation.stackset.AutoDeployment autoDeployment) { + if (autoDeployment == null) return null; + return AutoDeployment.builder() + .enabled(autoDeployment.getEnabled()) + .retainStacksOnAccountRemoval(autoDeployment.getRetainStacksOnAccountRemoval()) + .build(); + } + + /** + * Converts resource model DeploymentTargets to StackSet SDK DeploymentTargets + * @param deploymentTargets DeploymentTargets from resource model + * @return SDK DeploymentTargets + */ + static DeploymentTargets translateToSdkDeploymentTargets( + final software.amazon.cloudformation.stackset.DeploymentTargets deploymentTargets) { + return DeploymentTargets.builder() + .accounts(deploymentTargets.getAccounts()) + .organizationalUnitIds(deploymentTargets.getOrganizationalUnitIds()) + .build(); + } + + /** + * Converts resource model Parameters to StackSet SDK Parameters + * @param parameters Parameters collection from resource model + * @return SDK Parameter list + */ + static List translateToSdkParameters( + final Collection parameters) { + if (parameters == null) return null; + return parameters.stream() + .map(parameter -> Parameter.builder() + .parameterKey(parameter.getParameterKey()) + .parameterValue(parameter.getParameterValue()) + .build()) + .collect(Collectors.toList()); + } + + /** + * Converts resource model Parameters to StackSet SDK Parameters + * @param parameters Parameters from SDK + * @return resource model Parameters + */ + public static Set translateFromSdkParameters( + final Collection parameters) { + if (parameters == null) return null; + return parameters.stream() + .map(parameter -> software.amazon.cloudformation.stackset.Parameter.builder() + .parameterKey(parameter.parameterKey()) + .parameterValue(parameter.parameterValue()) + .build()) + .collect(Collectors.toSet()); + } + + /** + * Converts resource model OperationPreferences to StackSet SDK OperationPreferences + * @param operationPreferences OperationPreferences from resource model + * @return SDK OperationPreferences + */ + static StackSetOperationPreferences translateToSdkOperationPreferences( + final OperationPreferences operationPreferences) { + if (operationPreferences == null) return null; + return StackSetOperationPreferences.builder() + .maxConcurrentCount(operationPreferences.getMaxConcurrentCount()) + .maxConcurrentPercentage(operationPreferences.getMaxConcurrentPercentage()) + .failureToleranceCount(operationPreferences.getFailureToleranceCount()) + .failureTolerancePercentage(operationPreferences.getFailureTolerancePercentage()) + .regionOrder(operationPreferences.getRegionOrder()) + .build(); + } + + + /** + * Converts tags (from CFN resource model) to StackSet set (from StackSet SDK) + * @param tags Tags CFN resource model. + * @return SDK Tags. + */ + static Collection translateToSdkTags(final Collection tags) { + if (CollectionUtils.isNullOrEmpty(tags)) return null; + return tags.stream().map(tag -> Tag.builder() + .key(tag.getKey()) + .value(tag.getValue()) + .build()) + .collect(Collectors.toList()); + } + + /** + * Converts a list of tags (from StackSet SDK) to HostedZoneTag set (from CFN resource model) + * @param tags Tags from StackSet SDK. + * @return A set of CFN StackSet Tag. + */ + public static Set translateFromSdkTags(final Collection tags) { + if (CollectionUtils.isNullOrEmpty(tags)) return null; + return tags.stream().map(tag -> software.amazon.cloudformation.stackset.Tag.builder() + .key(tag.key()) + .value(tag.value()) + .build()) + .collect(Collectors.toSet()); + } +} 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 new file mode 100644 index 0000000..7e3b02a --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java @@ -0,0 +1,131 @@ +package software.amazon.cloudformation.stackset.translator; + +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.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.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 static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkAutoDeployment; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkDeploymentTargets; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkOperationPreferences; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkParameters; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateToSdkTags; + +public class RequestTranslator { + + private static int LIST_MAX_ITEMS = 100; + + public static CreateStackSetRequest createStackSetRequest( + final ResourceModel model, final String stackSetName, final String requestToken) { + return CreateStackSetRequest.builder() + .stackSetName(stackSetName) + .administrationRoleARN(model.getAdministrationRoleARN()) + .autoDeployment(translateToSdkAutoDeployment(model.getAutoDeployment())) + .clientRequestToken(requestToken) + .permissionModel(model.getPermissionModel()) + .capabilitiesWithStrings(model.getCapabilities()) + .description(model.getDescription()) + .executionRoleName(model.getExecutionRoleName()) + .parameters(translateToSdkParameters(model.getParameters())) + .tags(translateToSdkTags(model.getTags())) + .templateBody(model.getTemplateBody()) + .templateURL(model.getTemplateURL()) + .build(); + } + + public static CreateStackInstancesRequest createStackInstancesRequest( + final String stackSetName, + final OperationPreferences operationPreferences, + final DeploymentTargets deploymentTargets, + final Set regions) { + return CreateStackInstancesRequest.builder() + .stackSetName(stackSetName) + .regions(regions) + .operationPreferences(translateToSdkOperationPreferences(operationPreferences)) + .deploymentTargets(translateToSdkDeploymentTargets(deploymentTargets)) + .build(); + } + + public static DeleteStackSetRequest deleteStackSetRequest(final String stackSetName) { + return DeleteStackSetRequest.builder() + .stackSetName(stackSetName) + .build(); + } + + public static DeleteStackInstancesRequest deleteStackInstancesRequest( + final String stackSetName, + final OperationPreferences operationPreferences, + final DeploymentTargets deploymentTargets, + final Set regions) { + return DeleteStackInstancesRequest.builder() + .stackSetName(stackSetName) + .regions(regions) + .operationPreferences(translateToSdkOperationPreferences(operationPreferences)) + .deploymentTargets(translateToSdkDeploymentTargets(deploymentTargets)) + .build(); + } + + public static UpdateStackSetRequest updateStackSetRequest(final ResourceModel model) { + return UpdateStackSetRequest.builder() + .stackSetName(model.getStackSetId()) + .administrationRoleARN(model.getAdministrationRoleARN()) + .autoDeployment(translateToSdkAutoDeployment(model.getAutoDeployment())) + .capabilitiesWithStrings(model.getCapabilities()) + .description(model.getDescription()) + .executionRoleName(model.getExecutionRoleName()) + .parameters(translateToSdkParameters(model.getParameters())) + .templateURL(model.getTemplateURL()) + .templateBody(model.getTemplateBody()) + .tags(translateToSdkTags(model.getTags())) + .build(); + } + + public static ListStackSetsRequest listStackSetsRequest(final String nextToken) { + return ListStackSetsRequest.builder() + .maxResults(LIST_MAX_ITEMS) + .nextToken(nextToken) + .build(); + } + + public static ListStackInstancesRequest listStackInstancesRequest( + final String nextToken, final String stackSetName) { + return ListStackInstancesRequest.builder() + .maxResults(LIST_MAX_ITEMS) + .nextToken(nextToken) + .stackSetName(stackSetName) + .build(); + } + + public static DescribeStackSetRequest describeStackSetRequest(final String stackSetId) { + return DescribeStackSetRequest.builder() + .stackSetName(stackSetId) + .build(); + } + + public static DescribeStackSetOperationRequest describeStackSetOperationRequest( + final String stackSetName, final String operationId) { + return DescribeStackSetOperationRequest.builder() + .stackSetName(stackSetName) + .operationId(operationId) + .build(); + } + + public static GetObjectRequest getObjectRequest( + final String bucketName, final String key) { + return GetObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + } +} 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 new file mode 100644 index 0000000..56e2286 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java @@ -0,0 +1,249 @@ +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; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.awscore.AwsResponseMetadata; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Utility class to extract AWS Credentials Provider from {@link AmazonWebServicesClientProxy}. + * + * Because {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2(AwsRequest, Function)} doesn't extend + * {@link ResponseInputStream}, but S3 GetObject requires AWS Credentials Provider to authenticate user, + * we have to mimic dummy aws request, aws response and a function as input parameters to + * {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2(AwsRequest, Function)} to obtain credentials. + */ +public final class AwsCredentialsExtractor { + + private static final String AWS_CREDENTIALS_NOT_AVAILABLE_ERROR_MSG = "AWS credentials provider are not available"; + + /** + * Function to extract Aws Credentials Provider from {@link AmazonWebServicesClientProxy}. + * + * @param proxy {@link AmazonWebServicesClientProxy} + * @return {@link AwsCredentialsProvider} + */ + public static AwsCredentialsProvider extractAwsCredentialsProvider(final AmazonWebServicesClientProxy proxy) { + return proxy.injectCredentialsAndInvokeV2( + GetAwsCredentialsRequest.builder().build(), + AwsCredentialsExtractor::extract + ).awsCredentialsProvider; + } + + private static GetAwsCredentialsResponse extract(final GetAwsCredentialsRequest getAwsCredentialsRequest) { + final AwsCredentialsProvider awsCredentialsProvider = getAwsCredentialsRequest.overrideConfiguration() + .flatMap(AwsRequestOverrideConfiguration::credentialsProvider) + .orElseThrow(() -> new IllegalArgumentException(AWS_CREDENTIALS_NOT_AVAILABLE_ERROR_MSG)); + return GetAwsCredentialsResponse.builder().awsCredentialsProvider(awsCredentialsProvider).build(); + } + + /** + * Inner class to mimic {@link AwsRequest}. + * No additional input parameter is required. Other classes and functions are implemented by following interfaces + * and abstract method of {@link AwsRequest}. + */ + private final static class GetAwsCredentialsRequest extends AwsRequest + implements ToCopyableBuilder { + + private GetAwsCredentialsRequest(Builder builder) { + super(builder); + } + + static GetAwsCredentialsRequest.Builder builder() { + return new GetAwsCredentialsRequest.BuilderImpl(); + } + + @Override + public Builder toBuilder() { + return new GetAwsCredentialsRequest.BuilderImpl(); + } + + @Override + public List> sdkFields() { + return Collections.emptyList(); + } + + @Override + public boolean equalsBySdkFields(Object obj) { + return true; + } + + static final class BuilderImpl extends AwsRequest.BuilderImpl + implements GetAwsCredentialsRequest.Builder { + + BuilderImpl() { + } + + public Builder overrideConfiguration(AwsRequestOverrideConfiguration overrideConfiguration) { + super.overrideConfiguration(overrideConfiguration); + return this; + } + + public GetAwsCredentialsRequest.Builder overrideConfiguration( + Consumer builderConsumer + ) { + super.overrideConfiguration(builderConsumer); + return this; + } + + public GetAwsCredentialsRequest build() { + return new GetAwsCredentialsRequest(this); + } + + public List> sdkFields() { + return Collections.emptyList(); + } + } + + public interface Builder + extends AwsRequest.Builder, SdkPojo, CopyableBuilder { + @Override + GetAwsCredentialsRequest.Builder overrideConfiguration( + AwsRequestOverrideConfiguration awsRequestOverrideConfiguration + ); + + @Override + GetAwsCredentialsRequest.Builder overrideConfiguration( + Consumer builderConsumer + ); + } + } + + /** + * Inner class to mimic {@link AwsResponse} in order to obtain credentials from + * {@link AmazonWebServicesClientProxy}. + * + * {@link AwsCredentialsProvider} is the additional parameter in this class. Other classes and functions are + * implemented by following interfaces and abstract method of {@link AwsResponse}. + */ + private static class GetAwsCredentialsResponse extends AwsResponse + implements ToCopyableBuilder { + + private final GetAwsCredentialsResponseMetadata responseMetadata; + + private final AwsCredentialsProvider awsCredentialsProvider; + + private GetAwsCredentialsResponse(final GetAwsCredentialsResponse.BuilderImpl builder) { + super(builder); + this.awsCredentialsProvider = (builder.awsCredentialsProvider); + this.responseMetadata = builder.responseMetadata(); + } + + public AwsCredentialsProvider awsCredentialsProvider() { + return this.awsCredentialsProvider; + } + + @Override + public Builder toBuilder() { + return new GetAwsCredentialsResponse.BuilderImpl(this); + } + + @Override + public List> sdkFields() { + return Collections.emptyList(); + } + + @Override + public boolean equalsBySdkFields(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (!(obj instanceof GetAwsCredentialsResponse)) { + return false; + } else { + GetAwsCredentialsResponse other = (GetAwsCredentialsResponse) obj; + return Objects.equals(this.awsCredentialsProvider(), other.awsCredentialsProvider()); + } + } + + public static GetAwsCredentialsResponse.Builder builder() { + return new GetAwsCredentialsResponse.BuilderImpl(); + } + + static final class BuilderImpl extends AwsResponse.BuilderImpl + implements GetAwsCredentialsResponse.Builder { + + private GetAwsCredentialsResponseMetadata responseMetadata; + + private AwsCredentialsProvider awsCredentialsProvider; + + private BuilderImpl() { + } + + private BuilderImpl(GetAwsCredentialsResponse response) { + super(response); + this.awsCredentialsProvider = response.awsCredentialsProvider; + } + + public GetAwsCredentialsResponse build() { + return new GetAwsCredentialsResponse(this); + } + + public List> sdkFields() { + return Collections.emptyList(); + } + + public GetAwsCredentialsResponseMetadata responseMetadata() { + return this.responseMetadata; + } + + public GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata responseMetadata) { + this.responseMetadata = GetAwsCredentialsResponseMetadata.create(responseMetadata); + return this; + } + + public final GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { + this.awsCredentialsProvider = awsCredentialsProvider; + return this; + } + + public AwsCredentialsProvider getAwsCredentialsProvider() { + return awsCredentialsProvider; + } + + public void setAwsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { + this.awsCredentialsProvider = awsCredentialsProvider; + } + } + + public interface Builder extends AwsResponse.Builder, SdkPojo, + CopyableBuilder { + + GetAwsCredentialsResponse build(); + + GetAwsCredentialsResponseMetadata responseMetadata(); + + GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata awsResponseMetadata); + + GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider); + } + } + + /** + * Inner class to mimic {@link AwsResponseMetadata} which is required by {@link AwsResponse}. + */ + private static final class GetAwsCredentialsResponseMetadata extends AwsResponseMetadata { + private GetAwsCredentialsResponseMetadata(AwsResponseMetadata responseMetadata) { + super(responseMetadata); + } + + public static GetAwsCredentialsResponseMetadata create(AwsResponseMetadata responseMetadata) { + 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 new file mode 100644 index 0000000..ab4af02 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java @@ -0,0 +1,34 @@ +package software.amazon.cloudformation.stackset.util; + +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.cloudformation.LambdaWrapper; + +public class ClientBuilder { + + private ClientBuilder() {} + + /** + * Get CloudFormationClient for requests to interact with StackSet client + * @return {@link CloudFormationClient} + */ + public static CloudFormationClient getClient() { + return CloudFormationClient.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } + + /** + * Gets S3 client for requests to interact with getting/validating template content + * if {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL()} is passed in + * @param awsCredentialsProvider {@link AwsCredentialsProvider} + * @return {@link S3Client} + */ + public static S3Client getS3Client(final AwsCredentialsProvider awsCredentialsProvider) { + return S3Client.builder() + .credentialsProvider(awsCredentialsProvider) + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } +} 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 new file mode 100644 index 0000000..cfca487 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java @@ -0,0 +1,130 @@ +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.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 + * @param collection2 + * @return boolean indicates if two collections equal. + */ + public static boolean isEquals(final Collection collection1, final Collection collection2) { + if (collection1 == null) return collection2 == null ? true : false; + return CollectionUtils.isEqualCollection(collection1, collection2); + } +} 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 new file mode 100644 index 0000000..a02b5ff --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java @@ -0,0 +1,13 @@ +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/OperationOperator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java new file mode 100644 index 0000000..876757c --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java @@ -0,0 +1,214 @@ +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/ParseException.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java new file mode 100644 index 0000000..118fffe --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java @@ -0,0 +1,12 @@ +package software.amazon.cloudformation.stackset.util; + +/** + * Custom Exception Class to hold exception when parsing templates + */ +public class ParseException extends RuntimeException{ + private static final long serialVersionUID = 1L; + + public ParseException(final String message) { + super(message); + } +} 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 new file mode 100644 index 0000000..c6f2e69 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java @@ -0,0 +1,25 @@ +package software.amazon.cloudformation.stackset.util; + +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.resource.IdentifierUtils; +import software.amazon.cloudformation.stackset.ResourceModel; + +/** + * Utility class to generate Physical Resource Id from {@link ResourceHandlerRequest}. + */ +public class PhysicalIdGenerator { + + private static int MAX_LENGTH_CALLER_REFERENCE = 128; + + /** + * Generates a physical Id for creating a new resource. + * @param request CloudFormation's requested resource state. + * @return Physical ID. + */ + public static String generatePhysicalId(final ResourceHandlerRequest request) { + return IdentifierUtils.generateResourceIdentifier( + request.getLogicalResourceIdentifier(), + request.getClientRequestToken(), + MAX_LENGTH_CALLER_REFERENCE); + } +} 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 new file mode 100644 index 0000000..8d6e789 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java @@ -0,0 +1,121 @@ +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.ListStackInstancesResponse; +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.stackset.ResourceModel; + +import java.util.HashSet; + +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.RequestTranslator.listStackInstancesRequest; + +/** + * Utility class to construct {@link ResourceModel} for Read/List request based on {@link StackSet} + * that handler has retrieved. + */ +@AllArgsConstructor +@Builder +public class ResourceModelBuilder { + + private AmazonWebServicesClientProxy proxy; + private CloudFormationClient client; + private StackSet stackSet; + private PermissionModels permissionModel; + + /** + * 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(); + + // NOTE: TemplateURL from StackSet service client is currently not retrievable + final ResourceModel model = ResourceModel.builder() + .autoDeployment(translateFromSdkAutoDeployment(stackSet.autoDeployment())) + .stackSetId(stackSetId) + .description(stackSet.description()) + .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)) { + model.setAdministrationRoleARN(stackSet.administrationRoleARN()); + model.setExecutionRoleName(stackSet.executionRoleName()); + } + + String token = null; + // Retrieves all Stack Instances associated with the StackSet, + // Attaches regions and deploymentTargets to the constructing model + do { + putRegionsAndDeploymentTargets(stackSetId, model, token); + } while (token != null); + + return model; + } + + /** + * Loop through all stack instance details and attach to the constructing model + * @param stackSetId {@link ResourceModel#getStackSetId()} + * @param model {@link ResourceModel} + * @param token {@link ListStackInstancesResponse#nextToken()} + */ + private void putRegionsAndDeploymentTargets( + final String stackSetId, + final ResourceModel model, + String token) { + + final ListStackInstancesResponse listStackInstancesResponse = proxy.injectCredentialsAndInvokeV2( + listStackInstancesRequest(token, stackSetId), client::listStackInstances); + token = listStackInstancesResponse.nextToken(); + listStackInstancesResponse.summaries().forEach(member -> putRegionsAndDeploymentTargets(member, model)); + } + + /** + * 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)); + } + } +} 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 new file mode 100644 index 0000000..73ec5d0 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java @@ -0,0 +1,197 @@ +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/TemplateParser.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java new file mode 100644 index 0000000..6a61420 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java @@ -0,0 +1,164 @@ +package software.amazon.cloudformation.stackset.util; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.MarkedYAMLException; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.IOException; +import java.util.Map; +import java.util.regex.Pattern; + +public class TemplateParser { + + private static final Pattern JSON_INPUT_PATTERN = Pattern.compile("^\\s*\\{.*\\}\\s*$", Pattern.DOTALL); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final String UNKNOWN_LOCATION = "unknown location"; + private static final String INVALID_TEMPLATE_ERROR_MSG = "Template format error: not a valid template"; + private static final String UNSUPPORTED_TYPE_STRUCTURE_ERROR_MSG = + "Template format error: unsupported type or structure. (%s)"; + private static final String NOT_WELL_FORMATTED_ERROR_MSG = "Template format error: %s not well-formed. (%s)"; + private static final String FORMAT_LOCATION_ERROR_MSG = "line %s, column %s"; + + /** + * Deserializes template content which can be either JSON or YAML + * @param template Template Content + * @return Generic Map of template + */ + public static Map deserializeTemplate(final String template) { + // If the template does not follow valid Json pattern, parse as Yaml. + // Else, parse as Json first; if that fails parse as Yaml. + if (!isPossiblyJson(template)) { + return deserializeYaml(template); + } + + try { + return deserializeJson(template); + } catch (final ParseException e) { + return deserializeYaml(template); + } + + } + + /** + * Gets a Generic Map object from template + * @param templateMap Template Map + * @param key Key of the Map we are retrieving + * @return Generic Map object + */ + @SuppressWarnings("unchecked") + public static Map getMapFromTemplate(final Map templateMap, final String key) { + final Object value = templateMap.get(key); + if (value == null) return null; + if (value instanceof Map) { + return (Map) value; + } + throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); + } + + /** + * Gets String from the passed in value + * @param value + * @return String + */ + public static String getStringFromTemplate(final Object value) { + if (value == null) return null; + if (value instanceof String) { + return (String) value; + } + throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); + } + + /** + * Deserializes YAML from template content string + * @param templateString Template content + * @return Template map + * @throws ParseException if fails to parse the template + */ + @VisibleForTesting + protected static Map deserializeYaml(final String templateString) { + try { + final Map template = new Yaml().load(templateString); + if (template == null || template.isEmpty()) { + throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); + } + return template; + + } catch (final MarkedYAMLException e) { + throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "YAML", + formatErrorLocation(e.getProblemMark()))); + + } catch (final YAMLException e) { + throw new ParseException(String.format("Cannot parse as YAML : %s ", e.getMessage())); + + } catch (final ClassCastException e) { + throw new ParseException("Template format error: unsupported structure."); + + } + } + + /** + * Deserializes JSON from template content string + * @param templateString Template content + * @return Template map + * @throws ParseException if fails to parse the template + */ + @SuppressWarnings("unchecked") + @VisibleForTesting + protected static Map deserializeJson(final String templateString) { + Map template = null; + try { + JsonParser parser = new MappingJsonFactory().createParser(templateString); + template = OBJECT_MAPPER.readValue(parser, Map.class); + + } catch (final JsonMappingException e) { + throw new ParseException(String.format(UNSUPPORTED_TYPE_STRUCTURE_ERROR_MSG, + formatErrorLocation(e.getLocation()))); + + } catch (final JsonParseException e) { + throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "JSON", + formatErrorLocation(e.getLocation()))); + + } catch (final IOException e) { + throw new ParseException("Cannot parse template, I/O stream corrupt."); + } + + // The string "null" may be considered as valid JSON by the parser, but it is not a valid template. + if (template == null) { + throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); + } + return template; + } + + private static boolean isPossiblyJson(final String template) { + return JSON_INPUT_PATTERN.matcher(template).matches(); + } + + /** + * Gets the error location when parsing as JSON + * @param loc {@link JsonLocation} + * @return Error location + */ + private static String formatErrorLocation(final JsonLocation loc) { + if (loc == null) return UNKNOWN_LOCATION; + return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLineNr(), loc.getColumnNr()); + } + + /** + * Gets the error location when parsing as YAML + * @param loc {@link Mark} + * @return Error location + */ + private static String formatErrorLocation(final Mark loc) { + if (loc == null) return UNKNOWN_LOCATION; + return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLine() + 1, loc.getColumn() + 1); + } +} 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 new file mode 100644 index 0000000..4fdc417 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java @@ -0,0 +1,62 @@ +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/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java new file mode 100644 index 0000000..41d35e5 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -0,0 +1,157 @@ +package software.amazon.cloudformation.stackset.util; + +import com.amazonaws.services.s3.AmazonS3URI; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getObjectRequest; +import static software.amazon.cloudformation.stackset.util.TemplateParser.deserializeTemplate; +import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; +import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; + +/** + * Utility class to validate properties in {@link software.amazon.cloudformation.stackset.ResourceModel} + */ +public class Validator { + + private static final String TEMPLATE_RESOURCE_TYPE_KEY = "Type"; + private static final String TEMPLATE_RESOURCES_KEY = "Resources"; + private static final String TEMPLATE_PARAMETERS_KEY = "Parameters"; + + /** + * Gets template content from s3 bucket + * @param proxy {@link AmazonWebServicesClientProxy} + * @param templateLocation Template URL + * @return Template content from S3 object + */ + @VisibleForTesting + protected String getUrlContent(final AmazonWebServicesClientProxy proxy, final String templateLocation) { + final AmazonS3URI s3Uri = new AmazonS3URI(templateLocation, true); + final GetObjectRequest request = getObjectRequest(s3Uri.getBucket(), s3Uri.getKey()); + + /** + * Since currently response other than {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2} + * ${@link Result} only extends {@link AwsResponse}, which we can't inject credentials into + * {@link software.amazon.awssdk.services.s3.S3Client#getObject}. + * Hence, getting {@link AwsCredentialsProvider} using an aws dummy request. + */ + final AwsCredentialsProvider awsCredentialsProvider = + AwsCredentialsExtractor.extractAwsCredentialsProvider(proxy); + + final String content = ClientBuilder.getS3Client(awsCredentialsProvider) + .getObjectAsBytes(request).asString(StandardCharsets.UTF_8); + + return content; + } + + /** + * Validates template url is valid S3 URL + * @param s3Uri Template URL + */ + @VisibleForTesting + protected void validateS3Uri(final String s3Uri) { + try { + final AmazonS3URI validS3Uri = new AmazonS3URI(s3Uri, true); + if (Strings.isNullOrEmpty(validS3Uri.getBucket()) || Strings.isNullOrEmpty(validS3Uri.getKey())) { + throw new CfnInvalidRequestException("Both S3 bucket and key must be specified"); + } + } catch (final IllegalArgumentException | IllegalStateException | StringIndexOutOfBoundsException e) { + throw new CfnInvalidRequestException("S3 URL is not valid"); + } + } + + /** + * Validates template with following rules: + *
    + *
  • Only exact one template source can be specified + *
  • If using S3 URI, it must be valid + *
  • Template contents must be valid + *
+ * @param proxy {@link AmazonWebServicesClientProxy} + * @param templateBody {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateBody} + * @param templateLocation {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL} + * @param logger {@link Logger} + * @throws CfnInvalidRequestException if template is not valid + */ + public void validateTemplate( + final AmazonWebServicesClientProxy proxy, + final String templateBody, + final String templateLocation, + final Logger logger) { + + if (!(Strings.isNullOrEmpty(templateBody) ^ Strings.isNullOrEmpty(templateLocation))) { + throw new CfnInvalidRequestException("Exactly one of TemplateBody or TemplateUrl must be specified"); + } + String content = null; + try { + if (!Strings.isNullOrEmpty(templateLocation)) { + validateS3Uri(templateLocation); + content = getUrlContent(proxy, templateLocation); + } else { + content = templateBody; + } + validateTemplate(content); + + } catch (final ParseException e) { + logger.log(String.format("Failed to parse template content: %s", content)); + throw new CfnInvalidRequestException(e.getMessage()); + } + } + + /** + * Validates the template to make sure: + *
    + *
  • Template can be deserialized successfully + *
  • Resources block doesn't have embedded Stack or StackSet + *
  • Parameters block doesn't have embedded Stack or StackSet + *
+ * @param content Template content + */ + private static void validateTemplate(final String content) { + final Map template = deserializeTemplate(content); + validateBlocks(template, TEMPLATE_RESOURCES_KEY); + validateBlocks(template, TEMPLATE_PARAMETERS_KEY); + } + + /** + * Validates items in the block do not have any invalid resources + * @param templateMap Templates map + * @param block Block key, i.e. Resources + */ + @SuppressWarnings("unchecked") + private static void validateBlocks(final Map templateMap, final String block) { + final Map resourcesMap = (Map) templateMap.get(block); + + if (CollectionUtils.isNullOrEmpty(resourcesMap)) return; + for (final Map.Entry entry : resourcesMap.entrySet()) { + final String resourceId = entry.getKey(); + final Map resourceMap = getMapFromTemplate(resourcesMap, resourceId); + validateResource(resourceMap); + } + } + + /** + * Embedded Stack or StackSet is not allowed + * @param resourceMap Resource map + */ + private static void validateResource(final Map resourceMap) { + final String type = getStringFromTemplate(resourceMap.get(TEMPLATE_RESOURCE_TYPE_KEY)); + if (type != null) { + switch (type) { + case "AWS::CloudFormation::Stack": + case "AWS::CloudFormation::StackSet": + throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); + } + } + } + +} diff --git a/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json b/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json new file mode 100644 index 0000000..8cfdd86 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/invalid_format.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 0000000..2706e91 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..9e8c3b1 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/nested_stack.json @@ -0,0 +1,11 @@ +{ + "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 new file mode 100644 index 0000000..4571fe7 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json @@ -0,0 +1,11 @@ +{ + "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 new file mode 100644 index 0000000..ec747fa --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/text_null.json @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..0340a5b --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/valid.json @@ -0,0 +1,46 @@ +{ + "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 new file mode 100644 index 0000000..da653dd --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/resources/valid.yaml @@ -0,0 +1,27 @@ +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/CreateHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java new file mode 100644 index 0000000..bcfb432 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java @@ -0,0 +1,321 @@ +package software.amazon.cloudformation.stackset; + +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.AlreadyExistsException; +import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; +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.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.ResourceHandlerRequest; +import software.amazon.cloudformation.stackset.util.Validator; + +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 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.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; + +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest { + + private CreateHandler handler; + + private ResourceHandlerRequest request; + + @Mock + private Validator validator; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @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(); + } + + @Test + public void handleRequest_SimpleSuccess() { + + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + final CallbackContext inputContext = CallbackContext.builder() + .stabilizationStarted(true) + .operationId(OPERATION_ID_1) + .build(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, inputContext, 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() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_TEMPLATE_BODY_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(); + + 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_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) + .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/DeleteHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java new file mode 100644 index 0000000..dacdb25 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java @@ -0,0 +1,181 @@ +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 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.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.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; + +@ExtendWith(MockitoExtension.class) +public class DeleteHandlerTest { + + private DeleteHandler handler; + + private ResourceHandlerRequest request; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + handler = new DeleteHandler(); + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_MODEL) + .build(); + } + + @Test + public void handleRequest_SimpleSuccess() { + + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + final CallbackContext inputContext = CallbackContext.builder() + .stabilizationStarted(true) + .operationId(OPERATION_ID_1) + .build(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, inputContext, 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 new file mode 100644 index 0000000..3b2f502 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java @@ -0,0 +1,66 @@ +package software.amazon.cloudformation.stackset; + +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 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 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.LIST_STACK_SETS_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; + +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ListHandler handler = new ListHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + doReturn(LIST_STACK_SETS_RESPONSE, + 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()).isNull(); + assertThat(response.getResourceModels()).containsExactly(SERVICE_MANAGED_MODEL); + 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 new file mode 100644 index 0000000..a656d0c --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java @@ -0,0 +1,109 @@ +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 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 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.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.SERVICE_MANAGED_MODEL; + +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest { + + private ReadHandler handler; + + private ResourceHandlerRequest request; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + handler = new ReadHandler(); + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + 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()); + + 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(SELF_MANAGED_MODEL); + 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 new file mode 100644 index 0000000..5e886f8 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java @@ -0,0 +1,438 @@ +package software.amazon.cloudformation.stackset; + +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 software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_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.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.UPDATED_SELF_MANAGED_MODEL; +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 { + + private UpdateHandler handler; + + private ResourceHandlerRequest request; + + @Mock + private Validator validator; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @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(); + } + + @Test + public void handleRequest_NotUpdatable_Success() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_MODEL) + .previousResourceState(SIMPLE_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(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, inputContext, 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 new file mode 100644 index 0000000..0e4eaf2 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java @@ -0,0 +1,26 @@ +package software.amazon.cloudformation.stackset.translator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +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.translateToSdkTags; + +public class PropertyTranslatorTest { + + @Test + public void testNull_translateFromSdkParameters_isNull() { + assertThat(translateFromSdkParameters(null)).isNull(); + } + + @Test + public void test_translateToSdkTags_isNull() { + assertThat(translateToSdkTags(null)).isNull(); + } + + @Test + public void test_translateFromSdkTags_isNull() { + 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 new file mode 100644 index 0000000..e409a87 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java @@ -0,0 +1,97 @@ +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/TemplateParserTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java new file mode 100644 index 0000000..2d88b12 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java @@ -0,0 +1,36 @@ +package software.amazon.cloudformation.stackset.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; +import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; +import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_MAP; + +public class TemplateParserTest { + + @Test + public void testDeserializeYaml() { + assertThrows(ParseException.class, () -> TemplateParser.deserializeYaml("null")); + assertThrows(ParseException.class, () -> TemplateParser.deserializeYaml("")); + } + + @Test + public void testDeserializeJson() { + assertThrows(ParseException.class, () -> TemplateParser.deserializeJson("")); + assertThrows(ParseException.class, () -> TemplateParser.deserializeJson("null")); + } + + @Test + public void testGetMapFromTemplate() { + assertThat(getMapFromTemplate(TEMPLATE_MAP, "null")).isNull(); + assertThrows(ParseException.class, () -> getMapFromTemplate(TEMPLATE_MAP, "TemplateURL")); + } + + @Test + public void testGetStringFromTemplate() { + assertThat(getStringFromTemplate(null)).isNull(); + assertThrows(ParseException.class, () -> getStringFromTemplate(TEMPLATE_MAP)); + } +} 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 new file mode 100644 index 0000000..801004e --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java @@ -0,0 +1,435 @@ +package software.amazon.cloudformation.stackset.util; + +import com.google.common.collect.ImmutableMap; +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.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.StackInstanceSummary; +import software.amazon.awssdk.services.cloudformation.model.StackSet; +import software.amazon.awssdk.services.cloudformation.model.StackSetOperation; +import software.amazon.awssdk.services.cloudformation.model.StackSetOperationStatus; +import software.amazon.awssdk.services.cloudformation.model.StackSetSummary; +import software.amazon.awssdk.services.cloudformation.model.Tag; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesResponse; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; +import software.amazon.cloudformation.stackset.AutoDeployment; +import software.amazon.cloudformation.stackset.DeploymentTargets; +import software.amazon.cloudformation.stackset.OperationPreferences; +import software.amazon.cloudformation.stackset.ResourceModel; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TestUtils { + + public final static String DESCRIPTION = "description"; + public final static String UPDATED_DESCRIPTION = "description-new"; + + public final static String ADMINISTRATION_ROLE_ARN = "administration:role:arn"; + public final static String UPDATED_ADMINISTRATION_ROLE_ARN = "administration:role:arn-new"; + + public final static String EXECUTION_ROLE_NAME = "execution:role:arn"; + public final static String UPDATED_EXECUTION_ROLE_NAME = "execution:role:arn-new"; + + public final static String TEMPLATE_URL = "http://s3-us-west-2.amazonaws.com/example/example.json"; + public final static String UPDATED_TEMPLATE_URL = "http://s3-us-west-2.amazonaws.com/example/new-example.json"; + + public final static String TEMPLATE_BODY = new StringBuilder() + .append("{\n") + .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n" ) + .append(" \"Resources\" : {\n") + .append(" \"IntegrationTestWaitHandle\" : {\n") + .append(" \"Type\" : \"AWS::CloudFormation::WaitConditionHandle\",\n") + .append(" \"Properties\" : {\n") + .append(" }\n") + .append(" }\n") + .append(" }\n") + .append("}").toString(); + + public final static String UPDATED_TEMPLATE_BODY = new StringBuilder() + .append("{\n") + .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n" ) + .append(" \"Resources\" : {\n") + .append(" \"IntegrationTestWaitHandle\" : {\n") + .append(" \"Type\" : \"AWS::CloudFormation::WaitCondition\",\n") + .append(" \"Properties\" : {\n") + .append(" }\n") + .append(" }\n") + .append(" }\n") + .append("}").toString(); + + public final static String STACK_SET_NAME = "StackSet"; + public final static String STACK_SET_ID = "StackSet:stack-set-id"; + + public final static String OPERATION_ID_1 = "operation-id-1"; + + public final static String OPERATION_ID_2 = "operation-id-2"; + public final static String OPERATION_ID_3 = "operation-id-3"; + public final static String OPERATION_ID_4 = "operation-id-4"; + public final static String OPERATION_ID_5 = "operation-id-5"; + + public final static String LOGICAL_ID = "MyResource"; + public final static String REQUEST_TOKEN = "token"; + + public final static String SERVICE_MANAGED = "SERVICE_MANAGED"; + public final static String SELF_MANAGED = "SELF_MANAGED"; + + public final static String US_EAST_1 = "us-east-1"; + public final static String US_WEST_1 = "us-west-1"; + public final static String US_EAST_2 = "us-east-2"; + public final static String US_WEST_2 = "us-west-2"; + + 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"; + public final static String ORGANIZATION_UNIT_ID_4 = "ou-example-4"; + + public final static String ACCOUNT_ID_1 = "111111111111"; + public final static String ACCOUNT_ID_2 = "222222222222"; + public final static String ACCOUNT_ID_3 = "333333333333"; + public final static String ACCOUNT_ID_4 = "444444444444"; + public final static String ACCOUNT_ID_5 = "555555555555"; + public final static String ACCOUNT_ID_6 = "666666666666"; + + public final static String PARAMETER_KEY_1 = "parameter_key_1"; + public final static String PARAMETER_KEY_2 = "parameter_key_3"; + public final static String PARAMETER_KEY_3 = "parameter_key_3"; + + public final static String PARAMETER_VALUE_1 = "parameter_value_1"; + public final static String PARAMETER_VALUE_2 = "parameter_value_2"; + public final static String PARAMETER_VALUE_3 = "parameter_value_3"; + + public final static software.amazon.cloudformation.stackset.Parameter PARAMETER_1 = + software.amazon.cloudformation.stackset.Parameter.builder() + .parameterKey(PARAMETER_KEY_1) + .parameterValue(PARAMETER_VALUE_1) + .build(); + + public final static software.amazon.cloudformation.stackset.Parameter PARAMETER_2 = + software.amazon.cloudformation.stackset.Parameter.builder() + .parameterKey(PARAMETER_KEY_2) + .parameterValue(PARAMETER_VALUE_2) + .build(); + + public final static software.amazon.cloudformation.stackset.Parameter PARAMETER_3 = + software.amazon.cloudformation.stackset.Parameter.builder() + .parameterKey(PARAMETER_KEY_3) + .parameterValue(PARAMETER_VALUE_3) + .build(); + + public final static Parameter SDK_PARAMETER_1 = Parameter.builder() + .parameterKey(PARAMETER_KEY_1) + .parameterValue(PARAMETER_VALUE_1) + .build(); + + public final static Parameter SDK_PARAMETER_2 = Parameter.builder() + .parameterKey(PARAMETER_KEY_2) + .parameterValue(PARAMETER_VALUE_2) + .build(); + + public final static Parameter SDK_PARAMETER_3 = Parameter.builder() + .parameterKey(PARAMETER_KEY_3) + .parameterValue(PARAMETER_VALUE_3) + .build(); + + public final static Map TEMPLATE_MAP = ImmutableMap.of("TemplateURL", "foo"); + + public final static Map DESIRED_RESOURCE_TAGS = ImmutableMap.of( + "key1", "val1", "key2", "val2", "key3", "val3"); + public final static Map PREVIOUS_RESOURCE_TAGS = ImmutableMap.of( + "key-1", "val1", "key-2", "val2", "key-3", "val3"); + 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 DeploymentTargets SERVICE_MANAGED_TARGETS = DeploymentTargets.builder() + .organizationalUnitIds(new HashSet<>(Arrays.asList( + ORGANIZATION_UNIT_ID_1, ORGANIZATION_UNIT_ID_2))) + .build(); + + public final static DeploymentTargets UPDATED_SERVICE_MANAGED_TARGETS = DeploymentTargets.builder() + .organizationalUnitIds(new HashSet<>(Arrays.asList( + ORGANIZATION_UNIT_ID_3, ORGANIZATION_UNIT_ID_4))) + .build(); + + public final static DeploymentTargets SELF_MANAGED_TARGETS = DeploymentTargets.builder() + .accounts(new HashSet<>(Arrays.asList( + ACCOUNT_ID_1, ACCOUNT_ID_2))) + .build(); + + public final static DeploymentTargets UPDATED_SELF_MANAGED_TARGETS = DeploymentTargets.builder() + .accounts(new HashSet<>(Arrays.asList( + ACCOUNT_ID_3, ACCOUNT_ID_4))) + .build(); + + public final static Set CAPABILITIES = new HashSet<>(Arrays.asList( + "CAPABILITY_IAM", "CAPABILITY_NAMED_IAM")); + + public final static OperationPreferences OPERATION_PREFERENCES = OperationPreferences.builder() + .failureToleranceCount(0) + .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"), + new software.amazon.cloudformation.stackset.Tag("key3", "val3"))); + + public final static Set TAGS_TO_UPDATE = new HashSet<>(Arrays.asList( + new software.amazon.cloudformation.stackset.Tag("key-1", "val1"), + new software.amazon.cloudformation.stackset.Tag("key-2", "val2"), + new software.amazon.cloudformation.stackset.Tag("key-3", "val3"))); + + public final static Set TAGGED_RESOURCES = new HashSet<>(Arrays.asList( + Tag.builder().key("key1").value("val1").build(), + Tag.builder().key("key2").value("val2").build(), + Tag.builder().key("key3").value("val3").build())); + + public final static Set SDK_TAGS_TO_UPDATE = new HashSet<>(Arrays.asList( + Tag.builder().key("key-1").value("val1").build(), + Tag.builder().key("key-2").value("val2").build(), + Tag.builder().key("key-3").value("val3").build())); + + public final static AutoDeployment AUTO_DEPLOYMENT = AutoDeployment.builder() + .enabled(true) + .retainStacksOnAccountRemoval(true) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_1 = StackInstanceSummary.builder() + .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .region(US_EAST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_2 = StackInstanceSummary.builder() + .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .region(US_WEST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_3 = StackInstanceSummary.builder() + .organizationalUnitId(ORGANIZATION_UNIT_ID_2) + .region(US_EAST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_4 = StackInstanceSummary.builder() + .organizationalUnitId(ORGANIZATION_UNIT_ID_2) + .region(US_WEST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_5 = StackInstanceSummary.builder() + .account(ACCOUNT_ID_1) + .region(US_EAST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_6 = StackInstanceSummary.builder() + .account(ACCOUNT_ID_1) + .region(US_WEST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_7 = StackInstanceSummary.builder() + .account(ACCOUNT_ID_2) + .region(US_EAST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_8 = StackInstanceSummary.builder() + .account(ACCOUNT_ID_2) + .region(US_WEST_1) + .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); + + public final static List SERVICE_SELF_STACK_INSTANCE_SUMMARIES = Arrays.asList( + STACK_INSTANCE_SUMMARY_5, STACK_INSTANCE_SUMMARY_6, STACK_INSTANCE_SUMMARY_7, STACK_INSTANCE_SUMMARY_8); + + public final static software.amazon.awssdk.services.cloudformation.model.AutoDeployment SDK_AUTO_DEPLOYMENT = + software.amazon.awssdk.services.cloudformation.model.AutoDeployment.builder() + .retainStacksOnAccountRemoval(true) + .enabled(true) + .build(); + + public final static StackSetSummary STACK_SET_SUMMARY_1 = StackSetSummary.builder() + .autoDeployment(SDK_AUTO_DEPLOYMENT) + .description(DESCRIPTION) + .permissionModel(PermissionModels.SERVICE_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) + .autoDeployment(SDK_AUTO_DEPLOYMENT) + .capabilitiesWithStrings(CAPABILITIES) + .description(DESCRIPTION) + .organizationalUnitIds(ORGANIZATION_UNIT_ID_1, ORGANIZATION_UNIT_ID_2) + .parameters(SDK_PARAMETER_1, SDK_PARAMETER_2) + .permissionModel(PermissionModels.SERVICE_MANAGED) + .tags(TAGGED_RESOURCES) + .build(); + + public final static StackSet SELF_MANAGED_STACK_SET = StackSet.builder() + .stackSetId(STACK_SET_ID) + .stackSetName(STACK_SET_NAME) + .capabilitiesWithStrings(CAPABILITIES) + .description(DESCRIPTION) + .parameters(SDK_PARAMETER_1, SDK_PARAMETER_2) + .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) + .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) + .description(DESCRIPTION) + .regions(REGIONS) + .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) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_3))) + .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) + .templateURL(TEMPLATE_URL) + .tags(TAGS) + .operationPreferences(OPERATION_PREFERENCES) + .build(); + + public final static ResourceModel SIMPLE_TEMPLATE_BODY_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .deploymentTargets(SERVICE_MANAGED_TARGETS) + .permissionModel(SERVICE_MANAGED) + .autoDeployment(AUTO_DEPLOYMENT) + .regions(REGIONS) + .templateBody(TEMPLATE_BODY) + .tags(TAGS) + .operationPreferences(OPERATION_PREFERENCES) + .build(); + + + 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) + .templateURL(UPDATED_TEMPLATE_URL) + .tags(TAGS_TO_UPDATE) + .build(); + + public final static DescribeStackSetOperationResponse OPERATION_SUCCEED_RESPONSE = + DescribeStackSetOperationResponse.builder() + .stackSetOperation(StackSetOperation.builder() + .status(StackSetOperationStatus.SUCCEEDED) + .build()) + .build(); + + public final static DescribeStackSetOperationResponse OPERATION_RUNNING_RESPONSE = + DescribeStackSetOperationResponse.builder() + .stackSetOperation(StackSetOperation.builder() + .status(StackSetOperationStatus.RUNNING) + .build()) + .build(); + + public final static DescribeStackSetOperationResponse OPERATION_STOPPED_RESPONSE = + DescribeStackSetOperationResponse.builder() + .stackSetOperation(StackSetOperation.builder() + .status(StackSetOperationStatus.STOPPED) + .build()) + .build(); + + public final static CreateStackSetResponse CREATE_STACK_SET_RESPONSE = + CreateStackSetResponse.builder() + .stackSetId(STACK_SET_ID) + .build(); + + public final static CreateStackInstancesResponse CREATE_STACK_INSTANCES_RESPONSE = + CreateStackInstancesResponse.builder() + .operationId(OPERATION_ID_1) + .build(); + + public final static UpdateStackSetResponse UPDATE_STACK_SET_RESPONSE = + UpdateStackSetResponse.builder() + .operationId(OPERATION_ID_1) + .build(); + + public final static UpdateStackInstancesResponse UPDATE_STACK_INSTANCES_RESPONSE = + UpdateStackInstancesResponse.builder() + .operationId(OPERATION_ID_1) + .build(); + + public final static DeleteStackInstancesResponse DELETE_STACK_INSTANCES_RESPONSE = + DeleteStackInstancesResponse.builder() + .operationId(OPERATION_ID_1) + .build(); + + public final static DescribeStackSetResponse DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE = + DescribeStackSetResponse.builder() + .stackSet(SERVICE_MANAGED_STACK_SET) + .build(); + + public final static ListStackInstancesResponse LIST_SERVICE_MANAGED_STACK_SET_RESPONSE = + ListStackInstancesResponse.builder() + .summaries(SERVICE_MANAGED_STACK_INSTANCE_SUMMARIES) + .build(); + + public final static DescribeStackSetResponse DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE = + DescribeStackSetResponse.builder() + .stackSet(SELF_MANAGED_STACK_SET) + .build(); + + public final static ListStackInstancesResponse LIST_SELF_MANAGED_STACK_SET_RESPONSE = + ListStackInstancesResponse.builder() + .summaries(SERVICE_SELF_STACK_INSTANCE_SUMMARIES) + .build(); + + public final static ListStackSetsResponse LIST_STACK_SETS_RESPONSE = + ListStackSetsResponse.builder() + .summaries(STACK_SET_SUMMARY_1) + .build(); + +} 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 new file mode 100644 index 0000000..e4885d1 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java @@ -0,0 +1,108 @@ +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; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +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; +import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_URL; + +@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"); + + @Spy + private Validator validator; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + 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) { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, null, invalidS3Url, logger)); + } + } + + @Test + public void testValidateTemplate_BothBodyAndUriExist() { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, TEMPLATE_BODY, TEMPLATE_URL, logger)); + } + + @Test + public void testValidateTemplate_BothBodyAndUriNotExist() { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, null, null, logger)); + } + + @Test + 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); + } + } +} diff --git a/aws-cloudformation-stackset/template.yml b/aws-cloudformation-stackset/template.yml new file mode 100644 index 0000000..18e9bcf --- /dev/null +++ b/aws-cloudformation-stackset/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::CloudFormation::StackSet resource type + +Globals: + Function: + Timeout: 60 # docker start-up times can be long for SAM CLI + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.cloudformation.stackset.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-cloudformation-stackset-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.cloudformation.stackset.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-cloudformation-stackset-handler-1.0-SNAPSHOT.jar + From 705bb41f86bd159f2950c39cff3bcd999cb57f88 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Tue, 21 Apr 2020 18:57:22 -0700 Subject: [PATCH 02/19] Update to use progress chaining pattern for stackset --- aws-cloudformation-stackset/.rpdk-config | 3 +- aws-cloudformation-stackset/README.md | 13 +- .../aws-cloudformation-stackset.json | 97 ++-- aws-cloudformation-stackset/pom.xml | 17 +- .../stackset/BaseHandlerStd.java | 218 +++++++++ .../stackset/CallbackContext.java | 84 +--- .../stackset/Configuration.java | 24 - .../stackset/CreateHandler.java | 124 ++--- .../stackset/DeleteHandler.java | 118 ++--- .../cloudformation/stackset/ListHandler.java | 33 +- .../cloudformation/stackset/ReadHandler.java | 27 +- .../stackset/UpdateHandler.java | 162 +++---- .../translator/PropertyTranslator.java | 26 +- .../translator/RequestTranslator.java | 45 +- .../util/AwsCredentialsExtractor.java | 3 +- .../stackset/util/ClientBuilder.java | 8 +- .../stackset/util/Comparator.java | 112 +---- .../stackset/util/EnumUtils.java | 13 - .../stackset/util/InstancesAnalyzer.java | 216 +++++++++ .../stackset/util/OperationOperator.java | 214 --------- .../stackset/util/PhysicalIdGenerator.java | 2 +- .../stackset/util/ResourceModelBuilder.java | 84 ++-- .../stackset/util/Stabilizer.java | 197 -------- .../stackset/util/StackInstance.java | 24 + .../stackset/util/TemplateParser.java | 2 +- .../stackset/util/UpdatePlaceholder.java | 62 --- .../test/java/resources/invalid_format.json | 10 - .../test/java/resources/invalid_format.yaml | 6 - .../src/test/java/resources/nested_stack.json | 11 - .../test/java/resources/nested_stackset.json | 11 - .../src/test/java/resources/text_null.json | 1 - .../src/test/java/resources/valid.json | 46 -- .../src/test/java/resources/valid.yaml | 27 -- .../stackset/AbstractTestBase.java | 47 ++ .../stackset/CreateHandlerTest.java | 293 +++--------- .../stackset/DeleteHandlerTest.java | 177 ++------ .../stackset/ListHandlerTest.java | 69 ++- .../stackset/ReadHandlerTest.java | 82 ++-- .../stackset/UpdateHandlerTest.java | 429 ++---------------- .../translator/PropertyTranslatorTest.java | 6 +- .../stackset/util/ComparatorTest.java | 81 ---- .../stackset/util/TestUtils.java | 145 +++++- .../stackset/util/ValidatorTest.java | 38 -- 43 files changed, 1229 insertions(+), 2178 deletions(-) create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/BaseHandlerStd.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/EnumUtils.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/OperationOperator.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Stabilizer.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstance.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stack.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/text_null.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.yaml create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java 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); - } - } } From e242715957a8cedac19ca29afec080a2bd31aa1b Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Wed, 22 Apr 2020 17:41:23 -0700 Subject: [PATCH 03/19] Updated using default framework --- .../aws-cloudformation-stackset.json | 97 ++++--- .../stackset/CallbackContext.java | 47 ++-- .../stackset/CreateHandler.java | 89 +++--- .../stackset/DeleteHandler.java | 51 +--- .../cloudformation/stackset/ListHandler.java | 5 +- .../stackset/UpdateHandler.java | 93 ++----- .../translator/PropertyTranslator.java | 26 +- .../translator/RequestTranslator.java | 45 ++- .../stackset/util/ClientBuilder.java | 8 +- .../stackset/util/Comparator.java | 64 ++--- .../stackset/util/EnumUtils.java | 5 +- .../stackset/util/InstancesAnalyzer.java | 260 ++++++++++++++++++ .../stackset/util/OperationOperator.java | 162 ++++------- .../stackset/util/ResourceModelBuilder.java | 75 +++-- .../stackset/util/Stabilizer.java | 52 ++-- .../stackset/util/StackInstance.java | 24 ++ .../stackset/util/UpdatePlaceholder.java | 62 ----- .../test/java/resources/invalid_format.json | 10 - .../test/java/resources/invalid_format.yaml | 6 - .../src/test/java/resources/nested_stack.json | 11 - .../test/java/resources/nested_stackset.json | 11 - .../src/test/java/resources/text_null.json | 1 - .../src/test/java/resources/valid.json | 46 ---- .../src/test/java/resources/valid.yaml | 27 -- .../stackset/CreateHandlerTest.java | 123 +++++---- .../stackset/DeleteHandlerTest.java | 78 +++--- .../stackset/ListHandlerTest.java | 50 +++- .../stackset/ReadHandlerTest.java | 44 ++- .../stackset/UpdateHandlerTest.java | 243 ++++++++-------- .../stackset/util/ComparatorTest.java | 27 -- .../stackset/util/TestUtils.java | 252 ++++++++++++++--- .../stackset/util/ValidatorTest.java | 54 ++-- 32 files changed, 1183 insertions(+), 965 deletions(-) create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstance.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/UpdatePlaceholder.java delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/invalid_format.yaml delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stack.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/nested_stackset.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/text_null.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.json delete mode 100644 aws-cloudformation-stackset/src/test/java/resources/valid.yaml 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/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java index 9fa4130..2f96658 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 @@ -5,10 +5,12 @@ 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.stackset.util.EnumUtils.Operations; import java.util.Arrays; +import java.util.LinkedList; import java.util.Map; +import java.util.Queue; import java.util.stream.Collectors; @Data @@ -19,34 +21,42 @@ public class CallbackContext { // Operation Id to verify stabilization for StackSet operation. private String operationId; - // Elapsed counts of retries on specific exceptions. - private int retries; + // Indicates initiation of analyzing template. + private boolean templateAnalyzed; // Indicates initiation of resource stabilization. - private boolean stabilizationStarted; + private boolean stackSetCreated; // 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; + private boolean addStacksStarted; // Indicates initiation of stack instances delete. - private boolean deleteStacksByTargetsStarted; + private boolean deleteStacksStarted; // Indicates initiation of stack set update. private boolean updateStackSetStarted; // Indicates initiation of stack instances update. - private boolean updateStackInstancesStarted; + private boolean updateStacksStarted; // Total running time @Builder.Default private int elapsedTime = 0; + private StackInstances stackInstancesInOperation; + + // List to keep track on the complete status for creating + @Builder.Default + private Queue createStacksQueue = new LinkedList<>(); + + // List to keep track on stack instances for deleting + @Builder.Default + private Queue deleteStacksQueue = new LinkedList<>(); + + // List to keep track on stack instances for update + @Builder.Default + private Queue updateStacksQueue = new LinkedList<>(); + /** * Default as 0, will be {@link software.amazon.cloudformation.stackset.util.Stabilizer#BASE_CALLBACK_DELAY_SECONDS} * When it enters the first IN_PROGRESS callback @@ -55,14 +65,9 @@ public class CallbackContext { // Map to keep track on the complete status for operations in Update @Builder.Default - private Map operationsStabilizationMap = Arrays.stream(UpdateOperations.values()) + private Map operationsStabilizationMap = Arrays.stream(Operations.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 @@ -72,8 +77,4 @@ 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/CreateHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java index 6f7b049..cbc2a14 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 @@ -1,48 +1,31 @@ package software.amazon.cloudformation.stackset; -import lombok.AllArgsConstructor; -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.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.ResourceHandlerRequest; import software.amazon.cloudformation.stackset.util.ClientBuilder; +import software.amazon.cloudformation.stackset.util.InstancesAnalyzer; +import software.amazon.cloudformation.stackset.util.OperationOperator; 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.Comparator.isAddingStackInstances; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.ADD_INSTANCES; import static software.amazon.cloudformation.stackset.util.Stabilizer.getDelaySeconds; -@Builder -@NoArgsConstructor -@AllArgsConstructor public class CreateHandler extends BaseHandler { - 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( final AmazonWebServicesClientProxy proxy, @@ -50,20 +33,30 @@ public ProgressEvent handleRequest( final CallbackContext callbackContext, 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(); + 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(); + final OperationOperator operator = OperationOperator.builder() + .client(client).desiredModel(model) + .logger(logger).proxy(proxy).context(context) + .build(); + InstancesAnalyzer.builder().desiredModel(model).build().analyzeForCreate(context); // Create a resource when a creation has not initialed - if (!context.isStabilizationStarted()) { - validator.validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); + if (!context.isStackSetCreated()) { + new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); final String stackSetName = PhysicalIdGenerator.generatePhysicalId(request); - createStackSet(stackSetName, request.getClientRequestToken()); + createStackSet(proxy, model, logger, client, context, stackSetName, request.getClientRequestToken()); + } + + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksStarted(), null, + ADD_INSTANCES, context.getCreateStacksQueue(), model, context)) { - } else if (stabilizer.isStabilized(model, context)) { + operator.updateStackSet(ADD_INSTANCES); + } + + if (context.getOperationsStabilizationMap().get(ADD_INSTANCES)) { return ProgressEvent.defaultSuccessHandler(model); } @@ -73,15 +66,22 @@ public ProgressEvent handleRequest( model); } - private void createStackSet(final String stackSetName, final String requestToken) { + private void createStackSet( + final AmazonWebServicesClientProxy proxy, + final ResourceModel model, + final Logger logger, + final CloudFormationClient client, + final CallbackContext context, + final String stackSetName, + final String requestToken) { + try { final CreateStackSetResponse response = proxy.injectCredentialsAndInvokeV2( createStackSetRequest(model, stackSetName, requestToken), client::createStackSet); model.setStackSetId(response.stackSetId()); logger.log(String.format("%s [%s] StackSet creation succeeded", ResourceModel.TYPE_NAME, stackSetName)); - - createStackInstances(stackSetName); + context.setStackSetCreated(true); } catch (final AlreadyExistsException e) { throw new CfnAlreadyExistsException(e); @@ -93,25 +93,4 @@ private void createStackSet(final String stackSetName, final String requestToken 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); - - } catch (final OperationInProgressException e) { - context.incrementRetryCounter(); - } - } } 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..649cfe8 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,20 +1,20 @@ 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.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.ResourceHandlerRequest; import software.amazon.cloudformation.stackset.util.ClientBuilder; +import software.amazon.cloudformation.stackset.util.InstancesAnalyzer; +import software.amazon.cloudformation.stackset.util.OperationOperator; 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.Comparator.isDeletingStackInstances; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.DELETE_INSTANCES; import static software.amazon.cloudformation.stackset.util.Stabilizer.getDelaySeconds; public class DeleteHandler extends BaseHandler { @@ -29,14 +29,20 @@ public ProgressEvent handleRequest( 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(); + final OperationOperator operator = OperationOperator.builder() + .client(client).desiredModel(model) + .logger(logger).proxy(proxy).context(context) + .build(); + InstancesAnalyzer.builder().desiredModel(model).build().analyzeForDelete(context); + + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksStarted(), null, + DELETE_INSTANCES, context.getDeleteStacksQueue(), model, context)) { - // Delete resource - if (!context.isStabilizationStarted()) { - deleteStackInstances(proxy, model, logger, client, context); + operator.updateStackSet(DELETE_INSTANCES); + } - } else if (stabilizer.isStabilized(model, context)){ + if (context.getOperationsStabilizationMap().get(DELETE_INSTANCES)){ deleteStackSet(proxy, model.getStackSetId(), logger, client); return ProgressEvent.defaultSuccessHandler(model); @@ -62,31 +68,4 @@ private void deleteStackSet( throw new CfnNotFoundException(e); } } - - private void deleteStackInstances( - final AmazonWebServicesClientProxy proxy, - final ResourceModel model, - final Logger logger, - final CloudFormationClient client, - final CallbackContext context) { - - try { - final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( - deleteStackInstancesRequest(model.getStackSetId(), - model.getOperationPreferences(), model.getDeploymentTargets(), model.getRegions()), - client::deleteStackInstances); - - 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); - - } catch (final OperationInProgressException e) { - context.incrementRetryCounter(); - } - } } 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..c391269 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,22 +1,19 @@ 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.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 { 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..9c0d707 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 @@ -6,39 +6,25 @@ import software.amazon.cloudformation.proxy.ProgressEvent; 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.OperationOperator; import software.amazon.cloudformation.stackset.util.Stabilizer; -import software.amazon.cloudformation.stackset.util.UpdatePlaceholder; import software.amazon.cloudformation.stackset.util.Validator; -import java.util.Set; - 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.EnumUtils.Operations.ADD_INSTANCES; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.DELETE_INSTANCES; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.STACK_SET_CONFIGS; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.UPDATE_INSTANCES; 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 BaseHandler { - private Validator validator; - - public UpdateHandler() { - this.validator = new Validator(); - } - - public UpdateHandler(Validator validator) { - this.validator = validator; - } - @Override public ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, @@ -55,63 +41,33 @@ public ProgressEvent handleRequest( .client(client).desiredModel(desiredModel).previousModel(previousModel) .logger(logger).proxy(proxy).context(context) .build(); + InstancesAnalyzer.builder().desiredModel(desiredModel).previousModel(previousModel).build() + .analyzeForUpdate(context); 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 (stabilizer.isPerformingOperation(isStackSetUpdating, context.isUpdateStackSetStarted(),null, + STACK_SET_CONFIGS, null, desiredModel, context)) { - if (isDeletingStackInstances(regionsToDelete, targetsToDelete, context)) { - - if (stabilizer.isPerformingOperation( - !regionsToDelete.isEmpty(), context.isDeleteStacksByRegionsStarted(), - STACK_SET_CONFIGS, DELETE_INSTANCES_BY_REGIONS, desiredModel, context)) { - - 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)) { + new Validator().validateTemplate(proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger); + operator.updateStackSet(STACK_SET_CONFIGS); + } - operator.updateStackSet(DELETE_INSTANCES_BY_TARGETS, regionsToDelete, targetsToDelete); - } - } + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksStarted(), + STACK_SET_CONFIGS, DELETE_INSTANCES, context.getDeleteStacksQueue(), desiredModel, context)) { - if (isAddingStackInstances(regionsToAdd, targetsToAdd, context)) { + operator.updateStackSet(DELETE_INSTANCES); + } - if (stabilizer.isPerformingOperation( - !regionsToAdd.isEmpty(), context.isAddStacksByRegionsStarted(), - DELETE_INSTANCES_BY_TARGETS, ADD_INSTANCES_BY_REGIONS, desiredModel, context)) { + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksStarted(), + DELETE_INSTANCES, ADD_INSTANCES, context.getCreateStacksQueue(), desiredModel, context)) { - operator.updateStackSet(ADD_INSTANCES_BY_REGIONS, regionsToAdd, null); - } + operator.updateStackSet(ADD_INSTANCES); + } - if (stabilizer.isPerformingOperation( - !targetsToAdd.isEmpty(), context.isAddStacksByTargetsStarted(), - ADD_INSTANCES_BY_REGIONS, ADD_INSTANCES_BY_TARGETS, desiredModel, context)) { + if (stabilizer.isPerformingOperation(isUpdatingStackInstances(context), context.isUpdateStacksStarted(), + ADD_INSTANCES, UPDATE_INSTANCES, context.getUpdateStacksQueue(), desiredModel, context)) { - operator.updateStackSet(ADD_INSTANCES_BY_TARGETS, regionsToAdd, targetsToAdd); - } - } + operator.updateStackSet(UPDATE_INSTANCES); } if (isUpdateStabilized(context)) { @@ -123,8 +79,5 @@ public ProgressEvent handleRequest( getDelaySeconds(context), desiredModel); } - } - } - 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/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..4fa2e29 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 @@ -2,16 +2,11 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.services.cloudformation.model.PermissionModels; import software.amazon.cloudformation.stackset.CallbackContext; 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 @@ -41,10 +36,11 @@ public static boolean isStackSetConfigEquals( if (StringUtils.compare(previousModel.getExecutionRoleName(), desiredModel.getExecutionRoleName()) != 0) return false; - if (StringUtils.compare(previousModel.getTemplateURL(), desiredModel.getTemplateURL()) != 0) + if (StringUtils.compare(previousModel.getTemplateBody(), desiredModel.getTemplateBody()) != 0) return false; - if (StringUtils.compare(previousModel.getTemplateBody(), desiredModel.getTemplateBody()) != 0) + // If TemplateURL is specified, always call Update API, Service client will decide if it is updatable + if (desiredModel.getTemplateBody() == null && desiredModel.getTemplateURL() != null) return false; return true; @@ -52,24 +48,12 @@ public static boolean isStackSetConfigEquals( /** * 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); + public static boolean isUpdatingStackInstances(final CallbackContext context) { + // If no stack instances need to be updated + if (context.getUpdateStacksQueue().isEmpty() && !context.isUpdateStacksStarted()) { return false; } return true; @@ -77,20 +61,12 @@ public static boolean isUpdatingStackInstances( /** * 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); + public static boolean isDeletingStackInstances(final CallbackContext context) { + // If no stack instances need to be deleted + if (context.getDeleteStacksQueue().isEmpty() && !context.isDeleteStacksStarted()) { return false; } return true; @@ -98,20 +74,12 @@ public static boolean isDeletingStackInstances( /** * 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); + public static boolean isAddingStackInstances(final CallbackContext context) { + // If no stack instances need to be added + if (context.getCreateStacksQueue().isEmpty() && !context.isAddStacksStarted()) { return false; } return true; @@ -125,6 +93,10 @@ public static boolean isAddingStackInstances( */ public static boolean isEquals(final Collection collection1, final Collection collection2) { if (collection1 == null) return collection2 == null ? true : false; - return CollectionUtils.isEqualCollection(collection1, collection2); + return collection1.equals(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 index a02b5ff..616e6a1 100644 --- 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 @@ -5,9 +5,8 @@ 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 + public enum Operations { + STACK_SET_CONFIGS, ADD_INSTANCES, DELETE_INSTANCES, UPDATE_INSTANCES } } 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..0d9a70b --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java @@ -0,0 +1,260 @@ +package software.amazon.cloudformation.stackset.util; + +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +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.LinkedList; +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) { + if (context.isTemplateAnalyzed()) return; + + 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.setCreateStacksQueue(new LinkedList<>(stackInstancesGroupToAdd)); + context.setDeleteStacksQueue(new LinkedList<>(stackInstancesGroupToDelete)); + context.setUpdateStacksQueue(new LinkedList<>(stackInstancesGroupToUpdate)); + + context.setTemplateAnalyzed(true); + } + + /** + * 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 (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; + if (desiredModel.getStackInstancesGroup().size() == 1) { + context.setCreateStacksQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + } + final boolean isSelfManaged = isSelfManaged(desiredModel); + + final Set desiredStackInstances = + flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); + + final Set stackInstancesGroupToAdd = aggregateStackInstances(desiredStackInstances, isSelfManaged); + context.setCreateStacksQueue(new LinkedList<>(stackInstancesGroupToAdd)); + + context.setTemplateAnalyzed(true); + } + + /** + * Analyzes {@link StackInstances} that need to be modified during the update + * Updates callbackContext with the stack list to delete + * @param context {@link CallbackContext} + */ + public void analyzeForDelete(final CallbackContext context) { + if (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; + if (desiredModel.getStackInstancesGroup().size() == 1) { + context.setDeleteStacksQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + } + final boolean isSelfManaged = isSelfManaged(desiredModel); + + final Set desiredStackInstances = + flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); + + final Set stackInstancesGroupToDelete = aggregateStackInstances(desiredStackInstances, isSelfManaged); + context.setDeleteStacksQueue(new LinkedList<>(stackInstancesGroupToDelete)); + + context.setTemplateAnalyzed(true); + } + + /** + * 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); + } + + /** + * Aggregates flat {@link StackInstance} to a group of {@link StackInstances} to construct resource model + *

Note:

+ * This is being used only because currently we can not retrieve OUs from CloudFormation DescribeStackInstances API + * Hence, we are returning AccountIDs for stack instances. + * @param flatStackInstances {@link StackInstance} + * @return {@link StackInstances} set + */ + public static Set aggregateStackInstancesForRead(@NonNull final Set flatStackInstances) { + final Set groupedStacks = groupInstancesByTargets(flatStackInstances, true); + return aggregateInstancesByRegions(groupedStacks, true); + } + + + /** + * Group regions by {@link DeploymentTargets} and {@link StackInstance#getParameters()} + * @return {@link StackInstances} + */ + private static Set groupInstancesByTargets( + 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<>(); + if (CollectionUtils.isNullOrEmpty(stackInstancesGroup)) return flatStacks; + + 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 CfnInvalidRequestException( + 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 index 876757c..45f6f70 100644 --- 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 @@ -11,6 +11,8 @@ 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.UpdateStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesResponse; import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; @@ -19,13 +21,17 @@ import software.amazon.cloudformation.stackset.CallbackContext; import software.amazon.cloudformation.stackset.DeploymentTargets; import software.amazon.cloudformation.stackset.ResourceModel; +import software.amazon.cloudformation.stackset.StackInstances; import java.util.HashSet; +import java.util.Queue; 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.deleteStackSetRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetRequest; +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackInstancesRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest; /** @@ -57,143 +63,84 @@ private String updateStackSetConfig() { } /** - * 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()} + * Get {@link StackSet} from service client using stackSetId + * @param stackSetId StackSet Id + * @return {@link StackSet} */ - 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); + 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); } - - 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()} + * Invokes CreateStackInstances API to add new {@link StackInstances} based on {@link CallbackContext#getCreateStacksQueue()} + * @return Operation Id from {@link CreateStackInstancesResponse} */ - private String addStackInstancesByRegions(final Set regionsToAdd) { + private String addStackInstances() { + final Queue instancesQueue = context.getCreateStacksQueue(); final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( createStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), - desiredModel.getDeploymentTargets(), regionsToAdd), - client::createStackInstances); - - context.setAddStacksByRegionsStarted(true); + instancesQueue.peek()), client::createStackInstances); + context.setAddStacksStarted(true); + // We remove the stack instances from queue Only if API invocation succeeds + context.setStackInstancesInOperation(instancesQueue.remove()); 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()} + * Invokes DeleteStackInstances API to delete old {@link StackInstances} based on {@link CallbackContext#getDeleteStacksQueue()} + * @return Operation Id from {@link DeleteStackInstancesResponse} */ - 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); + private String deleteStackInstances() { + final Queue instancesQueue = context.getDeleteStacksQueue(); + final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( + deleteStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), + instancesQueue.peek()), client::deleteStackInstances); + context.setDeleteStacksStarted(true); + // We remove the stack instances from queue Only if API invocation succeeds + context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); } /** - * Get {@link StackSet} from service client using stackSetId - * @param stackSetId StackSet Id - * @return {@link StackSet} + * Invokes UpdateStackInstances API to update existing {@link StackInstances} based on {@link CallbackContext#getUpdateStacksQueue()} + * @return Operation Id from {@link UpdateStackInstancesResponse} */ - 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); - } + private String updateStackInstances() { + final Queue instancesQueue = context.getUpdateStacksQueue(); + final UpdateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( + updateStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), + instancesQueue.peek()), client::updateStackInstances); + context.setUpdateStacksStarted(true); + // We remove the stack instances from queue Only if API invocation succeeds + context.setStackInstancesInOperation(instancesQueue.remove()); + return response.operationId(); } /** - * 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 + * Update the StackSet with the {@link EnumUtils.Operations} passed in + * @param operation {@link EnumUtils.Operations} */ - public void updateStackSet( - final EnumUtils.UpdateOperations operation, - final Set regions, - final Set targets) { - + public void updateStackSet(final EnumUtils.Operations operation) { 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); + case DELETE_INSTANCES: + operationId = deleteStackInstances(); break; - case ADD_INSTANCES_BY_REGIONS: - operationId = addStackInstancesByRegions(regions); + case ADD_INSTANCES: + operationId = addStackInstances(); break; - case ADD_INSTANCES_BY_TARGETS: - operationId = addStackInstancesByTargets(regions, targets); + case UPDATE_INSTANCES: + operationId = updateStackInstances(); } logger.log(String.format("%s [%s] %s update initiated", @@ -208,7 +155,6 @@ public void updateStackSet( } 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/ResourceModelBuilder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java index 8d6e789..37a9edd 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.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.aggregateStackInstancesForRead; /** * Utility class to construct {@link ResourceModel} for Read/List request based on {@link StackSet} @@ -32,14 +36,13 @@ public class ResourceModelBuilder { private AmazonWebServicesClientProxy proxy; private CloudFormationClient client; 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 +54,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 = aggregateStackInstancesForRead(stackInstanceSet); + 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); 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 = proxy.injectCredentialsAndInvokeV2( + describeStackInstanceRequest(summary.account(), summary.region(), summary.stackSetId()), + 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 index 73ec5d0..35f58f2 100644 --- 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 @@ -5,14 +5,18 @@ 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.awssdk.utils.CollectionUtils; 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 software.amazon.cloudformation.stackset.StackInstances; +import software.amazon.cloudformation.stackset.util.EnumUtils.Operations; +import java.util.Collection; +import java.util.List; import java.util.Map; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetOperationRequest; @@ -83,12 +87,6 @@ public boolean isStabilized(final ResourceModel model, final CallbackContext con 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) { @@ -118,6 +116,7 @@ private StackSetOperationStatus getStackSetOperationStatus(final String stackSet private Boolean isStackSetOperationDone(final StackSetOperationStatus status, final String operationId) { switch (status) { case SUCCEEDED: + logger.log(String.format("%s has been successfully stabilized.", operationId)); return true; case RUNNING: case QUEUED: @@ -130,11 +129,10 @@ private Boolean isStackSetOperationDone(final StackSetOperationStatus status, fi } /** - * Checks if this operation {@link UpdateOperations} needs to run at this stabilization runtime - * @param isRequiredToRun If the operation is necessary to operate + * Checks if this operation {@link Operations} needs to run at this stabilization runtime * @param isStabilizedStarted If the operation has been initialed - * @param previousOperation Previous {@link UpdateOperations} - * @param operation {@link UpdateOperations} + * @param previousOperation Previous {@link Operations} + * @param operation {@link Operations} * @param model {@link ResourceModel} * @param context {@link CallbackContext} * @return boolean @@ -142,19 +140,20 @@ private Boolean isStackSetOperationDone(final StackSetOperationStatus status, fi public boolean isPerformingOperation( final boolean isRequiredToRun, final boolean isStabilizedStarted, - final UpdateOperations previousOperation, - final UpdateOperations operation, + final Operations previousOperation, + final Operations operation, + final Collection updateList, final ResourceModel model, final CallbackContext context) { - final Map operationsCompletionMap = context.getOperationsStabilizationMap(); + 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 it is not required to run, mark this kind of operation as complete if (!isRequiredToRun) { operationsCompletionMap.put(operation, true); return false; @@ -163,8 +162,19 @@ public boolean isPerformingOperation( // 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); + final boolean isCurrentOperationDone = isStabilized(model, context); + if (isCurrentOperationDone) { + // If current operation is complete and update list is empty, meaning this kind of Operations is complete + // otherwise, we still need to perform the rest of list + if (CollectionUtils.isNullOrEmpty(updateList)) { + operationsCompletionMap.put(operation, true); + return false; + } else { + return true; + } + } + + // If current operation is not stabilized return false; } @@ -175,21 +185,21 @@ public boolean isPerformingOperation( * @return boolean indicates whether the update is done */ public static boolean isUpdateStabilized(final CallbackContext context) { - for (Map.Entry entry : context.getOperationsStabilizationMap().entrySet()) { + for (Map.Entry entry : context.getOperationsStabilizationMap().entrySet()) { if (!entry.getValue()) return false; } return true; } /** - * Checks if previous {@link UpdateOperations} is complete + * Checks if previous {@link Operations} is complete * to avoid running other operations until previous operation is done * @param context {@link CallbackContext} - * @param previousOperation {@link UpdateOperations} + * @param previousOperation {@link Operations} * @return boolean indicates whether the previous operation is done */ public static boolean isPreviousOperationDone(final CallbackContext context, - final UpdateOperations previousOperation) { + final Operations 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/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/CreateHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java index bcfb432..4cc6dc8 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 @@ -22,29 +22,30 @@ 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.LinkedList; +import java.util.Set; 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.EnumUtils.Operations.ADD_INSTANCES; 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 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.INVALID_SELF_MANAGED_MODEL; 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 { @@ -53,9 +54,6 @@ public class CreateHandlerTest { private ResourceHandlerRequest request; - @Mock - private Validator validator; - @Mock private AmazonWebServicesClientProxy proxy; @@ -66,10 +64,9 @@ public class CreateHandlerTest { public void setup() { proxy = mock(AmazonWebServicesClientProxy.class); logger = mock(Logger.class); - validator = mock(Validator.class); - handler = CreateHandler.builder().validator(validator).build(); + handler = new CreateHandler(); request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_MODEL) + .desiredResourceState(SELF_MANAGED_MODEL) .logicalResourceIdentifier(LOGICAL_ID) .clientRequestToken(REQUEST_TOKEN) .build(); @@ -78,13 +75,15 @@ public void setup() { @Test public void handleRequest_SimpleSuccess() { - doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); - final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .addStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); + inputContext.getOperationsStabilizationMap().put(ADD_INSTANCES, true); + final ProgressEvent response = handler.handleRequest(proxy, request, inputContext, logger); @@ -99,22 +98,28 @@ public void handleRequest_SimpleSuccess() { } @Test - public void handleRequest_TemplateUrl_CreateNotYetStarted_InProgress() { - - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); + public void handleRequest_SelfManaged_CreateNotYetStarted_InProgress() { doReturn(CREATE_STACK_SET_RESPONSE, CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + final Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); + stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) + .templateAnalyzed(true) + .stackSetCreated(true) + .addStacksStarted(true) .operationId(OPERATION_ID_1) + .createStacksQueue(new LinkedList<>(stackInstancesSet)) + .stackInstancesInOperation(stackInstances) .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); @@ -126,26 +131,32 @@ public void handleRequest_TemplateUrl_CreateNotYetStarted_InProgress() { } @Test - public void handleRequest_TemplateBody_CreateNotYetStarted_InProgress() { + public void handleRequest_ServiceManaged_CreateNotYetStarted_InProgress() { request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_TEMPLATE_BODY_MODEL) + .desiredResourceState(SERVICE_MANAGED_MODEL) .logicalResourceIdentifier(LOGICAL_ID) .clientRequestToken(REQUEST_TOKEN) .build(); doReturn(CREATE_STACK_SET_RESPONSE, CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + final Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) + .templateAnalyzed(true) + .stackSetCreated(true) + .addStacksStarted(true) .operationId(OPERATION_ID_1) + .stackInstancesInOperation(stackInstances) .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); @@ -156,20 +167,23 @@ public void handleRequest_TemplateBody_CreateNotYetStarted_InProgress() { 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) + .stackSetCreated(true) + .addStacksStarted(true) .operationId(OPERATION_ID_1) + .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .addStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) @@ -188,13 +202,28 @@ public void handleRequest_CreateNotYetStabilized_InProgress() { assertThat(response.getErrorCode()).isNull(); } + @Test + public void handleRequest_CreateWithDuplicatedInstances_InvalidRequest() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(INVALID_SELF_MANAGED_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + assertThrows(CfnInvalidRequestException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + @Test public void handleRequest_OperationStopped_CfnNotStabilizedException() { doReturn(OPERATION_STOPPED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .addStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -209,7 +238,9 @@ public void handleRequest_OperationTimesOut_CfnNotStabilizedException() { doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .addStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(EXECUTION_TIMEOUT_SECONDS) .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) @@ -225,9 +256,10 @@ public void handleRequest_OperationMaxRetries_CfnNotStabilizedException() { doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .addStacksStarted(true) .operationId(OPERATION_ID_1) - .retries(MAX_RETRIES + 1) + .elapsedTime(EXECUTION_TIMEOUT_SECONDS) .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) .build(); @@ -238,8 +270,6 @@ public void handleRequest_OperationMaxRetries_CfnNotStabilizedException() { @Test public void handlerRequest_AlreadyExistsException() { - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - doThrow(AlreadyExistsException.class).when(proxy) .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); @@ -251,8 +281,6 @@ public void handlerRequest_AlreadyExistsException() { @Test public void handlerRequest_LimitExceededException() { - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - doThrow(LimitExceededException.class).when(proxy) .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); @@ -264,8 +292,6 @@ public void handlerRequest_LimitExceededException() { @Test public void handlerRequest_InsufficientCapabilitiesException() { - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - doThrow(InsufficientCapabilitiesException.class).when(proxy) .injectCredentialsAndInvokeV2(any(CreateStackSetRequest.class), any()); @@ -277,8 +303,6 @@ public void handlerRequest_InsufficientCapabilitiesException() { @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()); @@ -293,22 +317,23 @@ public void handlerRequest_StackSetNotFoundException() { @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) - .build(); final ProgressEvent response = handler.handleRequest(proxy, request, null, logger); + final CallbackContext outputContext = CallbackContext.builder() + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) + .stackSetCreated(true) + .templateAnalyzed(true) + .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) + .build(); + assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); assertThat(response.getCallbackContext()).isEqualTo(outputContext); 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..f2b8ad9 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 @@ -15,6 +15,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.LinkedList; +import java.util.Set; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -22,9 +25,11 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE; 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_SUCCEED_RESPONSE; +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.Stabilizer.BASE_CALLBACK_DELAY_SECONDS; @@ -47,17 +52,23 @@ public void setup() { logger = mock(Logger.class); handler = new DeleteHandler(); request = ResourceHandlerRequest.builder() - .desiredResourceState(SIMPLE_MODEL) + .desiredResourceState(SELF_MANAGED_MODEL) .build(); } @Test public void handleRequest_SimpleSuccess() { + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_MODEL) + .build(); + doReturn(OPERATION_SUCCEED_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .stackSetCreated(true) + .deleteStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); @@ -79,15 +90,24 @@ public void handleRequest_DeleteNotYetStarted_InProgress() { doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + final Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); + + stackInstancesSet.remove(stackInstances); + final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) + .deleteStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) + .deleteStacksQueue(new LinkedList<>(stackInstancesSet)) + .stackInstancesInOperation(stackInstances) .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); @@ -104,13 +124,15 @@ public void handleRequest_DeleteNotYetStabilized_InProgress() { doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); final CallbackContext outputContext = CallbackContext.builder() - .stabilizationStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) @@ -132,19 +154,14 @@ public void handleRequest_DeleteNotYetStabilized_InProgress() { @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() { + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_MODEL) + .build(); final CallbackContext inputContext = CallbackContext.builder() - .stabilizationStarted(true) + .templateAnalyzed(true) + .operationId(OPERATION_ID_1) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); doThrow(StackSetNotFoundException.class).when(proxy) @@ -156,26 +173,13 @@ public void handlerRequest_DeleteInstances_StackSetNotFoundException() { } @Test - public void handlerRequest_OperationInProgressException() { - - doThrow(OperationInProgressException.class).when(proxy) - .injectCredentialsAndInvokeV2(any(DeleteStackInstancesRequest.class), any()); + public void handlerRequest_DeleteInstances_StackSetNotFoundException() { - final CallbackContext outputContext = CallbackContext.builder() - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .retries(1) - .build(); + doThrow(StackSetNotFoundException.class).when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); - final ProgressEvent response - = handler.handleRequest(proxy, request, null, logger); + assertThrows(CfnNotFoundException.class, + () -> 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..3fb600a 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,5 +1,9 @@ package software.amazon.cloudformation.stackset; +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.ListStackSetsRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; @@ -15,14 +19,26 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +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 { + private ListHandler handler; + + private ResourceHandlerRequest request; + @Mock private AmazonWebServicesClientProxy proxy; @@ -31,35 +47,39 @@ public class ListHandlerTest { @BeforeEach public void setup() { + handler = new ListHandler(); proxy = mock(AmazonWebServicesClientProxy.class); logger = mock(Logger.class); + request = ResourceHandlerRequest.builder() + .desiredResourceState(READ_MODEL) + .build(); } @Test - public void handleRequest_SimpleSuccess() { - final ListHandler handler = new ListHandler(); - - final ResourceModel model = ResourceModel.builder().build(); + public void handleRequest_SelfManagedSS_Success() { - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); + doReturn(LIST_STACK_SETS_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(ListStackSetsRequest.class), any()); + doReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackSetRequest.class), any()); + doReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(ListStackInstancesRequest.class), any()); - doReturn(LIST_STACK_SETS_RESPONSE, - DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE, - LIST_SERVICE_MANAGED_STACK_SET_RESPONSE) - .when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); + doReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4).when(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackInstanceRequest.class), any()); - final ProgressEvent response = - handler.handleRequest(proxy, request, null, logger); + 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()).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..2137ed8 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 @@ -2,6 +2,10 @@ import software.amazon.awssdk.services.cloudformation.model.CreateStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; +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.ListStackSetsRequest; import software.amazon.awssdk.services.cloudformation.model.StackSetNotFoundException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -25,11 +29,18 @@ 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.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; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_FOR_READ; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL_FOR_READ; @ExtendWith(MockitoExtension.class) public class ReadHandlerTest { @@ -57,10 +68,17 @@ public void setup() { @Test public void handleRequest_ServiceManagedSS_Success() { - doReturn(DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE, - LIST_SERVICE_MANAGED_STACK_SET_RESPONSE) - .when(proxy) - .injectCredentialsAndInvokeV2(any(), any()); + doReturn(DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackSetRequest.class), any()); + + doReturn(LIST_SERVICE_MANAGED_STACK_SET_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(ListStackInstancesRequest.class), any()); + + doReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4).when(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackInstanceRequest.class), any()); final ProgressEvent response = handler.handleRequest(proxy, request, null, logger); @@ -69,7 +87,7 @@ public void handleRequest_ServiceManagedSS_Success() { assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(SERVICE_MANAGED_MODEL); + assertThat(response.getResourceModel()).isEqualTo(SERVICE_MANAGED_MODEL_FOR_READ); assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); @@ -78,10 +96,16 @@ public void handleRequest_ServiceManagedSS_Success() { @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(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackSetRequest.class), any()); + doReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE).when(proxy) + .injectCredentialsAndInvokeV2(any(ListStackInstancesRequest.class), any()); + + doReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4).when(proxy) + .injectCredentialsAndInvokeV2(any(DescribeStackInstanceRequest.class), any()); final ProgressEvent response = handler.handleRequest(proxy, request, null, logger); @@ -90,7 +114,7 @@ public void handleRequest_SelfManagedSS_Success() { 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(); 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..f8f79a2 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 @@ -19,6 +19,7 @@ import java.util.EnumMap; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -27,23 +28,27 @@ 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.EnumUtils.Operations; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.ADD_INSTANCES; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.DELETE_INSTANCES; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.STACK_SET_CONFIGS; +import static software.amazon.cloudformation.stackset.util.EnumUtils.Operations.UPDATE_INSTANCES; +import static software.amazon.cloudformation.stackset.util.Stabilizer.BASE_CALLBACK_DELAY_SECONDS; import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_SELF_MANAGED; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE; import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_SELF_MANAGED; +import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE; 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.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.UPDATED_SELF_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE; +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 { @@ -52,9 +57,6 @@ public class UpdateHandlerTest { private ResourceHandlerRequest request; - @Mock - private Validator validator; - @Mock private AmazonWebServicesClientProxy proxy; @@ -65,11 +67,10 @@ public class UpdateHandlerTest { public void setup() { proxy = mock(AmazonWebServicesClientProxy.class); logger = mock(Logger.class); - validator = mock(Validator.class); - handler = new UpdateHandler(validator); + handler = new UpdateHandler(); request = ResourceHandlerRequest.builder() - .desiredResourceState(UPDATED_MODEL) - .previousResourceState(SIMPLE_MODEL) + .desiredResourceState(UPDATED_SELF_MANAGED_MODEL) + .previousResourceState(SELF_MANAGED_MODEL) .build(); } @@ -97,19 +98,18 @@ public void handleRequest_NotUpdatable_Success() { @Test public void handleRequest_AllUpdatesStabilized_Success() { - final Map updateOperationsMap = new EnumMap<>(UpdateOperations.class); + final Map updateOperationsMap = new EnumMap<>(Operations.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); + updateOperationsMap.put(DELETE_INSTANCES, true); + updateOperationsMap.put(UPDATE_INSTANCES, true); + updateOperationsMap.put(ADD_INSTANCES, true); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByTargetsStarted(true) - .deleteStacksByRegionsStarted(true) - .addStacksByRegionsStarted(true) - .addStacksByTargetsStarted(true) + .deleteStacksStarted(true) + .addStacksStarted(true) + .updateStacksStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .operationsStabilizationMap(updateOperationsMap) .build(); @@ -130,20 +130,21 @@ public void handleRequest_AllUpdatesStabilized_Success() { @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 ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) + .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) + .deleteStacksQueue(response.getCallbackContext().getDeleteStacksQueue()) + .updateStacksQueue(response.getCallbackContext().getUpdateStacksQueue()) .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); @@ -161,18 +162,19 @@ public void handleRequest_UpdateStackSetNotStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) + .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) + .templateAnalyzed(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); @@ -187,69 +189,42 @@ public void handleRequest_UpdateStackSetNotStabilized_InProgress() { } @Test - public void handleRequest_DeleteStacksRegionsNotStarted_InProgress() { + public void handleRequest_SelfManaged_DeleteStacksNotStarted_InProgress() { doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) + .templateAnalyzed(true) + .deleteStacksQueue(DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .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 Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); + stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) + .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .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); @@ -261,29 +236,37 @@ public void handleRequest_SelfManaged_DeleteStacksRegionsNotStarted_InProgress() } @Test - public void handleRequest_DeleteStacksTargetsNotStarted_InProgress() { + public void handleRequest_DeleteStacksNotYetStabilized_InProgress() { - doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) + .deleteStacksStarted(true) + .templateAnalyzed(true) + .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .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) + .updateStackSetStarted(true) + .deleteStacksStarted(true) + .templateAnalyzed(true) + .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) + .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) + .operationId(OPERATION_ID_2) .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); @@ -299,36 +282,45 @@ public void handleRequest_DeleteStacksTargetsNotStarted_InProgress() { } @Test - public void handleRequest_AddStacksRegionsNotStarted_InProgress() { + public void handleRequest_AddStacksNotStarted_InProgress() { doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) .operationId(OPERATION_ID_2) + .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .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(DELETE_INSTANCES, true); + + final ProgressEvent response + = handler.handleRequest(proxy, request, inputContext, logger); + + final Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); + stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) + .addStacksStarted(true) .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) + .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) + .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .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); + outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES, true); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); @@ -341,40 +333,47 @@ public void handleRequest_AddStacksRegionsNotStarted_InProgress() { } @Test - public void handleRequest_AddStacksTargetsNotStarted_InProgress() { + public void handleRequest_UpdateStacksNotStarted_InProgress() { - doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + doReturn(UPDATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) + .addStacksStarted(true) .operationId(OPERATION_ID_2) + .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .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); + inputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES, true); + inputContext.getOperationsStabilizationMap().put(ADD_INSTANCES, true); + + final ProgressEvent response + = handler.handleRequest(proxy, request, inputContext, logger); + + final Set stackInstancesSet = request.getDesiredResourceState().getStackInstancesGroup(); + final StackInstances stackInstances = response.getCallbackContext().getStackInstancesInOperation(); + stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksByRegionsStarted(true) - .deleteStacksByTargetsStarted(true) - .addStacksByRegionsStarted(true) - .addStacksByTargetsStarted(true) + .templateAnalyzed(true) + .deleteStacksStarted(true) + .addStacksStarted(true) + .updateStacksStarted(true) .operationId(OPERATION_ID_1) - .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) + .stackInstancesInOperation(stackInstances) + .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .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); + outputContext.getOperationsStabilizationMap().put(DELETE_INSTANCES, true); + outputContext.getOperationsStabilizationMap().put(ADD_INSTANCES, true); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); @@ -386,7 +385,6 @@ public void handleRequest_AddStacksTargetsNotStarted_InProgress() { assertThat(response.getErrorCode()).isNull(); } - @Test public void handlerRequest_InvalidOperationException() { @@ -412,19 +410,20 @@ public void handlerRequest_StackSetNotFoundException() { @Test public void handlerRequest_OperationInProgressException() { - doNothing().when(validator).validateTemplate(any(), any(), any(), any()); - doThrow(OperationInProgressException.class).when(proxy) .injectCredentialsAndInvokeV2(any(), any()); + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + final CallbackContext outputContext = CallbackContext.builder() + .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .retries(1) + .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) + .deleteStacksQueue(response.getCallbackContext().getDeleteStacksQueue()) + .updateStacksQueue(response.getCallbackContext().getUpdateStacksQueue()) .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); 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..a8632ef 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,21 +1,14 @@ 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; @@ -69,26 +62,6 @@ public void testIsStackSetConfigEquals() { 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..e50a677 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,11 +25,14 @@ 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; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; public class TestUtils { @@ -67,6 +73,56 @@ public class TestUtils { .append(" }\n") .append("}").toString(); + public final static String VALID_YAML_TEMPLATE = + "Parameters:\n" + + " DomainName:\n" + + " Type: String\n" + + " Default: myexample.com\n" + + "Resources:\n" + + " BasicHealthCheck:\n" + + " Type: AWS::Route53::HealthCheck\n" + + " Properties:\n" + + " HealthCheckConfig:\n" + + " RequestInterval: 10\n" + + " FullyQualifiedDomainName:\n" + + " Ref: DomainName\n" + + " IPAddress: 98.139.180.149\n" + + " Port: \"88\"\n" + + " ResourcePath: /docs/route-53-health-check.html\n" + + " Type: HTTP\n" + + " HealthCheckTags:\n" + + " - Key: A\n" + + " Value: \"1\"\n" + + " - Key: B\n" + + " Value: \"1\"\n" + + " - Key: C\n" + + " Value: \"1\""; + + public final static String INVALID_EMBEDDED_STACK_TEMPLATE = + "{\n" + + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " },\n" + + " }\n" + + "}"; + + public final static String INVALID_EMBEDDED_STACKSET_TEMPLATE = + "{\n" + + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + public final static String STACK_SET_NAME = "StackSet"; public final static String STACK_SET_ID = "StackSet:stack-set-id"; @@ -88,6 +144,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 +212,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 +230,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 +246,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"), @@ -209,22 +273,26 @@ public class TestUtils { public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_1 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .account(ACCOUNT_ID_1) .region(US_EAST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_2 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .account(ACCOUNT_ID_1) .region(US_WEST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_3 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_2) - .region(US_EAST_1) + .account(ACCOUNT_ID_2) + .region(EU_EAST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_4 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_2) - .region(US_WEST_1) + .account(ACCOUNT_ID_2) + .region(EU_EAST_2) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_5 = StackInstanceSummary.builder() @@ -239,14 +307,53 @@ 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) + .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) .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 +367,44 @@ public class TestUtils { .enabled(true) .build(); + public final static StackInstances SERVICE_MANAGED_STACK_INSTANCES_1 = StackInstances.builder() + .regions(REGIONS_1) + .deploymentTargets(SERVICE_MANAGED_TARGETS) + .build(); + + public final static StackInstances SERVICE_MANAGED_STACK_INSTANCES_2 = StackInstances.builder() + .regions(REGIONS_1) + .deploymentTargets(UPDATED_SERVICE_MANAGED_TARGETS) + .build(); + + public final static StackInstances SELF_MANAGED_STACK_INSTANCES_1 = StackInstances.builder() + .regions(REGIONS_1) + .deploymentTargets(SELF_MANAGED_TARGETS) + .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) + .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) @@ -278,6 +414,7 @@ public class TestUtils { .organizationalUnitIds(ORGANIZATION_UNIT_ID_1, ORGANIZATION_UNIT_ID_2) .parameters(SDK_PARAMETER_1, SDK_PARAMETER_2) .permissionModel(PermissionModels.SERVICE_MANAGED) + .templateBody(TEMPLATE_BODY) .tags(TAGGED_RESOURCES) .build(); @@ -287,78 +424,118 @@ 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(SERVICE_MANAGED_STACK_INSTANCES_1, SERVICE_MANAGED_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 READ_MODEL = ResourceModel.builder() + public final static ResourceModel INVALID_SELF_MANAGED_MODEL = 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_2, SELF_MANAGED_STACK_INSTANCES_4))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) .build(); - public final static ResourceModel SIMPLE_MODEL = ResourceModel.builder() + public final static StackInstances CREATE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(US_EAST_2))) + .build(); + + public final static Queue CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(CREATE_STACK_INSTANCES_SELF_MANAGED)); + + public final static StackInstances DELETE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(US_EAST_1))) + .build(); + + public final static Queue DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(DELETE_STACK_INSTANCES_SELF_MANAGED)); + + public final static StackInstances UPDATE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(UPDATED_SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(EU_EAST_1, EU_EAST_2))) + .parameterOverrides(new HashSet<>(Arrays.asList(PARAMETER_1))) + .build(); + + public final static Queue UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(UPDATE_STACK_INSTANCES_SELF_MANAGED)); + + public final static ResourceModel SELF_MANAGED_MODEL_FOR_READ = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SERVICE_MANAGED_TARGETS) - .permissionModel(SERVICE_MANAGED) - .autoDeployment(AUTO_DEPLOYMENT) - .regions(REGIONS) - .templateURL(TEMPLATE_URL) + .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) - .operationPreferences(OPERATION_PREFERENCES) .build(); - public final static ResourceModel SIMPLE_TEMPLATE_BODY_MODEL = ResourceModel.builder() + public final static ResourceModel SERVICE_MANAGED_MODEL_FOR_READ = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(SERVICE_MANAGED_TARGETS) .permissionModel(SERVICE_MANAGED) .autoDeployment(AUTO_DEPLOYMENT) - .regions(REGIONS) + .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) - .operationPreferences(OPERATION_PREFERENCES) .build(); + public final static ResourceModel READ_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .build(); - public final static ResourceModel UPDATED_MODEL = ResourceModel.builder() + public final static ResourceModel SIMPLE_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) - .deploymentTargets(UPDATED_SERVICE_MANAGED_TARGETS) - .permissionModel(SERVICE_MANAGED) - .autoDeployment(AUTO_DEPLOYMENT) - .regions(UPDATED_REGIONS) - .templateURL(UPDATED_TEMPLATE_URL) - .tags(TAGS_TO_UPDATE) + .permissionModel(SELF_MANAGED) + .templateBody(TEMPLATE_BODY) + .tags(TAGS) + .operationPreferences(OPERATION_PREFERENCES) .build(); public final static DescribeStackSetOperationResponse OPERATION_SUCCEED_RESPONSE = @@ -392,6 +569,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..8547991 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,31 +10,22 @@ 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.INVALID_EMBEDDED_STACKSET_TEMPLATE; +import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; 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.VALID_YAML_TEMPLATE; @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 +46,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) { @@ -94,15 +67,20 @@ public void testValidateTemplate_BothBodyAndUriNotExist() { } @Test - public void testValidateTemplate_ValidTemplateBody() { - assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); + public void testValidateTemplate_InvalidTemplate() { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACK_TEMPLATE, null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACKSET_TEMPLATE, null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, "", null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, "null", null, logger)); } - public String read(final String fileName) { - try { - return IOUtils.toString(this.getClass().getResourceAsStream(fileName)); - } catch (IOException e) { - throw new RuntimeException(e); - } + @Test + public void testValidateTemplate_ValidTemplateBody() { + assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); + assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); } } From 4a7d85155c36943a6cf1ea4fc64cc33098ba2564 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Wed, 22 Apr 2020 18:13:06 -0700 Subject: [PATCH 04/19] Fix permissions for stackset schema --- .../aws-cloudformation-stackset.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 4148b8c..4c97b39 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -272,8 +272,8 @@ "read": { "permissions": [ "cloudformation:DescribeStackSet", - "cloudformation:CreateStackInstances", - "cloudformation:DescribeStackInstances" + "cloudformation:ListStackInstances", + "cloudformation:DescribeStackInstance" ] }, "update": { @@ -282,6 +282,7 @@ "cloudformation:UpdateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", + "cloudformation:UpdatetackInstances", "cloudformation:DescribeStackSetOperation" ] }, @@ -294,7 +295,10 @@ }, "list": { "permissions": [ - "cloudformation:ListStackSets" + "cloudformation:ListStackSets", + "cloudformation:DescribeStackSet", + "cloudformation:ListStackInstances", + "cloudformation:DescribeStackInstance" ] } } From c92f167c397e1fe263fb061adb847d643e72450d Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Thu, 23 Apr 2020 20:38:32 -0700 Subject: [PATCH 05/19] Add some drafts --- .../stackset/BaseHandlerStd.java | 13 +++- .../stackset/CreateHandler.java | 9 ++- .../stackset/UpdateHandler.java | 6 +- .../stackset/util/InstancesAnalyzer.java | 8 +-- .../stackset/BaseHandlerStdTest.java | 13 ++++ .../stackset/CreateHandlerTest.java | 55 +++++++++++++++ .../stackset/util/TestUtils.java | 70 +++++++++++++------ .../stackset/util/ValidatorTest.java | 16 +++++ 8 files changed, 161 insertions(+), 29 deletions(-) create mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java 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 index 0d4060a..07d6b11 100644 --- 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 @@ -13,6 +13,7 @@ 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.exceptions.TerminalException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -93,7 +94,6 @@ protected ProgressEvent createStackInstances( .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); @@ -210,9 +210,18 @@ protected static boolean isStackSetOperationDone( return false; default: logger.log(String.format("StackInstanceOperation [%s] unexpected status [%s]", operationId, status)); - throw new CfnServiceInternalErrorException( + throw new TerminalException( String.format("Stack set operation [%s] was unexpectedly stopped or failed", operationId)); } } + protected ProgressEvent handleException(AwsRequest request, + Exception exception, + ProxyClient client, + ResourceModel model, + CallbackContext context, + AmazonWebServicesClientProxy proxy) { + + return proxy.defaultHandler(request, exception, client, model, context); + } } 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 f0142e8..9c4438c 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 @@ -36,11 +36,16 @@ protected ProgressEvent handleRequest( analyzeTemplate(proxy, model, callbackContext); return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext) - .request(resourceModel -> createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken())) + .request(resourceModel -> + createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken())) + .retry(MULTIPLE_OF) .call((modelRequest, proxyInvocation) -> createResource(modelRequest, proxyClient, model)) .progress() .then(progress -> createStackInstances(proxy, proxyClient, progress, logger)) - .then(progress -> ProgressEvent.defaultSuccessHandler(model)); + .then(progress -> { + if (progress.isFailed()) return progress; + return ProgressEvent.defaultSuccessHandler(model); + }); } /** 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 4145c48..1cdaf79 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 @@ -31,7 +31,11 @@ protected ProgressEvent handleRequest( 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)); + .then(progress -> updateStackInstances(proxy, proxyClient, progress, logger)) + .then(progress -> { + if (progress.isFailed()) return progress; + return ProgressEvent.defaultSuccessHandler(model); + }); } /** 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 index 2d901c3..cf462cc 100644 --- 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 @@ -1,10 +1,9 @@ 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.awssdk.utils.CollectionUtils; import software.amazon.cloudformation.stackset.CallbackContext; import software.amazon.cloudformation.stackset.DeploymentTargets; import software.amazon.cloudformation.stackset.Parameter; @@ -104,8 +103,8 @@ public static Set aggregateStackInstances( * Group regions by {@link DeploymentTargets} and {@link StackInstance#getParameters()} * @return {@link StackInstances} */ - public static Set groupInstancesByTargets( - @NonNull final Set flatStackInstances, final boolean isSelfManaged) { + private static Set groupInstancesByTargets( + final Set flatStackInstances, final boolean isSelfManaged) { final Map, StackInstances> groupedStacksMap = new HashMap<>(); for (final StackInstance stackInstance : flatStackInstances) { @@ -191,6 +190,7 @@ private static Set flattenStackInstancesGroup( final Collection stackInstancesGroup, final boolean isSelfManaged) { final Set flatStacks = new HashSet<>(); + if (CollectionUtils.isNullOrEmpty(stackInstancesGroup)) return flatStacks; for (final StackInstances stackInstances : stackInstancesGroup) { for (final String region : stackInstances.getRegions()) { diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java new file mode 100644 index 0000000..7dae7bd --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java @@ -0,0 +1,13 @@ +package software.amazon.cloudformation.stackset; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateFromSdkTags; + +public class BaseHandlerStdTest { + @Test + public void test_translateFromSdkTags_IfIsNull() { + assertThat(translateFromSdkTags(null)).isNull(); + } +} 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 90cd7a7..8cacaa8 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 @@ -29,6 +29,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static software.amazon.cloudformation.proxy.HandlerErrorCode.InvalidRequest; 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; @@ -36,6 +37,7 @@ 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_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.SELF_MANAGED_MODEL; @@ -121,6 +123,34 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { assertThat(response.getErrorCode()).isNull(); } + @Test + public void handleRequest_SelfManagedSS_NotStabilized() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_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, 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.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + @Test public void handlerRequest_InsufficientCapabilitiesException() { @@ -141,4 +171,29 @@ public void handlerRequest_InsufficientCapabilitiesException() { assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getErrorCode()).isNotNull(); } + + @Test + public void handlerRequest_OperationStoppedError() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_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_STOPPED_RESPONSE).when(proxyClient.client()) + .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + + final ProgressEvent response + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getErrorCode()).isEqualTo(InvalidRequest); + } } 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 1759beb..d1f583a 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 @@ -71,6 +71,56 @@ public class TestUtils { .append(" }\n") .append("}").toString(); + public final static String VALID_YAML_TEMPLATE = + "Parameters:\n" + + " DomainName:\n" + + " Type: String\n" + + " Default: myexample.com\n" + + "Resources:\n" + + " BasicHealthCheck:\n" + + " Type: AWS::Route53::HealthCheck\n" + + " Properties:\n" + + " HealthCheckConfig:\n" + + " RequestInterval: 10\n" + + " FullyQualifiedDomainName:\n" + + " Ref: DomainName\n" + + " IPAddress: 98.139.180.149\n" + + " Port: \"88\"\n" + + " ResourcePath: /docs/route-53-health-check.html\n" + + " Type: HTTP\n" + + " HealthCheckTags:\n" + + " - Key: A\n" + + " Value: \"1\"\n" + + " - Key: B\n" + + " Value: \"1\"\n" + + " - Key: C\n" + + " Value: \"1\""; + + public final static String INVALID_EMBEDDED_STACK_TEMPLATE = + "{\n" + + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " },\n" + + " }\n" + + "}"; + + public final static String INVALID_EMBEDDED_STACKSET_TEMPLATE = + "{\n" + + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + public final static String STACK_SET_NAME = "StackSet"; public final static String STACK_SET_ID = "StackSet:stack-set-id"; @@ -437,26 +487,6 @@ public class TestUtils { .operationPreferences(OPERATION_PREFERENCES) .build(); - public final static ResourceModel SIMPLE_TEMPLATE_BODY_MODEL = ResourceModel.builder() - .stackSetId(STACK_SET_ID) - .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) - .permissionModel(SERVICE_MANAGED) - .autoDeployment(AUTO_DEPLOYMENT) - .templateBody(TEMPLATE_BODY) - .tags(TAGS) - .operationPreferences(OPERATION_PREFERENCES) - .build(); - - - public final static ResourceModel UPDATED_MODEL = ResourceModel.builder() - .stackSetId(STACK_SET_ID) - .permissionModel(SERVICE_MANAGED) - .autoDeployment(AUTO_DEPLOYMENT) - .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) - .templateURL(UPDATED_TEMPLATE_URL) - .tags(TAGS_TO_UPDATE) - .build(); - public final static DescribeStackSetOperationResponse OPERATION_SUCCEED_RESPONSE = DescribeStackSetOperationResponse.builder() .stackSetOperation(StackSetOperation.builder() 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 9be9856..8547991 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 @@ -17,8 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACKSET_TEMPLATE; +import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; 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.VALID_YAML_TEMPLATE; @ExtendWith(MockitoExtension.class) public class ValidatorTest { @@ -63,8 +66,21 @@ public void testValidateTemplate_BothBodyAndUriNotExist() { () -> validator.validateTemplate(proxy, null, null, logger)); } + @Test + public void testValidateTemplate_InvalidTemplate() { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACK_TEMPLATE, null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACKSET_TEMPLATE, null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, "", null, logger)); + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateTemplate(proxy, "null", null, logger)); + } + @Test public void testValidateTemplate_ValidTemplateBody() { assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); + assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); } } From 025726205119e6578fd3af101998fcbab25add1f Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 24 Apr 2020 12:14:49 -0700 Subject: [PATCH 06/19] Add TagResource permissions --- .../aws-cloudformation-stackset.json | 10 +++++++--- aws-cloudformation-stackset/resource-role.yaml | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 4c97b39..4314d47 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -266,7 +266,8 @@ "s3:GetObject", "cloudformation:CreateStackSet", "cloudformation:CreateStackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:DescribeStackSetOperation", + "cloudformation:TagResource" ] }, "read": { @@ -283,14 +284,17 @@ "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", "cloudformation:UpdatetackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:DescribeStackSetOperation", + "cloudformation:TagResource", + "cloudformation:UntagResource" ] }, "delete": { "permissions": [ "cloudformation:DeleteStackSet", "cloudformation:DeleteStackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:DescribeStackSetOperation", + "cloudformation:TagResource" ] }, "list": { diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index dda6531..ca72785 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -27,11 +27,15 @@ Resources: - "cloudformation:CreateStackSet" - "cloudformation:DeleteStackInstances" - "cloudformation:DeleteStackSet" - - "cloudformation:DescribeStackInstances" + - "cloudformation:DescribeStackInstance" - "cloudformation:DescribeStackSet" - "cloudformation:DescribeStackSetOperation" + - "cloudformation:ListStackInstances" - "cloudformation:ListStackSets" + - "cloudformation:TagResource" + - "cloudformation:UntagResource" - "cloudformation:UpdateStackSet" + - "cloudformation:UpdatetackInstances" - "s3:GetObject" Resource: "*" Outputs: From 3e786aca3da5a75a7e20692f8daea56f9514fde5 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 24 Apr 2020 16:16:19 -0700 Subject: [PATCH 07/19] Rename to stackinstance(s) and some schema fixes --- .../aws-cloudformation-stackset.json | 16 ++-- .../resource-role.yaml | 2 +- .../stackset/CallbackContext.java | 12 +-- .../stackset/CreateHandler.java | 4 +- .../stackset/DeleteHandler.java | 4 +- .../stackset/UpdateHandler.java | 12 +-- .../stackset/util/Comparator.java | 6 +- .../stackset/util/InstancesAnalyzer.java | 57 ++++++++------ .../stackset/util/OperationOperator.java | 18 ++--- .../stackset/util/Stabilizer.java | 2 +- .../stackset/CreateHandlerTest.java | 20 ++--- .../stackset/DeleteHandlerTest.java | 25 +++--- .../stackset/UpdateHandlerTest.java | 78 +++++++++---------- 13 files changed, 131 insertions(+), 125 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 4314d47..0fda484 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -8,23 +8,25 @@ "type": "object", "properties": { "Key": { - "description": "A string used to identify this tag. You can specify a maximum of 128 characters for a tag key.", + "description": "A string used to identify this tag. You can specify a maximum of 127 characters for a tag key.", "type": "string", "minLength": 1, - "maxLength": 128, - "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$" + "maxLength": 127, + "pattern": "^(?!aws:.*)[a-z0-9\\s\\_\\.\\/\\=\\+\\-]+$" }, "Value": { "description": "A string containing the value for this tag. You can specify a maximum of 256 characters for a tag value.", "type": "string", "minLength": 1, - "maxLength": 256 + "maxLength": 255, + "pattern": "^(?!aws:.*)[a-z0-9\\s\\_\\.\\/\\=\\+\\-]+$" } }, "required": [ "Value", "Key" - ] + ], + "additionalProperties": false }, "AutoDeployment": { "type": "object", @@ -283,7 +285,7 @@ "cloudformation:UpdateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", - "cloudformation:UpdatetackInstances", + "cloudformation:UpdateStackInstances", "cloudformation:DescribeStackSetOperation", "cloudformation:TagResource", "cloudformation:UntagResource" @@ -294,7 +296,7 @@ "cloudformation:DeleteStackSet", "cloudformation:DeleteStackInstances", "cloudformation:DescribeStackSetOperation", - "cloudformation:TagResource" + "cloudformation:UntagResource" ] }, "list": { diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index ca72785..a20ce75 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -34,8 +34,8 @@ Resources: - "cloudformation:ListStackSets" - "cloudformation:TagResource" - "cloudformation:UntagResource" + - "cloudformation:UpdateStackInstances" - "cloudformation:UpdateStackSet" - - "cloudformation:UpdatetackInstances" - "s3:GetObject" Resource: "*" Outputs: 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 2f96658..b1a7c46 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 @@ -28,16 +28,16 @@ public class CallbackContext { private boolean stackSetCreated; // Indicates initiation of stack instances creation. - private boolean addStacksStarted; + private boolean addStacksInstancesStarted; // Indicates initiation of stack instances delete. - private boolean deleteStacksStarted; + private boolean deleteStacksInstancesStarted; // Indicates initiation of stack set update. private boolean updateStackSetStarted; // Indicates initiation of stack instances update. - private boolean updateStacksStarted; + private boolean updateStacksInstancesStarted; // Total running time @Builder.Default @@ -47,15 +47,15 @@ public class CallbackContext { // List to keep track on the complete status for creating @Builder.Default - private Queue createStacksQueue = new LinkedList<>(); + private Queue createStacksInstancesQueue = new LinkedList<>(); // List to keep track on stack instances for deleting @Builder.Default - private Queue deleteStacksQueue = new LinkedList<>(); + private Queue deleteStacksInstancesQueue = new LinkedList<>(); // List to keep track on stack instances for update @Builder.Default - private Queue updateStacksQueue = new LinkedList<>(); + private Queue updateStacksInstancesQueue = new LinkedList<>(); /** * Default as 0, will be {@link software.amazon.cloudformation.stackset.util.Stabilizer#BASE_CALLBACK_DELAY_SECONDS} 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 cbc2a14..b6f589c 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 @@ -50,8 +50,8 @@ public ProgressEvent handleRequest( createStackSet(proxy, model, logger, client, context, stackSetName, request.getClientRequestToken()); } - if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksStarted(), null, - ADD_INSTANCES, context.getCreateStacksQueue(), model, context)) { + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksInstancesStarted(), null, + ADD_INSTANCES, context.getCreateStacksInstancesQueue(), model, context)) { operator.updateStackSet(ADD_INSTANCES); } 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 649cfe8..299b48e 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 @@ -36,8 +36,8 @@ public ProgressEvent handleRequest( .build(); InstancesAnalyzer.builder().desiredModel(model).build().analyzeForDelete(context); - if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksStarted(), null, - DELETE_INSTANCES, context.getDeleteStacksQueue(), model, context)) { + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksInstancesStarted(), null, + DELETE_INSTANCES, context.getDeleteStacksInstancesQueue(), model, context)) { operator.updateStackSet(DELETE_INSTANCES); } 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 9c0d707..3a44e44 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 @@ -52,20 +52,20 @@ public ProgressEvent handleRequest( operator.updateStackSet(STACK_SET_CONFIGS); } - if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksStarted(), - STACK_SET_CONFIGS, DELETE_INSTANCES, context.getDeleteStacksQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksInstancesStarted(), + STACK_SET_CONFIGS, DELETE_INSTANCES, context.getDeleteStacksInstancesQueue(), desiredModel, context)) { operator.updateStackSet(DELETE_INSTANCES); } - if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksStarted(), - DELETE_INSTANCES, ADD_INSTANCES, context.getCreateStacksQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksInstancesStarted(), + DELETE_INSTANCES, ADD_INSTANCES, context.getCreateStacksInstancesQueue(), desiredModel, context)) { operator.updateStackSet(ADD_INSTANCES); } - if (stabilizer.isPerformingOperation(isUpdatingStackInstances(context), context.isUpdateStacksStarted(), - ADD_INSTANCES, UPDATE_INSTANCES, context.getUpdateStacksQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isUpdatingStackInstances(context), context.isUpdateStacksInstancesStarted(), + ADD_INSTANCES, UPDATE_INSTANCES, context.getUpdateStacksInstancesQueue(), desiredModel, context)) { operator.updateStackSet(UPDATE_INSTANCES); } 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 4fa2e29..377dcc4 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 @@ -53,7 +53,7 @@ public static boolean isStackSetConfigEquals( */ public static boolean isUpdatingStackInstances(final CallbackContext context) { // If no stack instances need to be updated - if (context.getUpdateStacksQueue().isEmpty() && !context.isUpdateStacksStarted()) { + if (context.getUpdateStacksInstancesQueue().isEmpty() && !context.isUpdateStacksInstancesStarted()) { return false; } return true; @@ -66,7 +66,7 @@ public static boolean isUpdatingStackInstances(final CallbackContext context) { */ public static boolean isDeletingStackInstances(final CallbackContext context) { // If no stack instances need to be deleted - if (context.getDeleteStacksQueue().isEmpty() && !context.isDeleteStacksStarted()) { + if (context.getDeleteStacksInstancesQueue().isEmpty() && !context.isDeleteStacksInstancesStarted()) { return false; } return true; @@ -79,7 +79,7 @@ public static boolean isDeletingStackInstances(final CallbackContext context) { */ public static boolean isAddingStackInstances(final CallbackContext context) { // If no stack instances need to be added - if (context.getCreateStacksQueue().isEmpty() && !context.isAddStacksStarted()) { + if (context.getCreateStacksInstancesQueue().isEmpty() && !context.isAddStacksInstancesStarted()) { return false; } return true; 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 index 0d9a70b..d5b167b 100644 --- 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 @@ -67,9 +67,9 @@ public void analyzeForUpdate(final CallbackContext context) { final Set stackInstancesGroupToUpdate = aggregateStackInstances(stacksToUpdate, isSelfManaged); // Update the stack lists that need to write of callbackContext holder - context.setCreateStacksQueue(new LinkedList<>(stackInstancesGroupToAdd)); - context.setDeleteStacksQueue(new LinkedList<>(stackInstancesGroupToDelete)); - context.setUpdateStacksQueue(new LinkedList<>(stackInstancesGroupToUpdate)); + context.setCreateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); + context.setDeleteStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); + context.setUpdateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToUpdate)); context.setTemplateAnalyzed(true); } @@ -82,7 +82,7 @@ public void analyzeForUpdate(final CallbackContext context) { public void analyzeForCreate(final CallbackContext context) { if (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; if (desiredModel.getStackInstancesGroup().size() == 1) { - context.setCreateStacksQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + context.setCreateStacksInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); } final boolean isSelfManaged = isSelfManaged(desiredModel); @@ -90,7 +90,7 @@ public void analyzeForCreate(final CallbackContext context) { flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); final Set stackInstancesGroupToAdd = aggregateStackInstances(desiredStackInstances, isSelfManaged); - context.setCreateStacksQueue(new LinkedList<>(stackInstancesGroupToAdd)); + context.setCreateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); context.setTemplateAnalyzed(true); } @@ -103,7 +103,7 @@ public void analyzeForCreate(final CallbackContext context) { public void analyzeForDelete(final CallbackContext context) { if (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; if (desiredModel.getStackInstancesGroup().size() == 1) { - context.setDeleteStacksQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + context.setDeleteStacksInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); } final boolean isSelfManaged = isSelfManaged(desiredModel); @@ -111,7 +111,7 @@ public void analyzeForDelete(final CallbackContext context) { flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); final Set stackInstancesGroupToDelete = aggregateStackInstances(desiredStackInstances, isSelfManaged); - context.setDeleteStacksQueue(new LinkedList<>(stackInstancesGroupToDelete)); + context.setDeleteStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); context.setTemplateAnalyzed(true); } @@ -124,8 +124,8 @@ public void analyzeForDelete(final CallbackContext context) { */ public static Set aggregateStackInstances( @NonNull final Set flatStackInstances, final boolean isSelfManaged) { - final Set groupedStacks = groupInstancesByTargets(flatStackInstances, isSelfManaged); - return aggregateInstancesByRegions(groupedStacks, isSelfManaged); + final Set groupedStacksInstances = groupInstancesByTargets(flatStackInstances, isSelfManaged); + return aggregateInstancesByRegions(groupedStacksInstances, isSelfManaged); } /** @@ -137,8 +137,8 @@ public static Set aggregateStackInstances( * @return {@link StackInstances} set */ public static Set aggregateStackInstancesForRead(@NonNull final Set flatStackInstances) { - final Set groupedStacks = groupInstancesByTargets(flatStackInstances, true); - return aggregateInstancesByRegions(groupedStacks, true); + final Set groupedStacksInstances = groupInstancesByTargets(flatStackInstances, true); + return aggregateInstancesByRegions(groupedStacksInstances, true); } @@ -149,15 +149,15 @@ public static Set aggregateStackInstancesForRead(@NonNull final private static Set groupInstancesByTargets( final Set flatStackInstances, final boolean isSelfManaged) { - final Map, StackInstances> groupedStacksMap = new HashMap<>(); + final Map, StackInstances> groupedStacksInstancesMap = 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()); + if (groupedStacksInstancesMap.containsKey(compositeKey)) { + groupedStacksInstancesMap.get(compositeKey).getRegions().add(stackInstance.getRegion()); } else { final DeploymentTargets targets = DeploymentTargets.builder().build(); if (isSelfManaged) { @@ -171,39 +171,39 @@ private static Set groupInstancesByTargets( .deploymentTargets(targets) .parameterOverrides(parameterSet) .build(); - groupedStacksMap.put(compositeKey, stackInstances); + groupedStacksInstancesMap.put(compositeKey, stackInstances); } } - return new HashSet<>(groupedStacksMap.values()); + return new HashSet<>(groupedStacksInstancesMap.values()); } /** * Aggregates instances with similar {@link StackInstances#getRegions()} - * @param groupedStacks {@link StackInstances} set + * @param groupedStacksInstances {@link StackInstances} set * @return Aggregated {@link StackInstances} set */ private static Set aggregateInstancesByRegions( - final Set groupedStacks, + final Set groupedStacksInstances, final boolean isSelfManaged) { - final Map, StackInstances> groupedStacksMap = new HashMap<>(); - for (final StackInstances stackInstances : groupedStacks) { + final Map, StackInstances> groupedStacksInstancesMap = new HashMap<>(); + for (final StackInstances stackInstances : groupedStacksInstances) { final DeploymentTargets target = stackInstances.getDeploymentTargets(); final Set parameterSet = stackInstances.getParameterOverrides(); final List compositeKey = Arrays.asList(stackInstances.getRegions(), parameterSet); - if (groupedStacksMap.containsKey(compositeKey)) { + if (groupedStacksInstancesMap.containsKey(compositeKey)) { if (isSelfManaged) { - groupedStacksMap.get(compositeKey).getDeploymentTargets() + groupedStacksInstancesMap.get(compositeKey).getDeploymentTargets() .getAccounts().addAll(target.getAccounts()); } else { - groupedStacksMap.get(compositeKey).getDeploymentTargets() + groupedStacksInstancesMap.get(compositeKey).getDeploymentTargets() .getOrganizationalUnitIds().addAll(target.getOrganizationalUnitIds()); } } else { - groupedStacksMap.put(compositeKey, stackInstances); + groupedStacksInstancesMap.put(compositeKey, stackInstances); } } - return new HashSet<>(groupedStacksMap.values()); + return new HashSet<>(groupedStacksInstancesMap.values()); } /** @@ -241,6 +241,13 @@ private static Set flattenStackInstancesGroup( final Set targets = isSelfManaged ? stackInstances.getDeploymentTargets().getAccounts() : stackInstances.getDeploymentTargets().getOrganizationalUnitIds(); + if (CollectionUtils.isNullOrEmpty(targets)) { + throw new CfnInvalidRequestException( + String.format("%s should be specified in DeploymentTargets in [%s] model", + isSelfManaged ? "Accounts" : "OrganizationalUnitIds", + isSelfManaged ? "SELF_MANAGED" : "SERVICE_MANAGED")); + } + for (final String target : targets) { final StackInstance stackInstance = StackInstance.builder() .region(region).deploymentTarget(target).parameters(stackInstances.getParameterOverrides()) 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 index 45f6f70..cc18420 100644 --- 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 @@ -78,45 +78,45 @@ public StackSet getStackSet(final String stackSetId) { } /** - * Invokes CreateStackInstances API to add new {@link StackInstances} based on {@link CallbackContext#getCreateStacksQueue()} + * Invokes CreateStackInstances API to add new {@link StackInstances} based on {@link CallbackContext#getCreateStacksInstancesQueue()} * @return Operation Id from {@link CreateStackInstancesResponse} */ private String addStackInstances() { - final Queue instancesQueue = context.getCreateStacksQueue(); + final Queue instancesQueue = context.getCreateStacksInstancesQueue(); final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( createStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::createStackInstances); - context.setAddStacksStarted(true); + context.setAddStacksInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); } /** - * Invokes DeleteStackInstances API to delete old {@link StackInstances} based on {@link CallbackContext#getDeleteStacksQueue()} + * Invokes DeleteStackInstances API to delete old {@link StackInstances} based on {@link CallbackContext#getDeleteStacksInstancesQueue()} * @return Operation Id from {@link DeleteStackInstancesResponse} */ private String deleteStackInstances() { - final Queue instancesQueue = context.getDeleteStacksQueue(); + final Queue instancesQueue = context.getDeleteStacksInstancesQueue(); final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( deleteStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::deleteStackInstances); - context.setDeleteStacksStarted(true); + context.setDeleteStacksInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); } /** - * Invokes UpdateStackInstances API to update existing {@link StackInstances} based on {@link CallbackContext#getUpdateStacksQueue()} + * Invokes UpdateStackInstances API to update existing {@link StackInstances} based on {@link CallbackContext#getUpdateStacksInstancesQueue()} * @return Operation Id from {@link UpdateStackInstancesResponse} */ private String updateStackInstances() { - final Queue instancesQueue = context.getUpdateStacksQueue(); + final Queue instancesQueue = context.getUpdateStacksInstancesQueue(); final UpdateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( updateStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::updateStackInstances); - context.setUpdateStacksStarted(true); + context.setUpdateStacksInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); 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 index 35f58f2..cb41d7c 100644 --- 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 @@ -29,7 +29,7 @@ public class Stabilizer { private static final String INTERNAL_FAILURE = "Internal Failure"; - private static final int ONE_DAY_IN_SECONDS = 24 * 60 * 60; + private static final int ONE_DAY_IN_SECONDS = 12 * 60 * 60; public static final Double RATE = 1.1; public static final int MAX_RETRIES = 60; 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 4cc6dc8..90f874c 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 @@ -77,7 +77,7 @@ public void handleRequest_SimpleSuccess() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); @@ -113,9 +113,9 @@ public void handleRequest_SelfManaged_CreateNotYetStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_1) - .createStacksQueue(new LinkedList<>(stackInstancesSet)) + .createStacksInstancesQueue(new LinkedList<>(stackInstancesSet)) .stackInstancesInOperation(stackInstances) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -151,7 +151,7 @@ public void handleRequest_ServiceManaged_CreateNotYetStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(stackInstances) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -174,7 +174,7 @@ public void handleRequest_CreateNotYetStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -182,7 +182,7 @@ public void handleRequest_CreateNotYetStabilized_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) @@ -222,7 +222,7 @@ public void handleRequest_OperationStopped_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -239,7 +239,7 @@ public void handleRequest_OperationTimesOut_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(EXECUTION_TIMEOUT_SECONDS) @@ -257,7 +257,7 @@ public void handleRequest_OperationMaxRetries_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .elapsedTime(EXECUTION_TIMEOUT_SECONDS) .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) @@ -331,7 +331,7 @@ public void handlerRequest_OperationInProgressException() { .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .stackSetCreated(true) .templateAnalyzed(true) - .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) + .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) .build(); assertThat(response).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 f2b8ad9..2412b12 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,7 +1,10 @@ package software.amazon.cloudformation.stackset; -import software.amazon.awssdk.services.cloudformation.model.DeleteStackInstancesRequest; -import software.amazon.awssdk.services.cloudformation.model.OperationInProgressException; +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.StackSetNotFoundException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -9,11 +12,6 @@ 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 java.util.LinkedList; import java.util.Set; @@ -24,14 +22,13 @@ 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.TestUtils.DELETE_STACK_INSTANCES_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE; 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_SUCCEED_RESPONSE; 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.Stabilizer.BASE_CALLBACK_DELAY_SECONDS; @ExtendWith(MockitoExtension.class) public class DeleteHandlerTest { @@ -67,7 +64,7 @@ public void handleRequest_SimpleSuccess() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); @@ -100,11 +97,11 @@ public void handleRequest_DeleteNotYetStarted_InProgress() { stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .deleteStacksQueue(new LinkedList<>(stackInstancesSet)) + .deleteStacksInstancesQueue(new LinkedList<>(stackInstancesSet)) .stackInstancesInOperation(stackInstances) .build(); @@ -125,14 +122,14 @@ public void handleRequest_DeleteNotYetStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .templateAnalyzed(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) 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 f8f79a2..d855837 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 @@ -106,9 +106,9 @@ public void handleRequest_AllUpdatesStabilized_Success() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksStarted(true) - .addStacksStarted(true) - .updateStacksStarted(true) + .deleteStacksInstancesStarted(true) + .addStacksInstancesStarted(true) + .updateStacksInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .operationsStabilizationMap(updateOperationsMap) @@ -139,9 +139,9 @@ public void handleRequest_UpdateStackSetNotStarted_InProgress() { .updateStackSetStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) - .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) - .deleteStacksQueue(response.getCallbackContext().getDeleteStacksQueue()) - .updateStacksQueue(response.getCallbackContext().getUpdateStacksQueue()) + .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) + .deleteStacksInstancesQueue(response.getCallbackContext().getDeleteStacksInstancesQueue()) + .updateStacksInstancesQueue(response.getCallbackContext().getUpdateStacksInstancesQueue()) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -189,16 +189,16 @@ public void handleRequest_UpdateStackSetNotStabilized_InProgress() { } @Test - public void handleRequest_SelfManaged_DeleteStacksNotStarted_InProgress() { + public void handleRequest_SelfManaged_DeleteStacksInstancesNotStarted_InProgress() { doReturn(DELETE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksQueue(DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .deleteStacksInstancesQueue(DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) .build(); @@ -215,11 +215,11 @@ public void handleRequest_SelfManaged_DeleteStacksNotStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -236,17 +236,17 @@ public void handleRequest_SelfManaged_DeleteStacksNotStarted_InProgress() { } @Test - public void handleRequest_DeleteStacksNotYetStabilized_InProgress() { + public void handleRequest_DeleteStacksInstancesNotYetStabilized_InProgress() { doReturn(OPERATION_RUNNING_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .templateAnalyzed(true) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) .build(); @@ -256,11 +256,11 @@ public void handleRequest_DeleteStacksNotYetStabilized_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .updateStackSetStarted(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .templateAnalyzed(true) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) @@ -282,18 +282,18 @@ public void handleRequest_DeleteStacksNotYetStabilized_InProgress() { } @Test - public void handleRequest_AddStacksNotStarted_InProgress() { + public void handleRequest_AddStacksInstancesNotStarted_InProgress() { doReturn(CREATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksStarted(true) + .deleteStacksInstancesStarted(true) .operationId(OPERATION_ID_2) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -310,12 +310,12 @@ public void handleRequest_AddStacksNotStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksStarted(true) - .addStacksStarted(true) + .deleteStacksInstancesStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) - .createStacksQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -333,18 +333,18 @@ public void handleRequest_AddStacksNotStarted_InProgress() { } @Test - public void handleRequest_UpdateStacksNotStarted_InProgress() { + public void handleRequest_UpdateStacksInstancesNotStarted_InProgress() { doReturn(UPDATE_STACK_INSTANCES_RESPONSE).when(proxy).injectCredentialsAndInvokeV2(any(), any()); final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksStarted(true) - .addStacksStarted(true) + .deleteStacksInstancesStarted(true) + .addStacksInstancesStarted(true) .operationId(OPERATION_ID_2) .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -362,12 +362,12 @@ public void handleRequest_UpdateStacksNotStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksStarted(true) - .addStacksStarted(true) - .updateStacksStarted(true) + .deleteStacksInstancesStarted(true) + .addStacksInstancesStarted(true) + .updateStacksInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(stackInstances) - .updateStacksQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -419,9 +419,9 @@ public void handlerRequest_OperationInProgressException() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .createStacksQueue(response.getCallbackContext().getCreateStacksQueue()) - .deleteStacksQueue(response.getCallbackContext().getDeleteStacksQueue()) - .updateStacksQueue(response.getCallbackContext().getUpdateStacksQueue()) + .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) + .deleteStacksInstancesQueue(response.getCallbackContext().getDeleteStacksInstancesQueue()) + .updateStacksInstancesQueue(response.getCallbackContext().getUpdateStacksInstancesQueue()) .build(); assertThat(response).isNotNull(); From 90e985a6d635d918e0a3d9477d24a4e81adfe5fc Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 24 Apr 2020 16:22:25 -0700 Subject: [PATCH 08/19] Fix renames --- .../stackset/CallbackContext.java | 12 ++-- .../stackset/CreateHandler.java | 4 +- .../stackset/DeleteHandler.java | 4 +- .../stackset/UpdateHandler.java | 12 ++-- .../stackset/util/Comparator.java | 7 +- .../stackset/util/InstancesAnalyzer.java | 15 ++-- .../stackset/util/OperationOperator.java | 24 +++---- .../stackset/CreateHandlerTest.java | 20 +++--- .../stackset/DeleteHandlerTest.java | 10 +-- .../stackset/UpdateHandlerTest.java | 72 +++++++++---------- 10 files changed, 85 insertions(+), 95 deletions(-) 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 b1a7c46..d1a34db 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 @@ -28,16 +28,16 @@ public class CallbackContext { private boolean stackSetCreated; // Indicates initiation of stack instances creation. - private boolean addStacksInstancesStarted; + private boolean addStackInstancesStarted; // Indicates initiation of stack instances delete. - private boolean deleteStacksInstancesStarted; + private boolean deleteStackInstancesStarted; // Indicates initiation of stack set update. private boolean updateStackSetStarted; // Indicates initiation of stack instances update. - private boolean updateStacksInstancesStarted; + private boolean updateStackInstancesStarted; // Total running time @Builder.Default @@ -47,15 +47,15 @@ public class CallbackContext { // List to keep track on the complete status for creating @Builder.Default - private Queue createStacksInstancesQueue = new LinkedList<>(); + private Queue createStackInstancesQueue = new LinkedList<>(); // List to keep track on stack instances for deleting @Builder.Default - private Queue deleteStacksInstancesQueue = new LinkedList<>(); + private Queue deleteStackInstancesQueue = new LinkedList<>(); // List to keep track on stack instances for update @Builder.Default - private Queue updateStacksInstancesQueue = new LinkedList<>(); + private Queue updateStackInstancesQueue = new LinkedList<>(); /** * Default as 0, will be {@link software.amazon.cloudformation.stackset.util.Stabilizer#BASE_CALLBACK_DELAY_SECONDS} 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 b6f589c..0eccc21 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 @@ -50,8 +50,8 @@ public ProgressEvent handleRequest( createStackSet(proxy, model, logger, client, context, stackSetName, request.getClientRequestToken()); } - if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksInstancesStarted(), null, - ADD_INSTANCES, context.getCreateStacksInstancesQueue(), model, context)) { + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStackInstancesStarted(), null, + ADD_INSTANCES, context.getCreateStackInstancesQueue(), model, context)) { operator.updateStackSet(ADD_INSTANCES); } 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 299b48e..d3945b5 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 @@ -36,8 +36,8 @@ public ProgressEvent handleRequest( .build(); InstancesAnalyzer.builder().desiredModel(model).build().analyzeForDelete(context); - if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksInstancesStarted(), null, - DELETE_INSTANCES, context.getDeleteStacksInstancesQueue(), model, context)) { + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStackInstancesStarted(), null, + DELETE_INSTANCES, context.getDeleteStackInstancesQueue(), model, context)) { operator.updateStackSet(DELETE_INSTANCES); } 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 3a44e44..965a217 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 @@ -52,20 +52,20 @@ public ProgressEvent handleRequest( operator.updateStackSet(STACK_SET_CONFIGS); } - if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStacksInstancesStarted(), - STACK_SET_CONFIGS, DELETE_INSTANCES, context.getDeleteStacksInstancesQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isDeletingStackInstances(context), context.isDeleteStackInstancesStarted(), + STACK_SET_CONFIGS, DELETE_INSTANCES, context.getDeleteStackInstancesQueue(), desiredModel, context)) { operator.updateStackSet(DELETE_INSTANCES); } - if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStacksInstancesStarted(), - DELETE_INSTANCES, ADD_INSTANCES, context.getCreateStacksInstancesQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isAddingStackInstances(context), context.isAddStackInstancesStarted(), + DELETE_INSTANCES, ADD_INSTANCES, context.getCreateStackInstancesQueue(), desiredModel, context)) { operator.updateStackSet(ADD_INSTANCES); } - if (stabilizer.isPerformingOperation(isUpdatingStackInstances(context), context.isUpdateStacksInstancesStarted(), - ADD_INSTANCES, UPDATE_INSTANCES, context.getUpdateStacksInstancesQueue(), desiredModel, context)) { + if (stabilizer.isPerformingOperation(isUpdatingStackInstances(context), context.isUpdateStackInstancesStarted(), + ADD_INSTANCES, UPDATE_INSTANCES, context.getUpdateStackInstancesQueue(), desiredModel, context)) { operator.updateStackSet(UPDATE_INSTANCES); } 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 377dcc4..fa4a99f 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,6 +1,5 @@ package software.amazon.cloudformation.stackset.util; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import software.amazon.awssdk.services.cloudformation.model.PermissionModels; import software.amazon.cloudformation.stackset.CallbackContext; @@ -53,7 +52,7 @@ public static boolean isStackSetConfigEquals( */ public static boolean isUpdatingStackInstances(final CallbackContext context) { // If no stack instances need to be updated - if (context.getUpdateStacksInstancesQueue().isEmpty() && !context.isUpdateStacksInstancesStarted()) { + if (context.getUpdateStackInstancesQueue().isEmpty() && !context.isUpdateStackInstancesStarted()) { return false; } return true; @@ -66,7 +65,7 @@ public static boolean isUpdatingStackInstances(final CallbackContext context) { */ public static boolean isDeletingStackInstances(final CallbackContext context) { // If no stack instances need to be deleted - if (context.getDeleteStacksInstancesQueue().isEmpty() && !context.isDeleteStacksInstancesStarted()) { + if (context.getDeleteStackInstancesQueue().isEmpty() && !context.isDeleteStackInstancesStarted()) { return false; } return true; @@ -79,7 +78,7 @@ public static boolean isDeletingStackInstances(final CallbackContext context) { */ public static boolean isAddingStackInstances(final CallbackContext context) { // If no stack instances need to be added - if (context.getCreateStacksInstancesQueue().isEmpty() && !context.isAddStacksInstancesStarted()) { + if (context.getCreateStackInstancesQueue().isEmpty() && !context.isAddStackInstancesStarted()) { return false; } return true; 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 index d5b167b..2da72b3 100644 --- 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 @@ -11,7 +11,6 @@ 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; @@ -67,9 +66,9 @@ public void analyzeForUpdate(final CallbackContext context) { final Set stackInstancesGroupToUpdate = aggregateStackInstances(stacksToUpdate, isSelfManaged); // Update the stack lists that need to write of callbackContext holder - context.setCreateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); - context.setDeleteStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); - context.setUpdateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToUpdate)); + context.setCreateStackInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); + context.setDeleteStackInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); + context.setUpdateStackInstancesQueue(new LinkedList<>(stackInstancesGroupToUpdate)); context.setTemplateAnalyzed(true); } @@ -82,7 +81,7 @@ public void analyzeForUpdate(final CallbackContext context) { public void analyzeForCreate(final CallbackContext context) { if (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; if (desiredModel.getStackInstancesGroup().size() == 1) { - context.setCreateStacksInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + context.setCreateStackInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); } final boolean isSelfManaged = isSelfManaged(desiredModel); @@ -90,7 +89,7 @@ public void analyzeForCreate(final CallbackContext context) { flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); final Set stackInstancesGroupToAdd = aggregateStackInstances(desiredStackInstances, isSelfManaged); - context.setCreateStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); + context.setCreateStackInstancesQueue(new LinkedList<>(stackInstancesGroupToAdd)); context.setTemplateAnalyzed(true); } @@ -103,7 +102,7 @@ public void analyzeForCreate(final CallbackContext context) { public void analyzeForDelete(final CallbackContext context) { if (context.isTemplateAnalyzed() || desiredModel.getStackInstancesGroup() == null) return; if (desiredModel.getStackInstancesGroup().size() == 1) { - context.setDeleteStacksInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); + context.setDeleteStackInstancesQueue(new LinkedList<>(desiredModel.getStackInstancesGroup())); } final boolean isSelfManaged = isSelfManaged(desiredModel); @@ -111,7 +110,7 @@ public void analyzeForDelete(final CallbackContext context) { flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); final Set stackInstancesGroupToDelete = aggregateStackInstances(desiredStackInstances, isSelfManaged); - context.setDeleteStacksInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); + context.setDeleteStackInstancesQueue(new LinkedList<>(stackInstancesGroupToDelete)); context.setTemplateAnalyzed(true); } 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 index cc18420..005c7f6 100644 --- 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 @@ -8,10 +8,8 @@ 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.UpdateStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesResponse; import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; @@ -19,17 +17,13 @@ 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 software.amazon.cloudformation.stackset.StackInstances; -import java.util.HashSet; import java.util.Queue; -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.deleteStackSetRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.describeStackSetRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackInstancesRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest; @@ -78,45 +72,45 @@ public StackSet getStackSet(final String stackSetId) { } /** - * Invokes CreateStackInstances API to add new {@link StackInstances} based on {@link CallbackContext#getCreateStacksInstancesQueue()} + * Invokes CreateStackInstances API to add new {@link StackInstances} based on {@link CallbackContext#getCreateStackInstancesQueue()} * @return Operation Id from {@link CreateStackInstancesResponse} */ private String addStackInstances() { - final Queue instancesQueue = context.getCreateStacksInstancesQueue(); + final Queue instancesQueue = context.getCreateStackInstancesQueue(); final CreateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( createStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::createStackInstances); - context.setAddStacksInstancesStarted(true); + context.setAddStackInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); } /** - * Invokes DeleteStackInstances API to delete old {@link StackInstances} based on {@link CallbackContext#getDeleteStacksInstancesQueue()} + * Invokes DeleteStackInstances API to delete old {@link StackInstances} based on {@link CallbackContext#getDeleteStackInstancesQueue()} * @return Operation Id from {@link DeleteStackInstancesResponse} */ private String deleteStackInstances() { - final Queue instancesQueue = context.getDeleteStacksInstancesQueue(); + final Queue instancesQueue = context.getDeleteStackInstancesQueue(); final DeleteStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( deleteStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::deleteStackInstances); - context.setDeleteStacksInstancesStarted(true); + context.setDeleteStackInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); } /** - * Invokes UpdateStackInstances API to update existing {@link StackInstances} based on {@link CallbackContext#getUpdateStacksInstancesQueue()} + * Invokes UpdateStackInstances API to update existing {@link StackInstances} based on {@link CallbackContext#getUpdateStackInstancesQueue()} * @return Operation Id from {@link UpdateStackInstancesResponse} */ private String updateStackInstances() { - final Queue instancesQueue = context.getUpdateStacksInstancesQueue(); + final Queue instancesQueue = context.getUpdateStackInstancesQueue(); final UpdateStackInstancesResponse response = proxy.injectCredentialsAndInvokeV2( updateStackInstancesRequest(desiredModel.getStackSetId(), desiredModel.getOperationPreferences(), instancesQueue.peek()), client::updateStackInstances); - context.setUpdateStacksInstancesStarted(true); + context.setUpdateStackInstancesStarted(true); // We remove the stack instances from queue Only if API invocation succeeds context.setStackInstancesInOperation(instancesQueue.remove()); return response.operationId(); 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 90f874c..d70f1d8 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 @@ -77,7 +77,7 @@ public void handleRequest_SimpleSuccess() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); @@ -113,9 +113,9 @@ public void handleRequest_SelfManaged_CreateNotYetStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_1) - .createStacksInstancesQueue(new LinkedList<>(stackInstancesSet)) + .createStackInstancesQueue(new LinkedList<>(stackInstancesSet)) .stackInstancesInOperation(stackInstances) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -151,7 +151,7 @@ public void handleRequest_ServiceManaged_CreateNotYetStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(stackInstances) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -174,7 +174,7 @@ public void handleRequest_CreateNotYetStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_1) .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -182,7 +182,7 @@ public void handleRequest_CreateNotYetStabilized_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) @@ -222,7 +222,7 @@ public void handleRequest_OperationStopped_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) @@ -239,7 +239,7 @@ public void handleRequest_OperationTimesOut_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .elapsedTime(EXECUTION_TIMEOUT_SECONDS) @@ -257,7 +257,7 @@ public void handleRequest_OperationMaxRetries_CfnNotStabilizedException() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .addStacksInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_1) .elapsedTime(EXECUTION_TIMEOUT_SECONDS) .currentDelaySeconds(MAX_CALLBACK_DELAY_SECONDS) @@ -331,7 +331,7 @@ public void handlerRequest_OperationInProgressException() { .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .stackSetCreated(true) .templateAnalyzed(true) - .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) + .createStackInstancesQueue(response.getCallbackContext().getCreateStackInstancesQueue()) .build(); assertThat(response).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 2412b12..257dded 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 @@ -64,7 +64,7 @@ public void handleRequest_SimpleSuccess() { final CallbackContext inputContext = CallbackContext.builder() .stackSetCreated(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .build(); @@ -97,11 +97,11 @@ public void handleRequest_DeleteNotYetStarted_InProgress() { stackInstancesSet.remove(stackInstances); final CallbackContext outputContext = CallbackContext.builder() - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .deleteStacksInstancesQueue(new LinkedList<>(stackInstancesSet)) + .deleteStackInstancesQueue(new LinkedList<>(stackInstancesSet)) .stackInstancesInOperation(stackInstances) .build(); @@ -122,14 +122,14 @@ public void handleRequest_DeleteNotYetStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .operationId(OPERATION_ID_1) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .operationId(OPERATION_ID_1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) 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 d855837..d699e7e 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 @@ -15,7 +15,6 @@ 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; @@ -24,7 +23,6 @@ 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; @@ -106,9 +104,9 @@ public void handleRequest_AllUpdatesStabilized_Success() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksInstancesStarted(true) - .addStacksInstancesStarted(true) - .updateStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) + .addStackInstancesStarted(true) + .updateStackInstancesStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) .operationsStabilizationMap(updateOperationsMap) @@ -139,9 +137,9 @@ public void handleRequest_UpdateStackSetNotStarted_InProgress() { .updateStackSetStarted(true) .templateAnalyzed(true) .operationId(OPERATION_ID_1) - .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) - .deleteStacksInstancesQueue(response.getCallbackContext().getDeleteStacksInstancesQueue()) - .updateStacksInstancesQueue(response.getCallbackContext().getUpdateStacksInstancesQueue()) + .createStackInstancesQueue(response.getCallbackContext().getCreateStackInstancesQueue()) + .deleteStackInstancesQueue(response.getCallbackContext().getDeleteStackInstancesQueue()) + .updateStackInstancesQueue(response.getCallbackContext().getUpdateStackInstancesQueue()) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -196,9 +194,9 @@ public void handleRequest_SelfManaged_DeleteStacksInstancesNotStarted_InProgress final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesQueue(DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .deleteStackInstancesQueue(DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) .build(); @@ -215,11 +213,11 @@ public void handleRequest_SelfManaged_DeleteStacksInstancesNotStarted_InProgress final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -242,11 +240,11 @@ public void handleRequest_DeleteStacksInstancesNotYetStabilized_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .templateAnalyzed(true) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) .build(); @@ -256,11 +254,11 @@ public void handleRequest_DeleteStacksInstancesNotYetStabilized_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .updateStackSetStarted(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .templateAnalyzed(true) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .elapsedTime(BASE_CALLBACK_DELAY_SECONDS) .operationId(OPERATION_ID_2) @@ -289,11 +287,11 @@ public void handleRequest_AddStacksInstancesNotStarted_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) .operationId(OPERATION_ID_2) .stackInstancesInOperation(DELETE_STACK_INSTANCES_SELF_MANAGED) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -310,12 +308,12 @@ public void handleRequest_AddStacksInstancesNotStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) - .addStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) - .createStacksInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .createStackInstancesQueue(CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -340,11 +338,11 @@ public void handleRequest_UpdateStacksInstancesNotStarted_InProgress() { final CallbackContext inputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) - .addStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) + .addStackInstancesStarted(true) .operationId(OPERATION_ID_2) .stackInstancesInOperation(CREATE_STACK_INSTANCES_SELF_MANAGED) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) .build(); @@ -362,12 +360,12 @@ public void handleRequest_UpdateStacksInstancesNotStarted_InProgress() { final CallbackContext outputContext = CallbackContext.builder() .updateStackSetStarted(true) .templateAnalyzed(true) - .deleteStacksInstancesStarted(true) - .addStacksInstancesStarted(true) - .updateStacksInstancesStarted(true) + .deleteStackInstancesStarted(true) + .addStackInstancesStarted(true) + .updateStackInstancesStarted(true) .operationId(OPERATION_ID_1) .stackInstancesInOperation(stackInstances) - .updateStacksInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) + .updateStackInstancesQueue(UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS + 1) .build(); @@ -419,9 +417,9 @@ public void handlerRequest_OperationInProgressException() { final CallbackContext outputContext = CallbackContext.builder() .templateAnalyzed(true) .currentDelaySeconds(BASE_CALLBACK_DELAY_SECONDS) - .createStacksInstancesQueue(response.getCallbackContext().getCreateStacksInstancesQueue()) - .deleteStacksInstancesQueue(response.getCallbackContext().getDeleteStacksInstancesQueue()) - .updateStacksInstancesQueue(response.getCallbackContext().getUpdateStacksInstancesQueue()) + .createStackInstancesQueue(response.getCallbackContext().getCreateStackInstancesQueue()) + .deleteStackInstancesQueue(response.getCallbackContext().getDeleteStackInstancesQueue()) + .updateStackInstancesQueue(response.getCallbackContext().getUpdateStackInstancesQueue()) .build(); assertThat(response).isNotNull(); From f423e6fa62815b8fdfb69469efc3f694dfda47e0 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Mon, 4 May 2020 02:06:02 -0700 Subject: [PATCH 09/19] revision stackset using call chain --- .../aws-cloudformation-stackset.json | 32 ++- aws-cloudformation-stackset/pom.xml | 2 +- .../resource-role.yaml | 6 +- .../sam-tests/create.json | 36 +++ .../sam-tests/delete.json | 37 +++ .../stackset/BaseHandlerStd.java | 222 ++++++++-------- .../stackset/CallbackContext.java | 13 - .../stackset/CreateHandler.java | 78 ++---- .../stackset/DeleteHandler.java | 37 ++- .../stackset/UpdateHandler.java | 69 ++--- .../translator/PropertyTranslator.java | 11 +- .../util/AwsCredentialsExtractor.java | 58 ++--- .../stackset/util/ClientBuilder.java | 23 +- .../stackset/util/Comparator.java | 35 ++- .../stackset/util/InstancesAnalyzer.java | 159 +++++++----- .../stackset/util/ParseException.java | 2 +- .../stackset/util/PhysicalIdGenerator.java | 25 -- .../stackset/util/ResourceModelBuilder.java | 17 +- .../util/StackInstancesPlaceHolder.java | 17 ++ .../stackset/util/TemplateParser.java | 21 +- .../stackset/util/Validator.java | 110 ++++---- .../stackset/AbstractTestBase.java | 9 +- .../stackset/BaseHandlerStdTest.java | 13 - .../stackset/CreateHandlerTest.java | 190 ++++++++++---- .../stackset/DeleteHandlerTest.java | 95 +++++-- .../stackset/ListHandlerTest.java | 24 +- .../stackset/ReadHandlerTest.java | 52 ++-- .../stackset/UpdateHandlerTest.java | 116 ++++++--- .../stackset/util/ComparatorTest.java | 54 ++++ .../stackset/util/TemplateParserTest.java | 10 + .../stackset/util/TestUtils.java | 237 +++++++++++++----- aws-cloudformation-stackset/template.yml | 1 - 32 files changed, 1173 insertions(+), 638 deletions(-) create mode 100644 aws-cloudformation-stackset/sam-tests/create.json create mode 100644 aws-cloudformation-stackset/sam-tests/delete.json delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstancesPlaceHolder.java delete mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 4148b8c..0fda484 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -8,23 +8,25 @@ "type": "object", "properties": { "Key": { - "description": "A string used to identify this tag. You can specify a maximum of 128 characters for a tag key.", + "description": "A string used to identify this tag. You can specify a maximum of 127 characters for a tag key.", "type": "string", "minLength": 1, - "maxLength": 128, - "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$" + "maxLength": 127, + "pattern": "^(?!aws:.*)[a-z0-9\\s\\_\\.\\/\\=\\+\\-]+$" }, "Value": { "description": "A string containing the value for this tag. You can specify a maximum of 256 characters for a tag value.", "type": "string", "minLength": 1, - "maxLength": 256 + "maxLength": 255, + "pattern": "^(?!aws:.*)[a-z0-9\\s\\_\\.\\/\\=\\+\\-]+$" } }, "required": [ "Value", "Key" - ] + ], + "additionalProperties": false }, "AutoDeployment": { "type": "object", @@ -266,14 +268,15 @@ "s3:GetObject", "cloudformation:CreateStackSet", "cloudformation:CreateStackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:DescribeStackSetOperation", + "cloudformation:TagResource" ] }, "read": { "permissions": [ "cloudformation:DescribeStackSet", - "cloudformation:CreateStackInstances", - "cloudformation:DescribeStackInstances" + "cloudformation:ListStackInstances", + "cloudformation:DescribeStackInstance" ] }, "update": { @@ -282,19 +285,26 @@ "cloudformation:UpdateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:UpdateStackInstances", + "cloudformation:DescribeStackSetOperation", + "cloudformation:TagResource", + "cloudformation:UntagResource" ] }, "delete": { "permissions": [ "cloudformation:DeleteStackSet", "cloudformation:DeleteStackInstances", - "cloudformation:DescribeStackSetOperation" + "cloudformation:DescribeStackSetOperation", + "cloudformation:UntagResource" ] }, "list": { "permissions": [ - "cloudformation:ListStackSets" + "cloudformation:ListStackSets", + "cloudformation:DescribeStackSet", + "cloudformation:ListStackInstances", + "cloudformation:DescribeStackInstance" ] } } diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml index cd22281..7557af1 100644 --- a/aws-cloudformation-stackset/pom.xml +++ b/aws-cloudformation-stackset/pom.xml @@ -41,7 +41,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - 1.0.2 + 1.0.4 diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index dda6531..a20ce75 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -27,10 +27,14 @@ Resources: - "cloudformation:CreateStackSet" - "cloudformation:DeleteStackInstances" - "cloudformation:DeleteStackSet" - - "cloudformation:DescribeStackInstances" + - "cloudformation:DescribeStackInstance" - "cloudformation:DescribeStackSet" - "cloudformation:DescribeStackSetOperation" + - "cloudformation:ListStackInstances" - "cloudformation:ListStackSets" + - "cloudformation:TagResource" + - "cloudformation:UntagResource" + - "cloudformation:UpdateStackInstances" - "cloudformation:UpdateStackSet" - "s3:GetObject" Resource: "*" diff --git a/aws-cloudformation-stackset/sam-tests/create.json b/aws-cloudformation-stackset/sam-tests/create.json new file mode 100644 index 0000000..9410059 --- /dev/null +++ b/aws-cloudformation-stackset/sam-tests/create.json @@ -0,0 +1,36 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "CREATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "PermissionModel": "SELF_MANAGED", + "TemplateBody": "{\n \"AWSTemplateFormatVersion\": \"2010-09-09\",\n \"Resources\": {\n \"IntegrationTestWaitHandle\": {\n \"Type\": \"AWS::CloudFormation::WaitConditionHandle\",\n \"Properties\": {}\n }\n }\n}\n", + "StackInstancesGroup": [ + { + "Regions": [ + "us-east-1", + "us-west-2" + ], + "DeploymentTargets": { + "Accounts": [ + 111111111111 + ] + } + } + ], + "Tags": [ + { + "Key": "key1", + "Value": "value1" + } + ] + }, + "logicalResourceIdentifier": "MyStackSet" + }, + "callbackContext": null +} diff --git a/aws-cloudformation-stackset/sam-tests/delete.json b/aws-cloudformation-stackset/sam-tests/delete.json new file mode 100644 index 0000000..6e4b23b --- /dev/null +++ b/aws-cloudformation-stackset/sam-tests/delete.json @@ -0,0 +1,37 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "DELETE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "StackSetId": "MyStackSet-IsIHeS8hqtf3:278fd5ea-4b17-4c66-a93d-567c7daa7c0a", + "PermissionModel": "SELF_MANAGED", + "TemplateBody": "{\n \"AWSTemplateFormatVersion\": \"2010-09-09\",\n \"Resources\": {\n \"IntegrationTestWaitHandle\": {\n \"Type\": \"AWS::CloudFormation::WaitConditionHandle\",\n \"Properties\": {}\n }\n }\n}\n", + "StackInstancesGroup": [ + { + "Regions": [ + "us-east-1", + "us-west-2" + ], + "DeploymentTargets": { + "Accounts": [ + 111111111111 + ] + } + } + ], + "Tags": [ + { + "Key": "key1", + "Value": "value1" + } + ] + }, + "logicalResourceIdentifier": "MyStackSet" + }, + "callbackContext": null +} 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 index 07d6b11..a1a9b85 100644 --- 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 @@ -3,16 +3,15 @@ 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.CreateStackInstancesResponse; 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.StackInstanceNotFoundException; 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.awssdk.services.cloudformation.model.UpdateStackInstancesResponse; import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; @@ -23,7 +22,7 @@ import software.amazon.cloudformation.stackset.util.ClientBuilder; import java.time.Duration; -import java.util.function.BiFunction; +import java.util.List; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackInstancesRequest; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackInstancesRequest; @@ -36,7 +35,6 @@ */ public abstract class BaseHandlerStd extends BaseHandler { - protected static final int NO_CALLBACK_DELAY = 0; protected static final MultipleOf MULTIPLE_OF = MultipleOf.multipleOf() @@ -45,8 +43,48 @@ public abstract class BaseHandlerStd extends BaseHandler { .delay(Duration.ofSeconds(2L)) .build(); - protected static final BiFunction, ResourceModel> - EMPTY_CALL = (model, proxyClient) -> model; + /** + * 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 TerminalException( + String.format("Stack set operation [%s] was unexpectedly stopped or failed", operationId)); + } + } @Override public final ProgressEvent handleRequest( @@ -55,13 +93,7 @@ public final ProgressEvent handleRequest( final CallbackContext callbackContext, final Logger logger) { - return handleRequest( - proxy, - request, - callbackContext != null ? callbackContext : new CallbackContext(), - proxy.newProxy(ClientBuilder::getClient), - logger - ); + return handleRequest(proxy, request, callbackContext != null ? callbackContext : new CallbackContext(), proxy.newProxy(ClientBuilder::getClient), logger); } protected abstract ProgressEvent handleRequest( @@ -71,78 +103,117 @@ protected abstract ProgressEvent handleRequest( 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 boolean filterException(AwsRequest request, Exception e, ProxyClient client, ResourceModel model, CallbackContext context) { + return e instanceof OperationInProgressException; } protected ProgressEvent createStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, final ProgressEvent progress, + final List stackInstancesList, 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)) - .progress()); + for (final StackInstances stackInstances : stackInstancesList) { + final ProgressEvent progressEvent = proxy + .initiate("AWS-CloudFormation-StackSet::CreateStackInstances" + stackInstances.hashCode(), client, model, callbackContext) + .translateToServiceRequest(modelRequest -> createStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances)) + .backoffDelay(MULTIPLE_OF) + .makeServiceCall((modelRequest, proxyInvocation) -> { + final CreateStackInstancesResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::createStackInstances); + logger.log(String.format("%s CreateStackInstances in [%s] of [%s] initiated", ResourceModel.TYPE_NAME, stackInstances.getRegions(), stackInstances.getDeploymentTargets())); + return response; + }) + .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger)) + .success(); + + if (!progressEvent.isSuccess()) { + return progressEvent; + } + } - return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model); + return ProgressEvent.progress(model, callbackContext); } protected ProgressEvent deleteStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, final ProgressEvent progress, + final List stackInstancesList, 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()); + for (final StackInstances stackInstances : stackInstancesList) { + final ProgressEvent progressEvent = proxy + .initiate("AWS-CloudFormation-StackSet::DeleteStackInstances" + stackInstances.hashCode(), client, model, callbackContext) + .translateToServiceRequest(modelRequest -> deleteStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances)) + .backoffDelay(MULTIPLE_OF) + .makeServiceCall((modelRequest, proxyInvocation) -> { + final DeleteStackInstancesResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::deleteStackInstances); + logger.log(String.format("%s DeleteStackInstances in [%s] of [%s] initiated", ResourceModel.TYPE_NAME, stackInstances.getRegions(), stackInstances.getDeploymentTargets())); + return response; + }) + .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger)) + .handleError((request, e, proxyClient, model_, context) -> { + // If StackInstanceNotFoundException is thrown by the service, then we did succeed delete/stabilization call in case of out of band deletion. + if (e instanceof StackInstanceNotFoundException) { + return ProgressEvent.success(model_, context); + } + if (e instanceof OperationInProgressException) { + return ProgressEvent.progress(model_, context); + } + throw e; + }) + .success(); + + if (!progressEvent.isSuccess()) { + return progressEvent; + } + } - return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model); + return ProgressEvent.progress(model, callbackContext); } protected ProgressEvent updateStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, final ProgressEvent progress, + final List stackInstancesList, 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()); + for (final StackInstances stackInstances : stackInstancesList) { + final ProgressEvent progressEvent = proxy + .initiate("AWS-CloudFormation-StackSet::UpdateStackInstances" + stackInstances.hashCode(), client, model, callbackContext) + .translateToServiceRequest(modelRequest -> updateStackInstancesRequest(modelRequest.getStackSetId(), modelRequest.getOperationPreferences(), stackInstances)) + .backoffDelay(MULTIPLE_OF) + .makeServiceCall((modelRequest, proxyInvocation) -> { + final UpdateStackInstancesResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackInstances); + logger.log(String.format("%s UpdateStackInstances in [%s] of [%s] initiated", ResourceModel.TYPE_NAME, stackInstances.getRegions(), stackInstances.getDeploymentTargets())); + return response; + }) + .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger)) + .retryErrorFilter(this::filterException) + .progress(); + + if (!progressEvent.isSuccess()) { + return progressEvent; + } + } - return ProgressEvent.defaultInProgressHandler(callbackContext, NO_CALLBACK_DELAY, model); + return ProgressEvent.progress(model, callbackContext); } /** * Get {@link StackSet} from service client using stackSetId + * * @param stackSetId StackSet Id * @return {@link StackSet} */ @@ -158,9 +229,10 @@ protected StackSet describeStackSet( /** * Checks if the operation is stabilized using OperationId to interact with * {@link DescribeStackSetOperationResponse} - * @param model {@link ResourceModel} + * + * @param model {@link ResourceModel} * @param operationId OperationId from operation response - * @param logger Logger + * @param logger Logger * @return A boolean value indicates if operation is complete */ protected boolean isOperationStabilized(final ProxyClient proxyClient, @@ -172,56 +244,4 @@ protected boolean isOperationStabilized(final ProxyClient 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 TerminalException( - String.format("Stack set operation [%s] was unexpectedly stopped or failed", operationId)); - } - } - - protected ProgressEvent handleException(AwsRequest request, - Exception exception, - ProxyClient client, - ResourceModel model, - CallbackContext context, - AmazonWebServicesClientProxy proxy) { - - return proxy.defaultHandler(request, exception, client, model, context); - } } 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 1fd2336..f5436c9 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 @@ -2,22 +2,9 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; -import java.util.LinkedList; -import java.util.List; - @lombok.Getter @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) public class CallbackContext extends StdCallbackContext { - - // List to keep track on the complete status for creating - private List createStacksList = new LinkedList<>(); - - // List to keep track on stack instances for deleting - private List deleteStacksList = new LinkedList<>(); - - // List to keep track on stack instances for update - private List updateStacksList = new LinkedList<>(); - } 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 9c4438c..9c6b8a8 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 @@ -1,20 +1,14 @@ package software.amazon.cloudformation.stackset; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.NoArgsConstructor; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -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.cloudformation.exceptions.CfnInvalidRequestException; 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.InstancesAnalyzer; -import software.amazon.cloudformation.stackset.util.PhysicalIdGenerator; +import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; import software.amazon.cloudformation.stackset.util.Validator; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackSetRequest; @@ -24,67 +18,45 @@ public class CreateHandler extends BaseHandlerStd { private Logger logger; protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { this.logger = logger; final ResourceModel model = request.getDesiredResourceState(); - final String stackSetName = PhysicalIdGenerator.generatePhysicalId(request); - analyzeTemplate(proxy, model, callbackContext); + // Ensure the idempotency of StackSet, we should not generate a random StackSetName + final String stackSetName = request.getLogicalResourceIdentifier(); + final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); + analyzeTemplate(proxy, model, placeHolder); return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext) - .request(resourceModel -> - createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken())) - .retry(MULTIPLE_OF) - .call((modelRequest, proxyInvocation) -> createResource(modelRequest, proxyClient, model)) + .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken())) + .makeServiceCall((modelRequest, proxyInvocation) -> { + final CreateStackSetResponse response = proxyClient.injectCredentialsAndInvokeV2(modelRequest, proxyClient.client()::createStackSet); + model.setStackSetId(response.stackSetId()); + logger.log(String.format("%s [%s] StackSet creation succeeded", ResourceModel.TYPE_NAME, model.getStackSetId())); + return response; + }) .progress() - .then(progress -> createStackInstances(proxy, proxyClient, progress, logger)) - .then(progress -> { - if (progress.isFailed()) return progress; - return ProgressEvent.defaultSuccessHandler(model); - }); - } - - /** - * 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 { - response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createStackSet); - model.setStackSetId(response.stackSetId()); - - } catch (final InsufficientCapabilitiesException e) { - throw new CfnInvalidRequestException(e); - } - - logger.log(String.format("%s [%s] StackSet creation succeeded", ResourceModel.TYPE_NAME, model.getStackSetId())); - return response; + .then(progress -> createStackInstances(proxy, proxyClient, progress, placeHolder.getCreateStackInstances(), logger)) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } /** * Analyzes/validates template and StackInstancesGroup - * @param proxy {@link AmazonWebServicesClientProxy} - * @param model {@link ResourceModel} - * @param context {@link CallbackContext} + * + * @param proxy {@link AmazonWebServicesClientProxy} + * @param model {@link ResourceModel} + * @param placeHolder {@link StackInstancesPlaceHolder} */ private void analyzeTemplate( final AmazonWebServicesClientProxy proxy, final ResourceModel model, - final CallbackContext context) { + final StackInstancesPlaceHolder placeHolder) { new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); - InstancesAnalyzer.builder().desiredModel(model).build().analyzeForCreate(context); + InstancesAnalyzer.builder().desiredModel(model).build().analyzeForCreate(placeHolder); } } 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 8b3fd5b..0871e14 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 @@ -2,16 +2,13 @@ import software.amazon.awssdk.services.cloudformation.CloudFormationClient; 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.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 java.util.ArrayList; -import java.util.function.Function; +import software.amazon.cloudformation.stackset.util.InstancesAnalyzer; +import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackSetRequest; @@ -28,16 +25,13 @@ protected ProgressEvent handleRequest( this.logger = logger; final ResourceModel model = request.getDesiredResourceState(); - // Add all stack instances into delete list - callbackContext.setDeleteStacksList(new ArrayList<>(model.getStackInstancesGroup())); + // Analyzes stack instances group for delete + final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); + InstancesAnalyzer.builder().desiredModel(model).build().analyzeForDelete(placeHolder); - return proxy.initiate("AWS-CloudFormation-StackSet::Delete", proxyClient, model, callbackContext) - .request(Function.identity()) - .retry(MULTIPLE_OF) - .call(EMPTY_CALL) - .progress() + return ProgressEvent.progress(model, callbackContext) // delete/stabilize progress chain - delete all associated stack instances - .then(progress -> deleteStackInstances(proxy, proxyClient, progress, logger)) + .then(progress -> deleteStackInstances(proxy, proxyClient, progress, placeHolder.getDeleteStackInstances(), logger)) .then(progress -> deleteStackSet(proxy, proxyClient, progress)); } @@ -59,16 +53,13 @@ protected ProgressEvent deleteStackSet( final CallbackContext callbackContext = progress.getCallbackContext(); return proxy.initiate("AWS-CloudFormation-StackSet::DeleteStackSet", client, model, callbackContext) - .request(modelRequest -> deleteStackSetRequest(modelRequest.getStackSetId())) - .call((modelRequest, proxyInvocation) -> deleteStackSet(model.getStackSetId(), proxyInvocation)) + .translateToServiceRequest(modelRequest -> deleteStackSetRequest(modelRequest.getStackSetId())) + .makeServiceCall((modelRequest, proxyInvocation) -> { + final DeleteStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2( + deleteStackSetRequest(model.getStackSetId()), proxyInvocation.client()::deleteStackSet); + logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME)); + return response; + }) .success(); } - - 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/UpdateHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java index 1cdaf79..17bf767 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 @@ -1,15 +1,18 @@ package software.amazon.cloudformation.stackset; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; 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.InstancesAnalyzer; +import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; import software.amazon.cloudformation.stackset.util.Validator; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest; +import static software.amazon.cloudformation.stackset.util.Comparator.isStackSetConfigEquals; public class UpdateHandler extends BaseHandlerStd { @@ -26,59 +29,67 @@ protected ProgressEvent handleRequest( final ResourceModel model = request.getDesiredResourceState(); final ResourceModel previousModel = request.getPreviousResourceState(); - analyzeTemplate(proxy, previousModel, model, callbackContext); + final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); + analyzeTemplate(proxy, previousModel, placeHolder, model); - 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)) - .then(progress -> { - if (progress.isFailed()) return progress; - return ProgressEvent.defaultSuccessHandler(model); - }); + return ProgressEvent.progress(model, callbackContext) + .then(progress -> updateStackSet(proxy, proxyClient, progress, previousModel)) + .then(progress -> deleteStackInstances(proxy, proxyClient, progress, placeHolder.getDeleteStackInstances(), logger)) + .then(progress -> createStackInstances(proxy, proxyClient, progress, placeHolder.getCreateStackInstances(), logger)) + .then(progress -> updateStackInstances(proxy, proxyClient, progress, placeHolder.getUpdateStackInstances(), logger)) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } /** * 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} + * @param proxy {@link AmazonWebServicesClientProxy} to initiate proxy chain + * @param client the aws service client {@link ProxyClient} to make the call + * @param progress {@link ProgressEvent} to place hold the current progress data + * @param previousModel previous {@link ResourceModel} for comparing with desired model * @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) { + final ProgressEvent progress, + final ResourceModel previousModel) { - 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) + final ResourceModel desiredModel = progress.getResourceModel(); + final CallbackContext callbackContext = progress.getCallbackContext(); + + if (isStackSetConfigEquals(previousModel, desiredModel)) { + return ProgressEvent.progress(desiredModel, callbackContext); + } + return proxy.initiate("AWS-CloudFormation-StackSet::UpdateStackSet", client, desiredModel, callbackContext) + .translateToServiceRequest(modelRequest -> updateStackSetRequest(modelRequest)) + .makeServiceCall((modelRequest, proxyInvocation) -> { + UpdateStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackSet); + logger.log(String.format("%s UpdateStackSet initiated", ResourceModel.TYPE_NAME)); + return response; + }) + .stabilize((request, response, proxyInvocation, resourceModel, context) -> isOperationStabilized(proxyInvocation, resourceModel, response.operationId(), logger)) + .retryErrorFilter(this::filterException) .progress(); } /** * Analyzes/validates template and StackInstancesGroup - * @param proxy {@link AmazonWebServicesClientProxy} + * + * @param proxy {@link AmazonWebServicesClientProxy} * @param previousModel previous {@link ResourceModel} - * @param model {@link ResourceModel} - * @param context {@link CallbackContext} + * @param placeHolder {@link StackInstancesPlaceHolder} + * @param model {@link ResourceModel} */ private void analyzeTemplate( final AmazonWebServicesClientProxy proxy, final ResourceModel previousModel, - final ResourceModel model, - final CallbackContext context) { + final StackInstancesPlaceHolder placeHolder, + final ResourceModel model) { new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); - InstancesAnalyzer.builder().desiredModel(model).previousModel(previousModel).build().analyzeForUpdate(context); + InstancesAnalyzer.builder().desiredModel(model).previousModel(previousModel).build() + .analyzeForUpdate(placeHolder); } } 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 bbde220..d596fb3 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 @@ -19,6 +19,7 @@ public class PropertyTranslator { /** * Converts AutoDeployment (from StackSet SDK) to AutoDeployment (from CFN resource model) + * * @param autoDeployment SDK AutoDeployment * @return Resource model AutoDeployment */ @@ -33,6 +34,7 @@ public static software.amazon.cloudformation.stackset.AutoDeployment translateFr /** * Converts AutoDeployment (from CFN resource model) to AutoDeployment (from StackSet SDK) + * * @param autoDeployment AutoDeployment from resource model * @return SDK AutoDeployment */ @@ -47,6 +49,7 @@ public static AutoDeployment translateToSdkAutoDeployment( /** * Converts resource model DeploymentTargets to StackSet SDK DeploymentTargets + * * @param deploymentTargets DeploymentTargets from resource model * @return SDK DeploymentTargets */ @@ -60,6 +63,7 @@ static DeploymentTargets translateToSdkDeploymentTargets( /** * Converts resource model Parameters to StackSet SDK Parameters + * * @param parameters Parameters collection from resource model * @return SDK Parameter list */ @@ -76,6 +80,7 @@ static List translateToSdkParameters( /** * Converts resource model Parameters to StackSet SDK Parameters + * * @param parameters Parameters from SDK * @return resource model Parameters */ @@ -92,6 +97,7 @@ public static Set translateFr /** * Converts resource model OperationPreferences to StackSet SDK OperationPreferences + * * @param operationPreferences OperationPreferences from resource model * @return SDK OperationPreferences */ @@ -110,6 +116,7 @@ static StackSetOperationPreferences translateToSdkOperationPreferences( /** * Converts tags (from CFN resource model) to StackSet set (from StackSet SDK) + * * @param tags Tags CFN resource model. * @return SDK Tags. */ @@ -124,6 +131,7 @@ static Collection translateToSdkTags(final Collection translateFromSdkT /** * Converts {@link StackInstanceSummary} to {@link StackInstance} utility placeholder + * * @param isSelfManaged if PermissionModel is SELF_MANAGED - * @param summary {@link StackInstanceSummary} + * @param summary {@link StackInstanceSummary} * @return {@link StackInstance} */ public static StackInstance translateToStackInstance( 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 ef7f870..9d0389c 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 @@ -20,7 +20,7 @@ /** * Utility class to extract AWS Credentials Provider from {@link AmazonWebServicesClientProxy}. - * + *

* Because {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2(AwsRequest, Function)} doesn't extend * {@link ResponseInputStream}, but S3 GetObject requires AWS Credentials Provider to authenticate user, * we have to mimic dummy aws request, aws response and a function as input parameters to @@ -81,6 +81,19 @@ public boolean equalsBySdkFields(Object obj) { return true; } + public interface Builder + extends AwsRequest.Builder, SdkPojo, CopyableBuilder { + @Override + GetAwsCredentialsRequest.Builder overrideConfiguration( + AwsRequestOverrideConfiguration awsRequestOverrideConfiguration + ); + + @Override + GetAwsCredentialsRequest.Builder overrideConfiguration( + Consumer builderConsumer + ); + } + static final class BuilderImpl extends AwsRequest.BuilderImpl implements GetAwsCredentialsRequest.Builder { @@ -107,25 +120,12 @@ public List> sdkFields() { return Collections.emptyList(); } } - - public interface Builder - extends AwsRequest.Builder, SdkPojo, CopyableBuilder { - @Override - GetAwsCredentialsRequest.Builder overrideConfiguration( - AwsRequestOverrideConfiguration awsRequestOverrideConfiguration - ); - - @Override - GetAwsCredentialsRequest.Builder overrideConfiguration( - Consumer builderConsumer - ); - } } /** * Inner class to mimic {@link AwsResponse} in order to obtain credentials from * {@link AmazonWebServicesClientProxy}. - * + *

* {@link AwsCredentialsProvider} is the additional parameter in this class. Other classes and functions are * implemented by following interfaces and abstract method of {@link AwsResponse}. */ @@ -142,6 +142,10 @@ private GetAwsCredentialsResponse(final GetAwsCredentialsResponse.BuilderImpl bu this.responseMetadata = builder.responseMetadata(); } + public static GetAwsCredentialsResponse.Builder builder() { + return new GetAwsCredentialsResponse.BuilderImpl(); + } + public AwsCredentialsProvider awsCredentialsProvider() { return this.awsCredentialsProvider; } @@ -170,8 +174,16 @@ public boolean equalsBySdkFields(Object obj) { } } - public static GetAwsCredentialsResponse.Builder builder() { - return new GetAwsCredentialsResponse.BuilderImpl(); + public interface Builder extends AwsResponse.Builder, SdkPojo, + CopyableBuilder { + + GetAwsCredentialsResponse build(); + + GetAwsCredentialsResponseMetadata responseMetadata(); + + GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata awsResponseMetadata); + + GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider); } static final class BuilderImpl extends AwsResponse.BuilderImpl @@ -219,18 +231,6 @@ public void setAwsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvi this.awsCredentialsProvider = awsCredentialsProvider; } } - - public interface Builder extends AwsResponse.Builder, SdkPojo, - CopyableBuilder { - - GetAwsCredentialsResponse build(); - - GetAwsCredentialsResponseMetadata responseMetadata(); - - GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata awsResponseMetadata); - - GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider); - } } /** 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 3bfc9df..60a7936 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 @@ -7,16 +7,7 @@ public class ClientBuilder { - private ClientBuilder() {} - - /** - * Get CloudFormationClient for requests to interact with StackSet client - * @return {@link CloudFormationClient} - */ - private static class LazyHolder { - public static CloudFormationClient SERVICE_CLIENT = CloudFormationClient.builder() - .httpClient(LambdaWrapper.HTTP_CLIENT) - .build(); + private ClientBuilder() { } public static CloudFormationClient getClient() { @@ -26,6 +17,7 @@ public static CloudFormationClient getClient() { /** * Gets S3 client for requests to interact with getting/validating template content * if {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL()} is passed in + * * @param awsCredentialsProvider {@link AwsCredentialsProvider} * @return {@link S3Client} */ @@ -35,4 +27,15 @@ public static S3Client getS3Client(final AwsCredentialsProvider awsCredentialsPr .httpClient(LambdaWrapper.HTTP_CLIENT) .build(); } + + /** + * Get CloudFormationClient for requests to interact with StackSet client + * + * @return {@link CloudFormationClient} + */ + private static class LazyHolder { + public static CloudFormationClient SERVICE_CLIENT = CloudFormationClient.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } } 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 012ef5e..9a93150 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,6 +1,7 @@ package software.amazon.cloudformation.stackset.util; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import software.amazon.awssdk.services.cloudformation.model.PermissionModels; import software.amazon.cloudformation.stackset.ResourceModel; @@ -11,14 +12,46 @@ */ 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.getTemplateBody(), desiredModel.getTemplateBody()) != 0) + return false; + + // If TemplateURL is specified, always call Update API, Service client will decide if it is updatable + return desiredModel.getTemplateBody() != null || desiredModel.getTemplateURL() == null; + } + /** * Compares if two collections equal in a null-safe way. + * * @param collection1 * @param collection2 * @return boolean indicates if two collections equal. */ public static boolean isEquals(final Collection collection1, final Collection collection2) { - if (collection1 == null) return collection2 == null ? true : false; + if (collection1 == null) return collection2 == null; return CollectionUtils.isEqualCollection(collection1, collection2); } 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 index cf462cc..48b058d 100644 --- 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 @@ -4,7 +4,7 @@ import lombok.Data; import lombok.NonNull; import software.amazon.awssdk.utils.CollectionUtils; -import software.amazon.cloudformation.stackset.CallbackContext; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.stackset.DeploymentTargets; import software.amazon.cloudformation.stackset.Parameter; import software.amazon.cloudformation.stackset.ResourceModel; @@ -33,63 +33,10 @@ public class InstancesAnalyzer { 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 */ @@ -99,8 +46,23 @@ public static Set aggregateStackInstances( return aggregateInstancesByRegions(groupedStacks, isSelfManaged); } + /** + * Aggregates flat {@link StackInstance} to a group of {@link StackInstances} to construct resource model + *

Note:

+ * This is being used only because currently we can not retrieve OUs from CloudFormation DescribeStackInstances API + * Hence, we are returning AccountIDs for stack instances. + * + * @param flatStackInstances {@link StackInstance} + * @return {@link StackInstances} set + */ + public static Set aggregateStackInstancesForRead(@NonNull final Set flatStackInstances) { + final Set groupedStacksInstances = groupInstancesByTargets(flatStackInstances, true); + return aggregateInstancesByRegions(groupedStacksInstances, true); + } + /** * Group regions by {@link DeploymentTargets} and {@link StackInstance#getParameters()} + * * @return {@link StackInstances} */ private static Set groupInstancesByTargets( @@ -136,6 +98,7 @@ private static Set groupInstancesByTargets( /** * Aggregates instances with similar {@link StackInstances#getRegions()} + * * @param groupedStacks {@link StackInstances} set * @return Aggregated {@link StackInstances} set */ @@ -166,7 +129,8 @@ private static Set aggregateInstancesByRegions( /** * Compares {@link StackInstance#getParameters()} with previous {@link StackInstance#getParameters()} * Gets the StackInstances need to update - * @param intersection {@link StackInstance} retaining desired stack instances + * + * @param intersection {@link StackInstance} retaining desired stack instances * @param previousStackMap Map contains previous stack instances * @return {@link StackInstance} to update */ @@ -183,6 +147,7 @@ private static Set getUpdatingStackInstances( /** * 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 */ @@ -198,13 +163,21 @@ private static Set flattenStackInstancesGroup( final Set targets = isSelfManaged ? stackInstances.getDeploymentTargets().getAccounts() : stackInstances.getDeploymentTargets().getOrganizationalUnitIds(); + if (CollectionUtils.isNullOrEmpty(targets)) { + throw new CfnInvalidRequestException( + String.format("%s should be specified in DeploymentTargets in [%s] model", + isSelfManaged ? "Accounts" : "OrganizationalUnitIds", + isSelfManaged ? "SELF_MANAGED" : "SERVICE_MANAGED")); + } + 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)); + throw new CfnInvalidRequestException( + String.format("Stack instance [%s,%s] is duplicated", target, region)); } flatStacks.add(stackInstance); @@ -213,4 +186,76 @@ private static Set flattenStackInstancesGroup( } return flatStacks; } + + /** + * Analyzes {@link StackInstances} that need to be modified during the update + */ + public void analyzeForUpdate(final StackInstancesPlaceHolder placeHolder) { + 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 + placeHolder.setCreateStackInstances(new ArrayList<>(stackInstancesGroupToAdd)); + placeHolder.setDeleteStackInstances(new ArrayList<>(stackInstancesGroupToDelete)); + placeHolder.setUpdateStackInstances(new ArrayList<>(stackInstancesGroupToUpdate)); + } + + /** + * Analyzes {@link StackInstances} that need to be modified during the update + * Updates callbackContext with the stack list to create + */ + public void analyzeForCreate(final StackInstancesPlaceHolder placeHolder) { + if (desiredModel.getStackInstancesGroup() == null) return; + if (desiredModel.getStackInstancesGroup().size() == 1) { + placeHolder.setCreateStackInstances(new ArrayList<>(desiredModel.getStackInstancesGroup())); + return; + } + final boolean isSelfManaged = isSelfManaged(desiredModel); + + final Set desiredStackInstances = + flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); + + final Set stackInstancesGroupToAdd = aggregateStackInstances(desiredStackInstances, isSelfManaged); + placeHolder.setCreateStackInstances(new ArrayList<>(stackInstancesGroupToAdd)); + } + + /** + * Analyzes {@link StackInstances} that need to be modified during the update + * Updates callbackContext with the stack list to delete + */ + public void analyzeForDelete(final StackInstancesPlaceHolder placeHolder) { + if (desiredModel.getStackInstancesGroup() == null) return; + if (desiredModel.getStackInstancesGroup().size() == 1) { + placeHolder.setDeleteStackInstances(new ArrayList<>(desiredModel.getStackInstancesGroup())); + return; + } + final boolean isSelfManaged = isSelfManaged(desiredModel); + + final Set desiredStackInstances = + flattenStackInstancesGroup(desiredModel.getStackInstancesGroup(), isSelfManaged); + + final Set stackInstancesGroupToDelete = aggregateStackInstances(desiredStackInstances, isSelfManaged); + placeHolder.setDeleteStackInstances(new ArrayList<>(stackInstancesGroupToDelete)); + } } diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java index 118fffe..7a66a21 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java @@ -3,7 +3,7 @@ /** * Custom Exception Class to hold exception when parsing templates */ -public class ParseException extends RuntimeException{ +public class ParseException extends RuntimeException { private static final long serialVersionUID = 1L; public ParseException(final String message) { 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 deleted file mode 100644 index 53d1d08..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/PhysicalIdGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.resource.IdentifierUtils; -import software.amazon.cloudformation.stackset.ResourceModel; - -/** - * Utility class to generate Physical Resource Id from {@link ResourceHandlerRequest< ResourceModel >}. - */ -public class PhysicalIdGenerator { - - private static int MAX_LENGTH_CALLER_REFERENCE = 128; - - /** - * Generates a physical Id for creating a new resource. - * @param request CloudFormation's requested resource state. - * @return Physical ID. - */ - public static String generatePhysicalId(final ResourceHandlerRequest request) { - return IdentifierUtils.generateResourceIdentifier( - request.getLogicalResourceIdentifier(), - request.getClientRequestToken(), - MAX_LENGTH_CALLER_REFERENCE); - } -} 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 8583df0..bef3c80 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 @@ -23,7 +23,7 @@ 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; +import static software.amazon.cloudformation.stackset.util.InstancesAnalyzer.aggregateStackInstancesForRead; /** * Utility class to construct {@link ResourceModel} for Read/List request based on {@link StackSet} @@ -39,6 +39,7 @@ public class ResourceModelBuilder { /** * Returns the model we construct from StackSet service client using PrimaryIdentifier StackSetId + * * @return {@link ResourceModel} */ public ResourceModel buildModel() { @@ -51,7 +52,7 @@ public ResourceModel buildModel() { .stackSetId(stackSetId) .description(stackSet.description()) .permissionModel(stackSet.permissionModelAsString()) - .capabilities(new HashSet<>(stackSet.capabilitiesAsStrings())) + .capabilities(stackSet.hasCapabilities() ? new HashSet<>(stackSet.capabilitiesAsStrings()) : null) .tags(translateFromSdkTags(stackSet.tags())) .parameters(translateFromSdkParameters(stackSet.parameters())) .templateBody(stackSet.templateBody()) @@ -73,7 +74,7 @@ public ResourceModel buildModel() { } while (token != null); if (!stackInstanceSet.isEmpty()) { - final Set stackInstancesGroup = aggregateStackInstances(stackInstanceSet, isSelfManaged); + final Set stackInstancesGroup = aggregateStackInstancesForRead(stackInstanceSet); model.setStackInstancesGroup(stackInstancesGroup); } @@ -82,9 +83,10 @@ public ResourceModel buildModel() { /** * Loop through all stack instance details and attach to the constructing model - * @param stackSetId {@link ResourceModel#getStackSetId()} + * + * @param stackSetId {@link ResourceModel#getStackSetId()} * @param isSelfManaged if permission model is SELF_MANAGED - * @param token {@link ListStackInstancesResponse#nextToken()} + * @param token {@link ListStackInstancesResponse#nextToken()} */ private void attachStackInstances( final String stackSetId, @@ -95,9 +97,10 @@ private void attachStackInstances( final ListStackInstancesResponse listStackInstancesResponse = proxyClient.injectCredentialsAndInvokeV2( listStackInstancesRequest(token, stackSetId), proxyClient.client()::listStackInstances); token = listStackInstancesResponse.nextToken(); + if (!listStackInstancesResponse.hasSummaries()) return; listStackInstancesResponse.summaries().forEach(member -> { - final List parameters = getStackInstance(member); - stackInstanceSet.add(translateToStackInstance(isSelfManaged, member, parameters)); + final List parameters = getStackInstance(member); + stackInstanceSet.add(translateToStackInstance(isSelfManaged, member, parameters)); }); } diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstancesPlaceHolder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstancesPlaceHolder.java new file mode 100644 index 0000000..1b35a0b --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/StackInstancesPlaceHolder.java @@ -0,0 +1,17 @@ +package software.amazon.cloudformation.stackset.util; + +import lombok.Data; +import software.amazon.cloudformation.stackset.StackInstances; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class StackInstancesPlaceHolder { + + private List createStackInstances = new ArrayList<>(); + + private List deleteStackInstances = new ArrayList<>(); + + private List updateStackInstances = new ArrayList<>(); +} 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 372b313..820982e 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 @@ -30,6 +30,7 @@ public class TemplateParser { /** * Deserializes template content which can be either JSON or YAML + * * @param template Template Content * @return Generic Map of template */ @@ -50,8 +51,9 @@ public static Map deserializeTemplate(final String template) { /** * Gets a Generic Map object from template + * * @param templateMap Template Map - * @param key Key of the Map we are retrieving + * @param key Key of the Map we are retrieving * @return Generic Map object */ @SuppressWarnings("unchecked") @@ -66,6 +68,7 @@ public static Map getMapFromTemplate(final Map t /** * Gets String from the passed in value + * * @param value * @return String */ @@ -79,6 +82,7 @@ public static String getStringFromTemplate(final Object value) { /** * Deserializes YAML from template content string + * * @param templateString Template content * @return Template map * @throws ParseException if fails to parse the template @@ -94,7 +98,7 @@ protected static Map deserializeYaml(final String templateString } catch (final MarkedYAMLException e) { throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "YAML", - formatErrorLocation(e.getProblemMark()))); + formatYamlErrorLocation(e.getProblemMark()))); } catch (final YAMLException e) { throw new ParseException(String.format("Cannot parse as YAML : %s ", e.getMessage())); @@ -107,6 +111,7 @@ protected static Map deserializeYaml(final String templateString /** * Deserializes JSON from template content string + * * @param templateString Template content * @return Template map * @throws ParseException if fails to parse the template @@ -121,11 +126,11 @@ protected static Map deserializeJson(final String templateString } catch (final JsonMappingException e) { throw new ParseException(String.format(UNSUPPORTED_TYPE_STRUCTURE_ERROR_MSG, - formatErrorLocation(e.getLocation()))); + formatJsonErrorLocation(e.getLocation()))); } catch (final JsonParseException e) { throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "JSON", - formatErrorLocation(e.getLocation()))); + formatJsonErrorLocation(e.getLocation()))); } catch (final IOException e) { throw new ParseException("Cannot parse template, I/O stream corrupt."); @@ -144,20 +149,24 @@ private static boolean isPossiblyJson(final String template) { /** * Gets the error location when parsing as JSON + * * @param loc {@link JsonLocation} * @return Error location */ - private static String formatErrorLocation(final JsonLocation loc) { + @VisibleForTesting + protected static String formatJsonErrorLocation(final JsonLocation loc) { if (loc == null) return UNKNOWN_LOCATION; return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLineNr(), loc.getColumnNr()); } /** * Gets the error location when parsing as YAML + * * @param loc {@link Mark} * @return Error location */ - private static String formatErrorLocation(final Mark loc) { + @VisibleForTesting + protected static String formatYamlErrorLocation(final Mark loc) { if (loc == null) return UNKNOWN_LOCATION; return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLine() + 1, loc.getColumn() + 1); } diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java index 41d35e5..0ee41a9 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -27,9 +27,60 @@ public class Validator { private static final String TEMPLATE_RESOURCES_KEY = "Resources"; private static final String TEMPLATE_PARAMETERS_KEY = "Parameters"; + /** + * Validates the template to make sure: + *
    + *
  • Template can be deserialized successfully + *
  • Resources block doesn't have embedded Stack or StackSet + *
  • Parameters block doesn't have embedded Stack or StackSet + *
+ * + * @param content Template content + */ + private static void validateTemplate(final String content) { + final Map template = deserializeTemplate(content); + validateBlocks(template, TEMPLATE_RESOURCES_KEY); + validateBlocks(template, TEMPLATE_PARAMETERS_KEY); + } + + /** + * Validates items in the block do not have any invalid resources + * + * @param templateMap Templates map + * @param block Block key, i.e. Resources + */ + @SuppressWarnings("unchecked") + private static void validateBlocks(final Map templateMap, final String block) { + final Map resourcesMap = (Map) templateMap.get(block); + + if (CollectionUtils.isNullOrEmpty(resourcesMap)) return; + for (final Map.Entry entry : resourcesMap.entrySet()) { + final String resourceId = entry.getKey(); + final Map resourceMap = getMapFromTemplate(resourcesMap, resourceId); + validateResource(resourceMap); + } + } + + /** + * Embedded Stack or StackSet is not allowed + * + * @param resourceMap Resource map + */ + private static void validateResource(final Map resourceMap) { + final String type = getStringFromTemplate(resourceMap.get(TEMPLATE_RESOURCE_TYPE_KEY)); + if (type != null) { + switch (type) { + case "AWS::CloudFormation::Stack": + case "AWS::CloudFormation::StackSet": + throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); + } + } + } + /** * Gets template content from s3 bucket - * @param proxy {@link AmazonWebServicesClientProxy} + * + * @param proxy {@link AmazonWebServicesClientProxy} * @param templateLocation Template URL * @return Template content from S3 object */ @@ -55,6 +106,7 @@ protected String getUrlContent(final AmazonWebServicesClientProxy proxy, final S /** * Validates template url is valid S3 URL + * * @param s3Uri Template URL */ @VisibleForTesting @@ -76,10 +128,11 @@ protected void validateS3Uri(final String s3Uri) { *
  • If using S3 URI, it must be valid *
  • Template contents must be valid * - * @param proxy {@link AmazonWebServicesClientProxy} - * @param templateBody {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateBody} + * + * @param proxy {@link AmazonWebServicesClientProxy} + * @param templateBody {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateBody} * @param templateLocation {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL} - * @param logger {@link Logger} + * @param logger {@link Logger} * @throws CfnInvalidRequestException if template is not valid */ public void validateTemplate( @@ -88,7 +141,7 @@ public void validateTemplate( final String templateLocation, final Logger logger) { - if (!(Strings.isNullOrEmpty(templateBody) ^ Strings.isNullOrEmpty(templateLocation))) { + if (Strings.isNullOrEmpty(templateBody) == Strings.isNullOrEmpty(templateLocation)) { throw new CfnInvalidRequestException("Exactly one of TemplateBody or TemplateUrl must be specified"); } String content = null; @@ -107,51 +160,4 @@ public void validateTemplate( } } - /** - * Validates the template to make sure: - *
      - *
    • Template can be deserialized successfully - *
    • Resources block doesn't have embedded Stack or StackSet - *
    • Parameters block doesn't have embedded Stack or StackSet - *
    - * @param content Template content - */ - private static void validateTemplate(final String content) { - final Map template = deserializeTemplate(content); - validateBlocks(template, TEMPLATE_RESOURCES_KEY); - validateBlocks(template, TEMPLATE_PARAMETERS_KEY); - } - - /** - * Validates items in the block do not have any invalid resources - * @param templateMap Templates map - * @param block Block key, i.e. Resources - */ - @SuppressWarnings("unchecked") - private static void validateBlocks(final Map templateMap, final String block) { - final Map resourcesMap = (Map) templateMap.get(block); - - if (CollectionUtils.isNullOrEmpty(resourcesMap)) return; - for (final Map.Entry entry : resourcesMap.entrySet()) { - final String resourceId = entry.getKey(); - final Map resourceMap = getMapFromTemplate(resourcesMap, resourceId); - validateResource(resourceMap); - } - } - - /** - * Embedded Stack or StackSet is not allowed - * @param resourceMap Resource map - */ - private static void validateResource(final Map resourceMap) { - final String type = getStringFromTemplate(resourceMap.get(TEMPLATE_RESOURCE_TYPE_KEY)); - if (type != null) { - switch (type) { - case "AWS::CloudFormation::Stack": - case "AWS::CloudFormation::StackSet": - throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); - } - } - } - } 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 index 2787c26..6a5bb5c 100644 --- 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 @@ -2,6 +2,7 @@ import software.amazon.awssdk.awscore.AwsRequest; import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Credentials; @@ -33,11 +34,17 @@ static ProxyClient MOCK_PROXY( @Override public CompletableFuture - injectCredentialsAndInvokeV2Aync( + injectCredentialsAndInvokeV2Async( RequestT request, Function> requestFunction) { throw new UnsupportedOperationException(); } + @Override + public > IterableT + injectCredentialsAndInvokeIterableV2(RequestT requestT, Function function) { + throw new UnsupportedOperationException(); + } + @Override public CloudFormationClient client() { return sdkClient; diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java deleted file mode 100644 index 7dae7bd..0000000 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/BaseHandlerStdTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package software.amazon.cloudformation.stackset; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.cloudformation.stackset.translator.PropertyTranslator.translateFromSdkTags; - -public class BaseHandlerStdTest { - @Test - public void test_translateFromSdkTags_IfIsNull() { - assertThat(translateFromSdkTags(null)).isNull(); - } -} 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 8cacaa8..3fd849a 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 @@ -8,9 +8,9 @@ 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.DescribeStackInstanceRequest; 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.ListStackInstancesRequest; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -18,47 +18,56 @@ 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.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static software.amazon.cloudformation.proxy.HandlerErrorCode.InvalidRequest; +import static org.mockito.Mockito.when; +import static software.amazon.cloudformation.proxy.HandlerErrorCode.InternalFailure; 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.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_EMPTY_RESPONSE; +import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_ONE_INSTANCES_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_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.SELF_MANAGED_DUPLICATE_INSTANCES_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_INVALID_INSTANCES_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.SELF_MANAGED_MODEL_NO_INSTANCES_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_ONE_INSTANCES_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_NO_INSTANCES_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL_FOR_READ; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { + @Mock + CloudFormationClient sdkClient; private CreateHandler handler; - private ResourceHandlerRequest request; - @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - @Mock - CloudFormationClient sdkClient; - @BeforeEach public void setup() { proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); @@ -76,12 +85,21 @@ public void handleRequest_ServiceManagedSS_SimpleSuccess() { .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)); + when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) + .thenReturn(CREATE_STACK_SET_RESPONSE); + when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) + .thenReturn(CREATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SERVICE_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SERVICE_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -90,9 +108,16 @@ public void handleRequest_ServiceManagedSS_SimpleSuccess() { assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModel()).isEqualTo(SERVICE_MANAGED_MODEL_FOR_READ); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); + verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); + verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } @Test @@ -104,12 +129,21 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { .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)); + when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) + .thenReturn(CREATE_STACK_SET_RESPONSE); + when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) + .thenReturn(CREATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -118,26 +152,33 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_FOR_READ); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); + verify(proxyClient.client(), times(2)).createStackInstances(any(CreateStackInstancesRequest.class)); + verify(proxyClient.client(), times(2)).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } @Test - public void handleRequest_SelfManagedSS_NotStabilized() { + public void handleRequest_SelfManagedSS_NoInstances_SimpleSuccess() { request = ResourceHandlerRequest.builder() - .desiredResourceState(SELF_MANAGED_MODEL) + .desiredResourceState(SELF_MANAGED_NO_INSTANCES_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)); + when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) + .thenReturn(CREATE_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_EMPTY_RESPONSE); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -146,30 +187,55 @@ public void handleRequest_SelfManagedSS_NotStabilized() { assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_NO_INSTANCES_FOR_READ); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); } @Test - public void handlerRequest_InsufficientCapabilitiesException() { + public void handleRequest_SelfManagedSS_OneInstances_SimpleSuccess() { request = ResourceHandlerRequest.builder() - .desiredResourceState(SELF_MANAGED_MODEL) + .desiredResourceState(SELF_MANAGED_ONE_INSTANCES_MODEL) .logicalResourceIdentifier(LOGICAL_ID) .clientRequestToken(REQUEST_TOKEN) .build(); - doThrow(InsufficientCapabilitiesException.class).when(proxyClient.client()) - .createStackSet(any(CreateStackSetRequest.class)); + when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) + .thenReturn(CREATE_STACK_SET_RESPONSE); + when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) + .thenReturn(CREATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_ONE_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_ONE_INSTANCES_FOR_READ); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); + verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); + verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(2)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } @Test @@ -181,12 +247,12 @@ public void handlerRequest_OperationStoppedError() { .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_STOPPED_RESPONSE).when(proxyClient.client()) - .describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) + .thenReturn(CREATE_STACK_SET_RESPONSE); + when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) + .thenReturn(CREATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_STOPPED_RESPONSE); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -194,6 +260,36 @@ public void handlerRequest_OperationStoppedError() { assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getErrorCode()).isEqualTo(InvalidRequest); + assertThat(response.getErrorCode()).isEqualTo(InternalFailure); + + verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); + verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); + verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + } + + @Test + public void handlerRequest_CfnInvalidRequestException_DuplicateStackInstance() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_DUPLICATE_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + assertThrows(CfnInvalidRequestException.class, + () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + } + + @Test + public void handlerRequest_CfnInvalidRequestException_InvalidDeploymentTargets() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_INVALID_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + assertThrows(CfnInvalidRequestException.class, + () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); } } 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 e1b8f4a..97675de 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 @@ -6,14 +6,9 @@ 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; @@ -24,35 +19,31 @@ 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 software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_INSTANCES_RESPONSE; -import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static software.amazon.cloudformation.stackset.util.TestUtils.DELETE_STACK_INSTANCES_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.REQUEST_TOKEN; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_NO_INSTANCES_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_NO_INSTANCES_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; @ExtendWith(MockitoExtension.class) public class DeleteHandlerTest extends AbstractTestBase { + @Mock + CloudFormationClient sdkClient; private DeleteHandler handler; - private ResourceHandlerRequest request; - @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - @Mock - CloudFormationClient sdkClient; - @BeforeEach public void setup() { handler = new DeleteHandler(); @@ -68,13 +59,12 @@ public void setup() { @Test public void handleRequest_SimpleSuccess() { - - 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)); + when(proxyClient.client().deleteStackInstances(any(DeleteStackInstancesRequest.class))) + .thenReturn(DELETE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().deleteStackSet(any(DeleteStackSetRequest.class))) + .thenReturn(DELETE_STACK_SET_RESPONSE); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -86,5 +76,64 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).deleteStackInstances(any(DeleteStackInstancesRequest.class)); + verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).deleteStackSet(any(DeleteStackSetRequest.class)); + } + + @Test + public void handleRequest_SelfManagedSS_NoInstances_SimpleSuccess() { + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_NO_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + when(proxyClient.client().deleteStackSet(any(DeleteStackSetRequest.class))) + .thenReturn(DELETE_STACK_SET_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_NO_INSTANCES_FOR_READ); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).deleteStackSet(any(DeleteStackSetRequest.class)); + } + + @Test + public void handleRequest_SelfManagedSS_OneInstances_SimpleSuccess() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_ONE_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + when(proxyClient.client().deleteStackInstances(any(DeleteStackInstancesRequest.class))) + .thenReturn(DELETE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().deleteStackSet(any(DeleteStackSetRequest.class))) + .thenReturn(DELETE_STACK_SET_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).deleteStackInstances(any(DeleteStackInstancesRequest.class)); + verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).deleteStackSet(any(DeleteStackSetRequest.class)); } } 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 635d20b..195eaa6 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,22 +1,20 @@ package software.amazon.cloudformation.stackset; +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.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; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import java.time.Duration; @@ -24,36 +22,28 @@ 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 extends AbstractTestBase { + @Mock + CloudFormationClient sdkClient; private ListHandler handler; - private ResourceHandlerRequest request; - @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - @Mock - CloudFormationClient sdkClient; - @BeforeEach public void setup() { handler = new ListHandler(); 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 23efedb..c1efb21 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,7 +1,10 @@ package software.amazon.cloudformation.stackset; -import java.time.Duration; -import software.amazon.awssdk.core.SdkClient; +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.DescribeStackInstanceRequest; import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetRequest; @@ -11,45 +14,36 @@ 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 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.times; +import static org.mockito.Mockito.verify; +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.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 extends AbstractTestBase { + @Mock + CloudFormationClient sdkClient; private ReadHandler handler; - private ResourceHandlerRequest request; - @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - @Mock - CloudFormationClient sdkClient; - @BeforeEach public void setup() { handler = new ReadHandler(); @@ -64,15 +58,15 @@ public void setup() { @Test public void handleRequest_SelfManagedSS_Success() { - 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)); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -85,5 +79,9 @@ public void handleRequest_SelfManagedSS_Success() { assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } } 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 241cbfa..a14a431 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,12 +1,17 @@ package software.amazon.cloudformation.stackset; -import java.time.Duration; -import software.amazon.awssdk.core.SdkClient; +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.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.UpdateStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -14,25 +19,27 @@ 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 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.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; 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.LOGICAL_ID; +import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_SELF_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.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.SERVICE_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_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; @@ -40,19 +47,15 @@ @ExtendWith(MockitoExtension.class) public class UpdateHandlerTest extends AbstractTestBase { + @Mock + CloudFormationClient sdkClient; private UpdateHandler handler; - private ResourceHandlerRequest request; - @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - @Mock - CloudFormationClient sdkClient; - @BeforeEach public void setup() { proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); @@ -69,16 +72,64 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { .previousResourceState(SELF_MANAGED_MODEL) .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)); + when(proxyClient.client().updateStackSet(any(UpdateStackSetRequest.class))) + .thenReturn(UPDATE_STACK_SET_RESPONSE); + when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) + .thenReturn(CREATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().deleteStackInstances(any(DeleteStackInstancesRequest.class))) + .thenReturn(DELETE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().updateStackInstances(any(UpdateStackInstancesRequest.class))) + .thenReturn(UPDATE_STACK_INSTANCES_RESPONSE); + when(proxyClient.client().describeStackSetOperation(any(DescribeStackSetOperationRequest.class))) + .thenReturn(OPERATION_SUCCEED_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); + + 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(SELF_MANAGED_MODEL_FOR_READ); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).updateStackSet(any(UpdateStackSetRequest.class)); + verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); + verify(proxyClient.client()).updateStackInstances(any(UpdateStackInstancesRequest.class)); + verify(proxyClient.client()).deleteStackInstances(any(DeleteStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); + } + + @Test + public void handleRequest_NotUpdatable_Success() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SIMPLE_MODEL) + .previousResourceState(SIMPLE_MODEL) + .build(); + + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -87,9 +138,14 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_FOR_READ); + assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } } 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 5a2bf2e..a8632ef 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,13 +1,67 @@ package software.amazon.cloudformation.stackset.util; import org.junit.jupiter.api.Test; +import software.amazon.cloudformation.stackset.ResourceModel; import static org.assertj.core.api.Assertions.assertThat; 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.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.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 testIsEquals() { assertThat(isEquals(null, TAGS)).isFalse(); diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java index 2d88b12..3623f99 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java @@ -4,12 +4,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static software.amazon.cloudformation.stackset.util.TemplateParser.formatJsonErrorLocation; +import static software.amazon.cloudformation.stackset.util.TemplateParser.formatYamlErrorLocation; import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_MAP; public class TemplateParserTest { + private static final String UNKNOWN_LOCATION = "unknown location"; + @Test public void testDeserializeYaml() { assertThrows(ParseException.class, () -> TemplateParser.deserializeYaml("null")); @@ -33,4 +37,10 @@ public void testGetStringFromTemplate() { assertThat(getStringFromTemplate(null)).isNull(); assertThrows(ParseException.class, () -> getStringFromTemplate(TEMPLATE_MAP)); } + + @Test + public void testFormatErrorLocation_IfIsNull() { + assertThat(formatYamlErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); + assertThat(formatJsonErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); + } } 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 d1f583a..4fc52a7 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 @@ -29,8 +29,10 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; public class TestUtils { @@ -49,7 +51,7 @@ public class TestUtils { public final static String TEMPLATE_BODY = new StringBuilder() .append("{\n") - .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n" ) + .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n") .append(" \"Resources\" : {\n") .append(" \"IntegrationTestWaitHandle\" : {\n") .append(" \"Type\" : \"AWS::CloudFormation::WaitConditionHandle\",\n") @@ -61,7 +63,7 @@ public class TestUtils { public final static String UPDATED_TEMPLATE_BODY = new StringBuilder() .append("{\n") - .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n" ) + .append(" \"AWSTemplateFormatVersion\" : \"2010-09-09\",\n") .append(" \"Resources\" : {\n") .append(" \"IntegrationTestWaitHandle\" : {\n") .append(" \"Type\" : \"AWS::CloudFormation::WaitCondition\",\n") @@ -73,53 +75,53 @@ public class TestUtils { public final static String VALID_YAML_TEMPLATE = "Parameters:\n" + - " DomainName:\n" + - " Type: String\n" + - " Default: myexample.com\n" + - "Resources:\n" + - " BasicHealthCheck:\n" + - " Type: AWS::Route53::HealthCheck\n" + - " Properties:\n" + - " HealthCheckConfig:\n" + - " RequestInterval: 10\n" + - " FullyQualifiedDomainName:\n" + - " Ref: DomainName\n" + - " IPAddress: 98.139.180.149\n" + - " Port: \"88\"\n" + - " ResourcePath: /docs/route-53-health-check.html\n" + - " Type: HTTP\n" + - " HealthCheckTags:\n" + - " - Key: A\n" + - " Value: \"1\"\n" + - " - Key: B\n" + - " Value: \"1\"\n" + - " - Key: C\n" + - " Value: \"1\""; + " DomainName:\n" + + " Type: String\n" + + " Default: myexample.com\n" + + "Resources:\n" + + " BasicHealthCheck:\n" + + " Type: AWS::Route53::HealthCheck\n" + + " Properties:\n" + + " HealthCheckConfig:\n" + + " RequestInterval: 10\n" + + " FullyQualifiedDomainName:\n" + + " Ref: DomainName\n" + + " IPAddress: 98.139.180.149\n" + + " Port: \"88\"\n" + + " ResourcePath: /docs/route-53-health-check.html\n" + + " Type: HTTP\n" + + " HealthCheckTags:\n" + + " - Key: A\n" + + " Value: \"1\"\n" + + " - Key: B\n" + + " Value: \"1\"\n" + + " - Key: C\n" + + " Value: \"1\""; public final static String INVALID_EMBEDDED_STACK_TEMPLATE = "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " },\n" + - " }\n" + - "}"; + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " },\n" + + " }\n" + + "}"; public final static String INVALID_EMBEDDED_STACKSET_TEMPLATE = "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; public final static String STACK_SET_NAME = "StackSet"; public final static String STACK_SET_ID = "StackSet:stack-set-id"; @@ -271,22 +273,26 @@ public class TestUtils { public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_1 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .account(ACCOUNT_ID_1) .region(US_EAST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_2 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) + .account(ACCOUNT_ID_1) .region(US_WEST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_3 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_2) - .region(US_EAST_1) + .account(ACCOUNT_ID_2) + .region(EU_EAST_1) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_4 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_2) - .region(US_WEST_1) + .account(ACCOUNT_ID_2) + .region(EU_EAST_2) .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_5 = StackInstanceSummary.builder() @@ -312,7 +318,6 @@ public class TestUtils { 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 = @@ -323,7 +328,6 @@ public class TestUtils { 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 = @@ -354,29 +358,31 @@ public class TestUtils { 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); - public final static List SERVICE_SELF_STACK_INSTANCE_SUMMARIES = Arrays.asList( + public final static List SELF_MANAGED_STACK_INSTANCE_SUMMARIES = Arrays.asList( STACK_INSTANCE_SUMMARY_5, STACK_INSTANCE_SUMMARY_6, STACK_INSTANCE_SUMMARY_7, STACK_INSTANCE_SUMMARY_8); + public final static List SELF_MANAGED_STACK_ONE_INSTANCES_SUMMARIES = Arrays.asList( + STACK_INSTANCE_SUMMARY_5, STACK_INSTANCE_SUMMARY_6); + public final static software.amazon.awssdk.services.cloudformation.model.AutoDeployment SDK_AUTO_DEPLOYMENT = software.amazon.awssdk.services.cloudformation.model.AutoDeployment.builder() .retainStacksOnAccountRemoval(true) .enabled(true) .build(); - public final static StackInstances STACK_INSTANCES_1 = StackInstances.builder() + public final static StackInstances SERVICE_MANAGED_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) + public final static StackInstances SERVICE_MANAGED_STACK_INSTANCES_2 = StackInstances.builder() + .regions(REGIONS_1) .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() @@ -387,7 +393,6 @@ public class TestUtils { 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() @@ -412,6 +417,7 @@ public class TestUtils { .organizationalUnitIds(ORGANIZATION_UNIT_ID_1, ORGANIZATION_UNIT_ID_2) .parameters(SDK_PARAMETER_1, SDK_PARAMETER_2) .permissionModel(PermissionModels.SERVICE_MANAGED) + .templateBody(TEMPLATE_BODY) .tags(TAGGED_RESOURCES) .build(); @@ -433,8 +439,9 @@ public class TestUtils { .description(DESCRIPTION) .autoDeployment(AUTO_DEPLOYMENT) .templateBody(TEMPLATE_BODY) - .stackInstancesGroup(new HashSet<>(Arrays.asList(STACK_INSTANCES_1, STACK_INSTANCES_2))) + .stackInstancesGroup(new HashSet<>(Arrays.asList(SERVICE_MANAGED_STACK_INSTANCES_1, SERVICE_MANAGED_STACK_INSTANCES_2))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .operationPreferences(OPERATION_PREFERENCES) .tags(TAGS) .build(); @@ -447,6 +454,7 @@ public class TestUtils { .stackInstancesGroup( new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_1, SELF_MANAGED_STACK_INSTANCES_2))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .operationPreferences(OPERATION_PREFERENCES) .tags(TAGS) .build(); @@ -458,6 +466,99 @@ public class TestUtils { .stackInstancesGroup( new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_3, SELF_MANAGED_STACK_INSTANCES_4))) .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_3))) + .operationPreferences(OPERATION_PREFERENCES) + .tags(TAGS) + .build(); + + public final static ResourceModel SELF_MANAGED_NO_INSTANCES_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SELF_MANAGED) + .capabilities(CAPABILITIES) + .templateBody(TEMPLATE_BODY) + .description(DESCRIPTION) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + + public final static ResourceModel SELF_MANAGED_ONE_INSTANCES_MODEL = 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_2))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + + public final static ResourceModel SELF_MANAGED_DUPLICATE_INSTANCES_MODEL = 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_2, SELF_MANAGED_STACK_INSTANCES_4))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + + public final static ResourceModel SELF_MANAGED_INVALID_INSTANCES_MODEL = 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_2, SERVICE_MANAGED_STACK_INSTANCES_1))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + + public final static StackInstances CREATE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(US_EAST_2))) + .build(); + + public final static Queue CREATE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(CREATE_STACK_INSTANCES_SELF_MANAGED)); + + public final static StackInstances DELETE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(US_EAST_1))) + .build(); + + public final static Queue DELETE_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(DELETE_STACK_INSTANCES_SELF_MANAGED)); + + public final static StackInstances UPDATE_STACK_INSTANCES_SELF_MANAGED = StackInstances.builder() + .deploymentTargets(UPDATED_SELF_MANAGED_TARGETS) + .regions(new HashSet<>(Arrays.asList(EU_EAST_1, EU_EAST_2))) + .parameterOverrides(new HashSet<>(Arrays.asList(PARAMETER_1))) + .build(); + + public final static Queue UPDATED_STACK_INSTANCES_SELF_MANAGED_FOR_UPDATE = new LinkedList<>( + Arrays.asList(UPDATE_STACK_INSTANCES_SELF_MANAGED)); + + public final static ResourceModel SELF_MANAGED_MODEL_NO_INSTANCES_FOR_READ = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SELF_MANAGED) + .capabilities(CAPABILITIES) + .templateBody(TEMPLATE_BODY) + .description(DESCRIPTION) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + + public final static ResourceModel SELF_MANAGED_MODEL_ONE_INSTANCES_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))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) .tags(TAGS) .build(); @@ -473,6 +574,19 @@ public class TestUtils { .tags(TAGS) .build(); + public final static ResourceModel SERVICE_MANAGED_MODEL_FOR_READ = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SERVICE_MANAGED) + .autoDeployment(AUTO_DEPLOYMENT) + .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(); @@ -480,9 +594,7 @@ public class TestUtils { public final static ResourceModel SIMPLE_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) .permissionModel(SELF_MANAGED) - .stackInstancesGroup( - new HashSet<>(Arrays.asList(SELF_MANAGED_STACK_INSTANCES_1, SELF_MANAGED_STACK_INSTANCES_2))) - .templateURL(TEMPLATE_URL) + .templateBody(TEMPLATE_BODY) .tags(TAGS) .operationPreferences(OPERATION_PREFERENCES) .build(); @@ -553,7 +665,16 @@ public class TestUtils { public final static ListStackInstancesResponse LIST_SELF_MANAGED_STACK_SET_RESPONSE = ListStackInstancesResponse.builder() - .summaries(SERVICE_SELF_STACK_INSTANCE_SUMMARIES) + .summaries(SELF_MANAGED_STACK_INSTANCE_SUMMARIES) + .build(); + + public final static ListStackInstancesResponse LIST_SELF_MANAGED_STACK_SET_ONE_INSTANCES_RESPONSE = + ListStackInstancesResponse.builder() + .summaries(SELF_MANAGED_STACK_ONE_INSTANCES_SUMMARIES) + .build(); + + public final static ListStackInstancesResponse LIST_SELF_MANAGED_STACK_SET_EMPTY_RESPONSE = + ListStackInstancesResponse.builder() .build(); public final static ListStackSetsResponse LIST_STACK_SETS_RESPONSE = diff --git a/aws-cloudformation-stackset/template.yml b/aws-cloudformation-stackset/template.yml index 18e9bcf..445c0bc 100644 --- a/aws-cloudformation-stackset/template.yml +++ b/aws-cloudformation-stackset/template.yml @@ -20,4 +20,3 @@ Resources: Handler: software.amazon.cloudformation.stackset.HandlerWrapper::testEntrypoint Runtime: java8 CodeUri: ./target/aws-cloudformation-stackset-handler-1.0-SNAPSHOT.jar - From 18612ce82a448c47f3133ab0c3072f35948bb003 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Mon, 11 May 2020 13:15:14 -0700 Subject: [PATCH 10/19] Delete credentials extrator and Add docs --- .../aws-cloudformation-stackset.json | 15 +- aws-cloudformation-stackset/docs/README.md | 208 +++++++++++++++ .../docs/autodeployment.md | 43 +++ .../docs/operationpreferences.md | 72 +++++ .../docs/parameters.md | 43 +++ .../stackinstancesgroup-deploymenttargets.md | 47 ++++ .../stackinstancesgroup-parameteroverrides.md | 43 +++ .../docs/stackinstancesgroup.md | 59 +++++ aws-cloudformation-stackset/docs/tags.md | 57 ++++ aws-cloudformation-stackset/pom.xml | 8 +- .../resource-role.yaml | 2 +- .../stackset/BaseHandlerStd.java | 76 +++++- .../stackset/CreateHandler.java | 21 +- .../stackset/DeleteHandler.java | 4 +- .../cloudformation/stackset/ListHandler.java | 1 + .../stackset/UpdateHandler.java | 28 +- .../util/AwsCredentialsExtractor.java | 248 ------------------ .../stackset/util/ClientBuilder.java | 5 +- .../stackset/util/Comparator.java | 11 +- .../stackset/util/InstancesAnalyzer.java | 2 + .../stackset/util/Validator.java | 37 ++- .../stackset/AbstractTestBase.java | 14 + .../stackset/ListHandlerTest.java | 31 ++- .../stackset/UpdateHandlerTest.java | 3 +- .../stackset/util/TestUtils.java | 11 + .../stackset/util/ValidatorTest.java | 37 +++ .../org.mockito.plugins.MockMaker | 1 + 27 files changed, 788 insertions(+), 339 deletions(-) create mode 100644 aws-cloudformation-stackset/docs/README.md create mode 100644 aws-cloudformation-stackset/docs/autodeployment.md create mode 100644 aws-cloudformation-stackset/docs/operationpreferences.md create mode 100644 aws-cloudformation-stackset/docs/parameters.md create mode 100644 aws-cloudformation-stackset/docs/stackinstancesgroup-deploymenttargets.md create mode 100644 aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md create mode 100644 aws-cloudformation-stackset/docs/stackinstancesgroup.md create mode 100644 aws-cloudformation-stackset/docs/tags.md delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java create mode 100644 aws-cloudformation-stackset/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 0fda484..0b25f32 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -2,6 +2,12 @@ "typeName": "AWS::CloudFormation::StackSet", "description": "Resource Type definition for AWS::CloudFormation::StackSet", "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-cloudformation.git", + "resourceLink": { + "templateUri": "/cloudformation/home?region=${awsRegion}#/stacksets/${StackSetId}", + "mappings": { + "StackSetId": "/StackSetId" + } + }, "definitions": { "Tag": { "description": "Tag type enables you to specify a key-value pair that can be used to store information about an AWS CloudFormation StackSet.", @@ -270,7 +276,8 @@ "cloudformation:CreateStackInstances", "cloudformation:DescribeStackSetOperation", "cloudformation:TagResource" - ] + ], + "timeoutInMinutes": 720 }, "read": { "permissions": [ @@ -289,7 +296,8 @@ "cloudformation:DescribeStackSetOperation", "cloudformation:TagResource", "cloudformation:UntagResource" - ] + ], + "timeoutInMinutes": 720 }, "delete": { "permissions": [ @@ -297,7 +305,8 @@ "cloudformation:DeleteStackInstances", "cloudformation:DescribeStackSetOperation", "cloudformation:UntagResource" - ] + ], + "timeoutInMinutes": 720 }, "list": { "permissions": [ diff --git a/aws-cloudformation-stackset/docs/README.md b/aws-cloudformation-stackset/docs/README.md new file mode 100644 index 0000000..3483ebf --- /dev/null +++ b/aws-cloudformation-stackset/docs/README.md @@ -0,0 +1,208 @@ +# AWS::CloudFormation::StackSet + +Resource Type definition for AWS::CloudFormation::StackSet + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "Type" : "AWS::CloudFormation::StackSet",
    +    "Properties" : {
    +        "AdministrationRoleARN" : String,
    +        "AutoDeployment" : AutoDeployment,
    +        "Capabilities" : [ String, ... ],
    +        "Description" : String,
    +        "ExecutionRoleName" : String,
    +        "OperationPreferences" : OperationPreferences,
    +        "StackInstancesGroup" : [ StackInstancesGroup, ... ],
    +        "Parameters" : [ Parameters, ... ],
    +        "PermissionModel" : String,
    +        "Tags" : [ Tags, ... ],
    +        "TemplateBody" : String,
    +        "TemplateURL" : String
    +    }
    +}
    +
    + +### YAML + +
    +Type: AWS::CloudFormation::StackSet
    +Properties:
    +    AdministrationRoleARN: String
    +    AutoDeployment: AutoDeployment
    +    Capabilities: 
    +      - String
    +    Description: String
    +    ExecutionRoleName: String
    +    OperationPreferences: OperationPreferences
    +    StackInstancesGroup: 
    +      - StackInstancesGroup
    +    Parameters: 
    +      - Parameters
    +    PermissionModel: String
    +    Tags: 
    +      - Tags
    +    TemplateBody: String
    +    TemplateURL: String
    +
    + +## Properties + +#### AdministrationRoleARN + +The Amazon Resource Number (ARN) of the IAM role to use to create this stack set. Specify an IAM role only if you are using customized administrator roles to control which users or groups can manage specific stack sets within the same administrator account. + +_Required_: No + +_Type_: String + +_Minimum_: 20 + +_Maximum_: 2048 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### AutoDeployment + +_Required_: No + +_Type_: AutoDeployment + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Capabilities + +In some cases, you must explicitly acknowledge that your stack set template contains certain capabilities in order for AWS CloudFormation to create the stack set and related stack instances. + +_Required_: No + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Description + +A description of the stack set. You can use the description to identify the stack set's purpose or other important information. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 1024 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ExecutionRoleName + +The name of the IAM execution role to use to create the stack set. If you do not specify an execution role, AWS CloudFormation uses the AWSCloudFormationStackSetExecutionRole role for the stack set operation. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 64 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### OperationPreferences + +The user-specified preferences for how AWS CloudFormation performs a stack set operation. + +_Required_: No + +_Type_: OperationPreferences + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### StackInstancesGroup + +_Required_: No + +_Type_: List of StackInstancesGroup + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Parameters + +The input parameters for the stack set template. + +_Required_: No + +_Type_: List of Parameters + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PermissionModel + +Describes how the IAM roles required for stack set operations are created. By default, SELF-MANAGED is specified. + +_Required_: Yes + +_Type_: String + +_Allowed Values_: SERVICE_MANAGED | SELF_MANAGED + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### Tags + +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. + +_Required_: No + +_Type_: List of Tags + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### TemplateBody + +The structure that contains the template body, with a minimum length of 1 byte and a maximum length of 51,200 bytes. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 51200 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### TemplateURL + +Location of file containing the template body. The URL must point to a template (max size: 460,800 bytes) that is located in an Amazon S3 bucket. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 1024 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the StackSetId. + +### Fn::GetAtt + +The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. + +For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). + +#### StackSetId + +The ID of the stack set that you're creating. diff --git a/aws-cloudformation-stackset/docs/autodeployment.md b/aws-cloudformation-stackset/docs/autodeployment.md new file mode 100644 index 0000000..4389aaf --- /dev/null +++ b/aws-cloudformation-stackset/docs/autodeployment.md @@ -0,0 +1,43 @@ +# AWS::CloudFormation::StackSet AutoDeployment + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "Enabled" : Boolean,
    +    "RetainStacksOnAccountRemoval" : Boolean
    +}
    +
    + +### YAML + +
    +Enabled: Boolean
    +RetainStacksOnAccountRemoval: Boolean
    +
    + +## Properties + +#### Enabled + +If set to true, StackSets automatically deploys additional stack instances to AWS Organizations accounts that are added to a target organization or organizational unit (OU) in the specified Regions. If an account is removed from a target organization or OU, StackSets deletes stack instances from the account in the specified Regions. + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RetainStacksOnAccountRemoval + +If set to true, stack resources are retained when an account is removed from a target organization or OU. If set to false, stack resources are deleted. Specify only if Enabled is set to True. + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/operationpreferences.md b/aws-cloudformation-stackset/docs/operationpreferences.md new file mode 100644 index 0000000..7edc863 --- /dev/null +++ b/aws-cloudformation-stackset/docs/operationpreferences.md @@ -0,0 +1,72 @@ +# AWS::CloudFormation::StackSet OperationPreferences + +The user-specified preferences for how AWS CloudFormation performs a stack set operation. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "FailureToleranceCount" : Double,
    +    "FailureTolerancePercentage" : Double,
    +    "MaxConcurrentCount" : Double,
    +    "MaxConcurrentPercentage" : Double,
    +    "RegionOrder" : [ String, ... ]
    +}
    +
    + +### YAML + +
    +FailureToleranceCount: Double
    +FailureTolerancePercentage: Double
    +MaxConcurrentCount: Double
    +MaxConcurrentPercentage: Double
    +RegionOrder: 
    +      - String
    +
    + +## Properties + +#### FailureToleranceCount + +_Required_: No + +_Type_: Double + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### FailureTolerancePercentage + +_Required_: No + +_Type_: Double + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MaxConcurrentCount + +_Required_: No + +_Type_: Double + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MaxConcurrentPercentage + +_Required_: No + +_Type_: Double + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RegionOrder + +_Required_: No + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/parameters.md b/aws-cloudformation-stackset/docs/parameters.md new file mode 100644 index 0000000..587498d --- /dev/null +++ b/aws-cloudformation-stackset/docs/parameters.md @@ -0,0 +1,43 @@ +# AWS::CloudFormation::StackSet Parameters + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "ParameterKey" : String,
    +    "ParameterValue" : String
    +}
    +
    + +### YAML + +
    +ParameterKey: String
    +ParameterValue: String
    +
    + +## Properties + +#### ParameterKey + +The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ParameterValue + +The input value associated with the parameter. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/stackinstancesgroup-deploymenttargets.md b/aws-cloudformation-stackset/docs/stackinstancesgroup-deploymenttargets.md new file mode 100644 index 0000000..23b30fc --- /dev/null +++ b/aws-cloudformation-stackset/docs/stackinstancesgroup-deploymenttargets.md @@ -0,0 +1,47 @@ +# AWS::CloudFormation::StackSet StackInstancesGroup DeploymentTargets + + The AWS OrganizationalUnitIds or Accounts for which to create stack instances in the specified Regions. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "Accounts" : [ String, ... ],
    +    "OrganizationalUnitIds" : [ String, ... ]
    +}
    +
    + +### YAML + +
    +Accounts: 
    +      - String
    +OrganizationalUnitIds: 
    +      - String
    +
    + +## Properties + +#### Accounts + +AWS accounts that you want to create stack instances in the specified Region(s) for. + +_Required_: No + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### OrganizationalUnitIds + +The organization root ID or organizational unit (OU) IDs to which StackSets deploys. + +_Required_: No + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md b/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md new file mode 100644 index 0000000..a75ca6e --- /dev/null +++ b/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md @@ -0,0 +1,43 @@ +# AWS::CloudFormation::StackSet StackInstancesGroup ParameterOverrides + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "ParameterKey" : String,
    +    "ParameterValue" : String
    +}
    +
    + +### YAML + +
    +ParameterKey: String
    +ParameterValue: String
    +
    + +## Properties + +#### ParameterKey + +The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ParameterValue + +The input value associated with the parameter. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/stackinstancesgroup.md b/aws-cloudformation-stackset/docs/stackinstancesgroup.md new file mode 100644 index 0000000..80be8ad --- /dev/null +++ b/aws-cloudformation-stackset/docs/stackinstancesgroup.md @@ -0,0 +1,59 @@ +# AWS::CloudFormation::StackSet StackInstancesGroup + +Stack instances in some specific accounts and Regions. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "DeploymentTargets" : DeploymentTargets,
    +    "Regions" : [ String, ... ],
    +    "ParameterOverrides" : [ ParameterOverrides, ... ]
    +}
    +
    + +### YAML + +
    +DeploymentTargets: DeploymentTargets
    +Regions: 
    +      - String
    +ParameterOverrides: 
    +      - ParameterOverrides
    +
    + +## Properties + +#### DeploymentTargets + + The AWS OrganizationalUnitIds or Accounts for which to create stack instances in the specified Regions. + +_Required_: Yes + +_Type_: DeploymentTargets + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Regions + +The names of one or more Regions where you want to create stack instances using the specified AWS account(s). + +_Required_: Yes + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ParameterOverrides + +A list of stack set parameters whose values you want to override in the selected stack instances. + +_Required_: No + +_Type_: List of ParameterOverrides + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/docs/tags.md b/aws-cloudformation-stackset/docs/tags.md new file mode 100644 index 0000000..c736ad9 --- /dev/null +++ b/aws-cloudformation-stackset/docs/tags.md @@ -0,0 +1,57 @@ +# AWS::CloudFormation::StackSet Tags + +Tag type enables you to specify a key-value pair that can be used to store information about an AWS CloudFormation StackSet. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
    +{
    +    "Key" : String,
    +    "Value" : String
    +}
    +
    + +### YAML + +
    +Key: String
    +Value: String
    +
    + +## Properties + +#### Key + +A string used to identify this tag. You can specify a maximum of 127 characters for a tag key. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 127 + +_Pattern_: ^(?!aws:.*)[a-z0-9\s\_\.\/\=\+\-]+$ + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Value + +A string containing the value for this tag. You can specify a maximum of 256 characters for a tag value. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 255 + +_Pattern_: ^(?!aws:.*)[a-z0-9\s\_\.\/\=\+\-]+$ + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml index 7557af1..309a685 100644 --- a/aws-cloudformation-stackset/pom.xml +++ b/aws-cloudformation-stackset/pom.xml @@ -41,7 +41,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - 1.0.4 + 1.0.5 @@ -100,11 +100,6 @@ 2.26.0 test - - org.apache.commons - commons-collections4 - 4.4 - @@ -190,7 +185,6 @@ **/Configuration* - **/util/AwsCredentialsExtractor* **/util/ClientBuilder* **/BaseConfiguration* **/BaseHandler* diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index a20ce75..4ac9d93 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -7,7 +7,7 @@ Resources: ExecutionRole: Type: AWS::IAM::Role Properties: - MaxSessionDuration: 8400 + MaxSessionDuration: 43200 AssumeRolePolicyDocument: Version: '2012-10-17' Statement: 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 index a1a9b85..15f846d 100644 --- 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 @@ -12,6 +12,7 @@ import software.amazon.awssdk.services.cloudformation.model.StackSet; import software.amazon.awssdk.services.cloudformation.model.StackSetOperationStatus; import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesResponse; +import software.amazon.cloudformation.Action; import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; @@ -20,6 +21,9 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.cloudformation.proxy.delay.MultipleOf; import software.amazon.cloudformation.stackset.util.ClientBuilder; +import software.amazon.cloudformation.stackset.util.InstancesAnalyzer; +import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; +import software.amazon.cloudformation.stackset.util.Validator; import java.time.Duration; import java.util.List; @@ -35,8 +39,6 @@ */ 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)) @@ -93,7 +95,8 @@ public final ProgressEvent handleRequest( final CallbackContext callbackContext, final Logger logger) { - return handleRequest(proxy, request, callbackContext != null ? callbackContext : new CallbackContext(), proxy.newProxy(ClientBuilder::getClient), logger); + return handleRequest(proxy, request, callbackContext != null ? + callbackContext : new CallbackContext(), proxy.newProxy(ClientBuilder::getClient), logger); } protected abstract ProgressEvent handleRequest( @@ -107,6 +110,17 @@ protected boolean filterException(AwsRequest request, Exception e, ProxyClient} to make the call + * @param progress {@link ProgressEvent} to place hold the current progress data + * @param stackInstancesList StackInstances that need to create, see in {@link InstancesAnalyzer#analyzeForCreate} + * @param logger {@link Logger} + * @return {@link ProgressEvent} + */ protected ProgressEvent createStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, @@ -138,6 +152,17 @@ protected ProgressEvent createStackInstances( return ProgressEvent.progress(model, callbackContext); } + /** + * Invocation of DeleteStackInstances would possibly used by UPDATE/DELETE handler, after the template being analyzed + * by {@link InstancesAnalyzer} + * + * @param proxy {@link AmazonWebServicesClientProxy} to initiate proxy chain + * @param client the aws service client {@link ProxyClient} to make the call + * @param progress {@link ProgressEvent} to place hold the current progress data + * @param stackInstancesList StackInstances that need to create, see in {@link InstancesAnalyzer#analyzeForDelete} + * @param logger {@link Logger} + * @return {@link ProgressEvent} + */ protected ProgressEvent deleteStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, @@ -164,6 +189,7 @@ protected ProgressEvent deleteStackInstances( if (e instanceof StackInstanceNotFoundException) { return ProgressEvent.success(model_, context); } + // If OperationInProgressException is thrown by the service, then we retry if (e instanceof OperationInProgressException) { return ProgressEvent.progress(model_, context); } @@ -179,6 +205,17 @@ protected ProgressEvent deleteStackInstances( return ProgressEvent.progress(model, callbackContext); } + /** + * Invocation of DeleteStackInstances would possibly used by DELETE handler, after the template being analyzed + * by {@link InstancesAnalyzer} + * + * @param proxy {@link AmazonWebServicesClientProxy} to initiate proxy chain + * @param client the aws service client {@link ProxyClient} to make the call + * @param progress {@link ProgressEvent} to place hold the current progress data + * @param stackInstancesList StackInstances that need to create, see in {@link InstancesAnalyzer#analyzeForUpdate} + * @param logger {@link Logger} + * @return {@link ProgressEvent} + */ protected ProgressEvent updateStackInstances( final AmazonWebServicesClientProxy proxy, final ProxyClient client, @@ -244,4 +281,37 @@ protected boolean isOperationStabilized(final ProxyClient final StackSetOperationStatus status = getStackSetOperationStatus(proxyClient, stackSetId, operationId); return isStackSetOperationDone(status, operationId, logger); } + + /** + * Analyzes/validates template and StackInstancesGroup + * + * @param proxy {@link AmazonWebServicesClientProxy} + * @param request {@link ResourceHandlerRequest} + * @param placeHolder {@link StackInstancesPlaceHolder} + * @param logger {@link Logger} + * @param action {@link Action} + */ + protected void analyzeTemplate( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final StackInstancesPlaceHolder placeHolder, + final Logger logger, + final Action action) { + + final ResourceModel desiredModel = request.getDesiredResourceState(); + final ResourceModel previousModel = request.getPreviousResourceState(); + + switch (action) { + case CREATE: + new Validator().validateTemplate(proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger); + InstancesAnalyzer.builder().desiredModel(desiredModel).build().analyzeForCreate(placeHolder); + break; + case UPDATE: + new Validator().validateTemplate(proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger); + InstancesAnalyzer.builder().desiredModel(desiredModel).previousModel(previousModel).build().analyzeForUpdate(placeHolder); + break; + case DELETE: + InstancesAnalyzer.builder().desiredModel(desiredModel).build().analyzeForDelete(placeHolder); + } + } } 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 9c6b8a8..7739a34 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 @@ -2,14 +2,13 @@ import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.cloudformation.model.CreateStackSetResponse; +import software.amazon.cloudformation.Action; 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.InstancesAnalyzer; import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; -import software.amazon.cloudformation.stackset.util.Validator; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackSetRequest; @@ -29,7 +28,7 @@ protected ProgressEvent handleRequest( // Ensure the idempotency of StackSet, we should not generate a random StackSetName final String stackSetName = request.getLogicalResourceIdentifier(); final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - analyzeTemplate(proxy, model, placeHolder); + analyzeTemplate(proxy, request, placeHolder, logger, Action.CREATE); return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext) .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken())) @@ -43,20 +42,4 @@ protected ProgressEvent handleRequest( .then(progress -> createStackInstances(proxy, proxyClient, progress, placeHolder.getCreateStackInstances(), logger)) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } - - /** - * Analyzes/validates template and StackInstancesGroup - * - * @param proxy {@link AmazonWebServicesClientProxy} - * @param model {@link ResourceModel} - * @param placeHolder {@link StackInstancesPlaceHolder} - */ - private void analyzeTemplate( - final AmazonWebServicesClientProxy proxy, - final ResourceModel model, - final StackInstancesPlaceHolder placeHolder) { - - new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); - InstancesAnalyzer.builder().desiredModel(model).build().analyzeForCreate(placeHolder); - } } 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 0871e14..816dfaf 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 @@ -2,12 +2,12 @@ import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.cloudformation.model.DeleteStackSetResponse; +import software.amazon.cloudformation.Action; 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.InstancesAnalyzer; import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackSetRequest; @@ -27,7 +27,7 @@ protected ProgressEvent handleRequest( final ResourceModel model = request.getDesiredResourceState(); // Analyzes stack instances group for delete final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - InstancesAnalyzer.builder().desiredModel(model).build().analyzeForDelete(placeHolder); + analyzeTemplate(proxy, request, placeHolder, logger, Action.DELETE); return ProgressEvent.progress(model, callbackContext) // delete/stabilize progress chain - delete all associated stack instances 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 a83126f..f897846 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 @@ -40,6 +40,7 @@ protected ProgressEvent handleRequest( return ProgressEvent.builder() .resourceModels(models) .status(OperationStatus.SUCCESS) + .nextToken(response.nextToken()) .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 17bf767..df164e2 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 @@ -2,14 +2,13 @@ import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetResponse; +import software.amazon.cloudformation.Action; 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.InstancesAnalyzer; import software.amazon.cloudformation.stackset.util.StackInstancesPlaceHolder; -import software.amazon.cloudformation.stackset.util.Validator; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest; import static software.amazon.cloudformation.stackset.util.Comparator.isStackSetConfigEquals; @@ -30,7 +29,7 @@ protected ProgressEvent handleRequest( final ResourceModel model = request.getDesiredResourceState(); final ResourceModel previousModel = request.getPreviousResourceState(); final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - analyzeTemplate(proxy, previousModel, placeHolder, model); + analyzeTemplate(proxy, request, placeHolder, logger, Action.UPDATE); return ProgressEvent.progress(model, callbackContext) .then(progress -> updateStackSet(proxy, proxyClient, progress, previousModel)) @@ -50,7 +49,7 @@ protected ProgressEvent handleRequest( * @param previousModel previous {@link ResourceModel} for comparing with desired model * @return progressEvent indicating success, in progress with delay callback or failed state */ - protected ProgressEvent updateStackSet( + private ProgressEvent updateStackSet( final AmazonWebServicesClientProxy proxy, final ProxyClient client, final ProgressEvent progress, @@ -65,7 +64,7 @@ protected ProgressEvent updateStackSet( return proxy.initiate("AWS-CloudFormation-StackSet::UpdateStackSet", client, desiredModel, callbackContext) .translateToServiceRequest(modelRequest -> updateStackSetRequest(modelRequest)) .makeServiceCall((modelRequest, proxyInvocation) -> { - UpdateStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackSet); + final UpdateStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2(modelRequest, proxyInvocation.client()::updateStackSet); logger.log(String.format("%s UpdateStackSet initiated", ResourceModel.TYPE_NAME)); return response; }) @@ -73,23 +72,4 @@ protected ProgressEvent updateStackSet( .retryErrorFilter(this::filterException) .progress(); } - - /** - * Analyzes/validates template and StackInstancesGroup - * - * @param proxy {@link AmazonWebServicesClientProxy} - * @param previousModel previous {@link ResourceModel} - * @param placeHolder {@link StackInstancesPlaceHolder} - * @param model {@link ResourceModel} - */ - private void analyzeTemplate( - final AmazonWebServicesClientProxy proxy, - final ResourceModel previousModel, - final StackInstancesPlaceHolder placeHolder, - final ResourceModel model) { - - new Validator().validateTemplate(proxy, model.getTemplateBody(), model.getTemplateURL(), logger); - InstancesAnalyzer.builder().desiredModel(model).previousModel(previousModel).build() - .analyzeForUpdate(placeHolder); - } } 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 deleted file mode 100644 index 9d0389c..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/AwsCredentialsExtractor.java +++ /dev/null @@ -1,248 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.awscore.AwsRequest; -import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; -import software.amazon.awssdk.awscore.AwsResponse; -import software.amazon.awssdk.awscore.AwsResponseMetadata; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.SdkField; -import software.amazon.awssdk.core.SdkPojo; -import software.amazon.awssdk.utils.builder.CopyableBuilder; -import software.amazon.awssdk.utils.builder.ToCopyableBuilder; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * Utility class to extract AWS Credentials Provider from {@link AmazonWebServicesClientProxy}. - *

    - * Because {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2(AwsRequest, Function)} doesn't extend - * {@link ResponseInputStream}, but S3 GetObject requires AWS Credentials Provider to authenticate user, - * we have to mimic dummy aws request, aws response and a function as input parameters to - * {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2(AwsRequest, Function)} to obtain credentials. - */ -public final class AwsCredentialsExtractor { - - private static final String AWS_CREDENTIALS_NOT_AVAILABLE_ERROR_MSG = "AWS credentials provider are not available"; - - /** - * Function to extract Aws Credentials Provider from {@link AmazonWebServicesClientProxy}. - * - * @param proxy {@link AmazonWebServicesClientProxy} - * @return {@link AwsCredentialsProvider} - */ - public static AwsCredentialsProvider extractAwsCredentialsProvider(final AmazonWebServicesClientProxy proxy) { - return proxy.injectCredentialsAndInvokeV2( - GetAwsCredentialsRequest.builder().build(), - AwsCredentialsExtractor::extract - ).awsCredentialsProvider; - } - - private static GetAwsCredentialsResponse extract(final GetAwsCredentialsRequest getAwsCredentialsRequest) { - final AwsCredentialsProvider awsCredentialsProvider = getAwsCredentialsRequest.overrideConfiguration() - .flatMap(AwsRequestOverrideConfiguration::credentialsProvider) - .orElseThrow(() -> new IllegalArgumentException(AWS_CREDENTIALS_NOT_AVAILABLE_ERROR_MSG)); - return GetAwsCredentialsResponse.builder().awsCredentialsProvider(awsCredentialsProvider).build(); - } - - /** - * Inner class to mimic {@link AwsRequest}. - * No additional input parameter is required. Other classes and functions are implemented by following interfaces - * and abstract method of {@link AwsRequest}. - */ - private final static class GetAwsCredentialsRequest extends AwsRequest - implements ToCopyableBuilder { - - private GetAwsCredentialsRequest(Builder builder) { - super(builder); - } - - static GetAwsCredentialsRequest.Builder builder() { - return new GetAwsCredentialsRequest.BuilderImpl(); - } - - @Override - public Builder toBuilder() { - return new GetAwsCredentialsRequest.BuilderImpl(); - } - - @Override - public List> sdkFields() { - return Collections.emptyList(); - } - - @Override - public boolean equalsBySdkFields(Object obj) { - return true; - } - - public interface Builder - extends AwsRequest.Builder, SdkPojo, CopyableBuilder { - @Override - GetAwsCredentialsRequest.Builder overrideConfiguration( - AwsRequestOverrideConfiguration awsRequestOverrideConfiguration - ); - - @Override - GetAwsCredentialsRequest.Builder overrideConfiguration( - Consumer builderConsumer - ); - } - - static final class BuilderImpl extends AwsRequest.BuilderImpl - implements GetAwsCredentialsRequest.Builder { - - BuilderImpl() { - } - - public Builder overrideConfiguration(AwsRequestOverrideConfiguration overrideConfiguration) { - super.overrideConfiguration(overrideConfiguration); - return this; - } - - public GetAwsCredentialsRequest.Builder overrideConfiguration( - Consumer builderConsumer - ) { - super.overrideConfiguration(builderConsumer); - return this; - } - - public GetAwsCredentialsRequest build() { - return new GetAwsCredentialsRequest(this); - } - - public List> sdkFields() { - return Collections.emptyList(); - } - } - } - - /** - * Inner class to mimic {@link AwsResponse} in order to obtain credentials from - * {@link AmazonWebServicesClientProxy}. - *

    - * {@link AwsCredentialsProvider} is the additional parameter in this class. Other classes and functions are - * implemented by following interfaces and abstract method of {@link AwsResponse}. - */ - private static class GetAwsCredentialsResponse extends AwsResponse - implements ToCopyableBuilder { - - private final GetAwsCredentialsResponseMetadata responseMetadata; - - private final AwsCredentialsProvider awsCredentialsProvider; - - private GetAwsCredentialsResponse(final GetAwsCredentialsResponse.BuilderImpl builder) { - super(builder); - this.awsCredentialsProvider = (builder.awsCredentialsProvider); - this.responseMetadata = builder.responseMetadata(); - } - - public static GetAwsCredentialsResponse.Builder builder() { - return new GetAwsCredentialsResponse.BuilderImpl(); - } - - public AwsCredentialsProvider awsCredentialsProvider() { - return this.awsCredentialsProvider; - } - - @Override - public Builder toBuilder() { - return new GetAwsCredentialsResponse.BuilderImpl(this); - } - - @Override - public List> sdkFields() { - return Collections.emptyList(); - } - - @Override - public boolean equalsBySdkFields(Object obj) { - if (this == obj) { - return true; - } else if (obj == null) { - return false; - } else if (!(obj instanceof GetAwsCredentialsResponse)) { - return false; - } else { - GetAwsCredentialsResponse other = (GetAwsCredentialsResponse) obj; - return Objects.equals(this.awsCredentialsProvider(), other.awsCredentialsProvider()); - } - } - - public interface Builder extends AwsResponse.Builder, SdkPojo, - CopyableBuilder { - - GetAwsCredentialsResponse build(); - - GetAwsCredentialsResponseMetadata responseMetadata(); - - GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata awsResponseMetadata); - - GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider); - } - - static final class BuilderImpl extends AwsResponse.BuilderImpl - implements GetAwsCredentialsResponse.Builder { - - private GetAwsCredentialsResponseMetadata responseMetadata; - - private AwsCredentialsProvider awsCredentialsProvider; - - private BuilderImpl() { - } - - private BuilderImpl(GetAwsCredentialsResponse response) { - super(response); - this.awsCredentialsProvider = response.awsCredentialsProvider; - } - - public GetAwsCredentialsResponse build() { - return new GetAwsCredentialsResponse(this); - } - - public List> sdkFields() { - return Collections.emptyList(); - } - - public GetAwsCredentialsResponseMetadata responseMetadata() { - return this.responseMetadata; - } - - public GetAwsCredentialsResponse.Builder responseMetadata(AwsResponseMetadata responseMetadata) { - this.responseMetadata = GetAwsCredentialsResponseMetadata.create(responseMetadata); - return this; - } - - public final GetAwsCredentialsResponse.Builder awsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { - this.awsCredentialsProvider = awsCredentialsProvider; - return this; - } - - public AwsCredentialsProvider getAwsCredentialsProvider() { - return awsCredentialsProvider; - } - - public void setAwsCredentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { - this.awsCredentialsProvider = awsCredentialsProvider; - } - } - } - - /** - * Inner class to mimic {@link AwsResponseMetadata} which is required by {@link AwsResponse}. - */ - private static final class GetAwsCredentialsResponseMetadata extends AwsResponseMetadata { - private GetAwsCredentialsResponseMetadata(AwsResponseMetadata responseMetadata) { - super(responseMetadata); - } - - public static GetAwsCredentialsResponseMetadata create(AwsResponseMetadata responseMetadata) { - return new GetAwsCredentialsResponseMetadata(responseMetadata); - } - } -} 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 60a7936..c1dd84e 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 @@ -1,6 +1,5 @@ package software.amazon.cloudformation.stackset.util; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.cloudformation.LambdaWrapper; @@ -18,12 +17,10 @@ public static CloudFormationClient getClient() { * Gets S3 client for requests to interact with getting/validating template content * if {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL()} is passed in * - * @param awsCredentialsProvider {@link AwsCredentialsProvider} * @return {@link S3Client} */ - public static S3Client getS3Client(final AwsCredentialsProvider awsCredentialsProvider) { + public static S3Client getS3Client() { return S3Client.builder() - .credentialsProvider(awsCredentialsProvider) .httpClient(LambdaWrapper.HTTP_CLIENT) .build(); } 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 9a93150..38540b4 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,6 +1,5 @@ package software.amazon.cloudformation.stackset.util; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import software.amazon.awssdk.services.cloudformation.model.PermissionModels; import software.amazon.cloudformation.stackset.ResourceModel; @@ -51,8 +50,14 @@ public static boolean isStackSetConfigEquals( * @return boolean indicates if two collections equal. */ public static boolean isEquals(final Collection collection1, final Collection collection2) { - if (collection1 == null) return collection2 == null; - return CollectionUtils.isEqualCollection(collection1, collection2); + boolean equals = false; + if (collection1 != null && collection2 != null) { + equals = collection1.size() == collection2.size() + && collection1.containsAll(collection2) && collection2.containsAll(collection1); + } else if (collection1 == null && collection2 == null) { + equals = true; + } + return equals; } public static boolean isSelfManaged(final ResourceModel model) { 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 index 48b058d..25e13db 100644 --- 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 @@ -163,6 +163,7 @@ private static Set flattenStackInstancesGroup( final Set targets = isSelfManaged ? stackInstances.getDeploymentTargets().getAccounts() : stackInstances.getDeploymentTargets().getOrganizationalUnitIds(); + // Validates expected DeploymentTargets exist in the template if (CollectionUtils.isNullOrEmpty(targets)) { throw new CfnInvalidRequestException( String.format("%s should be specified in DeploymentTargets in [%s] model", @@ -175,6 +176,7 @@ private static Set flattenStackInstancesGroup( .region(region).deploymentTarget(target).parameters(stackInstances.getParameterOverrides()) .build(); + // Validates no duplicated stack instance is specified if (flatStacks.contains(stackInstance)) { throw new CfnInvalidRequestException( String.format("Stack instance [%s,%s] is duplicated", target, region)); diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java index 0ee41a9..9e703bf 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -3,7 +3,6 @@ import com.amazonaws.services.s3.AmazonS3URI; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; @@ -12,6 +11,7 @@ import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.regex.Pattern; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getObjectRequest; import static software.amazon.cloudformation.stackset.util.TemplateParser.deserializeTemplate; @@ -23,10 +23,15 @@ */ public class Validator { + // A stack name can contain only alphanumeric characters (case-sensitive) and hyphens. + // It must start with an alphabetic character and can't be longer than 128 characters. + private static final Pattern STACKSET_NAME_FORMAT = Pattern.compile("^[a-zA-Z][a-zA-Z0-9\\-]{0,127}$"); + private static final String TEMPLATE_RESOURCE_TYPE_KEY = "Type"; private static final String TEMPLATE_RESOURCES_KEY = "Resources"; private static final String TEMPLATE_PARAMETERS_KEY = "Parameters"; + /** * Validates the template to make sure: *

      @@ -89,17 +94,8 @@ protected String getUrlContent(final AmazonWebServicesClientProxy proxy, final S final AmazonS3URI s3Uri = new AmazonS3URI(templateLocation, true); final GetObjectRequest request = getObjectRequest(s3Uri.getBucket(), s3Uri.getKey()); - /** - * Since currently response other than {@link AmazonWebServicesClientProxy#injectCredentialsAndInvokeV2} - * ${@link Result} only extends {@link AwsResponse}, which we can't inject credentials into - * {@link software.amazon.awssdk.services.s3.S3Client#getObject}. - * Hence, getting {@link AwsCredentialsProvider} using an aws dummy request. - */ - final AwsCredentialsProvider awsCredentialsProvider = - AwsCredentialsExtractor.extractAwsCredentialsProvider(proxy); - - final String content = ClientBuilder.getS3Client(awsCredentialsProvider) - .getObjectAsBytes(request).asString(StandardCharsets.UTF_8); + final String content = proxy.injectCredentialsAndInvokeV2Bytes(request, + ClientBuilder.getS3Client()::getObjectAsBytes).asString(StandardCharsets.UTF_8); return content; } @@ -160,4 +156,21 @@ public void validateTemplate( } } + /** + * Validates StackSetName and CfnInvalidRequestException will be thrown if StackSetName does not meet: + *
        + *
      • Contains only alphanumeric characters (case-sensitive) and hyphens. + *
      • Starts with an alphabetic character + *
      • Length is no longer than 128 characters + *
      + * https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStackSet.html + * @param stackSetName + */ + public void validateStackSetName(final String stackSetName) { + if (!STACKSET_NAME_FORMAT.matcher(stackSetName).matches()) { + throw new CfnInvalidRequestException( + "A stack name can contain only alphanumeric characters (case-sensitive) and hyphens. " + + "It must start with an alphabetic character and can't be longer than 128 characters."); + } + } } 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 index 6a5bb5c..abf97b6 100644 --- 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 @@ -2,6 +2,8 @@ import software.amazon.awssdk.awscore.AwsRequest; import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -45,6 +47,18 @@ static ProxyClient MOCK_PROXY( throw new UnsupportedOperationException(); } + @Override + public ResponseInputStream + injectCredentialsAndInvokeV2InputStream(RequestT requestT, Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public ResponseBytes + injectCredentialsAndInvokeV2Bytes(RequestT requestT, Function> function) { + throw new UnsupportedOperationException(); + } + @Override public CloudFormationClient client() { return sdkClient; 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 195eaa6..21cfd73 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 @@ -20,8 +20,10 @@ 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.times; +import static org.mockito.Mockito.verify; +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_STACK_INSTANCE_RESPONSE_1; import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIBE_STACK_INSTANCE_RESPONSE_2; @@ -58,17 +60,17 @@ public void setup() { @Test public void handleRequest_SelfManagedSS_Success() { - 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)); + when(proxyClient.client().listStackSets(any(ListStackSetsRequest.class))) + .thenReturn(LIST_STACK_SETS_RESPONSE); + when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) + .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) + .thenReturn(LIST_SELF_MANAGED_STACK_SET_RESPONSE); + when(proxyClient.client().describeStackInstance(any(DescribeStackInstanceRequest.class))) + .thenReturn(DESCRIBE_STACK_INSTANCE_RESPONSE_1, + DESCRIBE_STACK_INSTANCE_RESPONSE_2, + DESCRIBE_STACK_INSTANCE_RESPONSE_3, + DESCRIBE_STACK_INSTANCE_RESPONSE_4); final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); @@ -81,5 +83,10 @@ public void handleRequest_SelfManagedSS_Success() { assertThat(response.getResourceModels()).containsExactly(SELF_MANAGED_MODEL_FOR_READ); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).listStackSets(any(ListStackSetsRequest.class)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } } 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 a14a431..09027dc 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 @@ -39,8 +39,10 @@ import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_SUCCEED_RESPONSE; 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.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_SELF_MANAGED_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_SELF_MANAGED_ONE_INSTANCES_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; @@ -147,5 +149,4 @@ public void handleRequest_NotUpdatable_Success() { verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); } - } 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 4fc52a7..a0e7d29 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 @@ -491,6 +491,17 @@ public class TestUtils { .tags(TAGS) .build(); + public final static ResourceModel UPDATED_SELF_MANAGED_ONE_INSTANCES_MODEL = 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_4))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .tags(TAGS) + .build(); + public final static ResourceModel SELF_MANAGED_DUPLICATE_INSTANCES_MODEL = ResourceModel.builder() .stackSetId(STACK_SET_ID) .permissionModel(SELF_MANAGED) 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 8547991..62bf135 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,11 +1,14 @@ package software.amazon.cloudformation.stackset.util; +import com.google.common.base.Strings; 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.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; @@ -14,9 +17,14 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; 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 org.mockito.Mockito.when; +import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACKSET_TEMPLATE; import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_BODY; @@ -30,6 +38,12 @@ public class ValidatorTest { "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"); + private static final List VALID_STACK_SET_NAMES = Arrays.asList( + "myStackSet", "myStackSet123", "MyStack-Set", Strings.repeat("a", 128)); + + private static final List INVALID_STACK_SET_NAMES = Arrays.asList( + "123myStackSet", "myStackSet!", "MyStack_Set", Strings.repeat("a", 129)); + @Spy private Validator validator; @@ -60,6 +74,14 @@ public void testValidateTemplate_BothBodyAndUriExist() { () -> validator.validateTemplate(proxy, TEMPLATE_BODY, TEMPLATE_URL, logger)); } + @Test + public void testGetUrlContent() { + final ResponseBytes responseBytes = mock(ResponseBytes.class); + doReturn(ResponseBytes.fromByteArray(responseBytes, TEMPLATE_BODY.getBytes())).when(proxy) + .injectCredentialsAndInvokeV2Bytes(any(), any()); + assertEquals(validator.getUrlContent(proxy, TEMPLATE_URL), TEMPLATE_BODY); + } + @Test public void testValidateTemplate_BothBodyAndUriNotExist() { assertThrows(CfnInvalidRequestException.class, @@ -83,4 +105,19 @@ public void testValidateTemplate_ValidTemplateBody() { assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); } + + @Test + public void testValidateStackSetName_ValidStackSetNames() { + for (final String validStackSetName : VALID_STACK_SET_NAMES) { + assertDoesNotThrow(() -> validator.validateStackSetName(validStackSetName)); + } + } + + @Test + public void testValidateStackSetName_InValidStackSetNames() { + for (final String invalidStackSetName : INVALID_STACK_SET_NAMES) { + assertThrows(CfnInvalidRequestException.class, + () -> validator.validateStackSetName(invalidStackSetName)); + } + } } diff --git a/aws-cloudformation-stackset/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/aws-cloudformation-stackset/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/aws-cloudformation-stackset/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline From 6f882bb7234d999b2b30b2c4f128c1701366cad8 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Tue, 12 May 2020 16:36:33 -0700 Subject: [PATCH 11/19] Add template size check and unit tests for coverage --- aws-cloudformation-stackset/pom.xml | 4 +-- .../translator/RequestTranslator.java | 9 ++++++ .../stackset/util/Comparator.java | 6 ++-- .../stackset/util/InstancesAnalyzer.java | 16 ++++++---- .../stackset/util/Validator.java | 19 ++++++++++-- .../stackset/UpdateHandlerTest.java | 2 -- .../stackset/util/ComparatorTest.java | 30 +++++++++++++++++-- .../stackset/util/ValidatorTest.java | 17 +++++++++-- 8 files changed, 81 insertions(+), 22 deletions(-) diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml index 309a685..5af7320 100644 --- a/aws-cloudformation-stackset/pom.xml +++ b/aws-cloudformation-stackset/pom.xml @@ -218,12 +218,12 @@ BRANCH COVEREDRATIO - 0.8 + 0.9 INSTRUCTION COVEREDRATIO - 0.8 + 0.9 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 eac24cb..cc38e94 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 @@ -12,6 +12,7 @@ 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.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.cloudformation.stackset.OperationPreferences; import software.amazon.cloudformation.stackset.ResourceModel; import software.amazon.cloudformation.stackset.StackInstances; @@ -151,4 +152,12 @@ public static GetObjectRequest getObjectRequest( .key(key) .build(); } + + public static HeadObjectRequest headObjectRequest( + final String bucketName, final String key) { + return HeadObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + } } 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 38540b4..c503f3b 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 @@ -22,7 +22,7 @@ public class Comparator { public static boolean isStackSetConfigEquals( final ResourceModel previousModel, final ResourceModel desiredModel) { - if (!isEquals(previousModel.getTags(), desiredModel.getTags())) + if (!equals(previousModel.getTags(), desiredModel.getTags())) return false; if (StringUtils.compare(previousModel.getAdministrationRoleARN(), @@ -39,7 +39,7 @@ public static boolean isStackSetConfigEquals( return false; // If TemplateURL is specified, always call Update API, Service client will decide if it is updatable - return desiredModel.getTemplateBody() != null || desiredModel.getTemplateURL() == null; + return desiredModel.getTemplateURL() == null; } /** @@ -49,7 +49,7 @@ public static boolean isStackSetConfigEquals( * @param collection2 * @return boolean indicates if two collections equal. */ - public static boolean isEquals(final Collection collection1, final Collection collection2) { + public static boolean equals(final Collection collection1, final Collection collection2) { boolean equals = false; if (collection1 != null && collection2 != null) { equals = collection1.size() == collection2.size() 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 index 25e13db..32e6dc6 100644 --- 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 @@ -139,7 +139,7 @@ private static Set getUpdatingStackInstances( final Map previousStackMap) { return intersection.stream() - .filter(stackInstance -> !Comparator.isEquals( + .filter(stackInstance -> !Comparator.equals( previousStackMap.get(stackInstance).getParameters(), stackInstance.getParameters())) .collect(Collectors.toSet()); } @@ -190,7 +190,9 @@ private static Set flattenStackInstancesGroup( } /** - * Analyzes {@link StackInstances} that need to be modified during the update + * Analyzes {@link StackInstances} that need to be modified during the update operations + * + * @param placeHolder {@link StackInstancesPlaceHolder} */ public void analyzeForUpdate(final StackInstancesPlaceHolder placeHolder) { final boolean isSelfManaged = isSelfManaged(desiredModel); @@ -224,8 +226,9 @@ public void analyzeForUpdate(final StackInstancesPlaceHolder placeHolder) { } /** - * Analyzes {@link StackInstances} that need to be modified during the update - * Updates callbackContext with the stack list to create + * Analyzes {@link StackInstances} that need to be modified during create operations + * + * @param placeHolder {@link StackInstancesPlaceHolder} */ public void analyzeForCreate(final StackInstancesPlaceHolder placeHolder) { if (desiredModel.getStackInstancesGroup() == null) return; @@ -243,8 +246,9 @@ public void analyzeForCreate(final StackInstancesPlaceHolder placeHolder) { } /** - * Analyzes {@link StackInstances} that need to be modified during the update - * Updates callbackContext with the stack list to delete + * Analyzes {@link StackInstances} that need to be modified during delete operations + * + * @param placeHolder {@link StackInstancesPlaceHolder} */ public void analyzeForDelete(final StackInstancesPlaceHolder placeHolder) { if (desiredModel.getStackInstancesGroup() == null) return; diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java index 9e703bf..170c23d 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -3,7 +3,7 @@ import com.amazonaws.services.s3.AmazonS3URI; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -14,6 +14,7 @@ import java.util.regex.Pattern; import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getObjectRequest; +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.headObjectRequest; import static software.amazon.cloudformation.stackset.util.TemplateParser.deserializeTemplate; import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; @@ -31,6 +32,8 @@ public class Validator { private static final String TEMPLATE_RESOURCES_KEY = "Resources"; private static final String TEMPLATE_PARAMETERS_KEY = "Parameters"; + private static final long TEMPLATE_CONTENT_LIMIT = 460800L; + /** * Validates the template to make sure: @@ -92,9 +95,18 @@ private static void validateResource(final Map resourceMap) { @VisibleForTesting protected String getUrlContent(final AmazonWebServicesClientProxy proxy, final String templateLocation) { final AmazonS3URI s3Uri = new AmazonS3URI(templateLocation, true); - final GetObjectRequest request = getObjectRequest(s3Uri.getBucket(), s3Uri.getKey()); + final S3Client client = ClientBuilder.getS3Client(); + + final Long contentLength = proxy.injectCredentialsAndInvokeV2( + headObjectRequest(s3Uri.getBucket(), s3Uri.getKey()), client::headObject).contentLength(); - final String content = proxy.injectCredentialsAndInvokeV2Bytes(request, + if (contentLength > TEMPLATE_CONTENT_LIMIT) { + throw new CfnInvalidRequestException(String.format("TemplateBody may not exceed the limit %d Bytes", + TEMPLATE_CONTENT_LIMIT)); + } + + final String content = proxy.injectCredentialsAndInvokeV2Bytes( + getObjectRequest(s3Uri.getBucket(), s3Uri.getKey()), ClientBuilder.getS3Client()::getObjectAsBytes).asString(StandardCharsets.UTF_8); return content; @@ -164,6 +176,7 @@ public void validateTemplate( *
    • Length is no longer than 128 characters *
    * https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStackSet.html + * * @param stackSetName */ public void validateStackSetName(final String stackSetName) { 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 09027dc..d91ac6f 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 @@ -39,10 +39,8 @@ import static software.amazon.cloudformation.stackset.util.TestUtils.OPERATION_SUCCEED_RESPONSE; 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.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SIMPLE_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_SELF_MANAGED_MODEL; -import static software.amazon.cloudformation.stackset.util.TestUtils.UPDATED_SELF_MANAGED_ONE_INSTANCES_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; 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 a8632ef..24c4057 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 @@ -4,7 +4,6 @@ import software.amazon.cloudformation.stackset.ResourceModel; import static org.assertj.core.api.Assertions.assertThat; -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.TestUtils.ADMINISTRATION_ROLE_ARN; import static software.amazon.cloudformation.stackset.util.TestUtils.DESCRIPTION; @@ -53,6 +52,26 @@ public void testIsStackSetConfigEquals() { assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + // Even if both TemplateURLs remain no change, we still need to call Update API + // The service client will decide if it needs to update + testDesiredModel.setTemplateURL(TEMPLATE_URL); + testPreviousModel.setTemplateURL(TEMPLATE_URL); + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + + // previously using TemplateURL, currently using TemplateBody + testPreviousModel.setTemplateURL(TEMPLATE_URL); + testDesiredModel.setTemplateURL(null); + testDesiredModel.setTemplateBody(TEMPLATE_BODY); + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + + // previously using TemplateBody, currently using TemplateURL + testPreviousModel.setTemplateBody(TEMPLATE_URL); + testPreviousModel.setTemplateURL(null); + testDesiredModel.setTemplateBody(null); + testDesiredModel.setTemplateURL(TEMPLATE_URL); + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + + // Both using TemplateBody testDesiredModel.setTemplateURL(null); testPreviousModel.setTemplateURL(null); @@ -60,11 +79,16 @@ public void testIsStackSetConfigEquals() { testPreviousModel.setTemplateBody(TEMPLATE_BODY); assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + + testDesiredModel.setTemplateBody(TEMPLATE_BODY); + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isTrue(); + } @Test - public void testIsEquals() { - assertThat(isEquals(null, TAGS)).isFalse(); + public void testEquals() { + assertThat(Comparator.equals(TAGS, null)).isFalse(); + assertThat(Comparator.equals(null, TAGS)).isFalse(); } } 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 62bf135..278ca36 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 @@ -8,7 +8,7 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.services.cloudformation.model.CreateStackSetRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; @@ -23,8 +23,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; -import static software.amazon.cloudformation.stackset.util.TestUtils.CREATE_STACK_SET_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACKSET_TEMPLATE; import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_BODY; @@ -44,6 +42,9 @@ public class ValidatorTest { private static final List INVALID_STACK_SET_NAMES = Arrays.asList( "123myStackSet", "myStackSet!", "MyStack_Set", Strings.repeat("a", 129)); + private static final long VALID_TEMPLATE_SIZE = 1000L; + private static final long INVALID_TEMPLATE_SIZE = 460801L; + @Spy private Validator validator; @@ -77,11 +78,21 @@ public void testValidateTemplate_BothBodyAndUriExist() { @Test public void testGetUrlContent() { final ResponseBytes responseBytes = mock(ResponseBytes.class); + doReturn(HeadObjectResponse.builder().contentLength(VALID_TEMPLATE_SIZE).build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); doReturn(ResponseBytes.fromByteArray(responseBytes, TEMPLATE_BODY.getBytes())).when(proxy) .injectCredentialsAndInvokeV2Bytes(any(), any()); assertEquals(validator.getUrlContent(proxy, TEMPLATE_URL), TEMPLATE_BODY); } + @Test + public void testGetUrlContent_TemplateTooLarge() { + doReturn(HeadObjectResponse.builder().contentLength(INVALID_TEMPLATE_SIZE).build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + assertThrows(CfnInvalidRequestException.class, + () -> validator.getUrlContent(proxy, TEMPLATE_URL)); + } + @Test public void testValidateTemplate_BothBodyAndUriNotExist() { assertThrows(CfnInvalidRequestException.class, From 63e377d6938de3023c29d7a32c9e9d2ac1b70436 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Thu, 14 May 2020 16:06:11 -0700 Subject: [PATCH 12/19] Update supports for cfn shorthands in template parser --- .../aws-cloudformation-stackset.json | 129 +++++++++++------- aws-cloudformation-stackset/docs/README.md | 2 +- .../docs/parameters.md | 4 +- .../stackinstancesgroup-parameteroverrides.md | 4 +- .../stackset/util/TemplateConstructor.java | 29 ++++ .../stackset/util/TemplateParser.java | 102 ++------------ .../stackset/util/TemplateParserTest.java | 16 +-- .../stackset/util/TestUtils.java | 94 +++++++------ .../stackset/util/ValidatorTest.java | 2 + 9 files changed, 179 insertions(+), 203 deletions(-) create mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 0b25f32..0782010 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -69,6 +69,36 @@ "CAPABILITY_AUTO_EXPAND" ] }, + "OperationPreferences": { + "description": "The user-specified preferences for how AWS CloudFormation performs a stack set operation.", + "type": "object", + "properties": { + "FailureToleranceCount": { + "type": "integer", + "minimum": 0 + }, + "FailureTolerancePercentage": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "MaxConcurrentCount": { + "type": "integer", + "minimum": 1 + }, + "MaxConcurrentPercentage": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "RegionOrder": { + "type": "array", + "items": { + "$ref": "#/definitions/Region" + } + } + } + }, "Parameter": { "type": "object", "properties": { @@ -81,6 +111,37 @@ "type": "string" } }, + "required": [ + "ParameterKey", + "ParameterValue" + ], + "additionalProperties": false + }, + "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", + "minItems": 1, + "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", + "minItems": 1, + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$ref": "#/definitions/OrganizationalUnitId" + } + } + }, "additionalProperties": false }, "StackInstances": { @@ -88,32 +149,12 @@ "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" - } - } - } + "$ref": "#/definitions/DeploymentTargets" }, "Regions": { "description": "The names of one or more Regions where you want to create stack instances using the specified AWS account(s).", "type": "array", + "minItems": 1, "uniqueItems": true, "insertionOrder": false, "items": { @@ -173,34 +214,7 @@ "maxLength": 64 }, "OperationPreferences": { - "description": "The user-specified preferences for how AWS CloudFormation performs a stack set operation.", - "type": "object", - "properties": { - "FailureToleranceCount": { - "type": "integer", - "minimum": 0 - }, - "FailureTolerancePercentage": { - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "MaxConcurrentCount": { - "type": "integer", - "minimum": 1 - }, - "MaxConcurrentPercentage": { - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "RegionOrder": { - "type": "array", - "items": { - "$ref": "#/definitions/Region" - } - } - } + "$ref": "#/definitions/OperationPreferences" }, "StackInstancesGroup": { "description": "", @@ -251,8 +265,19 @@ "maxLength": 1024 } }, - "required": [ - "PermissionModel" + "oneOf": [ + { + "required": [ + "TemplateURL", + "PermissionModel" + ] + }, + { + "required": [ + "TemplateBody", + "PermissionModel" + ] + } ], "additionalProperties": false, "createOnlyProperties": [ diff --git a/aws-cloudformation-stackset/docs/README.md b/aws-cloudformation-stackset/docs/README.md index 3483ebf..d721f36 100644 --- a/aws-cloudformation-stackset/docs/README.md +++ b/aws-cloudformation-stackset/docs/README.md @@ -145,7 +145,7 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati Describes how the IAM roles required for stack set operations are created. By default, SELF-MANAGED is specified. -_Required_: Yes +_Required_: No _Type_: String diff --git a/aws-cloudformation-stackset/docs/parameters.md b/aws-cloudformation-stackset/docs/parameters.md index 587498d..eed6d02 100644 --- a/aws-cloudformation-stackset/docs/parameters.md +++ b/aws-cloudformation-stackset/docs/parameters.md @@ -26,7 +26,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template. -_Required_: No +_Required_: Yes _Type_: String @@ -36,7 +36,7 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati The input value associated with the parameter. -_Required_: No +_Required_: Yes _Type_: String diff --git a/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md b/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md index a75ca6e..41e9fd7 100644 --- a/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md +++ b/aws-cloudformation-stackset/docs/stackinstancesgroup-parameteroverrides.md @@ -26,7 +26,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template. -_Required_: No +_Required_: Yes _Type_: String @@ -36,7 +36,7 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati The input value associated with the parameter. -_Required_: No +_Required_: Yes _Type_: String diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java new file mode 100644 index 0000000..8010dd4 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java @@ -0,0 +1,29 @@ +package software.amazon.cloudformation.stackset.util; + +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.Tag; + +import java.util.Arrays; +import java.util.List; + +public final class TemplateConstructor extends SafeConstructor { + + private final static String FUNCTION_PREFIX = "Fn::"; + private final static List FUNCTION_KEYS = Arrays.asList( + "Fn::And", "Fn::Base64", "Condition", "Fn::Contains", + "Fn::EachMemberEquals", "Fn::EachMemberIn", "Fn::Equals", + "Fn::FindInMap", "Fn::GetAtt", "Fn::GetAZs", "Fn::If", + "Fn::ImportValue", "Fn::Join", "Fn::Not", "Fn::Or", + "Ref", "Fn::RefAll", "Fn::Select", "Fn::Split", "Fn::Sub", + "Fn::ValueOf", "Fn::ValueOfAll", "Fn::Cidr"); + + TemplateConstructor() { + for (final String token : FUNCTION_KEYS) { + this.yamlConstructors.put(new Tag("!" + stripFn(token)), new ConstructYamlStr()); + } + } + + private static String stripFn(String keyword) { + return keyword.startsWith(FUNCTION_PREFIX) ? keyword.substring(4) : keyword; + } +} 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 820982e..b53b03a 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 @@ -1,54 +1,20 @@ package software.amazon.cloudformation.stackset.util; -import com.fasterxml.jackson.core.JsonLocation; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.MappingJsonFactory; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.MarkedYAMLException; import org.yaml.snakeyaml.error.YAMLException; -import java.io.IOException; import java.util.Map; -import java.util.regex.Pattern; public class TemplateParser { - private static final Pattern JSON_INPUT_PATTERN = Pattern.compile("^\\s*\\{.*\\}\\s*$", Pattern.DOTALL); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final String UNKNOWN_LOCATION = "unknown location"; private static final String INVALID_TEMPLATE_ERROR_MSG = "Template format error: not a valid template"; - private static final String UNSUPPORTED_TYPE_STRUCTURE_ERROR_MSG = - "Template format error: unsupported type or structure. (%s)"; - private static final String NOT_WELL_FORMATTED_ERROR_MSG = "Template format error: %s not well-formed. (%s)"; + private static final String NOT_WELL_FORMATTED_ERROR_MSG = "Template format error: not well-formed. (%s)"; private static final String FORMAT_LOCATION_ERROR_MSG = "line %s, column %s"; - /** - * Deserializes template content which can be either JSON or YAML - * - * @param template Template Content - * @return Generic Map of template - */ - public static Map deserializeTemplate(final String template) { - // If the template does not follow valid Json pattern, parse as Yaml. - // Else, parse as Json first; if that fails parse as Yaml. - if (!isPossiblyJson(template)) { - return deserializeYaml(template); - } - - try { - return deserializeJson(template); - } catch (final ParseException e) { - return deserializeYaml(template); - } - - } - /** * Gets a Generic Map object from template * @@ -81,27 +47,27 @@ public static String getStringFromTemplate(final Object value) { } /** - * Deserializes YAML from template content string + * Deserializes YAML/JSON from template content string, + * Since Yaml is a superset of JSON, we are parsing both with YAML library. * * @param templateString Template content * @return Template map * @throws ParseException if fails to parse the template */ - @VisibleForTesting - protected static Map deserializeYaml(final String templateString) { + public static Map deserializeTemplate(final String templateString) { try { - final Map template = new Yaml().load(templateString); + final Map template = new Yaml(new TemplateConstructor()).load(templateString); if (template == null || template.isEmpty()) { throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); } return template; } catch (final MarkedYAMLException e) { - throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "YAML", - formatYamlErrorLocation(e.getProblemMark()))); + throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, + formatTemplateErrorLocation(e.getProblemMark()))); } catch (final YAMLException e) { - throw new ParseException(String.format("Cannot parse as YAML : %s ", e.getMessage())); + throw new ParseException(String.format("Cannot parse the template : %s ", e.getMessage())); } catch (final ClassCastException e) { throw new ParseException("Template format error: unsupported structure."); @@ -109,56 +75,6 @@ protected static Map deserializeYaml(final String templateString } } - /** - * Deserializes JSON from template content string - * - * @param templateString Template content - * @return Template map - * @throws ParseException if fails to parse the template - */ - @SuppressWarnings("unchecked") - @VisibleForTesting - protected static Map deserializeJson(final String templateString) { - Map template; - try { - JsonParser parser = new MappingJsonFactory().createParser(templateString); - template = OBJECT_MAPPER.readValue(parser, Map.class); - - } catch (final JsonMappingException e) { - throw new ParseException(String.format(UNSUPPORTED_TYPE_STRUCTURE_ERROR_MSG, - formatJsonErrorLocation(e.getLocation()))); - - } catch (final JsonParseException e) { - throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, "JSON", - formatJsonErrorLocation(e.getLocation()))); - - } catch (final IOException e) { - throw new ParseException("Cannot parse template, I/O stream corrupt."); - } - - // The string "null" may be considered as valid JSON by the parser, but it is not a valid template. - if (template == null) { - throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); - } - return template; - } - - private static boolean isPossiblyJson(final String template) { - return JSON_INPUT_PATTERN.matcher(template).matches(); - } - - /** - * Gets the error location when parsing as JSON - * - * @param loc {@link JsonLocation} - * @return Error location - */ - @VisibleForTesting - protected static String formatJsonErrorLocation(final JsonLocation loc) { - if (loc == null) return UNKNOWN_LOCATION; - return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLineNr(), loc.getColumnNr()); - } - /** * Gets the error location when parsing as YAML * @@ -166,7 +82,7 @@ protected static String formatJsonErrorLocation(final JsonLocation loc) { * @return Error location */ @VisibleForTesting - protected static String formatYamlErrorLocation(final Mark loc) { + protected static String formatTemplateErrorLocation(final Mark loc) { if (loc == null) return UNKNOWN_LOCATION; return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLine() + 1, loc.getColumn() + 1); } diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java index 3623f99..1f946c0 100644 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java @@ -4,8 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static software.amazon.cloudformation.stackset.util.TemplateParser.formatJsonErrorLocation; -import static software.amazon.cloudformation.stackset.util.TemplateParser.formatYamlErrorLocation; +import static software.amazon.cloudformation.stackset.util.TemplateParser.formatTemplateErrorLocation; import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_MAP; @@ -16,14 +15,8 @@ public class TemplateParserTest { @Test public void testDeserializeYaml() { - assertThrows(ParseException.class, () -> TemplateParser.deserializeYaml("null")); - assertThrows(ParseException.class, () -> TemplateParser.deserializeYaml("")); - } - - @Test - public void testDeserializeJson() { - assertThrows(ParseException.class, () -> TemplateParser.deserializeJson("")); - assertThrows(ParseException.class, () -> TemplateParser.deserializeJson("null")); + assertThrows(ParseException.class, () -> TemplateParser.deserializeTemplate("null")); + assertThrows(ParseException.class, () -> TemplateParser.deserializeTemplate("")); } @Test @@ -40,7 +33,6 @@ public void testGetStringFromTemplate() { @Test public void testFormatErrorLocation_IfIsNull() { - assertThat(formatYamlErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); - assertThat(formatJsonErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); + assertThat(formatTemplateErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); } } 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 a0e7d29..840ca88 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 @@ -75,53 +75,65 @@ public class TestUtils { public final static String VALID_YAML_TEMPLATE = "Parameters:\n" + - " DomainName:\n" + - " Type: String\n" + - " Default: myexample.com\n" + - "Resources:\n" + - " BasicHealthCheck:\n" + - " Type: AWS::Route53::HealthCheck\n" + - " Properties:\n" + - " HealthCheckConfig:\n" + - " RequestInterval: 10\n" + - " FullyQualifiedDomainName:\n" + - " Ref: DomainName\n" + - " IPAddress: 98.139.180.149\n" + - " Port: \"88\"\n" + - " ResourcePath: /docs/route-53-health-check.html\n" + - " Type: HTTP\n" + - " HealthCheckTags:\n" + - " - Key: A\n" + - " Value: \"1\"\n" + - " - Key: B\n" + - " Value: \"1\"\n" + - " - Key: C\n" + - " Value: \"1\""; + " DomainName:\n" + + " Type: String\n" + + " Default: myexample.com\n" + + "Resources:\n" + + " BasicHealthCheck:\n" + + " Type: AWS::Route53::HealthCheck\n" + + " Properties:\n" + + " HealthCheckConfig:\n" + + " RequestInterval: 10\n" + + " FullyQualifiedDomainName:\n" + + " Ref: DomainName\n" + + " IPAddress: 98.139.180.149\n" + + " Port: \"88\"\n" + + " ResourcePath: /docs/route-53-health-check.html\n" + + " Type: HTTP\n" + + " HealthCheckTags:\n" + + " - Key: A\n" + + " Value: \"1\"\n" + + " - Key: B\n" + + " Value: \"1\"\n" + + " - Key: C\n" + + " Value: \"1\""; + + public final static String VALID_YAML_SHORTHANDS_TEMPLATE = + "Resources:\n" + + " MyCodeDeploy:\n" + + " Type: AWS::CodeDeploy::DeploymentGroup\n" + + " Properties:\n" + + " ApplicationName: !Ref CodeDeployApplication\n" + + " AutoScalingGroups:\n" + + " - !Ref AutoScalingGroup\n" + + " DeploymentConfigName: CodeDeployDefault.OneAtATime\n" + + " DeploymentGroupName: !Sub ${EnvironmentParameter}_${ServiceNameParameter}\n" + + " ServiceRoleArn: !GetAtt CodeDeployRole.Arn"; public final static String INVALID_EMBEDDED_STACK_TEMPLATE = "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " },\n" + - " }\n" + - "}"; + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " },\n" + + " }\n" + + "}"; public final static String INVALID_EMBEDDED_STACKSET_TEMPLATE = "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; + " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + + " \"Resources\": {\n" + + " \"MyStack\" : {\n" + + " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + + " \"Properties\" : {\n" + + " \"TemplateURL\" : \"test.url\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; public final static String STACK_SET_NAME = "StackSet"; public final static String STACK_SET_ID = "StackSet:stack-set-id"; 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 278ca36..7e6ed29 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 @@ -27,6 +27,7 @@ import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; 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.VALID_YAML_SHORTHANDS_TEMPLATE; import static software.amazon.cloudformation.stackset.util.TestUtils.VALID_YAML_TEMPLATE; @ExtendWith(MockitoExtension.class) @@ -115,6 +116,7 @@ public void testValidateTemplate_InvalidTemplate() { public void testValidateTemplate_ValidTemplateBody() { assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); + assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_SHORTHANDS_TEMPLATE, null, logger)); } @Test From 37b845573c5254819ecef17fefb9f1312153bb8f Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Thu, 14 May 2020 17:19:45 -0700 Subject: [PATCH 13/19] Make StackSetName as Required and CreateOnlyProperty --- .../aws-cloudformation-stackset.json | 11 ++++++++- aws-cloudformation-stackset/docs/README.md | 18 +++++++++++++- .../stackset/CreateHandler.java | 4 +--- .../translator/RequestTranslator.java | 4 ++-- .../stackset/util/Validator.java | 24 ------------------- .../stackset/util/ValidatorTest.java | 21 ---------------- 6 files changed, 30 insertions(+), 52 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 0782010..f12f6da 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -178,6 +178,12 @@ } }, "properties": { + "StackSetName": { + "description": "The name to associate with the stack set. The name must be unique in the Region where you create your stack set.", + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9\\-]{0,127}$", + "maxLength": 128 + }, "StackSetId": { "description": "The ID of the stack set that you're creating.", "type": "string" @@ -269,19 +275,22 @@ { "required": [ "TemplateURL", + "StackSetName", "PermissionModel" ] }, { "required": [ "TemplateBody", + "StackSetName", "PermissionModel" ] } ], "additionalProperties": false, "createOnlyProperties": [ - "/properties/PermissionModel" + "/properties/PermissionModel", + "/properties/StackSetName" ], "writeOnlyProperties": [ "/properties/TemplateURL", diff --git a/aws-cloudformation-stackset/docs/README.md b/aws-cloudformation-stackset/docs/README.md index d721f36..2840b1d 100644 --- a/aws-cloudformation-stackset/docs/README.md +++ b/aws-cloudformation-stackset/docs/README.md @@ -12,6 +12,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy { "Type" : "AWS::CloudFormation::StackSet", "Properties" : { + "StackSetName" : String, "AdministrationRoleARN" : String, "AutoDeployment" : AutoDeployment, "Capabilities" : [ String, ... ], @@ -33,6 +34,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy
     Type: AWS::CloudFormation::StackSet
     Properties:
    +    StackSetName: String
         AdministrationRoleARN: String
         AutoDeployment: AutoDeployment
         Capabilities: 
    @@ -53,6 +55,20 @@ Properties:
     
     ## Properties
     
    +#### StackSetName
    +
    +The name to associate with the stack set. The name must be unique in the Region where you create your stack set.
    +
    +_Required_: Yes
    +
    +_Type_: String
    +
    +_Maximum_: 128
    +
    +_Pattern_: ^[a-zA-Z][a-zA-Z0-9\-]{0,127}$
    +
    +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
    +
     #### AdministrationRoleARN
     
     The Amazon Resource Number (ARN) of the IAM role to use to create this stack set. Specify an IAM role only if you are using customized administrator roles to control which users or groups can manage specific stack sets within the same administrator account.
    @@ -145,7 +161,7 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati
     
     Describes how the IAM roles required for stack set operations are created. By default, SELF-MANAGED is specified.
     
    -_Required_: No
    +_Required_: Yes
     
     _Type_: String
     
    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 7739a34..0f74e07 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
    @@ -25,13 +25,11 @@ protected ProgressEvent handleRequest(
     
             this.logger = logger;
             final ResourceModel model = request.getDesiredResourceState();
    -        // Ensure the idempotency of StackSet, we should not generate a random StackSetName
    -        final String stackSetName = request.getLogicalResourceIdentifier();
             final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder();
             analyzeTemplate(proxy, request, placeHolder, logger, Action.CREATE);
     
             return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext)
    -                .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, stackSetName, request.getClientRequestToken()))
    +                .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, request.getClientRequestToken()))
                     .makeServiceCall((modelRequest, proxyInvocation) -> {
                         final CreateStackSetResponse response = proxyClient.injectCredentialsAndInvokeV2(modelRequest, proxyClient.client()::createStackSet);
                         model.setStackSetId(response.stackSetId());
    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 cc38e94..6ed9009 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
    @@ -28,9 +28,9 @@ public class RequestTranslator {
         private static int LIST_MAX_ITEMS = 100;
     
         public static CreateStackSetRequest createStackSetRequest(
    -            final ResourceModel model, final String stackSetName, final String requestToken) {
    +            final ResourceModel model, final String requestToken) {
             return CreateStackSetRequest.builder()
    -                .stackSetName(stackSetName)
    +                .stackSetName(model.getStackSetName())
                     .administrationRoleARN(model.getAdministrationRoleARN())
                     .autoDeployment(translateToSdkAutoDeployment(model.getAutoDeployment()))
                     .clientRequestToken(requestToken)
    diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java
    index 170c23d..b7b90dc 100644
    --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java
    +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java
    @@ -11,7 +11,6 @@
     
     import java.nio.charset.StandardCharsets;
     import java.util.Map;
    -import java.util.regex.Pattern;
     
     import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getObjectRequest;
     import static software.amazon.cloudformation.stackset.translator.RequestTranslator.headObjectRequest;
    @@ -24,10 +23,6 @@
      */
     public class Validator {
     
    -    // A stack name can contain only alphanumeric characters (case-sensitive) and hyphens.
    -    // It must start with an alphabetic character and can't be longer than 128 characters.
    -    private static final Pattern STACKSET_NAME_FORMAT = Pattern.compile("^[a-zA-Z][a-zA-Z0-9\\-]{0,127}$");
    -
         private static final String TEMPLATE_RESOURCE_TYPE_KEY = "Type";
         private static final String TEMPLATE_RESOURCES_KEY = "Resources";
         private static final String TEMPLATE_PARAMETERS_KEY = "Parameters";
    @@ -167,23 +162,4 @@ public void validateTemplate(
                 throw new CfnInvalidRequestException(e.getMessage());
             }
         }
    -
    -    /**
    -     * Validates StackSetName and CfnInvalidRequestException will be thrown if StackSetName does not meet:
    -     * 
      - *
    • Contains only alphanumeric characters (case-sensitive) and hyphens. - *
    • Starts with an alphabetic character - *
    • Length is no longer than 128 characters - *
    - * https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStackSet.html - * - * @param stackSetName - */ - public void validateStackSetName(final String stackSetName) { - if (!STACKSET_NAME_FORMAT.matcher(stackSetName).matches()) { - throw new CfnInvalidRequestException( - "A stack name can contain only alphanumeric characters (case-sensitive) and hyphens. " + - "It must start with an alphabetic character and can't be longer than 128 characters."); - } - } } 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 7e6ed29..eee9112 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 @@ -37,12 +37,6 @@ public class ValidatorTest { "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"); - private static final List VALID_STACK_SET_NAMES = Arrays.asList( - "myStackSet", "myStackSet123", "MyStack-Set", Strings.repeat("a", 128)); - - private static final List INVALID_STACK_SET_NAMES = Arrays.asList( - "123myStackSet", "myStackSet!", "MyStack_Set", Strings.repeat("a", 129)); - private static final long VALID_TEMPLATE_SIZE = 1000L; private static final long INVALID_TEMPLATE_SIZE = 460801L; @@ -118,19 +112,4 @@ public void testValidateTemplate_ValidTemplateBody() { assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_SHORTHANDS_TEMPLATE, null, logger)); } - - @Test - public void testValidateStackSetName_ValidStackSetNames() { - for (final String validStackSetName : VALID_STACK_SET_NAMES) { - assertDoesNotThrow(() -> validator.validateStackSetName(validStackSetName)); - } - } - - @Test - public void testValidateStackSetName_InValidStackSetNames() { - for (final String invalidStackSetName : INVALID_STACK_SET_NAMES) { - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateStackSetName(invalidStackSetName)); - } - } } From a88d9ab326f0c9e689b117a2a2e0cc55ac01d1b8 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 15 May 2020 11:17:58 -0700 Subject: [PATCH 14/19] Add map/seq node support for TemplateConstructor --- .../stackset/util/InstancesAnalyzer.java | 4 +- .../stackset/util/TemplateConstructor.java | 47 ++++++++++++++++++- .../stackset/util/TestUtils.java | 12 +++-- 3 files changed, 56 insertions(+), 7 deletions(-) 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 index 32e6dc6..518f206 100644 --- 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 @@ -41,7 +41,7 @@ public class InstancesAnalyzer { * @return {@link StackInstances} set */ public static Set aggregateStackInstances( - @NonNull final Set flatStackInstances, final boolean isSelfManaged) { + final Set flatStackInstances, final boolean isSelfManaged) { final Set groupedStacks = groupInstancesByTargets(flatStackInstances, isSelfManaged); return aggregateInstancesByRegions(groupedStacks, isSelfManaged); } @@ -55,7 +55,7 @@ public static Set aggregateStackInstances( * @param flatStackInstances {@link StackInstance} * @return {@link StackInstances} set */ - public static Set aggregateStackInstancesForRead(@NonNull final Set flatStackInstances) { + public static Set aggregateStackInstancesForRead(final Set flatStackInstances) { final Set groupedStacksInstances = groupInstancesByTargets(flatStackInstances, true); return aggregateInstancesByRegions(groupedStacksInstances, true); } diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java index 8010dd4..37202e7 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java @@ -1,9 +1,18 @@ package software.amazon.cloudformation.stackset.util; +import org.yaml.snakeyaml.constructor.AbstractConstruct; import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.resolver.Resolver; +import software.amazon.awssdk.utils.StringUtils; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; public final class TemplateConstructor extends SafeConstructor { @@ -17,9 +26,45 @@ public final class TemplateConstructor extends SafeConstructor { "Ref", "Fn::RefAll", "Fn::Select", "Fn::Split", "Fn::Sub", "Fn::ValueOf", "Fn::ValueOfAll", "Fn::Cidr"); + private final Resolver resolver = new Resolver(); + TemplateConstructor() { + this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlStr()); for (final String token : FUNCTION_KEYS) { - this.yamlConstructors.put(new Tag("!" + stripFn(token)), new ConstructYamlStr()); + this.yamlConstructors.put(new Tag("!" + stripFn(token)), new AbstractConstruct() { + + @Override + public Object construct(Node node) { + final LinkedHashMap retVal = new LinkedHashMap<>(2); + retVal.put(token, constructObject(getDelegateNode(node))); + return retVal; + } + + private Node getDelegateNode(Node node) { + if (node instanceof ScalarNode) { + final Tag nodeTag; + String nodeValue = ((ScalarNode) node).getValue(); + + if (nodeValue != null && StringUtils.isEmpty(nodeValue)) { + nodeTag = Tag.STR; + } else { + nodeTag = resolver.resolve(NodeId.scalar, nodeValue, true); + } + + return new ScalarNode(nodeTag, nodeValue, node.getStartMark(), + node.getEndMark(), ((ScalarNode) node).getScalarStyle()); + } + if (node instanceof SequenceNode) { + return new SequenceNode(Tag.SEQ, true, ((SequenceNode) node).getValue(), + node.getStartMark(), node.getEndMark(), ((SequenceNode) node).getFlowStyle()); + } + if (node instanceof MappingNode) { + return new MappingNode(Tag.MAP, true, ((MappingNode) node).getValue(), + node.getStartMark(), node.getEndMark(), ((MappingNode) node).getFlowStyle()); + } + throw new ParseException("Invalid node type for tag " + node.getTag().toString()); + } + }); } } 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 840ca88..36f3368 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 @@ -99,15 +99,19 @@ public class TestUtils { " Value: \"1\""; public final static String VALID_YAML_SHORTHANDS_TEMPLATE = + "Conditions:\n" + + " Fn::Equals:\n" + + " - !Ref\n" + + " - !Select [1, [Foo, Bar]]\n" + "Resources:\n" + " MyCodeDeploy:\n" + - " Type: AWS::CodeDeploy::DeploymentGroup\n" + + " Type: AWS::Test::Test\n" + " Properties:\n" + - " ApplicationName: !Ref CodeDeployApplication\n" + + " TestTrue: !And true\n" + " AutoScalingGroups:\n" + " - !Ref AutoScalingGroup\n" + - " DeploymentConfigName: CodeDeployDefault.OneAtATime\n" + - " DeploymentGroupName: !Sub ${EnvironmentParameter}_${ServiceNameParameter}\n" + + " TestMappingNode: !GetAtt { 1: Foo }\n" + + " DeploymentGroupName: !Sub ${Test}_${Test}\n" + " ServiceRoleArn: !GetAtt CodeDeployRole.Arn"; public final static String INVALID_EMBEDDED_STACK_TEMPLATE = From 2ab268d934cc9e621b9f18069e9fa1141c0d76ae Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Mon, 18 May 2020 14:03:32 -0700 Subject: [PATCH 15/19] Add permission for headobject and return empty instead of null for sdk tags and parameters --- .../aws-cloudformation-stackset.json | 6 ++++-- aws-cloudformation-stackset/resource-role.yaml | 1 + .../stackset/translator/PropertyTranslator.java | 9 ++++++--- .../stackset/translator/PropertyTranslatorTest.java | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index f12f6da..bd0c9d0 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -29,8 +29,8 @@ } }, "required": [ - "Value", - "Key" + "Key", + "Value" ], "additionalProperties": false }, @@ -306,6 +306,7 @@ "create": { "permissions": [ "s3:GetObject", + "s3:HeadObject", "cloudformation:CreateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DescribeStackSetOperation", @@ -323,6 +324,7 @@ "update": { "permissions": [ "s3:GetObject", + "s3:HeadObject", "cloudformation:UpdateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index 4ac9d93..a260e54 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -37,6 +37,7 @@ Resources: - "cloudformation:UpdateStackInstances" - "cloudformation:UpdateStackSet" - "s3:GetObject" + - "s3:HeadObject" Resource: "*" Outputs: ExecutionRoleArn: 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 d596fb3..b636edf 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 @@ -11,6 +11,7 @@ import software.amazon.cloudformation.stackset.util.StackInstance; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -69,7 +70,8 @@ static DeploymentTargets translateToSdkDeploymentTargets( */ static List translateToSdkParameters( final Collection parameters) { - if (parameters == null) return null; + // To remove Parameters from a StackSet or StackSetInstance, set it as an empty list + if (CollectionUtils.isNullOrEmpty(parameters)) return Collections.emptyList(); return parameters.stream() .map(parameter -> Parameter.builder() .parameterKey(parameter.getParameterKey()) @@ -120,8 +122,9 @@ static StackSetOperationPreferences translateToSdkOperationPreferences( * @param tags Tags CFN resource model. * @return SDK Tags. */ - static Collection translateToSdkTags(final Collection tags) { - if (CollectionUtils.isNullOrEmpty(tags)) return null; + static List translateToSdkTags(final Collection tags) { + // To remove Tags from a StackSet, set it as an empty list + if (CollectionUtils.isNullOrEmpty(tags)) return Collections.emptyList(); return tags.stream().map(tag -> Tag.builder() .key(tag.getKey()) .value(tag.getValue()) 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 5486ffb..8e20289 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 @@ -16,7 +16,7 @@ public void test_translateFromSdkParameters_IfIsNull() { @Test public void test_translateToSdkTags_IfIsNull() { - assertThat(translateToSdkTags(null)).isNull(); + assertThat(translateToSdkTags(null)).isEmpty(); } @Test From aa976dc298fcd7e237f97ba9d97804d02c883d52 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Tue, 19 May 2020 18:43:05 -0700 Subject: [PATCH 16/19] Update to use GetTemplateSummary --- .../aws-cloudformation-stackset.json | 6 +- aws-cloudformation-stackset/pom.xml | 11 -- .../resource-role.yaml | 3 +- .../stackset/BaseHandlerStd.java | 10 +- .../stackset/CreateHandler.java | 2 +- .../stackset/DeleteHandler.java | 2 +- .../stackset/UpdateHandler.java | 2 +- .../translator/RequestTranslator.java | 21 +-- .../stackset/util/ClientBuilder.java | 13 -- .../stackset/util/InstancesAnalyzer.java | 1 - .../stackset/util/ParseException.java | 12 -- .../stackset/util/TemplateConstructor.java | 74 --------- .../stackset/util/TemplateParser.java | 89 ----------- .../stackset/util/Validator.java | 148 +++--------------- .../stackset/CreateHandlerTest.java | 47 +++++- .../stackset/UpdateHandlerTest.java | 8 + .../stackset/util/TemplateParserTest.java | 38 ----- .../stackset/util/TestUtils.java | 82 ++-------- .../stackset/util/ValidatorTest.java | 115 -------------- 19 files changed, 97 insertions(+), 587 deletions(-) delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java delete mode 100644 aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java delete mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java delete mode 100644 aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index bd0c9d0..0586b24 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -305,8 +305,7 @@ "handlers": { "create": { "permissions": [ - "s3:GetObject", - "s3:HeadObject", + "cloudformation:GetTemplateSummary", "cloudformation:CreateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DescribeStackSetOperation", @@ -323,8 +322,7 @@ }, "update": { "permissions": [ - "s3:GetObject", - "s3:HeadObject", + "cloudformation:GetTemplateSummary", "cloudformation:UpdateStackSet", "cloudformation:CreateStackInstances", "cloudformation:DeleteStackInstances", diff --git a/aws-cloudformation-stackset/pom.xml b/aws-cloudformation-stackset/pom.xml index 5af7320..44930fc 100644 --- a/aws-cloudformation-stackset/pom.xml +++ b/aws-cloudformation-stackset/pom.xml @@ -54,17 +54,6 @@ snakeyaml 1.26 - - - software.amazon.awssdk - s3 - - - - com.amazonaws - aws-java-sdk-s3 - 1.11.759 - org.projectlombok diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml index a260e54..f4b5d4e 100644 --- a/aws-cloudformation-stackset/resource-role.yaml +++ b/aws-cloudformation-stackset/resource-role.yaml @@ -30,14 +30,13 @@ Resources: - "cloudformation:DescribeStackInstance" - "cloudformation:DescribeStackSet" - "cloudformation:DescribeStackSetOperation" + - "cloudformation:GetTemplateSummary" - "cloudformation:ListStackInstances" - "cloudformation:ListStackSets" - "cloudformation:TagResource" - "cloudformation:UntagResource" - "cloudformation:UpdateStackInstances" - "cloudformation:UpdateStackSet" - - "s3:GetObject" - - "s3:HeadObject" Resource: "*" Outputs: ExecutionRoleArn: 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 index 15f846d..dfd3c75 100644 --- 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 @@ -285,17 +285,15 @@ protected boolean isOperationStabilized(final ProxyClient /** * Analyzes/validates template and StackInstancesGroup * - * @param proxy {@link AmazonWebServicesClientProxy} + * @param proxyClient the aws service client {@link ProxyClient} to make the call * @param request {@link ResourceHandlerRequest} * @param placeHolder {@link StackInstancesPlaceHolder} - * @param logger {@link Logger} * @param action {@link Action} */ protected void analyzeTemplate( - final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, final ResourceHandlerRequest request, final StackInstancesPlaceHolder placeHolder, - final Logger logger, final Action action) { final ResourceModel desiredModel = request.getDesiredResourceState(); @@ -303,11 +301,11 @@ protected void analyzeTemplate( switch (action) { case CREATE: - new Validator().validateTemplate(proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger); + new Validator().validateTemplate(proxyClient, desiredModel.getTemplateBody(), desiredModel.getTemplateURL()); InstancesAnalyzer.builder().desiredModel(desiredModel).build().analyzeForCreate(placeHolder); break; case UPDATE: - new Validator().validateTemplate(proxy, desiredModel.getTemplateBody(), desiredModel.getTemplateURL(), logger); + new Validator().validateTemplate(proxyClient, desiredModel.getTemplateBody(), desiredModel.getTemplateURL()); InstancesAnalyzer.builder().desiredModel(desiredModel).previousModel(previousModel).build().analyzeForUpdate(placeHolder); break; case DELETE: 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 0f74e07..2fc896b 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 @@ -26,7 +26,7 @@ protected ProgressEvent handleRequest( this.logger = logger; final ResourceModel model = request.getDesiredResourceState(); final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - analyzeTemplate(proxy, request, placeHolder, logger, Action.CREATE); + analyzeTemplate(proxyClient, request, placeHolder, Action.CREATE); return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext) .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, request.getClientRequestToken())) 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 816dfaf..d090054 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 @@ -27,7 +27,7 @@ protected ProgressEvent handleRequest( final ResourceModel model = request.getDesiredResourceState(); // Analyzes stack instances group for delete final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - analyzeTemplate(proxy, request, placeHolder, logger, Action.DELETE); + analyzeTemplate(proxyClient, request, placeHolder, Action.DELETE); return ProgressEvent.progress(model, callbackContext) // delete/stabilize progress chain - delete all associated stack instances 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 df164e2..ffa1381 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 @@ -29,7 +29,7 @@ protected ProgressEvent handleRequest( final ResourceModel model = request.getDesiredResourceState(); final ResourceModel previousModel = request.getPreviousResourceState(); final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder(); - analyzeTemplate(proxy, request, placeHolder, logger, Action.UPDATE); + analyzeTemplate(proxyClient, request, placeHolder, Action.UPDATE); return ProgressEvent.progress(model, callbackContext) .then(progress -> updateStackSet(proxy, proxyClient, progress, previousModel)) 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 6ed9009..35b6614 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 @@ -7,12 +7,11 @@ 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.GetTemplateSummaryRequest; 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.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.cloudformation.stackset.OperationPreferences; import software.amazon.cloudformation.stackset.ResourceModel; import software.amazon.cloudformation.stackset.StackInstances; @@ -145,19 +144,11 @@ public static DescribeStackSetOperationRequest describeStackSetOperationRequest( .build(); } - public static GetObjectRequest getObjectRequest( - final String bucketName, final String key) { - return GetObjectRequest.builder() - .bucket(bucketName) - .key(key) - .build(); - } - - public static HeadObjectRequest headObjectRequest( - final String bucketName, final String key) { - return HeadObjectRequest.builder() - .bucket(bucketName) - .key(key) + public static GetTemplateSummaryRequest getTemplateSummaryRequest( + final String templateBody, final String templateUrl) { + return GetTemplateSummaryRequest.builder() + .templateBody(templateBody) + .templateURL(templateUrl) .build(); } } 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 c1dd84e..5c461c9 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 @@ -1,7 +1,6 @@ package software.amazon.cloudformation.stackset.util; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.s3.S3Client; import software.amazon.cloudformation.LambdaWrapper; public class ClientBuilder { @@ -13,18 +12,6 @@ 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 - * - * @return {@link S3Client} - */ - public static S3Client getS3Client() { - return S3Client.builder() - .httpClient(LambdaWrapper.HTTP_CLIENT) - .build(); - } - /** * Get CloudFormationClient for requests to interact with StackSet client * 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 index 518f206..e096877 100644 --- 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 @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Data; -import lombok.NonNull; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.stackset.DeploymentTargets; diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java deleted file mode 100644 index 7a66a21..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ParseException.java +++ /dev/null @@ -1,12 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -/** - * Custom Exception Class to hold exception when parsing templates - */ -public class ParseException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public ParseException(final String message) { - super(message); - } -} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java deleted file mode 100644 index 37202e7..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateConstructor.java +++ /dev/null @@ -1,74 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import org.yaml.snakeyaml.constructor.AbstractConstruct; -import org.yaml.snakeyaml.constructor.SafeConstructor; -import org.yaml.snakeyaml.nodes.MappingNode; -import org.yaml.snakeyaml.nodes.Node; -import org.yaml.snakeyaml.nodes.NodeId; -import org.yaml.snakeyaml.nodes.ScalarNode; -import org.yaml.snakeyaml.nodes.SequenceNode; -import org.yaml.snakeyaml.nodes.Tag; -import org.yaml.snakeyaml.resolver.Resolver; -import software.amazon.awssdk.utils.StringUtils; - -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; - -public final class TemplateConstructor extends SafeConstructor { - - private final static String FUNCTION_PREFIX = "Fn::"; - private final static List FUNCTION_KEYS = Arrays.asList( - "Fn::And", "Fn::Base64", "Condition", "Fn::Contains", - "Fn::EachMemberEquals", "Fn::EachMemberIn", "Fn::Equals", - "Fn::FindInMap", "Fn::GetAtt", "Fn::GetAZs", "Fn::If", - "Fn::ImportValue", "Fn::Join", "Fn::Not", "Fn::Or", - "Ref", "Fn::RefAll", "Fn::Select", "Fn::Split", "Fn::Sub", - "Fn::ValueOf", "Fn::ValueOfAll", "Fn::Cidr"); - - private final Resolver resolver = new Resolver(); - - TemplateConstructor() { - this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlStr()); - for (final String token : FUNCTION_KEYS) { - this.yamlConstructors.put(new Tag("!" + stripFn(token)), new AbstractConstruct() { - - @Override - public Object construct(Node node) { - final LinkedHashMap retVal = new LinkedHashMap<>(2); - retVal.put(token, constructObject(getDelegateNode(node))); - return retVal; - } - - private Node getDelegateNode(Node node) { - if (node instanceof ScalarNode) { - final Tag nodeTag; - String nodeValue = ((ScalarNode) node).getValue(); - - if (nodeValue != null && StringUtils.isEmpty(nodeValue)) { - nodeTag = Tag.STR; - } else { - nodeTag = resolver.resolve(NodeId.scalar, nodeValue, true); - } - - return new ScalarNode(nodeTag, nodeValue, node.getStartMark(), - node.getEndMark(), ((ScalarNode) node).getScalarStyle()); - } - if (node instanceof SequenceNode) { - return new SequenceNode(Tag.SEQ, true, ((SequenceNode) node).getValue(), - node.getStartMark(), node.getEndMark(), ((SequenceNode) node).getFlowStyle()); - } - if (node instanceof MappingNode) { - return new MappingNode(Tag.MAP, true, ((MappingNode) node).getValue(), - node.getStartMark(), node.getEndMark(), ((MappingNode) node).getFlowStyle()); - } - throw new ParseException("Invalid node type for tag " + node.getTag().toString()); - } - }); - } - } - - private static String stripFn(String keyword) { - return keyword.startsWith(FUNCTION_PREFIX) ? keyword.substring(4) : keyword; - } -} 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 deleted file mode 100644 index b53b03a..0000000 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/TemplateParser.java +++ /dev/null @@ -1,89 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import com.google.common.annotations.VisibleForTesting; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.error.Mark; -import org.yaml.snakeyaml.error.MarkedYAMLException; -import org.yaml.snakeyaml.error.YAMLException; - -import java.util.Map; - -public class TemplateParser { - - private static final String UNKNOWN_LOCATION = "unknown location"; - private static final String INVALID_TEMPLATE_ERROR_MSG = "Template format error: not a valid template"; - private static final String NOT_WELL_FORMATTED_ERROR_MSG = "Template format error: not well-formed. (%s)"; - private static final String FORMAT_LOCATION_ERROR_MSG = "line %s, column %s"; - - /** - * Gets a Generic Map object from template - * - * @param templateMap Template Map - * @param key Key of the Map we are retrieving - * @return Generic Map object - */ - @SuppressWarnings("unchecked") - public static Map getMapFromTemplate(final Map templateMap, final String key) { - final Object value = templateMap.get(key); - if (value == null) return null; - if (value instanceof Map) { - return (Map) value; - } - throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); - } - - /** - * Gets String from the passed in value - * - * @param value - * @return String - */ - public static String getStringFromTemplate(final Object value) { - if (value == null) return null; - if (value instanceof String) { - return (String) value; - } - throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); - } - - /** - * Deserializes YAML/JSON from template content string, - * Since Yaml is a superset of JSON, we are parsing both with YAML library. - * - * @param templateString Template content - * @return Template map - * @throws ParseException if fails to parse the template - */ - public static Map deserializeTemplate(final String templateString) { - try { - final Map template = new Yaml(new TemplateConstructor()).load(templateString); - if (template == null || template.isEmpty()) { - throw new ParseException(INVALID_TEMPLATE_ERROR_MSG); - } - return template; - - } catch (final MarkedYAMLException e) { - throw new ParseException(String.format(NOT_WELL_FORMATTED_ERROR_MSG, - formatTemplateErrorLocation(e.getProblemMark()))); - - } catch (final YAMLException e) { - throw new ParseException(String.format("Cannot parse the template : %s ", e.getMessage())); - - } catch (final ClassCastException e) { - throw new ParseException("Template format error: unsupported structure."); - - } - } - - /** - * Gets the error location when parsing as YAML - * - * @param loc {@link Mark} - * @return Error location - */ - @VisibleForTesting - protected static String formatTemplateErrorLocation(final Mark loc) { - if (loc == null) return UNKNOWN_LOCATION; - return String.format(FORMAT_LOCATION_ERROR_MSG, loc.getLine() + 1, loc.getColumn() + 1); - } -} diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java index b7b90dc..ef8c43b 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -1,126 +1,27 @@ package software.amazon.cloudformation.stackset.util; -import com.amazonaws.services.s3.AmazonS3URI; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.GetTemplateSummaryResponse; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProxyClient; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getObjectRequest; -import static software.amazon.cloudformation.stackset.translator.RequestTranslator.headObjectRequest; -import static software.amazon.cloudformation.stackset.util.TemplateParser.deserializeTemplate; -import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; -import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getTemplateSummaryRequest; /** * Utility class to validate properties in {@link software.amazon.cloudformation.stackset.ResourceModel} */ public class Validator { - private static final String TEMPLATE_RESOURCE_TYPE_KEY = "Type"; - private static final String TEMPLATE_RESOURCES_KEY = "Resources"; - private static final String TEMPLATE_PARAMETERS_KEY = "Parameters"; - - private static final long TEMPLATE_CONTENT_LIMIT = 460800L; - - - /** - * Validates the template to make sure: - *
      - *
    • Template can be deserialized successfully - *
    • Resources block doesn't have embedded Stack or StackSet - *
    • Parameters block doesn't have embedded Stack or StackSet - *
    - * - * @param content Template content - */ - private static void validateTemplate(final String content) { - final Map template = deserializeTemplate(content); - validateBlocks(template, TEMPLATE_RESOURCES_KEY); - validateBlocks(template, TEMPLATE_PARAMETERS_KEY); - } - - /** - * Validates items in the block do not have any invalid resources - * - * @param templateMap Templates map - * @param block Block key, i.e. Resources - */ - @SuppressWarnings("unchecked") - private static void validateBlocks(final Map templateMap, final String block) { - final Map resourcesMap = (Map) templateMap.get(block); - - if (CollectionUtils.isNullOrEmpty(resourcesMap)) return; - for (final Map.Entry entry : resourcesMap.entrySet()) { - final String resourceId = entry.getKey(); - final Map resourceMap = getMapFromTemplate(resourcesMap, resourceId); - validateResource(resourceMap); - } - } - /** * Embedded Stack or StackSet is not allowed * - * @param resourceMap Resource map + * @param type Resource type */ - private static void validateResource(final Map resourceMap) { - final String type = getStringFromTemplate(resourceMap.get(TEMPLATE_RESOURCE_TYPE_KEY)); - if (type != null) { - switch (type) { - case "AWS::CloudFormation::Stack": - case "AWS::CloudFormation::StackSet": - throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); - } - } - } - - /** - * Gets template content from s3 bucket - * - * @param proxy {@link AmazonWebServicesClientProxy} - * @param templateLocation Template URL - * @return Template content from S3 object - */ - @VisibleForTesting - protected String getUrlContent(final AmazonWebServicesClientProxy proxy, final String templateLocation) { - final AmazonS3URI s3Uri = new AmazonS3URI(templateLocation, true); - final S3Client client = ClientBuilder.getS3Client(); - - final Long contentLength = proxy.injectCredentialsAndInvokeV2( - headObjectRequest(s3Uri.getBucket(), s3Uri.getKey()), client::headObject).contentLength(); - - if (contentLength > TEMPLATE_CONTENT_LIMIT) { - throw new CfnInvalidRequestException(String.format("TemplateBody may not exceed the limit %d Bytes", - TEMPLATE_CONTENT_LIMIT)); - } - - final String content = proxy.injectCredentialsAndInvokeV2Bytes( - getObjectRequest(s3Uri.getBucket(), s3Uri.getKey()), - ClientBuilder.getS3Client()::getObjectAsBytes).asString(StandardCharsets.UTF_8); - - return content; - } - - /** - * Validates template url is valid S3 URL - * - * @param s3Uri Template URL - */ - @VisibleForTesting - protected void validateS3Uri(final String s3Uri) { - try { - final AmazonS3URI validS3Uri = new AmazonS3URI(s3Uri, true); - if (Strings.isNullOrEmpty(validS3Uri.getBucket()) || Strings.isNullOrEmpty(validS3Uri.getKey())) { - throw new CfnInvalidRequestException("Both S3 bucket and key must be specified"); - } - } catch (final IllegalArgumentException | IllegalStateException | StringIndexOutOfBoundsException e) { - throw new CfnInvalidRequestException("S3 URL is not valid"); + private static void validateResource(final String type) { + switch (type) { + case "AWS::CloudFormation::Stack": + case "AWS::CloudFormation::StackSet": + throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); } } @@ -132,34 +33,21 @@ protected void validateS3Uri(final String s3Uri) { *
  • Template contents must be valid * * - * @param proxy {@link AmazonWebServicesClientProxy} + * @param proxyClient {@link ProxyClient < CloudFormationClient >} * @param templateBody {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateBody} * @param templateLocation {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL} - * @param logger {@link Logger} * @throws CfnInvalidRequestException if template is not valid */ public void validateTemplate( - final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, final String templateBody, - final String templateLocation, - final Logger logger) { - - if (Strings.isNullOrEmpty(templateBody) == Strings.isNullOrEmpty(templateLocation)) { - throw new CfnInvalidRequestException("Exactly one of TemplateBody or TemplateUrl must be specified"); - } - String content = null; - try { - if (!Strings.isNullOrEmpty(templateLocation)) { - validateS3Uri(templateLocation); - content = getUrlContent(proxy, templateLocation); - } else { - content = templateBody; - } - validateTemplate(content); + final String templateLocation) { - } catch (final ParseException e) { - logger.log(String.format("Failed to parse template content: %s", content)); - throw new CfnInvalidRequestException(e.getMessage()); + final GetTemplateSummaryResponse response = proxyClient.injectCredentialsAndInvokeV2( + getTemplateSummaryRequest(templateBody, templateLocation), + proxyClient.client()::getTemplateSummary); + if (response.hasResourceTypes()) { + response.resourceTypes().forEach(Validator::validateResource); } } } 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 3fd849a..e706ce3 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 @@ -11,6 +11,7 @@ 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.GetTemplateSummaryRequest; import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -37,6 +38,7 @@ 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.INVALID_TEMPLATE_SUMMARY_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_EMPTY_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_ONE_INSTANCES_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_RESPONSE; @@ -55,6 +57,7 @@ import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.VALID_TEMPLATE_SUMMARY_RESPONSE; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { @@ -85,6 +88,8 @@ public void handleRequest_ServiceManagedSS_SimpleSuccess() { .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) .thenReturn(CREATE_STACK_SET_RESPONSE); when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) @@ -112,6 +117,7 @@ public void handleRequest_ServiceManagedSS_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); @@ -129,6 +135,8 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) .thenReturn(CREATE_STACK_SET_RESPONSE); when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) @@ -156,6 +164,7 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); verify(proxyClient.client(), times(2)).createStackInstances(any(CreateStackInstancesRequest.class)); verify(proxyClient.client(), times(2)).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); @@ -173,6 +182,8 @@ public void handleRequest_SelfManagedSS_NoInstances_SimpleSuccess() { .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) .thenReturn(CREATE_STACK_SET_RESPONSE); when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) @@ -191,6 +202,7 @@ public void handleRequest_SelfManagedSS_NoInstances_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); @@ -204,7 +216,8 @@ public void handleRequest_SelfManagedSS_OneInstances_SimpleSuccess() { .logicalResourceIdentifier(LOGICAL_ID) .clientRequestToken(REQUEST_TOKEN) .build(); - + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) .thenReturn(CREATE_STACK_SET_RESPONSE); when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) @@ -230,6 +243,7 @@ public void handleRequest_SelfManagedSS_OneInstances_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); @@ -247,6 +261,8 @@ public void handlerRequest_OperationStoppedError() { .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().createStackSet(any(CreateStackSetRequest.class))) .thenReturn(CREATE_STACK_SET_RESPONSE); when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) @@ -262,11 +278,30 @@ public void handlerRequest_OperationStoppedError() { assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getErrorCode()).isEqualTo(InternalFailure); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).createStackSet(any(CreateStackSetRequest.class)); verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); verify(proxyClient.client()).describeStackSetOperation(any(DescribeStackSetOperationRequest.class)); } + @Test + public void handlerRequest_CfnInvalidRequestException_NestedStack() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_DUPLICATE_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(INVALID_TEMPLATE_SUMMARY_RESPONSE); + + assertThrows(CfnInvalidRequestException.class, + () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); + } + @Test public void handlerRequest_CfnInvalidRequestException_DuplicateStackInstance() { @@ -276,8 +311,13 @@ public void handlerRequest_CfnInvalidRequestException_DuplicateStackInstance() { .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); + assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); } @Test @@ -289,7 +329,12 @@ public void handlerRequest_CfnInvalidRequestException_InvalidDeploymentTargets() .clientRequestToken(REQUEST_TOKEN) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); + assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); } } 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 d91ac6f..0d838e5 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 @@ -11,6 +11,7 @@ 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.GetTemplateSummaryRequest; import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.UpdateStackInstancesRequest; import software.amazon.awssdk.services.cloudformation.model.UpdateStackSetRequest; @@ -43,6 +44,7 @@ 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.TestUtils.VALID_TEMPLATE_SUMMARY_RESPONSE; @ExtendWith(MockitoExtension.class) public class UpdateHandlerTest extends AbstractTestBase { @@ -72,6 +74,8 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { .previousResourceState(SELF_MANAGED_MODEL) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().updateStackSet(any(UpdateStackSetRequest.class))) .thenReturn(UPDATE_STACK_SET_RESPONSE); when(proxyClient.client().createStackInstances(any(CreateStackInstancesRequest.class))) @@ -103,6 +107,7 @@ public void handleRequest_SelfManagedSS_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).updateStackSet(any(UpdateStackSetRequest.class)); verify(proxyClient.client()).createStackInstances(any(CreateStackInstancesRequest.class)); verify(proxyClient.client()).updateStackInstances(any(UpdateStackInstancesRequest.class)); @@ -121,6 +126,8 @@ public void handleRequest_NotUpdatable_Success() { .previousResourceState(SIMPLE_MODEL) .build(); + when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) + .thenReturn(VALID_TEMPLATE_SUMMARY_RESPONSE); when(proxyClient.client().describeStackSet(any(DescribeStackSetRequest.class))) .thenReturn(DESCRIBE_SELF_MANAGED_STACK_SET_RESPONSE); when(proxyClient.client().listStackInstances(any(ListStackInstancesRequest.class))) @@ -143,6 +150,7 @@ public void handleRequest_NotUpdatable_Success() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); verify(proxyClient.client(), times(4)).describeStackInstance(any(DescribeStackInstanceRequest.class)); diff --git a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java deleted file mode 100644 index 1f946c0..0000000 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TemplateParserTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static software.amazon.cloudformation.stackset.util.TemplateParser.formatTemplateErrorLocation; -import static software.amazon.cloudformation.stackset.util.TemplateParser.getMapFromTemplate; -import static software.amazon.cloudformation.stackset.util.TemplateParser.getStringFromTemplate; -import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_MAP; - -public class TemplateParserTest { - - private static final String UNKNOWN_LOCATION = "unknown location"; - - @Test - public void testDeserializeYaml() { - assertThrows(ParseException.class, () -> TemplateParser.deserializeTemplate("null")); - assertThrows(ParseException.class, () -> TemplateParser.deserializeTemplate("")); - } - - @Test - public void testGetMapFromTemplate() { - assertThat(getMapFromTemplate(TEMPLATE_MAP, "null")).isNull(); - assertThrows(ParseException.class, () -> getMapFromTemplate(TEMPLATE_MAP, "TemplateURL")); - } - - @Test - public void testGetStringFromTemplate() { - assertThat(getStringFromTemplate(null)).isNull(); - assertThrows(ParseException.class, () -> getStringFromTemplate(TEMPLATE_MAP)); - } - - @Test - public void testFormatErrorLocation_IfIsNull() { - assertThat(formatTemplateErrorLocation(null)).isEqualTo(UNKNOWN_LOCATION); - } -} 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 36f3368..7b7e2f5 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 @@ -8,6 +8,7 @@ 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.GetTemplateSummaryResponse; import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesResponse; import software.amazon.awssdk.services.cloudformation.model.ListStackSetsResponse; import software.amazon.awssdk.services.cloudformation.model.Parameter; @@ -73,82 +74,11 @@ public class TestUtils { .append(" }\n") .append("}").toString(); - public final static String VALID_YAML_TEMPLATE = - "Parameters:\n" + - " DomainName:\n" + - " Type: String\n" + - " Default: myexample.com\n" + - "Resources:\n" + - " BasicHealthCheck:\n" + - " Type: AWS::Route53::HealthCheck\n" + - " Properties:\n" + - " HealthCheckConfig:\n" + - " RequestInterval: 10\n" + - " FullyQualifiedDomainName:\n" + - " Ref: DomainName\n" + - " IPAddress: 98.139.180.149\n" + - " Port: \"88\"\n" + - " ResourcePath: /docs/route-53-health-check.html\n" + - " Type: HTTP\n" + - " HealthCheckTags:\n" + - " - Key: A\n" + - " Value: \"1\"\n" + - " - Key: B\n" + - " Value: \"1\"\n" + - " - Key: C\n" + - " Value: \"1\""; - - public final static String VALID_YAML_SHORTHANDS_TEMPLATE = - "Conditions:\n" + - " Fn::Equals:\n" + - " - !Ref\n" + - " - !Select [1, [Foo, Bar]]\n" + - "Resources:\n" + - " MyCodeDeploy:\n" + - " Type: AWS::Test::Test\n" + - " Properties:\n" + - " TestTrue: !And true\n" + - " AutoScalingGroups:\n" + - " - !Ref AutoScalingGroup\n" + - " TestMappingNode: !GetAtt { 1: Foo }\n" + - " DeploymentGroupName: !Sub ${Test}_${Test}\n" + - " ServiceRoleArn: !GetAtt CodeDeployRole.Arn"; - - public final static String INVALID_EMBEDDED_STACK_TEMPLATE = - "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::Stack\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " },\n" + - " }\n" + - "}"; - - public final static String INVALID_EMBEDDED_STACKSET_TEMPLATE = - "{\n" + - " \"AWSTemplateFormatVersion\": \"2010-09-09\",\n" + - " \"Resources\": {\n" + - " \"MyStack\" : {\n" + - " \"Type\" : \"AWS::CloudFormation::StackSet\",\n" + - " \"Properties\" : {\n" + - " \"TemplateURL\" : \"test.url\"\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; - public final static String STACK_SET_NAME = "StackSet"; public final static String STACK_SET_ID = "StackSet:stack-set-id"; public final static String OPERATION_ID_1 = "operation-id-1"; - public final static String OPERATION_ID_2 = "operation-id-2"; - public final static String OPERATION_ID_3 = "operation-id-3"; - public final static String OPERATION_ID_4 = "operation-id-4"; - public final static String OPERATION_ID_5 = "operation-id-5"; - public final static String LOGICAL_ID = "MyResource"; public final static String REQUEST_TOKEN = "token"; @@ -219,8 +149,6 @@ public class TestUtils { .parameterValue(PARAMETER_VALUE_3) .build(); - public final static Map TEMPLATE_MAP = ImmutableMap.of("TemplateURL", "foo"); - public final static Map DESIRED_RESOURCE_TAGS = ImmutableMap.of( "key1", "val1", "key2", "val2", "key3", "val3"); public final static Map PREVIOUS_RESOURCE_TAGS = ImmutableMap.of( @@ -287,6 +215,14 @@ public class TestUtils { .retainStacksOnAccountRemoval(true) .build(); + public final static GetTemplateSummaryResponse VALID_TEMPLATE_SUMMARY_RESPONSE = GetTemplateSummaryResponse.builder() + .resourceTypes(Arrays.asList("AWS::CloudFormation::WaitCondition")) + .build(); + + public final static GetTemplateSummaryResponse INVALID_TEMPLATE_SUMMARY_RESPONSE = GetTemplateSummaryResponse.builder() + .resourceTypes(Arrays.asList("AWS::CloudFormation::Stack")) + .build(); + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_1 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) .account(ACCOUNT_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 deleted file mode 100644 index eee9112..0000000 --- a/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ValidatorTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package software.amazon.cloudformation.stackset.util; - -import com.google.common.base.Strings; -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.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.services.s3.model.HeadObjectResponse; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -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.INVALID_EMBEDDED_STACKSET_TEMPLATE; -import static software.amazon.cloudformation.stackset.util.TestUtils.INVALID_EMBEDDED_STACK_TEMPLATE; -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.VALID_YAML_SHORTHANDS_TEMPLATE; -import static software.amazon.cloudformation.stackset.util.TestUtils.VALID_YAML_TEMPLATE; - -@ExtendWith(MockitoExtension.class) -public class ValidatorTest { - - 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"); - - private static final long VALID_TEMPLATE_SIZE = 1000L; - private static final long INVALID_TEMPLATE_SIZE = 460801L; - - @Spy - private Validator validator; - - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private Logger logger; - - @BeforeEach - public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); - validator = spy(Validator.class); - } - - @Test - public void testValidateTemplate_InvalidUri() { - for (final String invalidS3Url : INVALID_S3_URLS) { - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, null, invalidS3Url, logger)); - } - } - - @Test - public void testValidateTemplate_BothBodyAndUriExist() { - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, TEMPLATE_BODY, TEMPLATE_URL, logger)); - } - - @Test - public void testGetUrlContent() { - final ResponseBytes responseBytes = mock(ResponseBytes.class); - doReturn(HeadObjectResponse.builder().contentLength(VALID_TEMPLATE_SIZE).build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); - doReturn(ResponseBytes.fromByteArray(responseBytes, TEMPLATE_BODY.getBytes())).when(proxy) - .injectCredentialsAndInvokeV2Bytes(any(), any()); - assertEquals(validator.getUrlContent(proxy, TEMPLATE_URL), TEMPLATE_BODY); - } - - @Test - public void testGetUrlContent_TemplateTooLarge() { - doReturn(HeadObjectResponse.builder().contentLength(INVALID_TEMPLATE_SIZE).build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); - assertThrows(CfnInvalidRequestException.class, - () -> validator.getUrlContent(proxy, TEMPLATE_URL)); - } - - @Test - public void testValidateTemplate_BothBodyAndUriNotExist() { - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, null, null, logger)); - } - - @Test - public void testValidateTemplate_InvalidTemplate() { - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACK_TEMPLATE, null, logger)); - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, INVALID_EMBEDDED_STACKSET_TEMPLATE, null, logger)); - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, "", null, logger)); - assertThrows(CfnInvalidRequestException.class, - () -> validator.validateTemplate(proxy, "null", null, logger)); - } - - @Test - public void testValidateTemplate_ValidTemplateBody() { - assertDoesNotThrow(() -> validator.validateTemplate(proxy, TEMPLATE_BODY, null, logger)); - assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_TEMPLATE, null, logger)); - assertDoesNotThrow(() -> validator.validateTemplate(proxy, VALID_YAML_SHORTHANDS_TEMPLATE, null, logger)); - } -} From f134b121b31de1990b2443a73b150aa2650281fb Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 29 May 2020 10:37:35 -0700 Subject: [PATCH 17/19] Polish some log wordings and descriptions --- .../aws-cloudformation-stackset.json | 2 +- aws-cloudformation-stackset/docs/README.md | 2 +- .../amazon/cloudformation/stackset/BaseHandlerStd.java | 2 +- .../amazon/cloudformation/stackset/DeleteHandler.java | 2 +- .../stackset/translator/PropertyTranslator.java | 2 +- .../stackset/translator/RequestTranslator.java | 2 +- .../amazon/cloudformation/stackset/util/Validator.java | 6 ++++-- .../amazon/cloudformation/stackset/CreateHandlerTest.java | 4 ++-- .../amazon/cloudformation/stackset/util/TestUtils.java | 7 ++++--- 9 files changed, 16 insertions(+), 13 deletions(-) diff --git a/aws-cloudformation-stackset/aws-cloudformation-stackset.json b/aws-cloudformation-stackset/aws-cloudformation-stackset.json index 0586b24..14537a4 100644 --- a/aws-cloudformation-stackset/aws-cloudformation-stackset.json +++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json @@ -1,6 +1,6 @@ { "typeName": "AWS::CloudFormation::StackSet", - "description": "Resource Type definition for AWS::CloudFormation::StackSet", + "description": "StackSet as a resource provides one-click experience for provisioning a StackSet and StackInstances", "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-cloudformation.git", "resourceLink": { "templateUri": "/cloudformation/home?region=${awsRegion}#/stacksets/${StackSetId}", diff --git a/aws-cloudformation-stackset/docs/README.md b/aws-cloudformation-stackset/docs/README.md index 2840b1d..1a0ac2f 100644 --- a/aws-cloudformation-stackset/docs/README.md +++ b/aws-cloudformation-stackset/docs/README.md @@ -1,6 +1,6 @@ # AWS::CloudFormation::StackSet -Resource Type definition for AWS::CloudFormation::StackSet +StackSet as a resource provides one-click experience for provisioning a StackSet and StackInstances ## Syntax 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 index dfd3c75..d227249 100644 --- 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 @@ -76,7 +76,7 @@ protected static boolean isStackSetOperationDone( switch (status) { case SUCCEEDED: - logger.log(String.format("%s has been successfully stabilized.", operationId)); + logger.log(String.format("StackSet Operation [%s] has been successfully stabilized.", operationId)); return true; case RUNNING: case QUEUED: 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 d090054..35b8758 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 @@ -57,7 +57,7 @@ protected ProgressEvent deleteStackSet( .makeServiceCall((modelRequest, proxyInvocation) -> { final DeleteStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2( deleteStackSetRequest(model.getStackSetId()), proxyInvocation.client()::deleteStackSet); - logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME)); + logger.log(String.format("%s [%s] has been deleted successfully.", ResourceModel.TYPE_NAME, model.getStackSetId())); return response; }) .success(); 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 b636edf..d92c2a8 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 @@ -63,7 +63,7 @@ static DeploymentTargets translateToSdkDeploymentTargets( } /** - * Converts resource model Parameters to StackSet SDK Parameters + * Converts StackSet SDK Parameters to resource model Parameters * * @param parameters Parameters collection from resource model * @return SDK Parameter list 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 35b6614..fc7e28d 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 @@ -24,7 +24,7 @@ public class RequestTranslator { - private static int LIST_MAX_ITEMS = 100; + private static final int LIST_MAX_ITEMS = 100; public static CreateStackSetRequest createStackSetRequest( final ResourceModel model, final String requestToken) { diff --git a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java index ef8c43b..00fb769 100644 --- a/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -21,7 +21,8 @@ private static void validateResource(final String type) { switch (type) { case "AWS::CloudFormation::Stack": case "AWS::CloudFormation::StackSet": - throw new CfnInvalidRequestException(String.format("Nested %s is not allowed", type)); + throw new CfnInvalidRequestException( + String.format("Nested %s is not supported in AWS::CloudFormation::StackSet", type)); } } @@ -33,7 +34,7 @@ private static void validateResource(final String type) { *
  • Template contents must be valid * * - * @param proxyClient {@link ProxyClient < CloudFormationClient >} + * @param proxyClient {@link ProxyClient } * @param templateBody {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateBody} * @param templateLocation {@link software.amazon.cloudformation.stackset.ResourceModel#getTemplateURL} * @throws CfnInvalidRequestException if template is not valid @@ -46,6 +47,7 @@ public void validateTemplate( final GetTemplateSummaryResponse response = proxyClient.injectCredentialsAndInvokeV2( getTemplateSummaryRequest(templateBody, templateLocation), proxyClient.client()::getTemplateSummary); + if (response.hasResourceTypes()) { response.resourceTypes().forEach(Validator::validateResource); } 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 e706ce3..9e509cc 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 @@ -38,7 +38,6 @@ 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.INVALID_TEMPLATE_SUMMARY_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_EMPTY_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_ONE_INSTANCES_RESPONSE; import static software.amazon.cloudformation.stackset.util.TestUtils.LIST_SELF_MANAGED_STACK_SET_RESPONSE; @@ -57,6 +56,7 @@ import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_ONE_INSTANCES_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL; import static software.amazon.cloudformation.stackset.util.TestUtils.SERVICE_MANAGED_MODEL_FOR_READ; +import static software.amazon.cloudformation.stackset.util.TestUtils.TEMPLATE_SUMMARY_RESPONSE_WITH_NESTED_STACK; import static software.amazon.cloudformation.stackset.util.TestUtils.VALID_TEMPLATE_SUMMARY_RESPONSE; @ExtendWith(MockitoExtension.class) @@ -294,7 +294,7 @@ public void handlerRequest_CfnInvalidRequestException_NestedStack() { .build(); when(proxyClient.client().getTemplateSummary(any(GetTemplateSummaryRequest.class))) - .thenReturn(INVALID_TEMPLATE_SUMMARY_RESPONSE); + .thenReturn(TEMPLATE_SUMMARY_RESPONSE_WITH_NESTED_STACK); assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); 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 7b7e2f5..86f970f 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 @@ -219,9 +219,10 @@ public class TestUtils { .resourceTypes(Arrays.asList("AWS::CloudFormation::WaitCondition")) .build(); - public final static GetTemplateSummaryResponse INVALID_TEMPLATE_SUMMARY_RESPONSE = GetTemplateSummaryResponse.builder() - .resourceTypes(Arrays.asList("AWS::CloudFormation::Stack")) - .build(); + public final static GetTemplateSummaryResponse TEMPLATE_SUMMARY_RESPONSE_WITH_NESTED_STACK = + GetTemplateSummaryResponse.builder() + .resourceTypes(Arrays.asList("AWS::CloudFormation::Stack")) + .build(); public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_1 = StackInstanceSummary.builder() .organizationalUnitId(ORGANIZATION_UNIT_ID_1) From 1ab250820ac2d2ee13d5c9a49f42932b3d06f28f Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 29 May 2020 13:13:32 -0700 Subject: [PATCH 18/19] Update to perform deleteStackInstances first in update operation --- .../software/amazon/cloudformation/stackset/UpdateHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ffa1381..a29392b 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 @@ -32,8 +32,8 @@ protected ProgressEvent handleRequest( analyzeTemplate(proxyClient, request, placeHolder, Action.UPDATE); return ProgressEvent.progress(model, callbackContext) - .then(progress -> updateStackSet(proxy, proxyClient, progress, previousModel)) .then(progress -> deleteStackInstances(proxy, proxyClient, progress, placeHolder.getDeleteStackInstances(), logger)) + .then(progress -> updateStackSet(proxy, proxyClient, progress, previousModel)) .then(progress -> createStackInstances(proxy, proxyClient, progress, placeHolder.getCreateStackInstances(), logger)) .then(progress -> updateStackInstances(proxy, proxyClient, progress, placeHolder.getUpdateStackInstances(), logger)) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); From fc0041c6d067a3a2ac040087f136a08746a8ced2 Mon Sep 17 00:00:00 2001 From: Xiwei Huang Date: Fri, 29 May 2020 21:12:55 -0700 Subject: [PATCH 19/19] Add .travis.yml for stackset --- .travis.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3c703c5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: java +dist: bionic +jdk: openjdk11 +env: + global: + - PYENV_VERSION=3.7 + - AWS_REGION="us-east-1" + - AWS_DEFAULT_REGION=$AWS_REGION +install: + - pip3 install --user pre-commit cloudformation-cli-java-plugin +script: + - pre-commit run --all-files --verbose + - cd "$TRAVIS_BUILD_DIR/aws-cloudformation-stackset" + # from Maven 3.6.1+, should use `--no-transfer-progress` instead of Slf4jMavenTransferListener + - > + mvn + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + -B + clean verify +after_failure: + - cat "$TRAVIS_BUILD_DIR/aws-cloudformation-stackset/rpdk.log"