Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

r/aws_cloudformation_stack_set_instance: Empty OU refactor #24523

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3641b9e
Treat Deployment Targets as their own case
sbutler May 3, 2022
9360d70
Merge remote-tracking branch 'upstream/main' into b-aws_cloudformatio…
sbutler May 3, 2022
55a8f42
Adjust to change in func names
sbutler May 3, 2022
7e6d0c6
Add changelog for PR
sbutler May 3, 2022
7aa203f
Handle deletes with multipe deployment_targets
sbutler May 6, 2022
d2f3d26
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
sbutler May 9, 2022
dc7fb87
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
sbutler May 10, 2022
c7cbd9b
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
sbutler Jul 26, 2022
1a74a0d
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
sbutler May 1, 2023
5010468
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
jar-b Jul 18, 2023
0b8edd0
r/aws_cloudformation_stack_set_instance: fix build failures
jar-b Jul 18, 2023
c6965be
r/aws_cloudformation_stack_set_instance: improve support for OU deplo…
jar-b Jul 19, 2023
a720599
r/aws_cloudformation_stack_set_instance(test): improve support for OU…
jar-b Jul 19, 2023
07650d1
r/aws_cloudformation_stack_set_instance(doc): document stack_instance…
jar-b Jul 19, 2023
6b58e00
chore: add changelog entries
jar-b Jul 20, 2023
1af297d
r/aws_cloudformation_stack_set_instance: fix linter findings
jar-b Jul 20, 2023
ea0379c
r/aws_cloudformation_stack_set_instance(doc): fix attribute order
jar-b Jul 20, 2023
3d428e7
r/aws_cloudformation_stack_set_instance(doc): update id description, …
jar-b Jul 20, 2023
dfa7583
Merge branch 'main' into b-aws_cloudformation_stack_set_instance-empt…
jar-b Jul 20, 2023
20059ef
chore: adjust changelog
jar-b Jul 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/24523.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:bug
resource/aws_cloudformation_stack_set_instance: Fix error when deploying to organizational units with no accounts.
```

```release-note:enhancement
resource/aws_cloudformation_stack_set_instance: Changes to `deployment_targets` now force a new resource.
```

```release-note:enhancement
resource/aws_cloudformation_stack_set_instance: Added the `stack_instance_summaries` attribute to track all account and stack IDs for deployments to organizational units.
```
11 changes: 5 additions & 6 deletions internal/service/cloudformation/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func FindStackByID(ctx context.Context, conn *cloudformation.CloudFormation, id
return stack, nil
}

func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) (string, error) {
func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) ([]*cloudformation.StackInstanceSummary, error) {
input := &cloudformation.ListStackInstancesInput{
StackInstanceRegion: aws.String(region),
StackSetName: aws.String(stackSetName),
Expand All @@ -87,7 +87,7 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio
input.CallAs = aws.String(callAs)
}

var result string
var result []*cloudformation.StackInstanceSummary

err := conn.ListStackInstancesPagesWithContext(ctx, input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool {
if page == nil {
Expand All @@ -101,8 +101,7 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio

for _, orgID := range orgIDs {
if aws.StringValue(s.OrganizationalUnitId) == orgID {
result = aws.StringValue(s.Account)
return false
result = append(result, s)
}
}
}
Expand All @@ -111,14 +110,14 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio
})

if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) {
return "", &retry.NotFoundError{
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return "", err
return nil, err
}

return result, nil
Expand Down
178 changes: 123 additions & 55 deletions internal/service/cloudformation/stack_set_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func ResourceStackSetInstance() *schema.Resource {
"deployment_targets": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -147,6 +148,28 @@ func ResourceStackSetInstance() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"stack_instance_summaries": {
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{
"account_id": {
Type: schema.TypeString,
Computed: true,
},
"organizational_unit_id": {
Type: schema.TypeString,
Computed: true,
},
"stack_id": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
"stack_set_name": {
Type: schema.TypeString,
Required: true,
Expand All @@ -157,6 +180,10 @@ func ResourceStackSetInstance() *schema.Resource {
}
}

var (
accountIDRegexp = regexp.MustCompile(`^\d{12}$`)
)

func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).CloudFormationConn(ctx)
Expand All @@ -176,22 +203,25 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData,
if v, ok := d.GetOk("account_id"); ok {
accountID = v.(string)
}

callAs := d.Get("call_as").(string)
if v, ok := d.GetOk("call_as"); ok {
input.CallAs = aws.String(v.(string))
}
// accountOrOrgID will either be account_id or a slash-delimited list of
// organizational_unit_id's from the deployment_targets argument. This
// is composed with stack_set_name and region to form the resources ID.
accountOrOrgID := accountID

if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
dt := expandDeploymentTargets(v.([]interface{}))
// temporarily set the accountId to the DeploymentTarget IDs
// to later inform the Read CRUD operation if the true accountID needs to be determined
accountID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/")
accountOrOrgID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/")
input.DeploymentTargets = dt
} else {
d.Set("account_id", accountID)
input.Accounts = aws.StringSlice([]string{accountID})
}

callAs := d.Get("call_as").(string)
if v, ok := d.GetOk("call_as"); ok {
input.CallAs = aws.String(v.(string))
}

if v, ok := d.GetOk("parameter_overrides"); ok {
input.ParameterOverrides = expandParameters(v.(map[string]interface{}))
}
Expand All @@ -211,7 +241,7 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData,
return nil, err
}

d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region))
d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountOrOrgID, region))

operation, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutCreate))
if err != nil {
Expand Down Expand Up @@ -269,50 +299,56 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).CloudFormationConn(ctx)

stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id())

callAs := d.Get("call_as").(string)

stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id())
if err != nil {
return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}
if accountOrOrgID == "" {
return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): account_id or organizational_unit_id section empty", d.Id())
}
d.Set("region", region)
d.Set("stack_set_name", stackSetName)

// Determine correct account ID for the Instance if created with deployment targets;
// we only expect the accountID to be the organization root ID or organizational unit (OU) IDs
// separated by a slash after creation.
if regexp.MustCompile(`(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})`).MatchString(accountID) {
orgIDs := strings.Split(accountID, "/")
accountID, err = FindStackInstanceAccountIdByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs)
callAs := d.Get("call_as").(string)

if accountIDRegexp.MatchString(accountOrOrgID) {
// Stack instances deployed by account ID
stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountOrOrgID, region, callAs)
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}
if err != nil {
return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err)
return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}

d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region))
}

stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountID, region, callAs)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}
d.Set("account_id", stackInstance.Account)
d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId)
if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err)
}

if err != nil {
return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}
d.Set("stack_id", stackInstance.StackId)
d.Set("stack_instance_summaries", nil)
} else {
// Stack instances deployed by organizational unit ID
orgIDs := strings.Split(accountOrOrgID, "/")

summaries, err := FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs)
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}
if err != nil {
return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err)
}

d.Set("account_id", stackInstance.Account)
d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId)
if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err)
d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs))
d.Set("stack_instance_summaries", flattenStackInstanceSummaries(summaries))
}

d.Set("region", stackInstance.Region)
d.Set("stack_id", stackInstance.StackId)
d.Set("stack_set_name", stackSetName)

return diags
}

Expand All @@ -321,14 +357,14 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData,
conn := meta.(*conns.AWSClient).CloudFormationConn(ctx)

if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") {
stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id())
stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id())

if err != nil {
return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}

input := &cloudformation.UpdateStackInstancesInput{
Accounts: aws.StringSlice([]string{accountID}),
Accounts: aws.StringSlice([]string{accountOrOrgID}),
OperationId: aws.String(id.UniqueId()),
ParameterOverrides: []*cloudformation.Parameter{},
Regions: aws.StringSlice([]string{region}),
Expand All @@ -341,9 +377,10 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData,
}

if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
dt := expandDeploymentTargets(v.([]interface{}))
// reset input Accounts as the API accepts only 1 of Accounts and DeploymentTargets
input.Accounts = nil
input.DeploymentTargets = expandDeploymentTargets(v.([]interface{}))
input.DeploymentTargets = dt
}

if v, ok := d.GetOk("parameter_overrides"); ok {
Expand Down Expand Up @@ -373,14 +410,14 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).CloudFormationConn(ctx)

stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id())
stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id())

if err != nil {
return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}

input := &cloudformation.DeleteStackInstancesInput{
Accounts: aws.StringSlice([]string{accountID}),
Accounts: aws.StringSlice([]string{accountOrOrgID}),
OperationId: aws.String(id.UniqueId()),
Regions: aws.StringSlice([]string{region}),
RetainStacks: aws.Bool(d.Get("retain_stack").(bool)),
Expand All @@ -392,13 +429,12 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
input.CallAs = aws.String(v.(string))
}

if v, ok := d.GetOk("organizational_unit_id"); ok {
if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
dt := expandDeploymentTargets(v.([]interface{}))
// For instances associated with stack sets that use a self-managed permission model,
// the organizational unit must be provided;
input.Accounts = nil
input.DeploymentTargets = &cloudformation.DeploymentTargets{
OrganizationalUnitIds: aws.StringSlice([]string{v.(string)}),
}
input.DeploymentTargets = dt
}

log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id())
Expand All @@ -419,22 +455,54 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
return diags
}

func expandDeploymentTargets(l []interface{}) *cloudformation.DeploymentTargets {
if len(l) == 0 || l[0] == nil {
func expandDeploymentTargets(tfList []interface{}) *cloudformation.DeploymentTargets {
if len(tfList) == 0 || tfList[0] == nil {
return nil
}

tfMap, ok := l[0].(map[string]interface{})

tfMap, ok := tfList[0].(map[string]interface{})
if !ok {
return nil
}

dt := &cloudformation.DeploymentTargets{}

if v, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && v.Len() > 0 {
dt.OrganizationalUnitIds = flex.ExpandStringSet(v)
}

return dt
}

// flattenDeployment targets converts a list of organizational units (typically
// parsed from the resource ID) into the Terraform representation of the
// deployment_targets attribute.
func flattenDeploymentTargetsFromSlice(orgIDs []string) []interface{} {
tfList := []interface{}{}
for _, ou := range orgIDs {
tfList = append(tfList, ou)
}

m := map[string]interface{}{
"organizational_unit_ids": tfList,
}

return []interface{}{m}
}

func flattenStackInstanceSummaries(apiObject []*cloudformation.StackInstanceSummary) []interface{} {
if len(apiObject) == 0 {
return nil
}

tfList := []interface{}{}
for _, obj := range apiObject {
m := map[string]interface{}{
"account_id": obj.Account,
"organizational_unit_id": obj.OrganizationalUnitId,
"stack_id": obj.StackId,
}
tfList = append(tfList, m)
}

return tfList
}
Loading