diff --git a/.changelog/19092.txt b/.changelog/19092.txt new file mode 100644 index 00000000000..909fbdc4b95 --- /dev/null +++ b/.changelog/19092.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_cloudformation_stack_set: Fix `Can only set RetainStacksOnAccountRemoval if AutoDeployment is enabled` errors +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set: Support resource import with `call_as = "DELEGATED_ADMIN"` via __,__ syntax for `import` block or `terraform import` command +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Support resource import with `call_as = "DELEGATED_ADMIN"` via __,__,__,__ syntax for `import` block or `terraform import` command +``` \ No newline at end of file diff --git a/internal/service/cloudformation/id.go b/internal/service/cloudformation/id.go deleted file mode 100644 index c5f1b5b7f30..00000000000 --- a/internal/service/cloudformation/id.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package cloudformation - -import ( - "fmt" - "strings" -) - -const stackSetInstanceResourceIDSeparator = "," - -func StackSetInstanceCreateResourceID(stackSetName, accountID, region string) string { - parts := []string{stackSetName, accountID, region} - id := strings.Join(parts, stackSetInstanceResourceIDSeparator) - - return id -} - -func StackSetInstanceParseResourceID(id string) (string, string, string, error) { - parts := strings.Split(id, stackSetInstanceResourceIDSeparator) - - if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { - return parts[0], parts[1], parts[2], nil - } - - return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected STACKSETNAME%[2]sACCOUNTID%[2]sREGION", id, stackSetInstanceResourceIDSeparator) -} diff --git a/internal/service/cloudformation/stack_set.go b/internal/service/cloudformation/stack_set.go index 5fd470ad5ac..fc60f906918 100644 --- a/internal/service/cloudformation/stack_set.go +++ b/internal/service/cloudformation/stack_set.go @@ -5,7 +5,9 @@ package cloudformation import ( "context" + "fmt" "log" + "strings" "time" "github.com/YakDriver/regexache" @@ -35,7 +37,7 @@ func ResourceStackSet() *schema.Resource { DeleteWithoutTimeout: resourceStackSetDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceStackSetImport, }, Timeouts: &schema.ResourceTimeout{ @@ -228,6 +230,10 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in input.AutoDeployment = expandAutoDeployment(v.([]interface{})) } + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = aws.String(v.(string)) + } + if v, ok := d.GetOk("capabilities"); ok { input.Capabilities = flex.ExpandStringSet(v.(*schema.Set)) } @@ -252,10 +258,6 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in input.PermissionModel = aws.String(v.(string)) } - if v, ok := d.GetOk("call_as"); ok { - input.CallAs = aws.String(v.(string)) - } - if v, ok := d.GetOk("template_body"); ok { input.TemplateBody = aws.String(v.(string)) } @@ -264,7 +266,6 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in input.TemplateURL = aws.String(v.(string)) } - log.Printf("[DEBUG] Creating CloudFormation StackSet: %s", input) _, err := conn.CreateStackSetWithContext(ctx, input) if err != nil { @@ -295,35 +296,25 @@ func resourceStackSetRead(ctx context.Context, d *schema.ResourceData, meta inte d.Set("administration_role_arn", stackSet.AdministrationRoleARN) d.Set("arn", stackSet.StackSetARN) - if err := d.Set("auto_deployment", flattenStackSetAutoDeploymentResponse(stackSet.AutoDeployment)); err != nil { return sdkdiag.AppendErrorf(diags, "setting auto_deployment: %s", err) } - - if err := d.Set("capabilities", aws.StringValueSlice(stackSet.Capabilities)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting capabilities: %s", err) - } - + d.Set("capabilities", aws.StringValueSlice(stackSet.Capabilities)) d.Set("description", stackSet.Description) d.Set("execution_role_name", stackSet.ExecutionRoleName) - if err := d.Set("managed_execution", flattenStackSetManagedExecution(stackSet.ManagedExecution)); err != nil { return sdkdiag.AppendErrorf(diags, "setting managed_execution: %s", err) } - d.Set("name", stackSet.StackSetName) d.Set("permission_model", stackSet.PermissionModel) - if err := d.Set("parameters", flattenAllParameters(stackSet.Parameters)); err != nil { return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) } - d.Set("stack_set_id", stackSet.StackSetId) + d.Set("template_body", stackSet.TemplateBody) setTagsOut(ctx, stackSet.Tags) - d.Set("template_body", stackSet.TemplateBody) - return diags } @@ -342,6 +333,11 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in input.AdministrationRoleARN = aws.String(v.(string)) } + callAs := d.Get("call_as").(string) + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = aws.String(v.(string)) + } + if v, ok := d.GetOk("capabilities"); ok { input.Capabilities = flex.ExpandStringSet(v.(*schema.Set)) } @@ -370,11 +366,6 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in input.PermissionModel = aws.String(v.(string)) } - callAs := d.Get("call_as").(string) - if v, ok := d.GetOk("call_as"); ok { - input.CallAs = aws.String(v.(string)) - } - if tags := getTagsIn(ctx); len(tags) > 0 { input.Tags = tags } @@ -395,7 +386,6 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in input.AutoDeployment = expandAutoDeployment(v.([]interface{})) } - log.Printf("[DEBUG] Updating CloudFormation StackSet: %s", input) output, err := conn.UpdateStackSetWithContext(ctx, input) if err != nil { @@ -435,6 +425,21 @@ func resourceStackSetDelete(ctx context.Context, d *schema.ResourceData, meta in return diags } +func resourceStackSetImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + const stackSetImportIDSeparator = "," + + switch parts := strings.Split(d.Id(), stackSetImportIDSeparator); len(parts) { + case 1: + case 2: + d.SetId(parts[0]) + d.Set("call_as", parts[1]) + default: + return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSETNAME or STACKSETNAME%[2]sCALLAS", d.Id(), stackSetImportIDSeparator) + } + + return []*schema.ResourceData{d}, nil +} + func expandAutoDeployment(l []interface{}) *cloudformation.AutoDeployment { if len(l) == 0 { return nil @@ -442,9 +447,13 @@ func expandAutoDeployment(l []interface{}) *cloudformation.AutoDeployment { m := l[0].(map[string]interface{}) + enabled := m["enabled"].(bool) autoDeployment := &cloudformation.AutoDeployment{ - Enabled: aws.Bool(m["enabled"].(bool)), - RetainStacksOnAccountRemoval: aws.Bool(m["retain_stacks_on_account_removal"].(bool)), + Enabled: aws.Bool(enabled), + } + + if enabled { + autoDeployment.RetainStacksOnAccountRemoval = aws.Bool(m["retain_stacks_on_account_removal"].(bool)) } return autoDeployment diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 886c8c487a7..e384d89148b 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -33,7 +33,7 @@ func ResourceStackSetInstance() *schema.Resource { DeleteWithoutTimeout: resourceStackSetInstanceDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceStackSetInstanceImport, }, Timeouts: &schema.ResourceTimeout{ @@ -133,17 +133,17 @@ func ResourceStackSetInstance() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "retain_stack": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, "region": { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, + "retain_stack": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "stack_id": { Type: schema.TypeString, Computed: true, @@ -230,7 +230,6 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) } - log.Printf("[DEBUG] Creating CloudFormation StackSet Instance: %s", input) _, err := tfresource.RetryWhen(ctx, propagationTimeout, func() (interface{}, error) { input.OperationId = aws.String(id.UniqueId()) @@ -284,7 +283,7 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, return true, err } - return false, fmt.Errorf("waiting for CloudFormation StackSet Instance (%s) creation: %w", d.Id(), err) + return false, err }, ) @@ -301,11 +300,9 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) - } - if accountOrOrgID == "" { - return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): account_id or organizational_unit_id section empty", d.Id()) + return sdkdiag.AppendFromErr(diags, err) } + d.Set("region", region) d.Set("stack_set_name", stackSetName) @@ -314,11 +311,13 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m if accountIDRegexp.MatchString(accountOrOrgID) { // Stack instances deployed by account ID stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountOrOrgID, region, callAs) + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) d.SetId("") return diags } + if err != nil { return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) } @@ -336,13 +335,15 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m orgIDs := strings.Split(accountOrOrgID, "/") summaries, err := FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) d.SetId("") return diags } + if err != nil { - return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s): %s", d.Id(), err) } d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs)) @@ -358,9 +359,8 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") { stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) - if err != nil { - return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): %s", d.Id(), err) + return sdkdiag.AppendFromErr(diags, err) } input := &cloudformation.UpdateStackInstancesInput{ @@ -391,7 +391,6 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) } - log.Printf("[DEBUG] Updating CloudFormation StackSet Instance: %s", input) output, err := conn.UpdateStackInstancesWithContext(ctx, input) if err != nil { @@ -399,7 +398,7 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, } if _, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutUpdate)); err != nil { - return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): waiting for completion: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for CloudFormation StackSet Instance (%s) update: %s", d.Id(), err) } } @@ -411,9 +410,8 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) - if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): %s", d.Id(), err) + return sdkdiag.AppendFromErr(diags, err) } input := &cloudformation.DeleteStackInstancesInput{ @@ -440,7 +438,7 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) output, err := conn.DeleteStackInstancesWithContext(ctx, input) - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException, cloudformation.ErrCodeStackSetNotFoundException) { return diags } @@ -449,12 +447,44 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, } if _, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutDelete)); err != nil { - return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): waiting for completion: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for CloudFormation StackSet Instance (%s) delete: %s", d.Id(), err) } return diags } +func resourceStackSetInstanceImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + switch parts := strings.Split(d.Id(), stackSetInstanceResourceIDSeparator); len(parts) { + case 3: + case 4: + d.SetId(strings.Join([]string{parts[0], parts[1], parts[2]}, stackSetInstanceResourceIDSeparator)) + d.Set("call_as", parts[3]) + default: + return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSETNAME%[2]sACCOUNTID%[2]sREGION or STACKSETNAME%[2]sACCOUNTID%[2]sREGION%[2]sCALLAS", d.Id(), stackSetInstanceResourceIDSeparator) + } + + return []*schema.ResourceData{d}, nil +} + +const stackSetInstanceResourceIDSeparator = "," + +func StackSetInstanceCreateResourceID(stackSetName, accountID, region string) string { + parts := []string{stackSetName, accountID, region} + id := strings.Join(parts, stackSetInstanceResourceIDSeparator) + + return id +} + +func StackSetInstanceParseResourceID(id string) (string, string, string, error) { + parts := strings.Split(id, stackSetInstanceResourceIDSeparator) + + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected STACKSETNAME%[2]sACCOUNTID%[2]sREGION", id, stackSetInstanceResourceIDSeparator) +} + func expandDeploymentTargets(tfList []interface{}) *cloudformation.DeploymentTargets { if len(tfList) == 0 || tfList[0] == nil { return nil diff --git a/internal/service/cloudformation/stack_set_instance_test.go b/internal/service/cloudformation/stack_set_instance_test.go index 9bbdf602762..80ffac0876c 100644 --- a/internal/service/cloudformation/stack_set_instance_test.go +++ b/internal/service/cloudformation/stack_set_instance_test.go @@ -38,12 +38,15 @@ func TestAccCloudFormationStackSetInstance_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance1), acctest.CheckResourceAttrAccountID(resourceName, "account_id"), + resource.TestCheckResourceAttr(resourceName, "call_as", "SELF"), resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "0"), resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", "0"), + resource.TestCheckResourceAttr(resourceName, "organizational_unit_id", ""), resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", "0"), resource.TestCheckResourceAttr(resourceName, "region", acctest.Region()), resource.TestCheckResourceAttr(resourceName, "retain_stack", "false"), resource.TestCheckResourceAttrSet(resourceName, "stack_id"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", "0"), resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, "name"), ), }, @@ -56,6 +59,23 @@ func TestAccCloudFormationStackSetInstance_basic(t *testing.T) { "call_as", }, }, + // Test import with call_as. + { + ResourceName: resourceName, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s,SELF", rs.Primary.ID), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stack", + }, + }, }, }) } diff --git a/internal/service/cloudformation/stack_set_test.go b/internal/service/cloudformation/stack_set_test.go index de426313897..c1f57a6ea09 100644 --- a/internal/service/cloudformation/stack_set_test.go +++ b/internal/service/cloudformation/stack_set_test.go @@ -65,6 +65,16 @@ func TestAccCloudFormationStackSet_basic(t *testing.T) { "template_url", }, }, + // Test import with call_as. + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s,SELF", rName), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "template_url", + }, + }, }, }) } @@ -737,6 +747,84 @@ func TestAccCloudFormationStackSet_templateURL(t *testing.T) { }) } +// https://github.com/hashicorp/terraform-provider-aws/issues/19015. +func TestAccCloudFormationStackSet_autoDeploymentEnabled(t *testing.T) { + ctx := acctest.Context(t) + var stackSet cloudformation.StackSet + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_set.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetConfig_autoDeployment(rName, true, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetExists(ctx, resourceName, &stackSet), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.retain_stacks_on_account_removal", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "call_as", + "template_url", + }, + }, + }, + }) +} + +// https://github.com/hashicorp/terraform-provider-aws/issues/19015. +func TestAccCloudFormationStackSet_autoDeploymentDisabled(t *testing.T) { + ctx := acctest.Context(t) + var stackSet cloudformation.StackSet + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_set.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetConfig_autoDeployment(rName, false, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetExists(ctx, resourceName, &stackSet), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.retain_stacks_on_account_removal", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "call_as", + "template_url", + }, + }, + }, + }) +} + func testAccCheckStackSetExists(ctx context.Context, resourceName string, v *cloudformation.StackSet) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -744,11 +832,9 @@ func testAccCheckStackSetExists(ctx context.Context, resourceName string, v *clo return fmt.Errorf("Not found: %s", resourceName) } - callAs := rs.Primary.Attributes["call_as"] - conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) - output, err := tfcloudformation.FindStackSetByName(ctx, conn, rs.Primary.ID, callAs) + output, err := tfcloudformation.FindStackSetByName(ctx, conn, rs.Primary.ID, rs.Primary.Attributes["call_as"]) if err != nil { return err @@ -769,9 +855,7 @@ func testAccCheckStackSetDestroy(ctx context.Context) resource.TestCheckFunc { continue } - callAs := rs.Primary.Attributes["call_as"] - - _, err := tfcloudformation.FindStackSetByName(ctx, conn, rs.Primary.ID, callAs) + _, err := tfcloudformation.FindStackSetByName(ctx, conn, rs.Primary.ID, rs.Primary.Attributes["call_as"]) if tfresource.NotFound(err) { continue @@ -1316,3 +1400,21 @@ TEMPLATE } `, rName, failureTolerancePercentage, maxConcurrentPercentage, testAccStackSetTemplateBodyVPC(rName))) } + +func testAccStackSetConfig_autoDeployment(rName string, enabled, retainStacksOnAccountRemoval bool) string { + return fmt.Sprintf(` +resource "aws_cloudformation_stack_set" "test" { + name = %[1]q + permission_model = "SERVICE_MANAGED" + + auto_deployment { + enabled = %[3]t + retain_stacks_on_account_removal = %[4]t + } + + template_body = <