Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Stack set instances operation preferences #23666

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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