From f8aaad8fc349a8e2e17b8b4b8295eab15fb8531d Mon Sep 17 00:00:00 2001 From: alexknez Date: Tue, 9 Apr 2024 02:38:14 +0100 Subject: [PATCH 01/16] regions for cloudformaton_stack_set_instance --- .changelog/36794.txt | 3 + internal/service/cloudformation/find.go | 253 ++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 .changelog/36794.txt create mode 100644 internal/service/cloudformation/find.go diff --git a/.changelog/36794.txt b/.changelog/36794.txt new file mode 100644 index 00000000000..49f293a4487 --- /dev/null +++ b/.changelog/36794.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Replace `region` where resource is creating, with `regions` (list of regions) +``` diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go new file mode 100644 index 00000000000..088a94fdcb8 --- /dev/null +++ b/internal/service/cloudformation/find.go @@ -0,0 +1,253 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudformation + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func FindChangeSetByStackIDAndChangeSetName(ctx context.Context, conn *cloudformation.CloudFormation, stackID, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { + input := &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeSetName), + StackName: aws.String(stackID), + } + + output, err := conn.DescribeChangeSetWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeChangeSetNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName string, region []string, callAs string, orgIDs []string) ([]*cloudformation.StackInstanceSummary, error) { + input := &cloudformation.ListStackInstancesInput{ + StackInstanceRegion: aws.String(region[0]), + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = aws.String(callAs) + } + + var result []*cloudformation.StackInstanceSummary + + 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 = append(result, s) + } + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return result, nil +} + +func FindStackInstanceByName(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, accountID string, region []string, callAs string) (*cloudformation.StackInstance, error) { + input := &cloudformation.DescribeStackInstanceInput{ + StackInstanceAccount: aws.String(accountID), + StackInstanceRegion: aws.String(region[0]), + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = aws.String(callAs) + } + + output, err := conn.DescribeStackInstanceWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.StackInstance == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackInstance, nil +} + +func FindStackSetByName(ctx context.Context, conn *cloudformation.CloudFormation, name, callAs string) (*cloudformation.StackSet, error) { + input := &cloudformation.DescribeStackSetInput{ + StackSetName: aws.String(name), + } + + if callAs != "" { + input.CallAs = aws.String(callAs) + } + + output, err := conn.DescribeStackSetWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if callAs == cloudformation.CallAsDelegatedAdmin && tfawserr.ErrMessageContains(err, errCodeValidationError, "Failed to check account is Delegated Administrator") { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.StackSet == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackSet, nil +} + +func FindStackSetOperationByStackSetNameAndOperationID(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, operationID, callAs string) (*cloudformation.StackSetOperation, error) { + input := &cloudformation.DescribeStackSetOperationInput{ + OperationId: aws.String(operationID), + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = aws.String(callAs) + } + + output, err := conn.DescribeStackSetOperationWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeOperationNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.StackSetOperation == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackSetOperation, nil +} + +func FindTypeByARN(ctx context.Context, conn *cloudformation.CloudFormation, arn string) (*cloudformation.DescribeTypeOutput, error) { + input := &cloudformation.DescribeTypeInput{ + Arn: aws.String(arn), + } + + return FindType(ctx, conn, input) +} + +func FindTypeByName(ctx context.Context, conn *cloudformation.CloudFormation, name string) (*cloudformation.DescribeTypeOutput, error) { + input := &cloudformation.DescribeTypeInput{ + Type: aws.String(cloudformation.RegistryTypeResource), + TypeName: aws.String(name), + } + + return FindType(ctx, conn, input) +} + +func FindType(ctx context.Context, conn *cloudformation.CloudFormation, input *cloudformation.DescribeTypeInput) (*cloudformation.DescribeTypeOutput, error) { + output, err := conn.DescribeTypeWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeTypeNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if status := aws.StringValue(output.DeprecatedStatus); status == cloudformation.DeprecatedStatusDeprecated { + return nil, &retry.NotFoundError{ + LastRequest: input, + Message: status, + } + } + + return output, nil +} + +func FindTypeRegistrationByToken(ctx context.Context, conn *cloudformation.CloudFormation, registrationToken string) (*cloudformation.DescribeTypeRegistrationOutput, error) { + input := &cloudformation.DescribeTypeRegistrationInput{ + RegistrationToken: aws.String(registrationToken), + } + + output, err := conn.DescribeTypeRegistrationWithContext(ctx, input) + + if tfawserr.ErrMessageContains(err, cloudformation.ErrCodeCFNRegistryException, "No registration token matches") { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} From 41df1352fcb18f53da8aae7f61a4b35e18064506 Mon Sep 17 00:00:00 2001 From: alexknez Date: Tue, 16 Apr 2024 17:17:58 +0100 Subject: [PATCH 02/16] Added OperationPreferences in Delete operation --- internal/service/cloudformation/stack_set_instance.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 92f6023db84..87ef6faf4fb 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -472,6 +472,10 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, input.DeploymentTargets = dt } + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) outputRaw, err := tfresource.RetryWhenIsA[*awstypes.OperationInProgressException](ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { return conn.DeleteStackInstances(ctx, input) From de036f6b268d25eafd342c1c0b53b9f856b320d3 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:53:50 -0400 Subject: [PATCH 03/16] New resource: aws_cloudformation_stack_instances --- .../service/cloudformation/stack_instances.go | 773 ++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 internal/service/cloudformation/stack_instances.go diff --git a/internal/service/cloudformation/stack_instances.go b/internal/service/cloudformation/stack_instances.go new file mode 100644 index 00000000000..fc9f0a4889d --- /dev/null +++ b/internal/service/cloudformation/stack_instances.go @@ -0,0 +1,773 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudformation + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + stackInstancesResourceIDPartCount = 3 // stack_set_name, call_as, OU + regionAcctOrgIDSeparator = "/" + ResNameStackInstances = "Stack Instances" +) + +// @SDKResource("aws_cloudformation_stack_instances", name="Stack Instances") +func resourceStackInstances() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceStackInstancesCreate, + ReadWithoutTimeout: resourceStackInstancesRead, + UpdateWithoutTimeout: resourceStackInstancesUpdate, + DeleteWithoutTimeout: resourceStackInstancesDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceStackInstancesImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + AttrAccounts: { // create input, read input (single account), update input, delete input + Type: schema.TypeSet, + Optional: true, + Computed: true, + ConflictsWith: []string{"deployment_targets"}, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidAccountID, + }, + }, + "call_as": { // create input, read input, update input, delete input + Type: schema.TypeString, + Optional: true, + Default: awstypes.CallAsSelf, + ValidateDiagFunc: enum.Validate[awstypes.CallAs](), + }, + "deployment_targets": { // create input, update input, delete input + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_filter_type": { // create input + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(enum.Slice(awstypes.AccountFilterType.Values("")...), false), + ForceNew: true, + ConflictsWith: []string{AttrAccounts}, + }, + AttrAccounts: { // create input + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{AttrAccounts}, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidAccountID, + }, + }, + "accounts_url": { // create input + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{AttrAccounts}, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`(s3://|http(s?)://).+`), ""), + }, + "organizational_unit_ids": { // create input + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + ConflictsWith: []string{AttrAccounts}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`^(ou-[0-9a-z]{4,32}-[0-9a-z]{8,32}|r-[0-9a-z]{4,32})$`), ""), + }, + }, + }, + }, + ConflictsWith: []string{AttrAccounts}, + }, + "operation_preferences": { // create input, update input, delete input + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "concurrency_mode": { // create input + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.ConcurrencyMode](), + }, + "failure_tolerance_count": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + ConflictsWith: []string{"operation_preferences.0.failure_tolerance_percentage"}, + }, + "failure_tolerance_percentage": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100), + ConflictsWith: []string{"operation_preferences.0.failure_tolerance_count"}, + }, + "max_concurrent_count": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(1), + ConflictsWith: []string{"operation_preferences.0.max_concurrent_percentage"}, + }, + "max_concurrent_percentage": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 100), + ConflictsWith: []string{"operation_preferences.0.max_concurrent_count"}, + }, + "region_concurrency_type": { // create input + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.RegionConcurrencyType](), + }, + "region_order": { // create input + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z-]{1,128}$`), ""), + }, + }, + }, + }, + }, + "parameter_overrides": { // create input, update input + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + AttrRegions: { // create input - required, read input (single region), update input - required, delete input - required + Type: schema.TypeSet, + Optional: true, + Computed: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidRegionName, + }, + }, + "retain_stacks": { // delete input - required + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "stack_instance_summaries": { // read output + Type: schema.TypeList, + Computed: true, + 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{ + names.AttrAccountID: { // read output + Type: schema.TypeString, + Computed: true, + }, + "detailed_status": { // read output + Type: schema.TypeString, + Computed: true, + }, + "drift_status": { // read output + Type: schema.TypeString, + Computed: true, + }, + "organizational_unit_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "region": { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_set_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "status": { // read output + Type: schema.TypeString, + Computed: true, + }, + "status_reason": { // read output + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "stack_set_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_set_name": { // create input - required, read input - required, update input - required, delete input - required + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + }, + } +} + +func resourceStackInstancesCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + stackSetName := d.Get("stack_set_name").(string) + input := &cloudformation.CreateStackInstancesInput{ + StackSetName: aws.String(stackSetName), + } + + if v, ok := d.GetOk(AttrRegions); ok && v.(*schema.Set).Len() > 0 { + input.Regions = flex.ExpandStringValueSet(v.(*schema.Set)) + + } + + if v, ok := d.GetOk(AttrRegions); !ok || v.(*schema.Set).Len() == 0 { + input.Regions = []string{meta.(*conns.AWSClient).Region} + } + + if v, ok := d.GetOk(AttrAccounts); ok && v.(*schema.Set).Len() > 0 { + input.Accounts = flex.ExpandStringValueSet(v.(*schema.Set)) + } + + deployedByOU := "" + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.Accounts = nil + + if v, ok := d.GetOk("deployment_targets.0.organizational_unit_ids"); ok && len(v.(*schema.Set).List()) > 0 { + deployedByOU = "OU" + } + } else { + input.Accounts = []string{meta.(*conns.AWSClient).AccountID} + } + + callAs := d.Get("call_as").(string) + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("parameter_overrides"); ok { + 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 = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + id, err := flex.FlattenResourceId([]string{stackSetName, callAs, deployedByOU}, stackInstancesResourceIDPartCount, true) + if err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionFlatteningResourceId, ResNameStackInstances, stackSetName, err) + } + + _, err = tfresource.RetryWhen(ctx, propagationTimeout, + func() (interface{}, error) { + input.OperationId = aws.String(sdkid.UniqueId()) + + output, err := conn.CreateStackInstances(ctx, input) + if err != nil { + return nil, err + } + + d.SetId(id) + + operation, err := waitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.ToString(output.OperationId), callAs, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return nil, fmt.Errorf("waiting for create: %w", err) + } + + return operation, nil + }, + func(err error) (bool, error) { + if err == nil { + return false, nil + } + + message := err.Error() + + // IAM eventual consistency + if strings.Contains(message, "AccountGate check failed") { + return true, err + } + + // IAM eventual consistency + // User: XXX is not authorized to perform: cloudformation:CreateStack on resource: YYY + if strings.Contains(message, "is not authorized") { + return true, err + } + + // IAM eventual consistency + // XXX role has insufficient YYY permissions + if strings.Contains(message, "role has insufficient") { + return true, err + } + + // IAM eventual consistency + // Account XXX should have YYY role with trust relationship to Role ZZZ + if strings.Contains(message, "role with trust relationship") { + return true, err + } + + // IAM eventual consistency + if strings.Contains(message, "The security token included in the request is invalid") { + return true, err + } + + return false, err + }, + ) + + if err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionCreating, ResNameStackInstances, id, err) + } + + return append(diags, resourceStackInstancesRead(ctx, d, meta)...) +} + +func resourceStackInstancesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + // Dramatic simplification of the ID from stack_set_instance removing regions and accounts. The upside + // is simplicity. The downside is that we hoover up all stack instances for the stack set. + parts, err := flex.ExpandResourceId(d.Id(), stackInstancesResourceIDPartCount, true) + if err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionExpandingResourceId, ResNameStackInstances, d.Id(), err) + } + + stackSetName, callAs, deployedByOU := parts[0], parts[1], parts[2] + d.Set("stack_set_name", stackSetName) + + if callAs == "" { + callAs = d.Get("call_as").(string) + } + + stackInstances, err := findStackInstancesByNameCallAs(ctx, meta, stackSetName, callAs, deployedByOU == "OU", flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)), flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set))) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudFormation Stack Instances (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + } + + if len(stackInstances.OUs) > 0 { + d.Set("deployment_targets", replaceOrganizationalUnitIDs(d.Get("deployment_targets").([]interface{}), stackInstances.OUs)) + } + + d.Set(AttrAccounts, flex.FlattenStringValueList(stackInstances.Accounts)) + d.Set(AttrRegions, flex.FlattenStringValueList(stackInstances.Regions)) + d.Set("stack_instance_summaries", flattenStackInstancesSummaries(stackInstances.Summaries)) + d.Set("stack_set_id", stackInstances.StackSetID) + + if err := d.Set("parameter_overrides", flattenAllParameters(stackInstances.ParameterOverrides)); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + } + + return diags +} + +const ( + AttrAccounts = "accounts" + AttrDTAccounts = "deployment_targets.0.accounts" + AttrDTOUs = "deployment_targets.0.organizational_unit_ids" + AttrRegions = "regions" +) + +func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + accounts := flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)) + regions := flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)) + dtAccounts := flex.ExpandStringValueSet(d.Get(AttrDTAccounts).(*schema.Set)) + dtOUs := flex.ExpandStringValueSet(d.Get(AttrDTOUs).(*schema.Set)) + + if d.HasChange(AttrRegions) { + oRaw, nRaw := d.GetChange(AttrRegions) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, flex.ExpandStringValueList(axe.List()), dtAccounts, dtOUs); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrAccounts) { + oRaw, nRaw := d.GetChange(AttrAccounts) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, flex.ExpandStringValueList(axe.List()), regions, dtAccounts, dtOUs); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrDTAccounts) { + oRaw, nRaw := d.GetChange(AttrDTAccounts) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, regions, flex.ExpandStringValueList(axe.List()), dtOUs); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrDTOUs) { + oRaw, nRaw := d.GetChange(AttrDTOUs) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, flex.ExpandStringValueList(axe.List())); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChanges( + "call_as", + "deployment_targets.0.accounts_url", + "operation_preferences", + "parameter_overrides", + ) { + input := &cloudformation.UpdateStackInstancesInput{ + OperationId: aws.String(sdkid.UniqueId()), + ParameterOverrides: []awstypes.Parameter{}, + Regions: flex.ExpandStringValueList(d.Get(AttrRegions).(*schema.Set).List()), + StackSetName: aws.String(d.Get("stack_set_name").(string)), + } + + // can only give either accounts or deployment_targets + input.Accounts = []string{meta.(*conns.AWSClient).AccountID} + if v, ok := d.GetOk(AttrAccounts); ok && v.(*schema.Set).Len() > 0 { + input.Accounts = flex.ExpandStringValueSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.Accounts = nil + } + + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("parameter_overrides"); ok { + input.ParameterOverrides = expandParameters(v.(map[string]interface{})) + } + + output, err := conn.UpdateStackInstances(ctx, input) + if err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionUpdating, ResNameStackInstances, d.Id(), err) + } + + if _, err := waitStackSetOperationSucceeded(ctx, conn, d.Get("stack_set_name").(string), aws.ToString(output.OperationId), d.Get("call_as").(string), d.Timeout(schema.TimeoutUpdate)); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionWaitingForUpdate, ResNameStackInstances, d.Id(), err) + } + } + + return append(diags, resourceStackInstancesRead(ctx, d, meta)...) +} + +func resourceStackInstancesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // delete everything + accounts := flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)) + regions := flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)) + dtAccounts := flex.ExpandStringValueSet(d.Get(AttrDTAccounts).(*schema.Set)) + dtOUs := flex.ExpandStringValueSet(d.Get(AttrDTOUs).(*schema.Set)) + + if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, dtOUs); err != nil { + return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + + return diag.Diagnostics{} +} + +func deleteStackInstances(ctx context.Context, d *schema.ResourceData, meta interface{}, accounts, regions, dtAccounts, dtOUs []string) error { + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + input := &cloudformation.DeleteStackInstancesInput{ + OperationId: aws.String(sdkid.UniqueId()), + Accounts: accounts, + Regions: regions, + RetainStacks: aws.Bool(d.Get("retain_stacks").(bool)), + StackSetName: aws.String(d.Get("stack_set_name").(string)), + } + + // can only give either accounts or deployment_targets + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.DeploymentTargets.Accounts = dtAccounts + input.DeploymentTargets.OrganizationalUnitIds = dtOUs + input.Accounts = nil + } + + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + log.Printf("[DEBUG] Deleting CloudFormation Stack Instances: %s", d.Id()) + outputRaw, err := tfresource.RetryWhenIsA[*awstypes.OperationInProgressException](ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return conn.DeleteStackInstances(ctx, input) + }) + + if errs.IsA[*awstypes.StackInstanceNotFoundException](err) || errs.IsA[*awstypes.StackSetNotFoundException](err) { + return nil + } + + if err != nil { + return err + } + + if _, err := waitStackSetOperationSucceeded(ctx, conn, d.Get("stack_set_name").(string), aws.ToString(outputRaw.(*cloudformation.DeleteStackInstancesOutput).OperationId), d.Get("call_as").(string), d.Timeout(schema.TimeoutDelete)); err != nil { + return err + } + + return nil +} + +func resourceStackInstancesImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + switch parts := strings.Split(d.Id(), flex.ResourceIdSeparator); len(parts) { + case 1: + case 2: + case 3: + d.SetId(strings.Join([]string{parts[0], parts[1], parts[2]}, flex.ResourceIdSeparator)) + d.Set("call_as", parts[1]) + default: + return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSET_NAME or STACKSET_NAME%[2]sCALLAS or STACKSET_NAME%[2]sCALLAS%[2]sOU", d.Id(), flex.ResourceIdSeparator) + } + + return []*schema.ResourceData{d}, nil +} + +// StackInstances is a helper struct to hold the describeable/listable info for stack instances. +type StackInstances struct { + Accounts []string + OUs []string + Regions []string + StackSetID string + Summaries []awstypes.StackInstanceSummary + ParameterOverrides []awstypes.Parameter +} + +// findStackInstancesByNameCallAs is a helper function to find stack instances by stackSetName and callAs. +// accounts and regions are not used unless no summaries are returned. When no summaries are returned, +// drift detection is limited because we're just using the accounts and regions from config--we have no +// other choice. When there are no summaries, we use the first account and region to find a stack instance +// to get the parameter_overrides and stack_set_id. +func findStackInstancesByNameCallAs(ctx context.Context, meta interface{}, stackSetName, callAs string, deployedByOU bool, accounts, regions []string) (StackInstances, error) { + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + input := &cloudformation.ListStackInstancesInput{ + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = awstypes.CallAs(callAs) + } + + var output StackInstances + none := true + pages := cloudformation.NewListStackInstancesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.StackInstanceNotFoundException](err) || errs.IsA[*awstypes.StackSetNotFoundException](err) { + return output, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return output, err + } + + none = false + + for _, v := range page.Summaries { + if aws.ToString(v.StackSetId) != "" && output.StackSetID == "" { + output.StackSetID = aws.ToString(v.StackSetId) + } + + output.Summaries = append(output.Summaries, v) + + if aws.ToString(v.Account) != "" { + output.Accounts = append(output.Accounts, aws.ToString(v.Account)) + } + + if aws.ToString(v.Region) != "" { + output.Regions = append(output.Regions, aws.ToString(v.Region)) + } + + if aws.ToString(v.OrganizationalUnitId) != "" { + output.OUs = append(output.OUs, aws.ToString(v.OrganizationalUnitId)) + } + } + } + + if len(output.Accounts) == 0 && len(accounts) > 0 { + output.Accounts = accounts + } + + if len(output.Accounts) == 0 && len(accounts) == 0 { + output.Accounts = []string{meta.(*conns.AWSClient).AccountID} + } + + if len(output.Regions) == 0 && len(regions) > 0 { + output.Regions = regions + } + + if len(output.Regions) == 0 && len(regions) == 0 { + output.Regions = []string{meta.(*conns.AWSClient).Region} + } + + if deployedByOU { + return output, nil + } + + // set based on the first account and region which means they may not be accurate for all stack instances + stackInstance, err := findStackInstanceByFourPartKey(ctx, conn, stackSetName, output.Accounts[0], output.Regions[0], callAs) + if none || tfresource.NotFound(err) { + return output, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil && !tfresource.NotFound(err) { + return output, err + } + + if stackInstance != nil && output.StackSetID == "" { + output.StackSetID = aws.ToString(stackInstance.StackSetId) + } + + if stackInstance != nil && stackInstance.ParameterOverrides != nil { + output.ParameterOverrides = stackInstance.ParameterOverrides + } + + return output, nil +} + +func replaceOrganizationalUnitIDs(tfList []interface{}, newOUIDs []string) []interface{} { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + + // Update the "organizational_unit_ids" with the new value + tfMap["organizational_unit_ids"] = flex.FlattenStringValueList(newOUIDs) + + return []interface{}{tfMap} +} + +func flattenStackInstancesSummaries(apiObject []awstypes.StackInstanceSummary) []interface{} { + if len(apiObject) == 0 { + return nil + } + + tfList := []interface{}{} + for _, obj := range apiObject { + m := map[string]interface{}{ + names.AttrAccountID: obj.Account, + "drift_status": obj.DriftStatus, + "organizational_unit_id": obj.OrganizationalUnitId, + "region": obj.Region, + "stack_id": obj.StackId, + "stack_set_id": obj.StackSetId, + "status": obj.Status, + "status_reason": obj.StatusReason, + } + + if obj.StackInstanceStatus != nil { + m["detailed_status"] = obj.StackInstanceStatus.DetailedStatus + } + + tfList = append(tfList, m) + } + + return tfList +} From 6c4432839b2130166f95580a519caf9d2a760711 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:54:19 -0400 Subject: [PATCH 04/16] cf/stack_instances: Testing new resource --- .../cloudformation/stack_instances_test.go | 1127 +++++++++++++++++ 1 file changed, 1127 insertions(+) create mode 100644 internal/service/cloudformation/stack_instances_test.go diff --git a/internal/service/cloudformation/stack_instances_test.go b/internal/service/cloudformation/stack_instances_test.go new file mode 100644 index 00000000000..9d80a6b0365 --- /dev/null +++ b/internal/service/cloudformation/stack_instances_test.go @@ -0,0 +1,1127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudformation_test + +import ( + "context" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfcloudformation "github.com/hashicorp/terraform-provider-aws/internal/service/cloudformation" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccCloudFormationStackInstances_basic(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "accounts.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "accounts.0"), + resource.TestCheckResourceAttr(resourceName, "call_as", "SELF"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "regions.0", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "retain_stacks", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "stack_instance_summaries.0.account_id"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.drift_status", "NOT_CHECKED"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.region", acctest.Region()), + resource.TestCheckResourceAttrSet(resourceName, "stack_instance_summaries.0.stack_id"), + resource.TestCheckResourceAttrSet(resourceName, "stack_instance_summaries.0.stack_set_id"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.status", "CURRENT"), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + }, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_disappears(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackInstances(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Disappears_stackSet(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + var stackSet1 awstypes.StackSet + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + stackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetExists(ctx, stackSetResourceName, &stackSet1), + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackInstances(), resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackSet(), stackSetResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_increaseRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "accounts.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "accounts.0"), + resource.TestCheckResourceAttr(resourceName, "call_as", "SELF"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "retain_stacks", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region(), acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct2), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct2), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_decreaseRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region(), acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct2), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct2), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_swapRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_parameterOverrides(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2, stackInstances3, stackInstances4 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_parameterOverrides1(rName, "overridevalue1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + }, + }, + { + Config: testAccStackInstancesConfig_parameterOverrides2(rName, "overridevalue1updated", "overridevalue2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1updated"), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter2", "overridevalue2"), + ), + }, + { + Config: testAccStackInstancesConfig_parameterOverrides1(rName, "overridevalue1updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances3), + testAccCheckStackInstancesNotRecreated(&stackInstances2, &stackInstances3), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1updated"), + ), + }, + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances4), + testAccCheckStackInstancesNotRecreated(&stackInstances3, &stackInstances4), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_deploymentTargets(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.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, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.account_filter_type", "INTERSECTION"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.accounts.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.accounts_url", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + "deployment_targets", + }, + }, + { + Config: testAccStackInstancesConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_DeploymentTargets_emptyOU(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.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, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + "deployment_targets", + }, + }, + { + Config: testAccStackInstancesConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_operationPreferences(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.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, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_operationPreferences(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.concurrency_mode", ""), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_count", acctest.Ct10), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_concurrencyMode(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.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, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_concurrencyMode(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.concurrency_mode", "SOFT_FAILURE_TOLERANCE"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_count", acctest.Ct10), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), + ), + }, + }, + }) +} + +// https://github.com/hashicorp/terraform-provider-aws/issues/32536. +func TestAccCloudFormationStackInstances_delegatedAdministrator(t *testing.T) { + ctx := acctest.Context(t) + providers := make(map[string]*schema.Provider) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckAlternateAccount(t) + acctest.PreCheckOrganizationMemberAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesNamedAlternate(ctx, t, providers), + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + // Run a simple configuration to initialize the alternate providers + Config: testAccStackSetConfig_delegatedAdministratorInit, + }, + { + PreConfig: func() { + // Can only run check here because the provider is not available until the previous step. + acctest.PreCheckOrganizationManagementAccountWithProvider(ctx, t, acctest.NamedProviderFunc(acctest.ProviderNameAlternate, providers)) + acctest.PreCheckIAMServiceLinkedRoleWithProvider(ctx, t, acctest.NamedProviderFunc(acctest.ProviderNameAlternate, providers), "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + Config: testAccStackInstancesConfig_delegatedAdministrator(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + 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,DELEGATED_ADMIN", rs.Primary.ID), nil + }, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + }, + }, + }, + }) +} + +func testAccCheckStackInstancesExists(ctx context.Context, resourceName string, v *tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, true) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if err != nil { + return err + } + + *v = output + + return nil + } +} + +func attributeLength(attribute string) int { + return errs.Must(strconv.Atoi(attribute)) +} + +// testAccCheckStackInstancesForOrganizationalUnitExists is a variant of the +// standard CheckExistsFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackInstancesForOrganizationalUnitExists(ctx context.Context, resourceName string, v tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, false) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if err != nil { + return err + } + + v = output + + return nil + } +} + +// testAccCheckStackInstancesForOrganizationalUnitDestroy is a variant of the +// standard CheckDestroyFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_instances" { + continue + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, false) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if tfresource.NotFound(err) { + continue + } + if output.StackSetID == "" { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation Stack Instances %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckStackInstancesDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_instances" { + continue + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, true) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + deployedByOU = true + } + + _, err = tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation Stack Instances %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckStackInstancesNotRecreated(i, j *tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.ToString(&i.StackSetID) != aws.ToString(&j.StackSetID) { + return fmt.Errorf("CloudFormation Stack Instances (%s vs %s) recreated", i.StackSetID, j.StackSetID) + } + for _, v := range i.Summaries { + for _, w := range j.Summaries { + if aws.ToString(v.Region) != aws.ToString(w.Region) { + continue + } + if aws.ToString(v.StackId) != aws.ToString(w.StackId) { + return fmt.Errorf("CloudFormation Stack Instances (%s) recreated:\n\tregions:\n\t\t%s\n\t\t%s\n\tstack_ids:\n\t\t%s\n\t\t%s", i.StackSetID, aws.ToString(v.Region), aws.ToString(w.Region), aws.ToString(v.StackId), aws.ToString(w.StackId)) + } + } + } + + return nil + } +} + +func testAccStackInstancesBaseConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_role" "Administration" { + assume_role_policy = < Date: Tue, 10 Sep 2024 14:54:44 -0400 Subject: [PATCH 05/16] docs/cf/stack_instances: New resource --- ...oudformation_stack_instances.html.markdown | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 website/docs/r/cloudformation_stack_instances.html.markdown diff --git a/website/docs/r/cloudformation_stack_instances.html.markdown b/website/docs/r/cloudformation_stack_instances.html.markdown new file mode 100644 index 00000000000..8575d7c5533 --- /dev/null +++ b/website/docs/r/cloudformation_stack_instances.html.markdown @@ -0,0 +1,180 @@ +--- +subcategory: "CloudFormation" +layout: "aws" +page_title: "AWS: aws_cloudformation_stack_instances" +description: |- + Manages CloudFormation stack instances. +--- + +# Resource: aws_cloudformation_stack_instances + +Manages CloudFormation stack instances for the specified accounts, within the specified regions. A stack instance refers to a stack in a specific account and region. Additional information about stacks can be found in the [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html). + +~> **NOTE:** This resource will manage all stack instances for the specified `stack_set_name`. If you create stack instances outside of Terraform or import existing infrastructure, ensure that your configuration includes all accounts and regions where stack instances exist for the stack set. Failing to include all accounts and regions will cause Terraform to continuously report differences between your configuration and the actual infrastructure. + +~> **NOTE:** All target accounts must have an IAM Role created that matches the name of the execution role configured in the stack (the `execution_role_name` argument in the `aws_cloudformation_stack_set` resource) in a trust relationship with the administrative account or administration IAM Role. The execution role must have appropriate permissions to manage resources defined in the template along with those required for stacks to operate. See the [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html) for more details. + +~> **NOTE:** To retain the Stack during Terraform resource destroy, ensure `retain_stacks = true` has been successfully applied into the Terraform state first. This must be completed _before_ an apply that would destroy the resource. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_cloudformation_stack_instances" "example" { + accounts = ["123456789012", "234567890123"] + regions = ["us-east-1", "us-west-2"] + stack_set_name = aws_cloudformation_stack_set.example.name +} +``` + +### Example IAM Setup in Target Account + +```terraform +data "aws_iam_policy_document" "AWSCloudFormationStackSetExecutionRole_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + identifiers = [aws_iam_role.AWSCloudFormationStackSetAdministrationRole.arn] + type = "AWS" + } + } +} + +resource "aws_iam_role" "AWSCloudFormationStackSetExecutionRole" { + assume_role_policy = data.aws_iam_policy_document.AWSCloudFormationStackSetExecutionRole_assume_role_policy.json + name = "AWSCloudFormationStackSetExecutionRole" +} + +# Documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html +# Additional IAM permissions necessary depend on the resources defined in the StackSet template +data "aws_iam_policy_document" "AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy" { + statement { + actions = [ + "cloudformation:*", + "s3:*", + "sns:*", + ] + + effect = "Allow" + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy" { + name = "MinimumExecutionPolicy" + policy = data.aws_iam_policy_document.AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy.json + role = aws_iam_role.AWSCloudFormationStackSetExecutionRole.name +} +``` + +### Example Deployment across Organizations account + +```terraform +resource "aws_cloudformation_stack_instances" "example" { + deployment_targets { + organizational_unit_ids = [aws_organizations_organization.example.roots[0].id] + } + + regions = ["us-west-2", "us-east-1"] + stack_set_name = aws_cloudformation_stack_set.example.name +} +``` + +## Argument Reference + +The following arguments are required: + +* `stack_set_name` - (Required, Force new) Name of the stack set. + +The following arguments are optional: + +* `accounts` - (Optional) Accounts where you want to create stack instances in the specified `regions`. You can specify either `accounts` or `deployment_targets`, but not both. +* `deployment_targets` - (Optional) AWS Organizations accounts for which to create stack instances in the `regions`. stack sets doesn't deploy stack instances to the organization management account, even if the organization management account is in your organization or in an OU in your organization. Drift detection is not possible for most of this argument. See [deployment_targets](#deployment_targets) below. +* `parameter_overrides` - (Optional) Key-value map of input parameters to override from the stack set for these instances. This argument's drift detection is limited to the first account and region since each instance can have unique parameters. +* `regions` - (Optional) Regions where you want to create stack instances in the specified `accounts`. +* `retain_stacks` - (Optional) Whether to remove the stack instances from the stack set, but not delete the stacks. You can't reassociate a retained stack or add an existing, saved stack to a new stack set. To retain the stack, ensure `retain_stacks = true` has been successfully applied _before_ an apply that would destroy the resource. Defaults to `false`. +* `call_as` - (Optional) 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. See [operation_preferences](#operation_preferences) below. + +### `deployment_targets` + +The `deployment_targets` configuration block supports the following arguments: + +* `account_filter_type` - (Optional, Force new) Limit deployment targets to individual accounts or include additional accounts with provided OUs. Valid values: `INTERSECTION`, `DIFFERENCE`, `UNION`, `NONE`. +* `accounts` - (Optional) List of accounts to deploy stack set updates. +* `accounts_url` - (Optional) S3 URL of the file containing the list of accounts. +* `organizational_unit_ids` - (Optional) Organization root ID or organizational unit (OU) IDs to which stack sets deploy. + +### `operation_preferences` + +The `operation_preferences` configuration block supports the following arguments: + +* `concurrency_mode` - (Optional) How the concurrency level behaves during the operation execution. Valid values are `STRICT_FAILURE_TOLERANCE` and `SOFT_FAILURE_TOLERANCE`. +* `failure_tolerance_count` - (Optional) Number of accounts, per region, for which this operation can fail before CloudFormation stops the operation in that region. +* `failure_tolerance_percentage` - (Optional) Percentage of accounts, per region, for which this stack operation can fail before CloudFormation stops the operation in that region. +* `max_concurrent_count` - (Optional) Maximum number of accounts in which to perform this operation at one time. +* `max_concurrent_percentage` - (Optional) Maximum percentage of accounts in which to perform this operation at one time. +* `region_concurrency_type` - (Optional) Concurrency type of deploying stack sets operations in regions, could be in parallel or one region at a time. Valid values are `SEQUENTIAL` and `PARALLEL`. +* `region_order` - (Optional) Order of the regions where you want to perform the stack operation. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This may not always be set depending on whether CloudFormation returns summaries for your configuration. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). +* `stack_set_id` - Unique identifier of the stack set. + +### `stack_instance_summaries` + +* `account_id` - Account ID in which the instance is deployed. +* `detailed_status` - Detailed status of the stack instance. Values include `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `INOPERABLE`, `SKIPPED_SUSPENDED_ACCOUNT`, `FAILED_IMPORT`. +* `drift_status` - Status of the stack instance's actual configuration compared to the expected template and parameter configuration of the stack set to which it belongs. Values include `DRIFTED`, `IN_SYNC`, `UNKNOWN`, `NOT_CHECKED`. +* `organizational_unit_id` - Organization root ID or organizational unit (OU) IDs that you specified for `deployment_targets`. +* `region` - Region that the stack instance is associated with. +* `stack_id` - ID of the stack instance. +* `stack_set_id` - Name or unique ID of the stack set that the stack instance is associated with. +* `status` - Status of the stack instance, in terms of its synchronization with its associated stack set. Values include `CURRENT`, `OUTDATED`, `INOPERABLE`. +* `status_reason` - Explanation for the specific status code assigned to this stack instance. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targetting OUs, see the example below.) For example: + +```terraform +import { + to = aws_cloudformation_stack_instances.example + id = "example,SELF" +} +``` + +Import CloudFormation stack instances that target OUs, using the stack set name, `call_as`, and "OU" separated by commas (`,`). For example: + +```terraform +import { + to = aws_cloudformation_stack_instances.example + id = "example,SELF,OU" +} +``` + +Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targetting OUs, see the example below.) For example: + +```console +% terraform import aws_cloudformation_stack_instances.example example,SELF +``` + +Using `terraform import`, Import CloudFormation stack instances that target OUs, using the stack set name, `call_as`, and "OU" separated by commas (`,`). For example: + +```console +% terraform import aws_cloudformation_stack_instances.example example,SELF,OU +``` From b6b5aa5bd607e4d355d370b8f3f5049caa2bc088 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:55:37 -0400 Subject: [PATCH 06/16] iam/role: Add delay to help alleviate UID/ARN errors --- internal/service/iam/wait.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/service/iam/wait.go b/internal/service/iam/wait.go index ef25229ecbc..3e99d4c32a1 100644 --- a/internal/service/iam/wait.go +++ b/internal/service/iam/wait.go @@ -41,6 +41,7 @@ func waitRoleARNIsNotUniqueID(ctx context.Context, conn *iam.Client, id string, Timeout: propagationTimeout, NotFoundChecks: 10, ContinuousTargetOccurence: 5, + Delay: 10 * time.Second, } outputRaw, err := stateConf.WaitForStateContext(ctx) From 9231d75b896041124d5599e0c56d441f92d14c8f Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:56:10 -0400 Subject: [PATCH 07/16] test/exports: For new stack instances resource --- internal/service/cloudformation/exports_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/service/cloudformation/exports_test.go b/internal/service/cloudformation/exports_test.go index a0c5f886c51..16e775e949c 100644 --- a/internal/service/cloudformation/exports_test.go +++ b/internal/service/cloudformation/exports_test.go @@ -8,12 +8,15 @@ var ( ResourceStack = resourceStack ResourceStackSet = resourceStackSet ResourceStackSetInstance = resourceStackSetInstance + ResourceStackInstances = resourceStackInstances ResourceType = resourceType FindStackInstanceByFourPartKey = findStackInstanceByFourPartKey FindStackInstanceSummariesByFourPartKey = findStackInstanceSummariesByFourPartKey FindStackSetByName = findStackSetByName FindTypeByARN = findTypeByARN + FindStackInstancesByNameCallAs = findStackInstancesByNameCallAs StackSetInstanceResourceIDPartCount = stackSetInstanceResourceIDPartCount + StackInstancesResourceIDPartCount = stackInstancesResourceIDPartCount TypeVersionARNToTypeARNAndVersionID = typeVersionARNToTypeARNAndVersionID ) From 2b9060ac8c4f7f33c36c44773e4d0059e20cd8bc Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:56:59 -0400 Subject: [PATCH 08/16] gen: New resource stack instances --- internal/service/cloudformation/service_package_gen.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/service/cloudformation/service_package_gen.go b/internal/service/cloudformation/service_package_gen.go index d47138c21fb..b627b2a40c4 100644 --- a/internal/service/cloudformation/service_package_gen.go +++ b/internal/service/cloudformation/service_package_gen.go @@ -49,6 +49,11 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Name: "Stack", Tags: &types.ServicePackageResourceTags{}, }, + { + Factory: resourceStackInstances, + TypeName: "aws_cloudformation_stack_instances", + Name: "Stack Instances", + }, { Factory: resourceStackSet, TypeName: "aws_cloudformation_stack_set", From 6964413b74a02845a062e907e223f4de112baa37 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:57:54 -0400 Subject: [PATCH 09/16] Increase delay for waits for stack sets --- internal/service/cloudformation/stack_set.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/cloudformation/stack_set.go b/internal/service/cloudformation/stack_set.go index 8bb0e8921b4..3428c336185 100644 --- a/internal/service/cloudformation/stack_set.go +++ b/internal/service/cloudformation/stack_set.go @@ -651,7 +651,7 @@ func statusStackSetOperation(ctx context.Context, conn *cloudformation.Client, s func waitStackSetOperationSucceeded(ctx context.Context, conn *cloudformation.Client, stackSetName, operationID, callAs string, timeout time.Duration) (*awstypes.StackSetOperation, error) { const ( - stackSetOperationDelay = 5 * time.Second + stackSetOperationDelay = 10 * time.Second ) stateConf := &retry.StateChangeConf{ Pending: enum.Slice(awstypes.StackSetOperationStatusRunning, awstypes.StackSetOperationStatusQueued), From b4b9847d7a1e8ad267ed6cde19b012ef87a09a75 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 14:58:19 -0400 Subject: [PATCH 10/16] Fix conflicts --- internal/service/cloudformation/find.go | 253 ------------------------ 1 file changed, 253 deletions(-) delete mode 100644 internal/service/cloudformation/find.go diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go deleted file mode 100644 index 088a94fdcb8..00000000000 --- a/internal/service/cloudformation/find.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package cloudformation - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -func FindChangeSetByStackIDAndChangeSetName(ctx context.Context, conn *cloudformation.CloudFormation, stackID, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { - input := &cloudformation.DescribeChangeSetInput{ - ChangeSetName: aws.String(changeSetName), - StackName: aws.String(stackID), - } - - output, err := conn.DescribeChangeSetWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeChangeSetNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output, nil -} - -func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName string, region []string, callAs string, orgIDs []string) ([]*cloudformation.StackInstanceSummary, error) { - input := &cloudformation.ListStackInstancesInput{ - StackInstanceRegion: aws.String(region[0]), - StackSetName: aws.String(stackSetName), - } - - if callAs != "" { - input.CallAs = aws.String(callAs) - } - - var result []*cloudformation.StackInstanceSummary - - 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 = append(result, s) - } - } - } - - return !lastPage - }) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - return result, nil -} - -func FindStackInstanceByName(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, accountID string, region []string, callAs string) (*cloudformation.StackInstance, error) { - input := &cloudformation.DescribeStackInstanceInput{ - StackInstanceAccount: aws.String(accountID), - StackInstanceRegion: aws.String(region[0]), - StackSetName: aws.String(stackSetName), - } - - if callAs != "" { - input.CallAs = aws.String(callAs) - } - - output, err := conn.DescribeStackInstanceWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.StackInstance == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.StackInstance, nil -} - -func FindStackSetByName(ctx context.Context, conn *cloudformation.CloudFormation, name, callAs string) (*cloudformation.StackSet, error) { - input := &cloudformation.DescribeStackSetInput{ - StackSetName: aws.String(name), - } - - if callAs != "" { - input.CallAs = aws.String(callAs) - } - - output, err := conn.DescribeStackSetWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if callAs == cloudformation.CallAsDelegatedAdmin && tfawserr.ErrMessageContains(err, errCodeValidationError, "Failed to check account is Delegated Administrator") { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.StackSet == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.StackSet, nil -} - -func FindStackSetOperationByStackSetNameAndOperationID(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, operationID, callAs string) (*cloudformation.StackSetOperation, error) { - input := &cloudformation.DescribeStackSetOperationInput{ - OperationId: aws.String(operationID), - StackSetName: aws.String(stackSetName), - } - - if callAs != "" { - input.CallAs = aws.String(callAs) - } - - output, err := conn.DescribeStackSetOperationWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeOperationNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.StackSetOperation == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.StackSetOperation, nil -} - -func FindTypeByARN(ctx context.Context, conn *cloudformation.CloudFormation, arn string) (*cloudformation.DescribeTypeOutput, error) { - input := &cloudformation.DescribeTypeInput{ - Arn: aws.String(arn), - } - - return FindType(ctx, conn, input) -} - -func FindTypeByName(ctx context.Context, conn *cloudformation.CloudFormation, name string) (*cloudformation.DescribeTypeOutput, error) { - input := &cloudformation.DescribeTypeInput{ - Type: aws.String(cloudformation.RegistryTypeResource), - TypeName: aws.String(name), - } - - return FindType(ctx, conn, input) -} - -func FindType(ctx context.Context, conn *cloudformation.CloudFormation, input *cloudformation.DescribeTypeInput) (*cloudformation.DescribeTypeOutput, error) { - output, err := conn.DescribeTypeWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeTypeNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - if status := aws.StringValue(output.DeprecatedStatus); status == cloudformation.DeprecatedStatusDeprecated { - return nil, &retry.NotFoundError{ - LastRequest: input, - Message: status, - } - } - - return output, nil -} - -func FindTypeRegistrationByToken(ctx context.Context, conn *cloudformation.CloudFormation, registrationToken string) (*cloudformation.DescribeTypeRegistrationOutput, error) { - input := &cloudformation.DescribeTypeRegistrationInput{ - RegistrationToken: aws.String(registrationToken), - } - - output, err := conn.DescribeTypeRegistrationWithContext(ctx, input) - - if tfawserr.ErrMessageContains(err, cloudformation.ErrCodeCFNRegistryException, "No registration token matches") { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output, nil -} From fc15d97b864a9686f3b316ede5f85c8600872d8a Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 15:03:41 -0400 Subject: [PATCH 11/16] Fix changelog --- .changelog/36794.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.changelog/36794.txt b/.changelog/36794.txt index 49f293a4487..bb61b4c682c 100644 --- a/.changelog/36794.txt +++ b/.changelog/36794.txt @@ -1,3 +1,7 @@ -```release-note:enhancement -resource/aws_cloudformation_stack_set_instance: Replace `region` where resource is creating, with `regions` (list of regions) +```release-note:new-resource +aws_cloudformation_stack_instances ``` + +```release-note:bug +resource/aws_iam_role: Fix to reduce Terraform reporting differences when a role's ARN temporarily appears as the role's unique ID +``` \ No newline at end of file From a49a2f02ce84960174413615c911c35fc115c9cb Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 15:04:28 -0400 Subject: [PATCH 12/16] docs/cf/stack_instances: Fixpelling --- website/docs/r/cloudformation_stack_instances.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/cloudformation_stack_instances.html.markdown b/website/docs/r/cloudformation_stack_instances.html.markdown index 8575d7c5533..2219c10e617 100644 --- a/website/docs/r/cloudformation_stack_instances.html.markdown +++ b/website/docs/r/cloudformation_stack_instances.html.markdown @@ -149,7 +149,7 @@ This resource exports the following attributes in addition to the arguments abov ## Import -In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targetting OUs, see the example below.) For example: +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targeting OUs, see the example below.) For example: ```terraform import { @@ -167,7 +167,7 @@ import { } ``` -Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targetting OUs, see the example below.) For example: +Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targeting OUs, see the example below.) For example: ```console % terraform import aws_cloudformation_stack_instances.example example,SELF From 163ce7564da94a21a649836ce677c939de83e86a Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 15:19:14 -0400 Subject: [PATCH 13/16] docs/cf_stack_instances: Fix import section --- website/docs/r/cloudformation_stack_instances.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/cloudformation_stack_instances.html.markdown b/website/docs/r/cloudformation_stack_instances.html.markdown index 2219c10e617..7a8e21a59e6 100644 --- a/website/docs/r/cloudformation_stack_instances.html.markdown +++ b/website/docs/r/cloudformation_stack_instances.html.markdown @@ -149,7 +149,7 @@ This resource exports the following attributes in addition to the arguments abov ## Import -In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targeting OUs, see the example below.) For example: +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). If you are importing a stack instance targeting OUs, see the example below. For example: ```terraform import { @@ -167,7 +167,7 @@ import { } ``` -Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). (If you are importing a stack instance targeting OUs, see the example below.) For example: +Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). If you are importing a stack instance targeting OUs, see the example below. For example: ```console % terraform import aws_cloudformation_stack_instances.example example,SELF From 4f795d465ca4fccd12ddd677171deea8020e3336 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 15:23:27 -0400 Subject: [PATCH 14/16] cf_stack_instances: Use constants, remove newline --- internal/service/cloudformation/stack_instances.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/service/cloudformation/stack_instances.go b/internal/service/cloudformation/stack_instances.go index fc9f0a4889d..0d4dc4a0471 100644 --- a/internal/service/cloudformation/stack_instances.go +++ b/internal/service/cloudformation/stack_instances.go @@ -209,7 +209,7 @@ func resourceStackInstances() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "region": { // read output + names.AttrRegion: { // read output Type: schema.TypeString, Computed: true, }, @@ -221,11 +221,11 @@ func resourceStackInstances() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "status": { // read output + names.AttrStatus: { // read output Type: schema.TypeString, Computed: true, }, - "status_reason": { // read output + names.AttrStatusReason: { // read output Type: schema.TypeString, Computed: true, }, @@ -257,7 +257,6 @@ func resourceStackInstancesCreate(ctx context.Context, d *schema.ResourceData, m if v, ok := d.GetOk(AttrRegions); ok && v.(*schema.Set).Len() > 0 { input.Regions = flex.ExpandStringValueSet(v.(*schema.Set)) - } if v, ok := d.GetOk(AttrRegions); !ok || v.(*schema.Set).Len() == 0 { @@ -755,11 +754,11 @@ func flattenStackInstancesSummaries(apiObject []awstypes.StackInstanceSummary) [ names.AttrAccountID: obj.Account, "drift_status": obj.DriftStatus, "organizational_unit_id": obj.OrganizationalUnitId, - "region": obj.Region, + names.AttrRegion: obj.Region, "stack_id": obj.StackId, "stack_set_id": obj.StackSetId, - "status": obj.Status, - "status_reason": obj.StatusReason, + names.AttrStatus: obj.Status, + names.AttrStatusReason: obj.StatusReason, } if obj.StackInstanceStatus != nil { From 76fcf6b592a741ebc2ce85f5bc0e3d2dff26839f Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 15:23:46 -0400 Subject: [PATCH 15/16] tests/cf_stack_instances: Use constants --- internal/service/cloudformation/stack_instances_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/cloudformation/stack_instances_test.go b/internal/service/cloudformation/stack_instances_test.go index 9d80a6b0365..b4a123ce682 100644 --- a/internal/service/cloudformation/stack_instances_test.go +++ b/internal/service/cloudformation/stack_instances_test.go @@ -556,7 +556,7 @@ func testAccCheckStackInstancesExists(ctx context.Context, resourceName string, } deployedByOU := false - if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { deployedByOU = true } @@ -604,7 +604,7 @@ func testAccCheckStackInstancesForOrganizationalUnitExists(ctx context.Context, } deployedByOU := false - if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { deployedByOU = true } @@ -648,7 +648,7 @@ func testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx context.Context) } deployedByOU := false - if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { deployedByOU = true } @@ -697,7 +697,7 @@ func testAccCheckStackInstancesDestroy(ctx context.Context) resource.TestCheckFu } deployedByOU := false - if rs.Primary.Attributes["deployment_targets.#"] != "0" && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != "0" { + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { deployedByOU = true } From 69429cac8fa2722d973c57ad56a4d35d51a52d15 Mon Sep 17 00:00:00 2001 From: Dirk Avery Date: Tue, 10 Sep 2024 16:13:01 -0400 Subject: [PATCH 16/16] cf_stack_instances: Fixes for code quality --- .../service/cloudformation/stack_instances.go | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/internal/service/cloudformation/stack_instances.go b/internal/service/cloudformation/stack_instances.go index 0d4dc4a0471..a32f57ce49e 100644 --- a/internal/service/cloudformation/stack_instances.go +++ b/internal/service/cloudformation/stack_instances.go @@ -294,7 +294,7 @@ func resourceStackInstancesCreate(ctx context.Context, d *schema.ResourceData, m id, err := flex.FlattenResourceId([]string{stackSetName, callAs, deployedByOU}, stackInstancesResourceIDPartCount, true) if err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionFlatteningResourceId, ResNameStackInstances, stackSetName, err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionFlatteningResourceId, ResNameStackInstances, stackSetName, err) } _, err = tfresource.RetryWhen(ctx, propagationTimeout, @@ -355,7 +355,7 @@ func resourceStackInstancesCreate(ctx context.Context, d *schema.ResourceData, m ) if err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionCreating, ResNameStackInstances, id, err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionCreating, ResNameStackInstances, id, err) } return append(diags, resourceStackInstancesRead(ctx, d, meta)...) @@ -368,7 +368,7 @@ func resourceStackInstancesRead(ctx context.Context, d *schema.ResourceData, met // is simplicity. The downside is that we hoover up all stack instances for the stack set. parts, err := flex.ExpandResourceId(d.Id(), stackInstancesResourceIDPartCount, true) if err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionExpandingResourceId, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionExpandingResourceId, ResNameStackInstances, d.Id(), err) } stackSetName, callAs, deployedByOU := parts[0], parts[1], parts[2] @@ -386,7 +386,7 @@ func resourceStackInstancesRead(ctx context.Context, d *schema.ResourceData, met } if err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) } if len(stackInstances.OUs) > 0 { @@ -399,7 +399,7 @@ func resourceStackInstancesRead(ctx context.Context, d *schema.ResourceData, met d.Set("stack_set_id", stackInstances.StackSetID) if err := d.Set("parameter_overrides", flattenAllParameters(stackInstances.ParameterOverrides)); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) } return diags @@ -426,8 +426,8 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) if axe := o.Difference(n); axe.Len() > 0 { - if err := deleteStackInstances(ctx, d, meta, accounts, flex.ExpandStringValueList(axe.List()), dtAccounts, dtOUs); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + if err := deleteStackInstances(ctx, d, meta, accounts, flex.ExpandStringValueSet(axe), dtAccounts, dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) } } @@ -444,8 +444,8 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) if axe := o.Difference(n); axe.Len() > 0 { - if err := deleteStackInstances(ctx, d, meta, flex.ExpandStringValueList(axe.List()), regions, dtAccounts, dtOUs); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + if err := deleteStackInstances(ctx, d, meta, flex.ExpandStringValueSet(axe), regions, dtAccounts, dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) } } @@ -462,8 +462,8 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) if axe := o.Difference(n); axe.Len() > 0 { - if err := deleteStackInstances(ctx, d, meta, accounts, regions, flex.ExpandStringValueList(axe.List()), dtOUs); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + if err := deleteStackInstances(ctx, d, meta, accounts, regions, flex.ExpandStringValueSet(axe), dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) } } @@ -480,8 +480,8 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) if axe := o.Difference(n); axe.Len() > 0 { - if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, flex.ExpandStringValueList(axe.List())); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, flex.ExpandStringValueSet(axe)); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) } } @@ -502,7 +502,7 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m input := &cloudformation.UpdateStackInstancesInput{ OperationId: aws.String(sdkid.UniqueId()), ParameterOverrides: []awstypes.Parameter{}, - Regions: flex.ExpandStringValueList(d.Get(AttrRegions).(*schema.Set).List()), + Regions: flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)), StackSetName: aws.String(d.Get("stack_set_name").(string)), } @@ -531,11 +531,11 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m output, err := conn.UpdateStackInstances(ctx, input) if err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionUpdating, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionUpdating, ResNameStackInstances, d.Id(), err) } if _, err := waitStackSetOperationSucceeded(ctx, conn, d.Get("stack_set_name").(string), aws.ToString(output.OperationId), d.Get("call_as").(string), d.Timeout(schema.TimeoutUpdate)); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionWaitingForUpdate, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionWaitingForUpdate, ResNameStackInstances, d.Id(), err) } } @@ -543,14 +543,15 @@ func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, m } func resourceStackInstancesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // delete everything + var diags diag.Diagnostics + accounts := flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)) regions := flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)) dtAccounts := flex.ExpandStringValueSet(d.Get(AttrDTAccounts).(*schema.Set)) dtOUs := flex.ExpandStringValueSet(d.Get(AttrDTOUs).(*schema.Set)) if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, dtOUs); err != nil { - return create.DiagError(names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) } return diag.Diagnostics{}