Skip to content

Commit

Permalink
Merge pull request #23666 from Popsiclestick/stack-set-instances-oper…
Browse files Browse the repository at this point in the history
…ation-preferences

Feature: Stack set instances operation preferences
  • Loading branch information
ewbankkit committed Mar 23, 2022
2 parents 7fc82c0 + 424a374 commit 8c596ef
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 24 deletions.
3 changes: 3 additions & 0 deletions .changelog/23666.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_cloudformation_stack_set_instance: Add `operation_preferences` argument
```
95 changes: 88 additions & 7 deletions internal/service/cloudformation/stack_set_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func ResourceStackSetInstance() *schema.Resource {
ValidateFunc: verify.ValidAccountID,
ConflictsWith: []string{"deployment_targets"},
},
"call_as": {
Type: schema.TypeString,
Optional: true,
Default: cloudformation.CallAsSelf,
ValidateFunc: validation.StringInSlice(cloudformation.CallAs_Values(), false),
},
"deployment_targets": {
Type: schema.TypeList,
Optional: true,
Expand All @@ -64,6 +70,53 @@ func ResourceStackSetInstance() *schema.Resource {
},
ConflictsWith: []string{"account_id"},
},
"operation_preferences": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"failure_tolerance_count": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(0),
ConflictsWith: []string{"operation_preferences.0.failure_tolerance_percentage"},
},
"failure_tolerance_percentage": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(0, 100),
ConflictsWith: []string{"operation_preferences.0.failure_tolerance_count"},
},
"max_concurrent_count": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
ConflictsWith: []string{"operation_preferences.0.max_concurrent_percentage"},
},
"max_concurrent_percentage": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(1, 100),
ConflictsWith: []string{"operation_preferences.0.max_concurrent_count"},
},
"region_concurrency_type": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice(cloudformation.RegionConcurrencyType_Values(), false),
},
"region_order": {
Type: schema.TypeList,
Optional: true,
MinItems: 1,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9-]{1,128}$`), ""),
},
},
},
},
},
"organizational_unit_id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -94,12 +147,6 @@ func ResourceStackSetInstance() *schema.Resource {
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
},
"call_as": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice(cloudformation.CallAs_Values(), false),
Default: cloudformation.CallAsSelf,
},
},
}
}
Expand Down Expand Up @@ -142,6 +189,10 @@ func resourceStackSetInstanceCreate(d *schema.ResourceData, meta interface{}) er
input.ParameterOverrides = expandParameters(v.(map[string]interface{}))
}

if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.OperationPreferences = expandCloudFormationOperationPreferences(d)
}

log.Printf("[DEBUG] Creating CloudFormation StackSet Instance: %s", input)
_, err := tfresource.RetryWhen(
tfiam.PropagationTimeout,
Expand Down Expand Up @@ -257,7 +308,7 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro
func resourceStackSetInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).CloudFormationConn

if d.HasChanges("deployment_targets", "parameter_overrides") {
if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") {
stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id())

if err != nil {
Expand Down Expand Up @@ -287,6 +338,10 @@ func resourceStackSetInstanceUpdate(d *schema.ResourceData, meta interface{}) er
input.ParameterOverrides = expandParameters(v.(map[string]interface{}))
}

if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.OperationPreferences = expandCloudFormationOperationPreferences(d)
}

log.Printf("[DEBUG] Updating CloudFormation StackSet Instance: %s", input)
output, err := conn.UpdateStackInstances(input)

Expand Down Expand Up @@ -370,3 +425,29 @@ func expandCloudFormationDeploymentTargets(l []interface{}) *cloudformation.Depl

return dt
}

