diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 2dddabfc2d04..82b1d5d139cf 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -33,6 +33,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/inspector2" inspector2types "github.com/aws/aws-sdk-go-v2/service/inspector2/types" + "github.com/aws/aws-sdk-go-v2/service/organizations" organizationstypes "github.com/aws/aws-sdk-go-v2/service/organizations/types" "github.com/aws/aws-sdk-go-v2/service/outposts" "github.com/aws/aws-sdk-go-v2/service/pinpoint" @@ -1189,6 +1190,63 @@ func PreCheckOrganizationMemberAccountWithProvider(ctx context.Context, t *testi } } +func PreCheckMultipleOrganizationalUnitsWithAccounts(ctx context.Context, t *testing.T) { + t.Helper() + + PreCheckMultipleOrganizationalUnitsWithAccountsWithProvider(ctx, t, func() *schema.Provider { return Provider }) +} + +func PreCheckMultipleOrganizationalUnitsWithAccountsWithProvider(ctx context.Context, t *testing.T, providerF ProviderFunc) { + t.Helper() + + conn := providerF().Meta().(*conns.AWSClient).OrganizationsClient(ctx) + + listRootsOutput, err := conn.ListRoots(ctx, &organizations.ListRootsInput{}) + if err != nil { + t.Fatalf("Error listing roots: %v", err) + } + + rootId := "" + if len(listRootsOutput.Roots) != 0 { + rootId = *listRootsOutput.Roots[0].Id + } else { + t.Fatal("AWS Organization has no valid root ID") + } + + ousOutput, err := conn.ListOrganizationalUnitsForParent(ctx, &organizations.ListOrganizationalUnitsForParentInput{ + ParentId: &rootId, + }) + + if err != nil { + t.Fatalf("error listing organizational units for root: %s", err) + } + + if len(ousOutput.OrganizationalUnits) < 2 { + t.Skip("less than 2 organizational units found, skipping") + } + + counter := 0 + + for _, ou := range ousOutput.OrganizationalUnits { + if ou.Id == nil { + t.Fatal("organizational unit has no valid ID") + } + accountsOutput, err := conn.ListAccountsForParent(ctx, &organizations.ListAccountsForParentInput{ + ParentId: ou.Id, + }) + if err != nil { + t.Fatalf("error listing accounts for organizational unit %s: %s", aws.ToString(ou.Name), err) + } + + if len(accountsOutput.Accounts) != 0 { + counter++ + } + } + if counter < 2 { + t.Fatal("At least two OUs need one account") + } +} + func PreCheckPinpointApp(ctx context.Context, t *testing.T) { conn := Provider.Meta().(*conns.AWSClient).PinpointClient(ctx) diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index b282ac8386d8..70b7609ed585 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -416,13 +416,21 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, stackSetName, accountOrOrgID, region := parts[0], parts[1], parts[2] input := &cloudformation.UpdateStackInstancesInput{ - Accounts: []string{accountOrOrgID}, OperationId: aws.String(sdkid.UniqueId()), ParameterOverrides: []awstypes.Parameter{}, Regions: []string{region}, StackSetName: aws.String(stackSetName), } + if itypes.IsAWSAccountID(accountOrOrgID) { + input.Accounts = []string{accountOrOrgID} + } else { + orgIDs := strings.Split(accountOrOrgID, "/") + input.DeploymentTargets = &awstypes.DeploymentTargets{ + OrganizationalUnitIds: orgIDs, + } + } + callAs := d.Get("call_as").(string) if v, ok := d.GetOk("call_as"); ok { input.CallAs = awstypes.CallAs(v.(string)) diff --git a/internal/service/cloudformation/stack_set_instance_test.go b/internal/service/cloudformation/stack_set_instance_test.go index 0e37d564b3ab..7714e079c97f 100644 --- a/internal/service/cloudformation/stack_set_instance_test.go +++ b/internal/service/cloudformation/stack_set_instance_test.go @@ -418,6 +418,93 @@ func TestAccCloudFormationStackSetInstance_delegatedAdministrator(t *testing.T) }) } +func TestAccCloudFormationStackSetInstance_multipleDeploymentTargets(t *testing.T) { + ctx := acctest.Context(t) + var stackInstanceSummaries []awstypes.StackInstanceSummary + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_set_instance.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + acctest.PreCheckMultipleOrganizationalUnitsWithAccounts(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetInstanceConfig_multipleDeploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stack", + "call_as", + "deployment_targets", + }, + }, + { + Config: testAccStackSetInstanceConfig_multipleDeploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackSetInstance_updateWithOUTargets(t *testing.T) { + ctx := acctest.Context(t) + var stackInstanceSummaries []awstypes.StackInstanceSummary + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_set_instance.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + acctest.PreCheckMultipleOrganizationalUnitsWithAccounts(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetInstanceConfig_multipleDeploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + ), + }, + { + Config: testAccStackSetInstanceConfig_updateWithOUTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", "1"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", "1"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", "100"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.concurrency_mode", "SOFT_FAILURE_TOLERANCE"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", "PARALLEL"), + ), + }, + }, + }) +} + func testAccCheckStackSetInstanceExists(ctx context.Context, resourceName string, v *awstypes.StackInstance) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -935,3 +1022,52 @@ resource "aws_cloudformation_stack_set_instance" "test" { } `) } + +func testAccStackSetInstanceConfig_multipleDeploymentTargets(rName string) string { + return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), ` +data "aws_organizations_organizational_units" "test" { + parent_id = data.aws_organizations_organization.test.roots[0].id +} + +resource "aws_cloudformation_stack_set_instance" "test" { + depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution] + + deployment_targets { + organizational_unit_ids = [ + data.aws_organizations_organizational_units.test.children[0].id, + data.aws_organizations_organizational_units.test.children[1].id, + ] + } + + stack_set_name = aws_cloudformation_stack_set.test.name +} +`) +} + +func testAccStackSetInstanceConfig_updateWithOUTargets(rName string) string { + return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), ` +data "aws_organizations_organizational_units" "test" { + parent_id = data.aws_organizations_organization.test.roots[0].id +} + +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_percentage = 100 + concurrency_mode = "SOFT_FAILURE_TOLERANCE" + region_concurrency_type = "PARALLEL" + } + + deployment_targets { + organizational_unit_ids = [ + data.aws_organizations_organizational_units.test.children[0].id, + data.aws_organizations_organizational_units.test.children[1].id, + ] + } + + stack_set_name = aws_cloudformation_stack_set.test.name +} +`) +}