Skip to content

Commit

Permalink
feat: add support for deployment targets
Browse files Browse the repository at this point in the history
  • Loading branch information
anGie44 committed Oct 6, 2021
1 parent bf09414 commit 9b941b1
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 12 deletions.
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
106 changes: 94 additions & 12 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,31 @@ 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"},
},
"parameter_overrides": {
Type: schema.TypeMap,
Expand Down Expand Up @@ -76,23 +97,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 +198,26 @@ 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
// seperated 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 !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error 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 Down Expand Up @@ -196,7 +246,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 +261,12 @@ func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, met
StackSetName: aws.String(stackSetName),
}

if v, ok := d.GetOk("deployment_targets"); ok {
// 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 +303,12 @@ func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, met
StackSetName: aws.String(stackSetName),
}

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

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

Expand All @@ -264,3 +326,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

0 comments on commit 9b941b1

Please sign in to comment.