func expandCloudFormationOperationPreferences(d *schema.ResourceData) *cloudformation.StackSetOperationPreferences {

operationPreferences := &cloudformation.StackSetOperationPreferences{}

if v, ok := d.GetOk("operation_preferences.0.failure_tolerance_count"); ok {
operationPreferences.FailureToleranceCount = aws.Int64(int64(v.(int)))
}
if v, ok := d.GetOk("operation_preferences.0.failure_tolerance_percentage"); ok {
operationPreferences.FailureTolerancePercentage = aws.Int64(int64(v.(int)))
}
if v, ok := d.GetOk("operation_preferences.0.max_concurrent_count"); ok {
operationPreferences.MaxConcurrentCount = aws.Int64(int64(v.(int)))
}
if v, ok := d.GetOk("operation_preferences.0.max_concurrent_percentage"); ok {
operationPreferences.MaxConcurrentPercentage = aws.Int64(int64(v.(int)))
}
if v, ok := d.GetOk("operation_preferences.0.region_concurrency_type"); ok {
operationPreferences.RegionConcurrencyType = aws.String(v.(string))
}
if v, ok := d.GetOk("operation_preferences.0.region_order"); ok {
operationPreferences.RegionOrder = flex.ExpandStringSet(v.(*schema.Set))
}

return operationPreferences
}
90 changes: 73 additions & 17 deletions internal/service/cloudformation/stack_set_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestAccCloudFormationStackSetInstance_basic(t *testing.T) {
testAccCheckCloudFormationStackSetInstanceExists(resourceName, &stackInstance1),
acctest.CheckResourceAttrAccountID(resourceName, "account_id"),
resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "0"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", "0"),
resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", "0"),
resource.TestCheckResourceAttr(resourceName, "region", acctest.Region()),
resource.TestCheckResourceAttr(resourceName, "retain_stack", "false"),
Expand Down Expand Up @@ -224,8 +225,6 @@ func TestAccCloudFormationStackSetInstance_retainStack(t *testing.T) {
}

func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) {
acctest.Skip(t, "API does not support enabling Organizations access (in particular, creating the Stack Sets IAM Service-Linked Role)")

var stackInstance cloudformation.StackInstance
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_cloudformation_stack_set_instance.test"
Expand All @@ -234,7 +233,9 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) {
PreCheck: func() {
acctest.PreCheck(t)
testAccPreCheckStackSet(t)
acctest.PreCheckOrganizationsAccount(t)
acctest.PreCheckOrganizationsEnabled(t)
acctest.PreCheckOrganizationManagementAccount(t)
acctest.PreCheckIAMServiceLinkedRole(t, "/aws-service-role/stacksets.cloudformation.amazonaws.com")
},
ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID, "organizations"),
Providers: acctest.Providers,
Expand All @@ -255,6 +256,7 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) {
ImportStateVerifyIgnore: []string{
"retain_stack",
"deployment_targets",
"call_as",
},
},
{
Expand All @@ -267,6 +269,45 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) {
})
}

func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) {
var stackInstance cloudformation.StackInstance
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_cloudformation_stack_set_instance.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(t)
testAccPreCheckStackSet(t)
acctest.PreCheckOrganizationsEnabled(t)
acctest.PreCheckOrganizationManagementAccount(t)
acctest.PreCheckIAMServiceLinkedRole(t, "/aws-service-role/stacksets.cloudformation.amazonaws.com")
},
ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID),
Providers: acctest.Providers,
CheckDestroy: testAccCheckStackSetInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccStackSetInstanceOperationPreferencesConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudFormationStackSetInstanceExists(resourceName, &stackInstance),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", "1"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", "1"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", "0"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_count", "10"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", "0"),
resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""),
),
},
{
Config: testAccStackSetInstanceConfig_ServiceManagedStackSet(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudFormationStackSetInstanceExists(resourceName, &stackInstance),
),
},
},
})
}

func testAccCheckCloudFormationStackSetInstanceExists(resourceName string, v *cloudformation.StackInstance) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[resourceName]
Expand Down Expand Up @@ -485,17 +526,17 @@ TEMPLATE
}

func testAccStackSetInstanceConfig(rName string) string {
return testAccStackSetInstanceBaseConfig(rName) + `
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig(rName), `
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
stack_set_name = aws_cloudformation_stack_set.test.name
}
`
`)
}

