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 + +
+{
+    "Type" : "AWS::CloudFormation::StackSet",
+    "Properties" : {
+        "StackSetName" : String,
+        "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:
+    StackSetName: String
+    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 + +#### 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 + +
+{
+    "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..eed6d02 --- /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_: 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 + +
+{
+    "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..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 + +
+{
+    "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_: 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 + +
+{
+    "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/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 + + + + ${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/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 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.equals( + 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(); + + // 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", + 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(); + + // Validates no duplicated stack instance is specified + if (flatStacks.contains(stackInstance)) { + throw new CfnInvalidRequestException( + String.format("Stack instance [%s,%s] is duplicated", target, region)); + } + + flatStacks.add(stackInstance); + } + } + } + return flatStacks; + } + + /** + * 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); + + 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 create operations + * + * @param placeHolder {@link StackInstancesPlaceHolder} + */ + 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 delete operations + * + * @param placeHolder {@link StackInstancesPlaceHolder} + */ + 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/ResourceModelBuilder.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java new file mode 100644 index 0000000..bef3c80 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/ResourceModelBuilder.java @@ -0,0 +1,114 @@ +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.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.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.aggregateStackInstancesForRead; + +/** + * Utility class to construct {@link ResourceModel} for Read/List request based on {@link StackSet} + * that handler has retrieved. + */ +@AllArgsConstructor +@Builder +public class ResourceModelBuilder { + + private ProxyClient proxyClient; + private StackSet stackSet; + private boolean isSelfManaged; + + /** + * Returns the model we construct from StackSet service client using PrimaryIdentifier StackSetId + * + * @return {@link ResourceModel} + */ + public ResourceModel buildModel() { + + 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(stackSet.hasCapabilities() ? new HashSet<>(stackSet.capabilitiesAsStrings()) : null) + .tags(translateFromSdkTags(stackSet.tags())) + .parameters(translateFromSdkParameters(stackSet.parameters())) + .templateBody(stackSet.templateBody()) + .build(); + + 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 { + 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 isSelfManaged if permission model is SELF_MANAGED + * @param token {@link ListStackInstancesResponse#nextToken()} + */ + private void attachStackInstances( + final String stackSetId, + final boolean isSelfManaged, + final Set stackInstanceSet, + String token) { + + 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)); + }); + } + + 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/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/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/Validator.java b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java new file mode 100644 index 0000000..00fb769 --- /dev/null +++ b/aws-cloudformation-stackset/src/main/java/software/amazon/cloudformation/stackset/util/Validator.java @@ -0,0 +1,55 @@ +package software.amazon.cloudformation.stackset.util; + +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.ProxyClient; + +import static software.amazon.cloudformation.stackset.translator.RequestTranslator.getTemplateSummaryRequest; + +/** + * Utility class to validate properties in {@link software.amazon.cloudformation.stackset.ResourceModel} + */ +public class Validator { + + /** + * Embedded Stack or StackSet is not allowed + * + * @param type Resource type + */ + 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 supported in AWS::CloudFormation::StackSet", type)); + } + } + + /** + * 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 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 + */ + public void validateTemplate( + final ProxyClient proxyClient, + final String templateBody, + final String templateLocation) { + + 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/AbstractTestBase.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java new file mode 100644 index 0000000..abf97b6 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/AbstractTestBase.java @@ -0,0 +1,68 @@ +package software.amazon.cloudformation.stackset; + +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; +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 + injectCredentialsAndInvokeV2Async( + RequestT request, Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public > IterableT + injectCredentialsAndInvokeIterableV2(RequestT requestT, Function function) { + 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/CreateHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java new file mode 100644 index 0000000..9e509cc --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/CreateHandlerTest.java @@ -0,0 +1,340 @@ +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.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.GetTemplateSummaryRequest; +import software.amazon.awssdk.services.cloudformation.model.ListStackInstancesRequest; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +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.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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; +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) +public class CreateHandlerTest extends AbstractTestBase { + + @Mock + CloudFormationClient sdkClient; + private CreateHandler handler; + private ResourceHandlerRequest request; + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + + @BeforeEach + public void setup() { + 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_ServiceManagedSS_SimpleSuccess() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SERVICE_MANAGED_MODEL) + .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))) + .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); + + 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_FOR_READ); + 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)); + 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_SimpleSuccess() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_MODEL) + .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))) + .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); + + 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()).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)); + 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_NoInstances_SimpleSuccess() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_NO_INSTANCES_MODEL) + .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().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); + + 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_NO_INSTANCES_FOR_READ); + 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)); + } + + @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().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))) + .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.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(SELF_MANAGED_MODEL_ONE_INSTANCES_FOR_READ); + 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)); + verify(proxyClient.client()).describeStackSet(any(DescribeStackSetRequest.class)); + verify(proxyClient.client()).listStackInstances(any(ListStackInstancesRequest.class)); + verify(proxyClient.client(), times(2)).describeStackInstance(any(DescribeStackInstanceRequest.class)); + } + + @Test + public void handlerRequest_OperationStoppedError() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_MODEL) + .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))) + .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); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + 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(TEMPLATE_SUMMARY_RESPONSE_WITH_NESTED_STACK); + + assertThrows(CfnInvalidRequestException.class, + () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + + verify(proxyClient.client()).getTemplateSummary(any(GetTemplateSummaryRequest.class)); + } + + @Test + public void handlerRequest_CfnInvalidRequestException_DuplicateStackInstance() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_DUPLICATE_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .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 + public void handlerRequest_CfnInvalidRequestException_InvalidDeploymentTargets() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(SELF_MANAGED_INVALID_INSTANCES_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .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/DeleteHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java new file mode 100644 index 0000000..97675de --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/DeleteHandlerTest.java @@ -0,0 +1,139 @@ +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.DeleteStackInstancesRequest; +import software.amazon.awssdk.services.cloudformation.model.DeleteStackSetRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStackSetOperationRequest; +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +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.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; + + @BeforeEach + public void setup() { + 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(SERVICE_MANAGED_MODEL) + .logicalResourceIdentifier(LOGICAL_ID) + .clientRequestToken(REQUEST_TOKEN) + .build(); + } + + @Test + public void handleRequest_SimpleSuccess() { + 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.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 new file mode 100644 index 0000000..21cfd73 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ListHandlerTest.java @@ -0,0 +1,92 @@ +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.ListStackSetsRequest; +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.mockito.ArgumentMatchers.any; +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; +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_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; + +@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; + + @BeforeEach + public void setup() { + 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_SelfManagedSS_Success() { + + 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); + + 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(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/ReadHandlerTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java new file mode 100644 index 0000000..c1efb21 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/ReadHandlerTest.java @@ -0,0 +1,87 @@ +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.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.mockito.ArgumentMatchers.any; +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; +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.READ_MODEL; +import static software.amazon.cloudformation.stackset.util.TestUtils.SELF_MANAGED_MODEL_FOR_READ; + +@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; + + @BeforeEach + public void setup() { + handler = new ReadHandler(); + 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_SelfManagedSS_Success() { + + 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.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 new file mode 100644 index 0000000..0d838e5 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/UpdateHandlerTest.java @@ -0,0 +1,158 @@ +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.CreateStackInstancesRequest; +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.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; +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.mockito.ArgumentMatchers.any; +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.DELETE_STACK_INSTANCES_RESPONSE; +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.SELF_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; +import static software.amazon.cloudformation.stackset.util.TestUtils.VALID_TEMPLATE_SUMMARY_RESPONSE; + +@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; + + @BeforeEach + public void setup() { + 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_SelfManagedSS_SimpleSuccess() { + + request = ResourceHandlerRequest.builder() + .desiredResourceState(UPDATED_SELF_MANAGED_MODEL) + .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))) + .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()).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)); + 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().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))) + .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.getResourceModels()).isNull(); + 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/translator/PropertyTranslatorTest.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/translator/PropertyTranslatorTest.java new file mode 100644 index 0000000..8e20289 --- /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 test_translateFromSdkParameters_IfIsNull() { + assertThat(translateFromSdkParameters(null)).isNull(); + } + + @Test + public void test_translateToSdkTags_IfIsNull() { + assertThat(translateToSdkTags(null)).isEmpty(); + } + + @Test + 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 new file mode 100644 index 0000000..24c4057 --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/ComparatorTest.java @@ -0,0 +1,94 @@ +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.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(); + + // 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); + + testDesiredModel.setTemplateBody(UPDATED_TEMPLATE_BODY); + testPreviousModel.setTemplateBody(TEMPLATE_BODY); + + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isFalse(); + + testDesiredModel.setTemplateBody(TEMPLATE_BODY); + assertThat(isStackSetConfigEquals(testPreviousModel, testDesiredModel)).isTrue(); + + } + + @Test + 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/TestUtils.java b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java new file mode 100644 index 0000000..86f970f --- /dev/null +++ b/aws-cloudformation-stackset/src/test/java/software/amazon/cloudformation/stackset/util/TestUtils.java @@ -0,0 +1,649 @@ +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.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.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; +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; +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 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 { + + 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 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 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"; + 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 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_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( + 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))) + .build(); + + public final static DeploymentTargets UPDATED_SELF_MANAGED_TARGETS = DeploymentTargets.builder() + .accounts(new HashSet<>(Arrays.asList( + ACCOUNT_ID_2))) + .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 GetTemplateSummaryResponse VALID_TEMPLATE_SUMMARY_RESPONSE = GetTemplateSummaryResponse.builder() + .resourceTypes(Arrays.asList("AWS::CloudFormation::WaitCondition")) + .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) + .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) + .account(ACCOUNT_ID_2) + .region(EU_EAST_1) + .build(); + + public final static StackInstanceSummary STACK_INSTANCE_SUMMARY_4 = StackInstanceSummary.builder() + .organizationalUnitId(ORGANIZATION_UNIT_ID_2) + .account(ACCOUNT_ID_2) + .region(EU_EAST_2) + .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(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); + + 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 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() + .description(DESCRIPTION) + .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) + .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) + .templateBody(TEMPLATE_BODY) + .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) + .templateBody(TEMPLATE_BODY) + .permissionModel(PermissionModels.SELF_MANAGED) + .tags(TAGGED_RESOURCES) + .build(); + + public final static ResourceModel SERVICE_MANAGED_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SERVICE_MANAGED) + .capabilities(CAPABILITIES) + .description(DESCRIPTION) + .autoDeployment(AUTO_DEPLOYMENT) + .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))) + .operationPreferences(OPERATION_PREFERENCES) + .tags(TAGS) + .build(); + + public final static ResourceModel 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_1, SELF_MANAGED_STACK_INSTANCES_2))) + .parameters(new HashSet<>(Arrays.asList(PARAMETER_1, PARAMETER_2))) + .operationPreferences(OPERATION_PREFERENCES) + .tags(TAGS) + .build(); + + public final static ResourceModel UPDATED_SELF_MANAGED_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SELF_MANAGED) + .capabilities(CAPABILITIES) + .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))) + .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 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) + .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(); + + 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 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(); + + public final static ResourceModel SIMPLE_MODEL = ResourceModel.builder() + .stackSetId(STACK_SET_ID) + .permissionModel(SELF_MANAGED) + .templateBody(TEMPLATE_BODY) + .tags(TAGS) + .operationPreferences(OPERATION_PREFERENCES) + .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 DeleteStackSetResponse DELETE_STACK_SET_RESPONSE = + DeleteStackSetResponse.builder().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(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 = + ListStackSetsResponse.builder() + .summaries(STACK_SET_SUMMARY_1) + .build(); + +} 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 diff --git a/aws-cloudformation-stackset/template.yml b/aws-cloudformation-stackset/template.yml new file mode 100644 index 0000000..445c0bc --- /dev/null +++ b/aws-cloudformation-stackset/template.yml @@ -0,0 +1,22 @@ +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