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/cloudformation_stack_set_instance: add support for organizations deployment targets #21193

Merged
merged 2 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .changelog/21193.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_cloudformation_stack_set_instance: Add `deployment_targets` `organizational_unit_ids` argument
```
44 changes: 44 additions & 0 deletions aws/internal/service/cloudformation/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,50 @@ func StackByID(conn *cloudformation.CloudFormation, id string) (*cloudformation.
return stack, nil
}

func StackInstanceAccountIdByOrgIds(conn *cloudformation.CloudFormation, stackSetName, region string, orgIDs []string) (string, error) {
input := &cloudformation.ListStackInstancesInput{
StackInstanceRegion: aws.String(region),
StackSetName: aws.String(stackSetName),
}

var result string

err := conn.ListStackInstancesPages(input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, s := range page.Summaries {
if s == nil {
continue
}

for _, orgID := range orgIDs {
if aws.StringValue(s.OrganizationalUnitId) == orgID {
result = aws.StringValue(s.Account)
return false
}
}

}

return !lastPage
})

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

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

return result, nil
}

func StackInstanceByName(conn *cloudformation.CloudFormation, stackSetName, accountID, region string) (*cloudformation.StackInstance, error) {
input := &cloudformation.DescribeStackInstanceInput{
StackInstanceAccount: aws.String(accountID),
Expand Down
109 changes: 96 additions & 13 deletions aws/resource_aws_cloudformation_stack_set_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aws
import (
"fmt"
"log"
"regexp"
"strings"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -37,11 +38,35 @@ func resourceAwsCloudFormationStackSetInstance() *schema.Resource {

Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateAwsAccountId,
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateAwsAccountId,
ConflictsWith: []string{"deployment_targets"},
},
"deployment_targets": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"organizational_unit_ids": {
Type: schema.TypeSet,
Optional: true,
MinItems: 1,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})$`), ""),
},
},
},
},
ConflictsWith: []string{"account_id"},
},
"organizational_unit_id": {
Type: schema.TypeString,
Computed: true,
},
"parameter_overrides": {
Type: schema.TypeMap,
Expand Down Expand Up @@ -76,23 +101,32 @@ func resourceAwsCloudFormationStackSetInstance() *schema.Resource {
func resourceAwsCloudFormationStackSetInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn

accountID := meta.(*AWSClient).accountid
if v, ok := d.GetOk("account_id"); ok {
accountID = v.(string)
}

region := meta.(*AWSClient).region
if v, ok := d.GetOk("region"); ok {
region = v.(string)
}

stackSetName := d.Get("stack_set_name").(string)
input := &cloudformation.CreateStackInstancesInput{
Accounts: aws.StringSlice([]string{accountID}),
Regions: aws.StringSlice([]string{region}),
StackSetName: aws.String(stackSetName),
}

accountID := meta.(*AWSClient).accountid
if v, ok := d.GetOk("account_id"); ok {
accountID = v.(string)
}

if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
dt := expandCloudFormationDeploymentTargets(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), "/")
input.DeploymentTargets = dt
} else {
input.Accounts = aws.StringSlice([]string{accountID})
}

if v, ok := d.GetOk("parameter_overrides"); ok {
input.ParameterOverrides = expandCloudFormationParameters(v.(map[string]interface{}))
}
Expand Down Expand Up @@ -168,6 +202,20 @@ func resourceAwsCloudFormationStackSetInstanceRead(d *schema.ResourceData, meta
return err
}

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

if err != nil {
return fmt.Errorf("error finding CloudFormation StackSet Instance (%s) Account: %w", d.Id(), err)
}

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

stackInstance, err := finder.StackInstanceByName(conn, stackSetName, accountID, region)

if !d.IsNewResource() && tfresource.NotFound(err) {
Expand All @@ -181,7 +229,7 @@ func resourceAwsCloudFormationStackSetInstanceRead(d *schema.ResourceData, meta
}

d.Set("account_id", stackInstance.Account)

d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId)
if err := d.Set("parameter_overrides", flattenAllCloudFormationParameters(stackInstance.ParameterOverrides)); err != nil {
return fmt.Errorf("error setting parameters: %w", err)
}
Expand All @@ -196,7 +244,7 @@ func resourceAwsCloudFormationStackSetInstanceRead(d *schema.ResourceData, meta
func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn

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

if err != nil {
Expand All @@ -211,6 +259,12 @@ func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, met
StackSetName: aws.String(stackSetName),
}

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

if v, ok := d.GetOk("parameter_overrides"); ok {
input.ParameterOverrides = expandCloudFormationParameters(v.(map[string]interface{}))
}
Expand Down Expand Up @@ -247,6 +301,15 @@ func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, met
StackSetName: aws.String(stackSetName),
}

if v, ok := d.GetOk("organizational_unit_id"); ok {
// 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)}),
}
}

log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id())
output, err := conn.DeleteStackInstances(input)

Expand All @@ -264,3 +327,23 @@ func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, met

return nil
}

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

tfMap, ok := l[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 = expandStringSet(v)
}

return dt
}
Loading