func testAccStackSetInstanceParameterOverrides1Config(rName, value1 string) string {
return testAccStackSetInstanceBaseConfig(rName) + fmt.Sprintf(`
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig(rName), fmt.Sprintf(`
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
Expand All @@ -505,11 +546,11 @@ resource "aws_cloudformation_stack_set_instance" "test" {
stack_set_name = aws_cloudformation_stack_set.test.name
}
`, value1)
`, value1))
}

func testAccStackSetInstanceParameterOverrides2Config(rName, value1, value2 string) string {
return testAccStackSetInstanceBaseConfig(rName) + fmt.Sprintf(`
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig(rName), fmt.Sprintf(`
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
Expand All @@ -520,18 +561,18 @@ resource "aws_cloudformation_stack_set_instance" "test" {
stack_set_name = aws_cloudformation_stack_set.test.name
}
`, value1, value2)
`, value1, value2))
}

func testAccStackSetInstanceRetainStackConfig(rName string, retainStack bool) string {
return testAccStackSetInstanceBaseConfig(rName) + fmt.Sprintf(`
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig(rName), fmt.Sprintf(`
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
retain_stack = %[1]t
stack_set_name = aws_cloudformation_stack_set.test.name
}
`, retainStack)
`, retainStack))
}

func testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName string) string {
Expand Down Expand Up @@ -651,9 +692,7 @@ TEMPLATE
}

func testAccStackSetInstanceDeploymentTargetsConfig(rName string) string {
return acctest.ConfigCompose(
testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName),
`
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), `
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
Expand All @@ -667,12 +706,29 @@ resource "aws_cloudformation_stack_set_instance" "test" {
}

func testAccStackSetInstanceConfig_ServiceManagedStackSet(rName string) string {
return acctest.ConfigCompose(
testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName),
`
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), `
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
stack_set_name = aws_cloudformation_stack_set.test.name
}
`)
}

func testAccStackSetInstanceOperationPreferencesConfig(rName string) string {
return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), `
resource "aws_cloudformation_stack_set_instance" "test" {
depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution]
operation_preferences {
failure_tolerance_count = 1
max_concurrent_count = 10
}
deployment_targets {
organizational_unit_ids = [data.aws_organizations_organization.test.roots[0].id]
}
stack_set_name = aws_cloudformation_stack_set.test.name
}
`)
Expand Down
12 changes: 12 additions & 0 deletions website/docs/r/cloudformation_stack_set_instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,25 @@ The following arguments are supported:
* `region` - (Optional) Target AWS Region to create a Stack based on the StackSet. Defaults to current region.
* `retain_stack` - (Optional) During Terraform resource destroy, remove Instance from StackSet while keeping the Stack and its associated resources. Must be enabled in Terraform state _before_ destroy operation to take effect. You cannot reassociate a retained Stack or add an existing, saved Stack to a new StackSet. Defaults to `false`.
* `call_as` - (Optional) Specifies whether you are acting as an account administrator in the organization's management account or as a delegated administrator in a member account. Valid values: `SELF` (default), `DELEGATED_ADMIN`.
* `operation_preferences` - (Optional) Preferences for how AWS CloudFormation performs a stack set operation.

### `deployment_targets` Argument Reference

The `deployment_targets` configuration block supports the following arguments:

*`organizational_unit_ids` - (Optional) The organization root ID or organizational unit (OU) IDs to which StackSets deploys.

### `operation_preferences` Argument Reference

The `operation_preferences` configuration block supports the following arguments:

*`failure_tolerance_count` - (Optional) The number of accounts, per Region, for which this operation can fail before AWS CloudFormation stops the operation in that Region.
*`failure_tolerance_percentage` - (Optional) The percentage of accounts, per Region, for which this stack operation can fail before AWS CloudFormation stops the operation in that Region.
*`max_concurrent_count` - (Optional) The maximum number of accounts in which to perform this operation at one time.
*`max_concurrent_percentage` - (Optional) The maximum percentage of accounts in which to perform this operation at one time.
*`region_concurrency_type` - (Optional) The concurrency type of deploying StackSets operations in Regions, could be in parallel or one Region at a time.
*`region_order` - (Optional) The order of the Regions in where you want to perform the stack operation.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down

0 comments on commit 8c596ef

Please sign in to comment.