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"
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..f09ba84
--- /dev/null
+++ b/aws-cloudformation-stackset/.rpdk-config
@@ -0,0 +1,16 @@
+{
+ "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"
+ ],
+ "codegen_template_path": "guided_aws"
+ }
+}
diff --git a/aws-cloudformation-stackset/README.md b/aws-cloudformation-stackset/README.md
new file mode 100644
index 0000000..74ef28c
--- /dev/null
+++ b/aws-cloudformation-stackset/README.md
@@ -0,0 +1,12 @@
+# AWS::CloudFormation::StackSet
+
+Congratulations on starting development! Next steps:
+
+1. Write the JSON schema describing your resource, `aws-cloudformation-stackset.json`
+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.
+
+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
new file mode 100644
index 0000000..14537a4
--- /dev/null
+++ b/aws-cloudformation-stackset/aws-cloudformation-stackset.json
@@ -0,0 +1,354 @@
+{
+ "typeName": "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}",
+ "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.",
+ "type": "object",
+ "properties": {
+ "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": 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": 255,
+ "pattern": "^(?!aws:.*)[a-z0-9\\s\\_\\.\\/\\=\\+\\-]+$"
+ }
+ },
+ "required": [
+ "Key",
+ "Value"
+ ],
+ "additionalProperties": false
+ },
+ "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"
+ ]
+ },
+ "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": {
+ "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"
+ }
+ },
+ "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": {
+ "description": "Stack instances in some specific accounts and Regions.",
+ "type": "object",
+ "properties": {
+ "DeploymentTargets": {
+ "$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": {
+ "$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": {
+ "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"
+ },
+ "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"
+ }
+ },
+ "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": {
+ "$ref": "#/definitions/OperationPreferences"
+ },
+ "StackInstancesGroup": {
+ "description": "",
+ "type": "array",
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "items": {
+ "$ref": "#/definitions/StackInstances"
+ }
+ },
+ "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"
+ ]
+ },
+ "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
+ }
+ },
+ "oneOf": [
+ {
+ "required": [
+ "TemplateURL",
+ "StackSetName",
+ "PermissionModel"
+ ]
+ },
+ {
+ "required": [
+ "TemplateBody",
+ "StackSetName",
+ "PermissionModel"
+ ]
+ }
+ ],
+ "additionalProperties": false,
+ "createOnlyProperties": [
+ "/properties/PermissionModel",
+ "/properties/StackSetName"
+ ],
+ "writeOnlyProperties": [
+ "/properties/TemplateURL",
+ "/properties/OperationPreferences"
+ ],
+ "readOnlyProperties": [
+ "/properties/StackSetId"
+ ],
+ "primaryIdentifier": [
+ "/properties/StackSetId"
+ ],
+ "handlers": {
+ "create": {
+ "permissions": [
+ "cloudformation:GetTemplateSummary",
+ "cloudformation:CreateStackSet",
+ "cloudformation:CreateStackInstances",
+ "cloudformation:DescribeStackSetOperation",
+ "cloudformation:TagResource"
+ ],
+ "timeoutInMinutes": 720
+ },
+ "read": {
+ "permissions": [
+ "cloudformation:DescribeStackSet",
+ "cloudformation:ListStackInstances",
+ "cloudformation:DescribeStackInstance"
+ ]
+ },
+ "update": {
+ "permissions": [
+ "cloudformation:GetTemplateSummary",
+ "cloudformation:UpdateStackSet",
+ "cloudformation:CreateStackInstances",
+ "cloudformation:DeleteStackInstances",
+ "cloudformation:UpdateStackInstances",
+ "cloudformation:DescribeStackSetOperation",
+ "cloudformation:TagResource",
+ "cloudformation:UntagResource"
+ ],
+ "timeoutInMinutes": 720
+ },
+ "delete": {
+ "permissions": [
+ "cloudformation:DeleteStackSet",
+ "cloudformation:DeleteStackInstances",
+ "cloudformation:DescribeStackSetOperation",
+ "cloudformation:UntagResource"
+ ],
+ "timeoutInMinutes": 720
+ },
+ "list": {
+ "permissions": [
+ "cloudformation:ListStackSets",
+ "cloudformation:DescribeStackSet",
+ "cloudformation:ListStackInstances",
+ "cloudformation:DescribeStackInstance"
+ ]
+ }
+ }
+}
diff --git a/aws-cloudformation-stackset/docs/README.md b/aws-cloudformation-stackset/docs/README.md
new file mode 100644
index 0000000..1a0ac2f
--- /dev/null
+++ b/aws-cloudformation-stackset/docs/README.md
@@ -0,0 +1,224 @@
+# AWS::CloudFormation::StackSet
+
+StackSet as a resource provides one-click experience for provisioning a StackSet and StackInstances
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+
+## 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.
+
+_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
+
+
+
+## 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
+
+
+
+## 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_: Yes
+
+_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_: Yes
+
+_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
+
+
+
+## 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..41e9fd7
--- /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
+
+
+
+## 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_: Yes
+
+_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_: Yes
+
+_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
+
+
+
+## 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
+
+
+
+## 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/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..44930fc
--- /dev/null
+++ b/aws-cloudformation-stackset/pom.xml
@@ -0,0 +1,234 @@
+
+
+ 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.10.70
+ pom
+ import
+
+
+
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ 1.0.5
+
+
+
+ software.amazon.awssdk
+ cloudformation
+
+
+
+ org.yaml
+ snakeyaml
+ 1.26
+
+
+
+ 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.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
+
+
+
+
+
+
+
+
+
+
+ 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/ClientBuilder*
+ **/BaseConfiguration*
+ **/BaseHandler*
+ **/HandlerWrapper*
+ **/ResourceModel*
+
+
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+ jacoco-check
+
+ check
+
+
+
+
+ PACKAGE
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.9
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.9
+
+
+
+
+
+
+
+
+
+
+
+ ${project.basedir}
+
+ aws-cloudformation-stackset.json
+
+
+
+
+
diff --git a/aws-cloudformation-stackset/resource-role.yaml b/aws-cloudformation-stackset/resource-role.yaml
new file mode 100644
index 0000000..f4b5d4e
--- /dev/null
+++ b/aws-cloudformation-stackset/resource-role.yaml
@@ -0,0 +1,44 @@
+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: 43200
+ 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:DescribeStackInstance"
+ - "cloudformation:DescribeStackSet"
+ - "cloudformation:DescribeStackSetOperation"
+ - "cloudformation:GetTemplateSummary"
+ - "cloudformation:ListStackInstances"
+ - "cloudformation:ListStackSets"
+ - "cloudformation:TagResource"
+ - "cloudformation:UntagResource"
+ - "cloudformation:UpdateStackInstances"
+ - "cloudformation:UpdateStackSet"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
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
new file mode 100644
index 0000000..d227249
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/BaseHandlerStd.java
@@ -0,0 +1,315 @@
+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.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.OperationInProgressException;
+import software.amazon.awssdk.services.cloudformation.model.StackInstanceNotFoundException;
+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;
+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 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;
+
+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 MultipleOf MULTIPLE_OF = MultipleOf.multipleOf()
+ .multiple(2)
+ .timeout(Duration.ofHours(24L))
+ .delay(Duration.ofSeconds(2L))
+ .build();
+
+ /**
+ * 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("StackSet Operation [%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(
+ 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;
+ }
+
+ /**
+ * Invocation of CreateStackInstances would possibly used by CREATE/UPDATE 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#analyzeForCreate}
+ * @param logger {@link Logger}
+ * @return {@link ProgressEvent}
+ */
+ 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();
+
+ 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.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,
+ final ProgressEvent progress,
+ final List stackInstancesList,
+ final Logger logger) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ 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 OperationInProgressException is thrown by the service, then we retry
+ if (e instanceof OperationInProgressException) {
+ return ProgressEvent.progress(model_, context);
+ }
+ throw e;
+ })
+ .success();
+
+ if (!progressEvent.isSuccess()) {
+ return progressEvent;
+ }
+ }
+
+ 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,
+ final ProgressEvent progress,
+ final List stackInstancesList,
+ final Logger logger) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ 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.progress(model, callbackContext);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Analyzes/validates template and StackInstancesGroup
+ *
+ * @param proxyClient the aws service client {@link ProxyClient} to make the call
+ * @param request {@link ResourceHandlerRequest}
+ * @param placeHolder {@link StackInstancesPlaceHolder}
+ * @param action {@link Action}
+ */
+ protected void analyzeTemplate(
+ final ProxyClient proxyClient,
+ final ResourceHandlerRequest request,
+ final StackInstancesPlaceHolder placeHolder,
+ final Action action) {
+
+ final ResourceModel desiredModel = request.getDesiredResourceState();
+ final ResourceModel previousModel = request.getPreviousResourceState();
+
+ switch (action) {
+ case CREATE:
+ new Validator().validateTemplate(proxyClient, desiredModel.getTemplateBody(), desiredModel.getTemplateURL());
+ InstancesAnalyzer.builder().desiredModel(desiredModel).build().analyzeForCreate(placeHolder);
+ break;
+ case UPDATE:
+ new Validator().validateTemplate(proxyClient, desiredModel.getTemplateBody(), desiredModel.getTemplateURL());
+ 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/CallbackContext.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java
new file mode 100644
index 0000000..f5436c9
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CallbackContext.java
@@ -0,0 +1,10 @@
+package software.amazon.cloudformation.stackset;
+
+import software.amazon.cloudformation.proxy.StdCallbackContext;
+
+@lombok.Getter
+@lombok.Setter
+@lombok.ToString
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext {
+}
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..1432145
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/Configuration.java
@@ -0,0 +1,8 @@
+package software.amazon.cloudformation.stackset;
+
+class Configuration extends BaseConfiguration {
+
+ public Configuration() {
+ super("aws-cloudformation-stackset.json");
+ }
+}
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..2fc896b
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/CreateHandler.java
@@ -0,0 +1,43 @@
+package software.amazon.cloudformation.stackset;
+
+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.StackInstancesPlaceHolder;
+
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.createStackSetRequest;
+
+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) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+ final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder();
+ analyzeTemplate(proxyClient, request, placeHolder, Action.CREATE);
+
+ return proxy.initiate("AWS-CloudFormation-StackSet::Create", proxyClient, model, callbackContext)
+ .translateToServiceRequest(resourceModel -> createStackSetRequest(resourceModel, 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, placeHolder.getCreateStackInstances(), logger))
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+}
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..35b8758
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/DeleteHandler.java
@@ -0,0 +1,65 @@
+package software.amazon.cloudformation.stackset;
+
+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.StackInstancesPlaceHolder;
+
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.deleteStackSetRequest;
+
+public class DeleteHandler extends BaseHandlerStd {
+
+ 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();
+ // Analyzes stack instances group for delete
+ final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder();
+ analyzeTemplate(proxyClient, request, placeHolder, Action.DELETE);
+
+ return ProgressEvent.progress(model, callbackContext)
+ // delete/stabilize progress chain - delete all associated stack instances
+ .then(progress -> deleteStackInstances(proxy, proxyClient, progress, placeHolder.getDeleteStackInstances(), logger))
+ .then(progress -> deleteStackSet(proxy, proxyClient, progress));
+ }
+
+ /**
+ * 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 ProxyClient client,
+ final ProgressEvent progress) {
+
+ final ResourceModel model = progress.getResourceModel();
+ final CallbackContext callbackContext = progress.getCallbackContext();
+
+ return proxy.initiate("AWS-CloudFormation-StackSet::DeleteStackSet", client, model, callbackContext)
+ .translateToServiceRequest(modelRequest -> deleteStackSetRequest(modelRequest.getStackSetId()))
+ .makeServiceCall((modelRequest, proxyInvocation) -> {
+ final DeleteStackSetResponse response = proxyInvocation.injectCredentialsAndInvokeV2(
+ deleteStackSetRequest(model.getStackSetId()), proxyInvocation.client()::deleteStackSet);
+ 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/ListHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java
new file mode 100644
index 0000000..f897846
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ListHandler.java
@@ -0,0 +1,46 @@
+package software.amazon.cloudformation.stackset;
+
+import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
+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.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.ResourceModelBuilder;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.listStackSetsRequest;
+
+public class ListHandler extends BaseHandlerStd {
+
+ @Override
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ final ListStackSetsResponse response = proxyClient.injectCredentialsAndInvokeV2(
+ listStackSetsRequest(request.getNextToken()), proxyClient.client()::listStackSets);
+
+ final List models = response
+ .summaries()
+ .stream()
+ .map(stackSetSummary -> ResourceModelBuilder.builder()
+ .proxyClient(proxyClient)
+ .stackSet(describeStackSet(proxyClient, stackSetSummary.stackSetId()))
+ .build().buildModel())
+ .collect(Collectors.toList());
+
+ 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/ReadHandler.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java
new file mode 100644
index 0000000..76dca3a
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/ReadHandler.java
@@ -0,0 +1,34 @@
+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.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.cloudformation.stackset.util.ResourceModelBuilder;
+
+public class ReadHandler extends BaseHandlerStd {
+
+ 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();
+
+ return ProgressEvent.builder()
+ .resourceModel(ResourceModelBuilder.builder()
+ .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
new file mode 100644
index 0000000..a29392b
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/UpdateHandler.java
@@ -0,0 +1,75 @@
+package software.amazon.cloudformation.stackset;
+
+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.StackInstancesPlaceHolder;
+
+import static software.amazon.cloudformation.stackset.translator.RequestTranslator.updateStackSetRequest;
+import static software.amazon.cloudformation.stackset.util.Comparator.isStackSetConfigEquals;
+
+public class UpdateHandler extends BaseHandlerStd {
+
+ 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 ResourceModel previousModel = request.getPreviousResourceState();
+ final StackInstancesPlaceHolder placeHolder = new StackInstancesPlaceHolder();
+ analyzeTemplate(proxyClient, request, placeHolder, Action.UPDATE);
+
+ return ProgressEvent.progress(model, callbackContext)
+ .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));
+ }
+
+ /**
+ * 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 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
+ */
+ private ProgressEvent updateStackSet(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient client,
+ final ProgressEvent progress,
+ final ResourceModel previousModel) {
+
+ 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) -> {
+ final 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();
+ }
+}
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..d92c2a8
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/PropertyTranslator.java
@@ -0,0 +1,172 @@
+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.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.Collections;
+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 StackSet SDK Parameters to resource model Parameters
+ *
+ * @param parameters Parameters collection from resource model
+ * @return SDK Parameter list
+ */
+ static List translateToSdkParameters(
+ final Collection parameters) {
+ // 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())
+ .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 (CollectionUtils.isNullOrEmpty(parameters)) 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 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())
+ .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());
+ }
+
+ /**
+ * 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
new file mode 100644
index 0000000..fc7e28d
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/translator/RequestTranslator.java
@@ -0,0 +1,154 @@
+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.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.cloudformation.stackset.OperationPreferences;
+import software.amazon.cloudformation.stackset.ResourceModel;
+import software.amazon.cloudformation.stackset.StackInstances;
+
+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 final int LIST_MAX_ITEMS = 100;
+
+ public static CreateStackSetRequest createStackSetRequest(
+ final ResourceModel model, final String requestToken) {
+ return CreateStackSetRequest.builder()
+ .stackSetName(model.getStackSetName())
+ .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 StackInstances stackInstances) {
+ return CreateStackInstancesRequest.builder()
+ .stackSetName(stackSetName)
+ .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(stackInstances.getDeploymentTargets()))
+ .parameterOverrides(translateToSdkParameters(stackInstances.getParameterOverrides()))
+ .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 StackInstances stackInstances) {
+ return DeleteStackInstancesRequest.builder()
+ .stackSetName(stackSetName)
+ .regions(stackInstances.getRegions())
+ .operationPreferences(translateToSdkOperationPreferences(operationPreferences))
+ .deploymentTargets(translateToSdkDeploymentTargets(stackInstances.getDeploymentTargets()))
+ .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 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()
+ .stackSetName(stackSetName)
+ .operationId(operationId)
+ .build();
+ }
+
+ 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
new file mode 100644
index 0000000..5c461c9
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ClientBuilder.java
@@ -0,0 +1,25 @@
+package software.amazon.cloudformation.stackset.util;
+
+import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
+import software.amazon.cloudformation.LambdaWrapper;
+
+public class ClientBuilder {
+
+ private ClientBuilder() {
+ }
+
+ public static CloudFormationClient getClient() {
+ return LazyHolder.SERVICE_CLIENT;
+ }
+
+ /**
+ * 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
new file mode 100644
index 0000000..c503f3b
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Comparator.java
@@ -0,0 +1,66 @@
+package software.amazon.cloudformation.stackset.util;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.services.cloudformation.model.PermissionModels;
+import software.amazon.cloudformation.stackset.ResourceModel;
+
+import java.util.Collection;
+
+/**
+ * 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 (!equals(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.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 equals(final Collection> collection1, final Collection> 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) {
+ return PermissionModels.fromValue(model.getPermissionModel()).equals(PermissionModels.SELF_MANAGED);
+ }
+}
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..e096877
--- /dev/null
+++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/InstancesAnalyzer.java
@@ -0,0 +1,266 @@
+package software.amazon.cloudformation.stackset.util;
+
+import lombok.Builder;
+import lombok.Data;
+import software.amazon.awssdk.utils.CollectionUtils;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+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;
+
+ /**
+ * 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(
+ 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(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(
+ 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