Skip to content

Commit

Permalink
Merge pull request #19092 from Steffen911/b-aws_cloudformation_stack_…
Browse files Browse the repository at this point in the history
…set_auto_deployment-19015

fix(r/aws_cloudformation_stack_set): handle auto_deployment enabled false
  • Loading branch information
ewbankkit authored Aug 31, 2023
2 parents 9fd9b30 + f246980 commit 2145a43
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 85 deletions.
11 changes: 11 additions & 0 deletions .changelog/19092.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:bug
resource/aws_cloudformation_stack_set: Fix `Can only set RetainStacksOnAccountRemoval if AutoDeployment is enabled` errors
```

```release-note:enhancement
resource/aws_cloudformation_stack_set: Support resource import with `call_as = "DELEGATED_ADMIN"` via _<StackSetName>_,_<CallAs>_ syntax for `import` block or `terraform import` command
```

```release-note:enhancement
resource/aws_cloudformation_stack_set_instance: Support resource import with `call_as = "DELEGATED_ADMIN"` via _<StackSetName>_,_<AccountID>_,_<Region>_,_<CallAs>_ syntax for `import` block or `terraform import` command
```
28 changes: 0 additions & 28 deletions internal/service/cloudformation/id.go

This file was deleted.

61 changes: 35 additions & 26 deletions internal/service/cloudformation/stack_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package cloudformation

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/YakDriver/regexache"
Expand Down Expand Up @@ -35,7 +37,7 @@ func ResourceStackSet() *schema.Resource {
DeleteWithoutTimeout: resourceStackSetDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
StateContext: resourceStackSetImport,
},

Timeouts: &schema.ResourceTimeout{
Expand Down Expand Up @@ -228,6 +230,10 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in
input.AutoDeployment = expandAutoDeployment(v.([]interface{}))
}

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

if v, ok := d.GetOk("capabilities"); ok {
input.Capabilities = flex.ExpandStringSet(v.(*schema.Set))
}
Expand All @@ -252,10 +258,6 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in
input.PermissionModel = aws.String(v.(string))
}

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

if v, ok := d.GetOk("template_body"); ok {
input.TemplateBody = aws.String(v.(string))
}
Expand All @@ -264,7 +266,6 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in
input.TemplateURL = aws.String(v.(string))
}

log.Printf("[DEBUG] Creating CloudFormation StackSet: %s", input)
_, err := conn.CreateStackSetWithContext(ctx, input)

if err != nil {
Expand Down Expand Up @@ -295,35 +296,25 @@ func resourceStackSetRead(ctx context.Context, d *schema.ResourceData, meta inte

d.Set("administration_role_arn", stackSet.AdministrationRoleARN)
d.Set("arn", stackSet.StackSetARN)

if err := d.Set("auto_deployment", flattenStackSetAutoDeploymentResponse(stackSet.AutoDeployment)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting auto_deployment: %s", err)
}

if err := d.Set("capabilities", aws.StringValueSlice(stackSet.Capabilities)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting capabilities: %s", err)
}

d.Set("capabilities", aws.StringValueSlice(stackSet.Capabilities))
d.Set("description", stackSet.Description)
d.Set("execution_role_name", stackSet.ExecutionRoleName)

if err := d.Set("managed_execution", flattenStackSetManagedExecution(stackSet.ManagedExecution)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting managed_execution: %s", err)
}

d.Set("name", stackSet.StackSetName)
d.Set("permission_model", stackSet.PermissionModel)

if err := d.Set("parameters", flattenAllParameters(stackSet.Parameters)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err)
}

d.Set("stack_set_id", stackSet.StackSetId)
d.Set("template_body", stackSet.TemplateBody)

setTagsOut(ctx, stackSet.Tags)

d.Set("template_body", stackSet.TemplateBody)

return diags
}

Expand All @@ -342,6 +333,11 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in
input.AdministrationRoleARN = aws.String(v.(string))
}

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

if v, ok := d.GetOk("capabilities"); ok {
input.Capabilities = flex.ExpandStringSet(v.(*schema.Set))
}
Expand Down Expand Up @@ -370,11 +366,6 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in
input.PermissionModel = aws.String(v.(string))
}

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

if tags := getTagsIn(ctx); len(tags) > 0 {
input.Tags = tags
}
Expand All @@ -395,7 +386,6 @@ func resourceStackSetUpdate(ctx context.Context, d *schema.ResourceData, meta in
input.AutoDeployment = expandAutoDeployment(v.([]interface{}))
}

log.Printf("[DEBUG] Updating CloudFormation StackSet: %s", input)
output, err := conn.UpdateStackSetWithContext(ctx, input)

if err != nil {
Expand Down Expand Up @@ -435,16 +425,35 @@ func resourceStackSetDelete(ctx context.Context, d *schema.ResourceData, meta in
return diags
}

func resourceStackSetImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
const stackSetImportIDSeparator = ","

switch parts := strings.Split(d.Id(), stackSetImportIDSeparator); len(parts) {
case 1:
case 2:
d.SetId(parts[0])
d.Set("call_as", parts[1])
default:
return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSETNAME or STACKSETNAME%[2]sCALLAS", d.Id(), stackSetImportIDSeparator)
}

return []*schema.ResourceData{d}, nil
}

func expandAutoDeployment(l []interface{}) *cloudformation.AutoDeployment {
if len(l) == 0 {
return nil
}

m := l[0].(map[string]interface{})

enabled := m["enabled"].(bool)
autoDeployment := &cloudformation.AutoDeployment{
Enabled: aws.Bool(m["enabled"].(bool)),
RetainStacksOnAccountRemoval: aws.Bool(m["retain_stacks_on_account_removal"].(bool)),
Enabled: aws.Bool(enabled),
}

if enabled {
autoDeployment.RetainStacksOnAccountRemoval = aws.Bool(m["retain_stacks_on_account_removal"].(bool))
}

return autoDeployment
Expand Down
72 changes: 51 additions & 21 deletions internal/service/cloudformation/stack_set_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func ResourceStackSetInstance() *schema.Resource {
DeleteWithoutTimeout: resourceStackSetInstanceDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
StateContext: resourceStackSetInstanceImport,
},

Timeouts: &schema.ResourceTimeout{
Expand Down Expand Up @@ -133,17 +133,17 @@ func ResourceStackSetInstance() *schema.Resource {
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"retain_stack": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"region": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"retain_stack": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"stack_id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -230,7 +230,6 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData,
input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{}))
}

log.Printf("[DEBUG] Creating CloudFormation StackSet Instance: %s", input)
_, err := tfresource.RetryWhen(ctx, propagationTimeout,
func() (interface{}, error) {
input.OperationId = aws.String(id.UniqueId())
Expand Down Expand Up @@ -284,7 +283,7 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData,
return true, err
}

return false, fmt.Errorf("waiting for CloudFormation StackSet Instance (%s) creation: %w", d.Id(), err)
return false, err
},
)

Expand All @@ -301,11 +300,9 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m

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

d.Set("region", region)
d.Set("stack_set_name", stackSetName)

Expand All @@ -314,11 +311,13 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m
if accountIDRegexp.MatchString(accountOrOrgID) {
// Stack instances deployed by account ID
stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountOrOrgID, region, callAs)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}

if err != nil {
return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err)
}
Expand All @@ -336,13 +335,15 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m
orgIDs := strings.Split(accountOrOrgID, "/")

summaries, err := FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}

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

d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs))
Expand All @@ -358,9 +359,8 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData,

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

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

input := &cloudformation.UpdateStackInstancesInput{
Expand Down Expand Up @@ -391,15 +391,14 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData,
input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{}))
}

log.Printf("[DEBUG] Updating CloudFormation StackSet Instance: %s", input)
output, err := conn.UpdateStackInstancesWithContext(ctx, input)

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

if _, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutUpdate)); err != nil {
return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): waiting for completion: %s", d.Id(), err)
return sdkdiag.AppendErrorf(diags, "waiting for CloudFormation StackSet Instance (%s) update: %s", d.Id(), err)
}
}

Expand All @@ -411,9 +410,8 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
conn := meta.(*conns.AWSClient).CloudFormationConn(ctx)

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

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

input := &cloudformation.DeleteStackInstancesInput{
Expand All @@ -440,7 +438,7 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id())
output, err := conn.DeleteStackInstancesWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) {
if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException, cloudformation.ErrCodeStackSetNotFoundException) {
return diags
}

Expand All @@ -449,12 +447,44 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData,
}

if _, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): waiting for completion: %s", d.Id(), err)
return sdkdiag.AppendErrorf(diags, "waiting for CloudFormation StackSet Instance (%s) delete: %s", d.Id(), err)
}

return diags
}

func resourceStackSetInstanceImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
switch parts := strings.Split(d.Id(), stackSetInstanceResourceIDSeparator); len(parts) {
case 3:
case 4:
d.SetId(strings.Join([]string{parts[0], parts[1], parts[2]}, stackSetInstanceResourceIDSeparator))
d.Set("call_as", parts[3])
default:
return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSETNAME%[2]sACCOUNTID%[2]sREGION or STACKSETNAME%[2]sACCOUNTID%[2]sREGION%[2]sCALLAS", d.Id(), stackSetInstanceResourceIDSeparator)
}

return []*schema.ResourceData{d}, nil
}

const stackSetInstanceResourceIDSeparator = ","

func StackSetInstanceCreateResourceID(stackSetName, accountID, region string) string {
parts := []string{stackSetName, accountID, region}
id := strings.Join(parts, stackSetInstanceResourceIDSeparator)

return id
}

func StackSetInstanceParseResourceID(id string) (string, string, string, error) {
parts := strings.Split(id, stackSetInstanceResourceIDSeparator)

if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" {
return parts[0], parts[1], parts[2], nil
}

return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected STACKSETNAME%[2]sACCOUNTID%[2]sREGION", id, stackSetInstanceResourceIDSeparator)
}

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

0 comments on commit 2145a43

Please sign in to comment.