From 3641b9e67149e0d32e7651ff087983b314dbd8a8 Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Tue, 3 May 2022 11:49:00 -0500 Subject: [PATCH 01/13] Treat Deployment Targets as their own case The original code tried to add deployment target support by finding an account in one of the targets, and then using all the same code as account stack set instances. This is deficient: - If none of the targets contain an account the code fails, due to the resource Id not having an accountID value. - It does not expose information like account IDs and stack set IDs for any accounts in the deployment targets, except the arbitrary accoun it used. - It can cause the resource Id to needlessly drift due to the account selection process changing (account moved OU's, or the API decided to return things in a different order, etc). This refactor treats deployment targets as their own branch of the logic, with their own new attributes. --- internal/service/cloudformation/find.go | 47 ++++++++++ .../cloudformation/stack_set_instance.go | 88 +++++++++++++------ 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go index 3b42e503b664..2677aceec6a9 100644 --- a/internal/service/cloudformation/find.go +++ b/internal/service/cloudformation/find.go @@ -74,6 +74,53 @@ func FindStackByID(conn *cloudformation.CloudFormation, id string) (*cloudformat return stack, nil } +func FindStackInstanceSummariesByOrgIDs(conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) ([]*cloudformation.StackInstanceSummary, error) { + input := &cloudformation.ListStackInstancesInput{ + StackInstanceRegion: aws.String(region), + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = aws.String(callAs) + } + + var result []*cloudformation.StackInstanceSummary + + err := conn.ListStackInstancesPages(input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, s := range page.Summaries { + if s == nil { + continue + } + + for _, orgID := range orgIDs { + if aws.StringValue(s.OrganizationalUnitId) == orgID { + result = append(result, s) + } + } + + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return result, nil +} + func FindStackInstanceAccountIdByOrgIDs(conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) (string, error) { input := &cloudformation.ListStackInstancesInput{ StackInstanceRegion: aws.String(region), diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index b8d2e428830a..84ece1f3ff74 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -45,6 +45,11 @@ func ResourceStackSetInstance() *schema.Resource { ValidateFunc: verify.ValidAccountID, ConflictsWith: []string{"deployment_targets"}, }, + "account_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "call_as": { Type: schema.TypeString, Optional: true, @@ -141,6 +146,11 @@ func ResourceStackSetInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "stack_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "stack_set_name": { Type: schema.TypeString, Required: true, @@ -259,48 +269,70 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro conn := meta.(*conns.AWSClient).CloudFormationConn stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) - - callAs := d.Get("call_as").(string) - if err != nil { return err } - // Determine correct account ID for the Instance if created with deployment targets; - // we only expect the accountID to be the organization root ID or organizational unit (OU) IDs - // separated by a slash after creation. - if regexp.MustCompile(`(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})`).MatchString(accountID) { - orgIDs := strings.Split(accountID, "/") - accountID, err = FindStackInstanceAccountIdByOrgIDs(conn, stackSetName, region, callAs, orgIDs) + accountIDIsDT := regexp.MustCompile(`(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})`).MatchString(accountID) + callAs := d.Get("call_as").(string) + + d.Set("region", region) + d.Set("stack_set_name", stackSetName) + + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + // Process this as a deployment target instance + orgIDs := make([]string, 0, len(v.([]interface{}))) + for _, orgID := range expandCloudFormationDeploymentTargets(v.([]interface{})).OrganizationalUnitIds { + orgIDs = append(orgIDs, *orgID) + } + if !accountIDIsDT { + accountID = strings.Join(orgIDs, "/") + d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) + } + + summaries, err := FindStackInstanceSummariesByOrgIDs(conn, stackSetName, region, callAs, orgIDs) if err != nil { return fmt.Errorf("error finding CloudFormation StackSet Instance (%s) Account: %w", d.Id(), err) } - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) - } + accountIDs := make([]*string, 0, len(summaries)) + stackIDs := make([]*string, 0, len(summaries)) + for _, si := range summaries { + accountIDs = append(accountIDs, si.Account) + stackIDs = append(stackIDs, si.StackId) + } + d.Set("account_ids", accountIDs) + d.Set("stack_ids", stackIDs) + } else if v, ok := d.GetOk("account_id"); ok && v != nil { + // Process this as an account ID instance + if accountIDIsDT { + accountID = v.(string) + d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) + } - stackInstance, err := FindStackInstanceByName(conn, stackSetName, accountID, region, callAs) + stackInstance, err := FindStackInstanceByName(conn, stackSetName, accountID, 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 nil - } + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } - if err != nil { - return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): %w", d.Id(), err) - } + if err != nil { + return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): %w", d.Id(), err) + } - d.Set("account_id", stackInstance.Account) - d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) - if err := d.Set("parameter_overrides", flattenAllCloudFormationParameters(stackInstance.ParameterOverrides)); err != nil { - return fmt.Errorf("error setting parameters: %w", err) - } + d.Set("account_id", stackInstance.Account) + d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) + if err := d.Set("parameter_overrides", flattenAllCloudFormationParameters(stackInstance.ParameterOverrides)); err != nil { + return fmt.Errorf("error setting parameters: %w", err) + } - d.Set("region", stackInstance.Region) - d.Set("stack_id", stackInstance.StackId) - d.Set("stack_set_name", stackSetName) + d.Set("stack_id", stackInstance.StackId) + } else { + return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): no account_id or deployment_targets", d.Id()) + } return nil } From 55a8f4276f043d604527def4b09c75166d50ede6 Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Tue, 3 May 2022 12:00:34 -0500 Subject: [PATCH 02/13] Adjust to change in func names --- internal/service/cloudformation/stack_set_instance.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 06dbfe94a436..c757dca31435 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -282,7 +282,7 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { // Process this as a deployment target instance orgIDs := make([]string, 0, len(v.([]interface{}))) - for _, orgID := range expandCloudFormationDeploymentTargets(v.([]interface{})).OrganizationalUnitIds { + for _, orgID := range expandDeploymentTargets(v.([]interface{})).OrganizationalUnitIds { orgIDs = append(orgIDs, *orgID) } if !accountIDIsDT { @@ -325,7 +325,7 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro d.Set("account_id", stackInstance.Account) d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) - if err := d.Set("parameter_overrides", flattenAllCloudFormationParameters(stackInstance.ParameterOverrides)); err != nil { + if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil { return fmt.Errorf("error setting parameters: %w", err) } From 7e6d0c6aeb7e2308180c54a9b21c5382997068e7 Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Tue, 3 May 2022 12:09:37 -0500 Subject: [PATCH 03/13] Add changelog for PR --- .changelog/24523.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/24523.txt diff --git a/.changelog/24523.txt b/.changelog/24523.txt new file mode 100644 index 000000000000..ccb6339be9ec --- /dev/null +++ b/.changelog/24523.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudformation_stack_set_instance: Refactor to better support deployment targets. +``` \ No newline at end of file From 7aa203fade7b5bcf129243799fda0a4e0c43790d Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Fri, 6 May 2022 15:50:28 -0500 Subject: [PATCH 04/13] Handle deletes with multipe deployment_targets --- .../cloudformation/stack_set_instance.go | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index c757dca31435..3a0f9a087bc6 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -185,8 +185,7 @@ func resourceStackSetInstanceCreate(d *schema.ResourceData, meta interface{}) er input.CallAs = aws.String(v.(string)) } - if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - dt := expandDeploymentTargets(v.([]interface{})) + if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { // temporarily set the accountId to the DeploymentTarget IDs // to later inform the Read CRUD operation if the true accountID needs to be determined accountID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/") @@ -279,12 +278,9 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro d.Set("region", region) d.Set("stack_set_name", stackSetName) - if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { // Process this as a deployment target instance - orgIDs := make([]string, 0, len(v.([]interface{}))) - for _, orgID := range expandDeploymentTargets(v.([]interface{})).OrganizationalUnitIds { - orgIDs = append(orgIDs, *orgID) - } + orgIDs := aws.StringValueSlice(dt.OrganizationalUnitIds) if !accountIDIsDT { accountID = strings.Join(orgIDs, "/") d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) @@ -299,8 +295,12 @@ func resourceStackSetInstanceRead(d *schema.ResourceData, meta interface{}) erro accountIDs := make([]*string, 0, len(summaries)) stackIDs := make([]*string, 0, len(summaries)) for _, si := range summaries { - accountIDs = append(accountIDs, si.Account) - stackIDs = append(stackIDs, si.StackId) + if si.Account != nil && len(*si.Account) > 0 { + accountIDs = append(accountIDs, si.Account) + } + if si.StackId != nil && len(*si.StackId) > 0 { + stackIDs = append(stackIDs, si.StackId) + } } d.Set("account_ids", accountIDs) d.Set("stack_ids", stackIDs) @@ -360,10 +360,10 @@ func resourceStackSetInstanceUpdate(d *schema.ResourceData, meta interface{}) er input.CallAs = aws.String(v.(string)) } - if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { // reset input Accounts as the API accepts only 1 of Accounts and DeploymentTargets input.Accounts = nil - input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.DeploymentTargets = dt } if v, ok := d.GetOk("parameter_overrides"); ok { @@ -411,13 +411,11 @@ func resourceStackSetInstanceDelete(d *schema.ResourceData, meta interface{}) er input.CallAs = aws.String(v.(string)) } - if v, ok := d.GetOk("organizational_unit_id"); ok { + if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { // For instances associated with stack sets that use a self-managed permission model, // the organizational unit must be provided; input.Accounts = nil - input.DeploymentTargets = &cloudformation.DeploymentTargets{ - OrganizationalUnitIds: aws.StringSlice([]string{v.(string)}), - } + input.DeploymentTargets = dt } log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) @@ -438,7 +436,13 @@ func resourceStackSetInstanceDelete(d *schema.ResourceData, meta interface{}) er return nil } -func expandDeploymentTargets(l []interface{}) *cloudformation.DeploymentTargets { +func expandDeploymentTargets(d *schema.ResourceData) *cloudformation.DeploymentTargets { + v, ok := d.GetOk("deployment_targets") + if !ok { + return nil + } + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { return nil } @@ -451,8 +455,8 @@ func expandDeploymentTargets(l []interface{}) *cloudformation.DeploymentTargets dt := &cloudformation.DeploymentTargets{} - if v, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && v.Len() > 0 { - dt.OrganizationalUnitIds = flex.ExpandStringSet(v) + if ou_id, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && ou_id.Len() > 0 { + dt.OrganizationalUnitIds = flex.ExpandStringSet(ou_id) } return dt From 0b8edd0d61c90e4fbd476d0896b47b1da0587c34 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Tue, 18 Jul 2023 14:38:16 -0400 Subject: [PATCH 05/13] r/aws_cloudformation_stack_set_instance: fix build failures --- internal/service/cloudformation/stack_set_instance.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index f84eb4d88b45..2e338cccc6bf 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -331,13 +331,13 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m } if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %w", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) } d.Set("account_id", stackInstance.Account) d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting parameters: %w", err) + return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) } d.Set("stack_id", stackInstance.StackId) From c6965befe6cd31a7b1584289290a43fc3f2cac3a Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 16:30:15 -0400 Subject: [PATCH 06/13] r/aws_cloudformation_stack_set_instance: improve support for OU deployments - persist list of organizational unit ids in the resource identifier, allowing for consistent read operations and better import support - force a new resource when deployment_targets change to ensure changes to stack sets across multiple organizational units are handled correctly - track full list of account IDs and stack IDs deployed when stack set instances are created for an organizational unit - prevent failures when deploying to an empty organizational unit --- internal/service/cloudformation/find.go | 47 ----- .../cloudformation/stack_set_instance.go | 184 ++++++++++-------- 2 files changed, 108 insertions(+), 123 deletions(-) diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go index fca611d26367..4a5838d96c5a 100644 --- a/internal/service/cloudformation/find.go +++ b/internal/service/cloudformation/find.go @@ -124,53 +124,6 @@ func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformatio return result, nil } -func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) (string, error) { - input := &cloudformation.ListStackInstancesInput{ - StackInstanceRegion: aws.String(region), - StackSetName: aws.String(stackSetName), - } - - if callAs != "" { - input.CallAs = aws.String(callAs) - } - - var result string - - err := conn.ListStackInstancesPagesWithContext(ctx, input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, s := range page.Summaries { - if s == nil { - continue - } - - for _, orgID := range orgIDs { - if aws.StringValue(s.OrganizationalUnitId) == orgID { - result = aws.StringValue(s.Account) - return false - } - } - } - - return !lastPage - }) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { - return "", &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return "", err - } - - return result, nil -} - func FindStackInstanceByName(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, accountID, region, callAs string) (*cloudformation.StackInstance, error) { input := &cloudformation.DescribeStackInstanceInput{ StackInstanceAccount: aws.String(accountID), diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 2e338cccc6bf..9a9cf18dfd05 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -51,11 +51,6 @@ func ResourceStackSetInstance() *schema.Resource { ValidateFunc: verify.ValidAccountID, ConflictsWith: []string{"deployment_targets"}, }, - "account_ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, "call_as": { Type: schema.TypeString, Optional: true, @@ -65,6 +60,7 @@ func ResourceStackSetInstance() *schema.Resource { "deployment_targets": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -152,10 +148,27 @@ func ResourceStackSetInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "stack_ids": { + "stack_instance_summaries": { Type: schema.TypeList, Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Description: "List of stack instances created from an organizational unit deployment target. " + + "This will only be populated when `deployment_targets` is set.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + "organizational_unit_id": { + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, }, "stack_set_name": { Type: schema.TypeString, @@ -167,6 +180,10 @@ func ResourceStackSetInstance() *schema.Resource { } } +var ( + awsAccountRegexp = regexp.MustCompile(`^\d{12}$`) +) + func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) @@ -186,21 +203,25 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, if v, ok := d.GetOk("account_id"); ok { accountID = v.(string) } + // accountOrOrgID will either be account_id or a slash-delimited list of + // organizational_unit_id's from the deployment_targets argument. This + // is composed with stack_set_name and region to form the resources ID. + accountOrOrgID := accountID + + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + dt := expandDeploymentTargets(v.([]interface{})) + accountOrOrgID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/") + input.DeploymentTargets = dt + } else { + d.Set("account_id", accountID) + input.Accounts = aws.StringSlice([]string{accountID}) + } callAs := d.Get("call_as").(string) if v, ok := d.GetOk("call_as"); ok { input.CallAs = aws.String(v.(string)) } - if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { - // temporarily set the accountId to the DeploymentTarget IDs - // to later inform the Read CRUD operation if the true accountID needs to be determined - accountID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/") - input.DeploymentTargets = dt - } else { - input.Accounts = aws.StringSlice([]string{accountID}) - } - if v, ok := d.GetOk("parameter_overrides"); ok { input.ParameterOverrides = expandParameters(v.(map[string]interface{})) } @@ -220,7 +241,7 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, return nil, err } - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) + d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountOrOrgID, region)) operation, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutCreate)) if err != nil { @@ -278,58 +299,26 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) } - - accountIDIsDT := regexp.MustCompile(`(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})`).MatchString(accountID) - callAs := d.Get("call_as").(string) - + if accountOrOrgID == "" { + return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): account_id or organizational_unit_id section empty", d.Id()) + } d.Set("region", region) d.Set("stack_set_name", stackSetName) - if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { - // Process this as a deployment target instance - orgIDs := aws.StringValueSlice(dt.OrganizationalUnitIds) - if !accountIDIsDT { - accountID = strings.Join(orgIDs, "/") - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) - } - - summaries, err := FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) - - if err != nil { - return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err) - } - - accountIDs := make([]*string, 0, len(summaries)) - stackIDs := make([]*string, 0, len(summaries)) - for _, si := range summaries { - if si.Account != nil && len(*si.Account) > 0 { - accountIDs = append(accountIDs, si.Account) - } - if si.StackId != nil && len(*si.StackId) > 0 { - stackIDs = append(stackIDs, si.StackId) - } - } - d.Set("account_ids", accountIDs) - d.Set("stack_ids", stackIDs) - } else if v, ok := d.GetOk("account_id"); ok && v != nil { - // Process this as an account ID instance - if accountIDIsDT { - accountID = v.(string) - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) - } - - stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountID, region, callAs) + callAs := d.Get("call_as").(string) + if awsAccountRegexp.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) } @@ -341,8 +330,23 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m } d.Set("stack_id", stackInstance.StackId) + d.Set("stack_instance_summaries", nil) } else { - return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): no account_id or deployment_targets", d.Id()) + // Stack instances deployed by organizational unit ID + 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) + } + + d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs)) + d.Set("stack_instance_summaries", flattenStackInstanceSummaries(summaries)) } return diags @@ -353,14 +357,14 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") { - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): %s", d.Id(), err) } input := &cloudformation.UpdateStackInstancesInput{ - Accounts: aws.StringSlice([]string{accountID}), + Accounts: aws.StringSlice([]string{accountOrOrgID}), OperationId: aws.String(id.UniqueId()), ParameterOverrides: []*cloudformation.Parameter{}, Regions: aws.StringSlice([]string{region}), @@ -372,7 +376,8 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, input.CallAs = aws.String(v.(string)) } - if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + dt := expandDeploymentTargets(v.([]interface{})) // reset input Accounts as the API accepts only 1 of Accounts and DeploymentTargets input.Accounts = nil input.DeploymentTargets = dt @@ -405,14 +410,14 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): %s", d.Id(), err) } input := &cloudformation.DeleteStackInstancesInput{ - Accounts: aws.StringSlice([]string{accountID}), + Accounts: aws.StringSlice([]string{accountOrOrgID}), OperationId: aws.String(id.UniqueId()), Regions: aws.StringSlice([]string{region}), RetainStacks: aws.Bool(d.Get("retain_stack").(bool)), @@ -424,7 +429,8 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, input.CallAs = aws.String(v.(string)) } - if dt := expandDeploymentTargets(d); dt != nil && len(dt.OrganizationalUnitIds) > 0 { + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + dt := expandDeploymentTargets(v.([]interface{})) // For instances associated with stack sets that use a self-managed permission model, // the organizational unit must be provided; input.Accounts = nil @@ -449,28 +455,54 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, return diags } -func expandDeploymentTargets(d *schema.ResourceData) *cloudformation.DeploymentTargets { - v, ok := d.GetOk("deployment_targets") - if !ok { +func expandDeploymentTargets(tfList []interface{}) *cloudformation.DeploymentTargets { + if len(tfList) == 0 || tfList[0] == nil { return nil } - l := v.([]interface{}) - if len(l) == 0 || l[0] == nil { + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { return nil } - tfMap, ok := l[0].(map[string]interface{}) + dt := &cloudformation.DeploymentTargets{} + if v, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && v.Len() > 0 { + dt.OrganizationalUnitIds = flex.ExpandStringSet(v) + } - if !ok { - return nil + return dt +} + +// flattenDeployment targets converts a list of organizational units (typically +// parsed from the resource ID) into the Terraform representation of the +// deployment_targets attribute. +func flattenDeploymentTargetsFromSlice(orgIDs []string) []interface{} { + tfList := []interface{}{} + for _, ou := range orgIDs { + tfList = append(tfList, ou) } - dt := &cloudformation.DeploymentTargets{} + m := map[string]interface{}{ + "organizational_unit_ids": tfList, + } - if ou_id, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && ou_id.Len() > 0 { - dt.OrganizationalUnitIds = flex.ExpandStringSet(ou_id) + return []interface{}{m} +} + +func flattenStackInstanceSummaries(apiObject []*cloudformation.StackInstanceSummary) []interface{} { + if len(apiObject) == 0 { + return nil } - return dt + tfList := []interface{}{} + for _, obj := range apiObject { + m := map[string]interface{}{ + "account_id": obj.Account, + "organizational_unit_id": obj.OrganizationalUnitId, + "stack_id": obj.StackId, + } + tfList = append(tfList, m) + } + + return tfList } From a7205995fdae685099af78ab5411ef2579da6e1b Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 16:32:50 -0400 Subject: [PATCH 07/13] r/aws_cloudformation_stack_set_instance(test): improve support for OU deployments - add CheckExists and CheckDestroy handlers tailored to handle resources where organizational unit ids are stored in the ID - add test deploying to an empty organizational unit --- .../cloudformation/stack_set_instance_test.go | 156 ++++++++++++++++-- 1 file changed, 138 insertions(+), 18 deletions(-) diff --git a/internal/service/cloudformation/stack_set_instance_test.go b/internal/service/cloudformation/stack_set_instance_test.go index aa75120eea77..0d142dd6aee7 100644 --- a/internal/service/cloudformation/stack_set_instance_test.go +++ b/internal/service/cloudformation/stack_set_instance_test.go @@ -6,6 +6,7 @@ package cloudformation_test import ( "context" "fmt" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -235,7 +236,7 @@ func TestAccCloudFormationStackSetInstance_retainStack(t *testing.T) { func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { ctx := acctest.Context(t) - var stackInstance cloudformation.StackInstance + var stackInstanceSummaries []*cloudformation.StackInstanceSummary rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudformation_stack_set_instance.test" @@ -249,12 +250,57 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID, "organizations"), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckStackSetInstanceDestroy(ctx), + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccStackSetInstanceConfig_deploymentTargets(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stack", + "call_as", + }, + }, + { + Config: testAccStackSetInstanceConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackSetInstance_DeploymentTargets_emptyOU(t *testing.T) { + ctx := acctest.Context(t) + var stackInstanceSummaries []*cloudformation.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") + }, + ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "1"), resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", "1"), ), @@ -265,14 +311,13 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "retain_stack", - "deployment_targets", "call_as", }, }, { - Config: testAccStackSetInstanceConfig_serviceManaged(rName), + Config: testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), ), }, }, @@ -281,7 +326,7 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { ctx := acctest.Context(t) - var stackInstance cloudformation.StackInstance + var stackInstanceSummaries []*cloudformation.StackInstanceSummary rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudformation_stack_set_instance.test" @@ -295,12 +340,12 @@ func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckStackSetInstanceDestroy(ctx), + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccStackSetInstanceConfig_operationPreferences(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + 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.failure_tolerance_percentage", "0"), @@ -309,12 +354,6 @@ func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), ), }, - { - Config: testAccStackSetInstanceConfig_serviceManaged(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), - ), - }, }, }) } @@ -348,6 +387,78 @@ func testAccCheckStackSetInstanceExists(ctx context.Context, resourceName string } } +// testAccCheckStackSetInstanceForOrganizationalUnitExists is a variant of the +// standard CheckExistsFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx context.Context, resourceName string, v []*cloudformation.StackInstanceSummary) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + callAs := rs.Primary.Attributes["call_as"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) + + stackSetName, accountOrOrgID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + orgIDs := strings.Split(accountOrOrgID, "/") + + output, err := tfcloudformation.FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + + if err != nil { + return err + } + + v = output + + return nil + } +} + +// testAccCheckStackSetInstanceForOrganizationalUnitDestroy is a variant of the +// standard CheckDestroyFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_set_instance" { + continue + } + + callAs := rs.Primary.Attributes["call_as"] + + stackSetName, accountOrOrgID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + orgIDs := strings.Split(accountOrOrgID, "/") + + output, err := tfcloudformation.FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + + if tfresource.NotFound(err) { + continue + } + if len(output) == 0 { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation StackSet Instance %s still exists", rs.Primary.ID) + } + + return nil + } +} + func testAccCheckStackSetInstanceStackExists(ctx context.Context, stackInstance *cloudformation.StackInstance, v *cloudformation.Stack) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) @@ -718,14 +829,23 @@ resource "aws_cloudformation_stack_set_instance" "test" { `) } -func testAccStackSetInstanceConfig_serviceManaged(rName string) string { - return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), ` +func testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName string) string { + return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), fmt.Sprintf(` +resource "aws_organizations_organizational_unit" "test" { + name = %[1]q + 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 = [aws_organizations_organizational_unit.test.id] + } + stack_set_name = aws_cloudformation_stack_set.test.name } -`) +`, rName)) } func testAccStackSetInstanceConfig_operationPreferences(rName string) string { From 07650d1566a3be319b8b7e278af7f15036b596a8 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 16:38:01 -0400 Subject: [PATCH 08/13] r/aws_cloudformation_stack_set_instance(doc): document stack_instance_summaries attribute --- .../r/cloudformation_stack_set_instance.html.markdown | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/website/docs/r/cloudformation_stack_set_instance.html.markdown b/website/docs/r/cloudformation_stack_set_instance.html.markdown index 703fa4d76f70..3f3c0dd72d4e 100644 --- a/website/docs/r/cloudformation_stack_set_instance.html.markdown +++ b/website/docs/r/cloudformation_stack_set_instance.html.markdown @@ -115,7 +115,14 @@ This resource exports the following attributes in addition to the arguments abov * `id` - StackSet name, target AWS account ID, and target AWS region separated by commas (`,`) * `organizational_unit_id` - The organization root ID or organizational unit (OU) IDs specified for `deployment_targets`. -* `stack_id` - Stack identifier +* `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This will only be populated when `deployment_targets` is set. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). +* `stack_id` - Stack identifier. + +### `stack_instance_summaries` Attribute Reference + +* `account_id` - AWS account in which the stack is deployed. +* `organizational_unit_id` - Organizational unit in which the stack is deployed. +* `stack_id` - Stack identifier. ## Timeouts From 6b58e006023781f6a7d436c0f1a71add59151521 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 20:10:41 -0400 Subject: [PATCH 09/13] chore: add changelog entries --- .changelog/24523.txt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.changelog/24523.txt b/.changelog/24523.txt index ccb6339be9ec..da52f5ca533b 100644 --- a/.changelog/24523.txt +++ b/.changelog/24523.txt @@ -1,3 +1,11 @@ ```release-note:bug -resource/aws_cloudformation_stack_set_instance: Refactor to better support deployment targets. -``` \ No newline at end of file +resource/aws_cloudformation_stack_set_instance: Refactor to better support organizational unit deployment targets. +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Changes to `deployment_targets` now force a new resource. +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Added the `stack_instance_summaries` attribute to track all stack IDs for deployments to organizational units. +``` From 1af297d98b741099341fd204fb35392865b25941 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 20:11:19 -0400 Subject: [PATCH 10/13] r/aws_cloudformation_stack_set_instance: fix linter findings --- internal/service/cloudformation/find.go | 1 - internal/service/cloudformation/stack_set_instance.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go index 4a5838d96c5a..29e0cdf7a12c 100644 --- a/internal/service/cloudformation/find.go +++ b/internal/service/cloudformation/find.go @@ -104,7 +104,6 @@ func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformatio result = append(result, s) } } - } return !lastPage diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 9a9cf18dfd05..aacf18ad9309 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -181,7 +181,7 @@ func ResourceStackSetInstance() *schema.Resource { } var ( - awsAccountRegexp = regexp.MustCompile(`^\d{12}$`) + accountIDRegexp = regexp.MustCompile(`^\d{12}$`) ) func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -311,7 +311,7 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m callAs := d.Get("call_as").(string) - if awsAccountRegexp.MatchString(accountOrOrgID) { + 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) { From ea0379c9c05073fc093687e6e7cc257812724add Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 19 Jul 2023 20:22:40 -0400 Subject: [PATCH 11/13] r/aws_cloudformation_stack_set_instance(doc): fix attribute order --- .../r/cloudformation_stack_set_instance.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/r/cloudformation_stack_set_instance.html.markdown b/website/docs/r/cloudformation_stack_set_instance.html.markdown index 3f3c0dd72d4e..98fe44f8f328 100644 --- a/website/docs/r/cloudformation_stack_set_instance.html.markdown +++ b/website/docs/r/cloudformation_stack_set_instance.html.markdown @@ -114,14 +114,14 @@ The `operation_preferences` configuration block supports the following arguments This resource exports the following attributes in addition to the arguments above: * `id` - StackSet name, target AWS account ID, and target AWS region separated by commas (`,`) -* `organizational_unit_id` - The organization root ID or organizational unit (OU) IDs specified for `deployment_targets`. -* `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This will only be populated when `deployment_targets` is set. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). +* `organizational_unit_id` - The organization root ID or organizational unit (OU) ID in which the stack is deployed. * `stack_id` - Stack identifier. +* `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This will only be populated when `deployment_targets` is set. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). ### `stack_instance_summaries` Attribute Reference -* `account_id` - AWS account in which the stack is deployed. -* `organizational_unit_id` - Organizational unit in which the stack is deployed. +* `account_id` - AWS account ID in which the stack is deployed. +* `organizational_unit_id` - Organizational unit ID in which the stack is deployed. * `stack_id` - Stack identifier. ## Timeouts From 3d428e7b28ba80830ab9632eaac38ca2a393f34f Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Thu, 20 Jul 2023 09:43:07 -0400 Subject: [PATCH 12/13] r/aws_cloudformation_stack_set_instance(doc): update id description, tidy import section --- .../r/cloudformation_stack_set_instance.html.markdown | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/docs/r/cloudformation_stack_set_instance.html.markdown b/website/docs/r/cloudformation_stack_set_instance.html.markdown index 98fe44f8f328..270738ee93b1 100644 --- a/website/docs/r/cloudformation_stack_set_instance.html.markdown +++ b/website/docs/r/cloudformation_stack_set_instance.html.markdown @@ -16,6 +16,8 @@ Manages a CloudFormation StackSet Instance. Instances are managed in the account ## Example Usage +### Basic Usage + ```terraform resource "aws_cloudformation_stack_set_instance" "example" { account_id = "123456789012" @@ -113,7 +115,7 @@ The `operation_preferences` configuration block supports the following arguments This resource exports the following attributes in addition to the arguments above: -* `id` - StackSet name, target AWS account ID, and target AWS region separated by commas (`,`) +* `id` - Unique identifier for the resource. If `deployment_targets` is set, this is a comma-delimited string combining stack set name, organizational unit IDs (`/`-delimited), and region (ie. `mystack,ou-123/ou-456,us-east-1`). Otherwise, this is a comma-delimited string combining stack set name, AWS account ID, and region (ie. `mystack,123456789012,us-east-1`). * `organizational_unit_id` - The organization root ID or organizational unit (OU) ID in which the stack is deployed. * `stack_id` - Stack identifier. * `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This will only be populated when `deployment_targets` is set. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). @@ -134,13 +136,13 @@ This resource exports the following attributes in addition to the arguments abov ## Import -CloudFormation StackSet Instances that target an AWS Account ID can be imported using the StackSet name, target AWS account ID, and target AWS region separated by commas (`,`) e.g. +CloudFormation StackSet Instances that target an AWS account can be imported using the stack set name, AWS account ID, and region separated by commas (`,`) e.g. ``` $ terraform import aws_cloudformation_stack_set_instance.example example,123456789012,us-east-1 ``` -CloudFormation StackSet Instances that target AWS Organizational Units can be imported using the StackSet name, a slash (`/`) separated list of organizational unit IDs, and target AWS region separated by commas (`,`) e.g. +CloudFormation StackSet Instances that target AWS organizational units can be imported using the stack set name, a slash (`/`) separated list of organizational unit IDs, and region separated by commas (`,`) e.g. ``` $ terraform import aws_cloudformation_stack_set_instance.example example,ou-sdas-123123123/ou-sdas-789789789,us-east-1 From 20059ef8b3bdc38a87788c72026e9a0ab21b3a54 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Thu, 20 Jul 2023 10:29:32 -0400 Subject: [PATCH 13/13] chore: adjust changelog specifically mentioning failures related to OUs with no accounts --- .changelog/24523.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changelog/24523.txt b/.changelog/24523.txt index da52f5ca533b..964f3ccc17ea 100644 --- a/.changelog/24523.txt +++ b/.changelog/24523.txt @@ -1,5 +1,5 @@ ```release-note:bug -resource/aws_cloudformation_stack_set_instance: Refactor to better support organizational unit deployment targets. +resource/aws_cloudformation_stack_set_instance: Fix error when deploying to organizational units with no accounts. ``` ```release-note:enhancement @@ -7,5 +7,5 @@ resource/aws_cloudformation_stack_set_instance: Changes to `deployment_targets` ``` ```release-note:enhancement -resource/aws_cloudformation_stack_set_instance: Added the `stack_instance_summaries` attribute to track all stack IDs for deployments to organizational units. +resource/aws_cloudformation_stack_set_instance: Added the `stack_instance_summaries` attribute to track all account and stack IDs for deployments to organizational units. ```