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

add support for Organizational Units #4207

Merged
merged 11 commits into from
May 8, 2019
Merged
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ func Provider() terraform.ResourceProvider {
"aws_organizations_account": resourceAwsOrganizationsAccount(),
"aws_organizations_policy": resourceAwsOrganizationsPolicy(),
"aws_organizations_policy_attachment": resourceAwsOrganizationsPolicyAttachment(),
"aws_organizations_unit": resourceAwsOrganizationsUnit(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been cautiously including full naming for AWS service "objects", even if the naming becomes a little redundant to prevent any future issues where other similarly named "objects" are added. In this case, we would prefer aws_organizations_organizational_unit to match the Organizations service and service API concept of "Organizational Unit", in case the Organizations service ever releases other types of "Unit".

Suggested change
"aws_organizations_unit": resourceAwsOrganizationsUnit(),
"aws_organizations_organizational_unit": resourceAwsOrganizationsOrganizationalUnit(),

"aws_placement_group": resourceAwsPlacementGroup(),
"aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(),
"aws_ram_principal_association": resourceAwsRamPrincipalAssociation(),
Expand Down
5 changes: 5 additions & 0 deletions aws/resource_aws_organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ func TestAccAWSOrganizations(t *testing.T) {
"Account": {
"basic": testAccAwsOrganizationsAccount_basic,
},
"Unit": {
"basic": testAccAwsOrganizationsUnit_basic,
"importBasic": testAccAwsOrganizationsUnit_importBasic,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We are now preferring to add ImportState: true testing to all relevant resources tests instead of creating a separate test to cover additional configurations and remove mostly duplicate testing time/infrastructure. 👍 I don't want this to hold up this pull request though so just leaving this as a note here for future contributions!

"update": testAccAwsOrganizationsUnitUpdate,
},
}

for group, m := range testCases {
Expand Down
171 changes: 171 additions & 0 deletions aws/resource_aws_organizations_unit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package aws

import (
"fmt"
"log"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/organizations"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)

func resourceAwsOrganizationsUnit() *schema.Resource {
return &schema.Resource{
Create: resourceAwsOrganizationsUnitCreate,
Read: resourceAwsOrganizationsUnitRead,
Update: resourceAwsOrganizationsUnitUpdate,
Delete: resourceAwsOrganizationsUnitDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringLenBetween(1, 128),
},
"parent_id": {
ForceNew: true,
agomezvidalee marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile("^(r-[0-9a-z]{4,32})|(ou-[0-9a-z]{4,32}-[a-z0-9]{8,32})$"), "see https://docs.aws.amazon.com/organizations/latest/APIReference/API_CreateOrganizationalUnit.html#organizations-CreateOrganizationalUnit-request-ParentId"),
},
},
}
}

func resourceAwsOrganizationsUnitCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).organizationsconn

// Create the organizational unit
createOpts := &organizations.CreateOrganizationalUnitInput{
Name: aws.String(d.Get("name").(string)),
ParentId: aws.String(d.Get("parent_id").(string)),
}

log.Printf("[DEBUG] Organizational Unit create config: %#v", createOpts)

var err error
var resp *organizations.CreateOrganizationalUnitOutput
err = resource.Retry(4*time.Minute, func() *resource.RetryError {
resp, err = conn.CreateOrganizationalUnit(createOpts)

if err != nil {
if isAWSErr(err, organizations.ErrCodeFinalizingOrganizationException, "") {
log.Printf("[DEBUG] Trying to create organizational unit again: %q", err.Error())
return resource.RetryableError(err)
}

return resource.NonRetryableError(err)
}

return nil
})

if err != nil {
return fmt.Errorf("Error creating organizational unit: %s", err)
}
log.Printf("[DEBUG] Organizational Unit create response: %#v", resp)

// Store the ID
ouId := resp.OrganizationalUnit.Id
d.SetId(*ouId)

return resourceAwsOrganizationsUnitRead(d, meta)
}

func resourceAwsOrganizationsUnitRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).organizationsconn
describeOpts := &organizations.DescribeOrganizationalUnitInput{
OrganizationalUnitId: aws.String(d.Id()),
}
resp, err := conn.DescribeOrganizationalUnit(describeOpts)
if err != nil {
if isAWSErr(err, organizations.ErrCodeOrganizationalUnitNotFoundException, "") {
log.Printf("[WARN] Organizational Unit does not exist, removing from state: %s", d.Id())
d.SetId("")
return nil
}
return err
}

ou := resp.OrganizationalUnit
if ou == nil {
log.Printf("[WARN] Organizational Unit does not exist, removing from state: %s", d.Id())
d.SetId("")
return nil
}

parentId, err := resourceAwsOrganizationsUnitGetParentId(conn, d.Id())
if err != nil {
log.Printf("[WARN] Unable to find parent organizational unit, removing from state: %s", d.Id())
d.SetId("")
return nil
}

d.Set("arn", ou.Arn)
d.Set("name", ou.Name)
d.Set("parent_id", parentId)
return nil
}

func resourceAwsOrganizationsUnitUpdate(d *schema.ResourceData, meta interface{}) error {
if d.HasChange("name") {
conn := meta.(*AWSClient).organizationsconn

updateOpts := &organizations.UpdateOrganizationalUnitInput{
Name: aws.String(d.Get("name").(string)),
OrganizationalUnitId: aws.String(d.Id()),
}

log.Printf("[DEBUG] Organizational Unit update config: %#v", updateOpts)
resp, err := conn.UpdateOrganizationalUnit(updateOpts)
if err != nil {
return fmt.Errorf("Error creating organizational unit: %s", err)
}
log.Printf("[DEBUG] Organizational Unit update response: %#v", resp)
}

return nil
}

func resourceAwsOrganizationsUnitDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).organizationsconn

input := &organizations.DeleteOrganizationalUnitInput{
OrganizationalUnitId: aws.String(d.Id()),
}
log.Printf("[DEBUG] Removing AWS organizational unit from organization: %s", input)
_, err := conn.DeleteOrganizationalUnit(input)
if err != nil {
if isAWSErr(err, organizations.ErrCodeOrganizationalUnitNotFoundException, "") {
return nil
}
return err
}
return nil
}

func resourceAwsOrganizationsUnitGetParentId(conn *organizations.Organizations, childId string) (string, error) {
input := &organizations.ListParentsInput{
ChildId: aws.String(childId),
}
resp, err := conn.ListParents(input)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API documentation for this call notes:

Note

Always check the NextToken response parameter for a null value when calling a List* operation. These operations can occasionally return an empty set of results even when there are more results available. The NextToken response parameter value is null only when there are no more results to display.

It looks like the AWS Go SDK has a paginated function available here, ListParentsPages, which is likely the quickest option here.

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

// assume there is only a single parent
// https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListParents.html
parent := resp.Parents[0]
return aws.StringValue(parent.Id), nil
}
186 changes: 186 additions & 0 deletions aws/resource_aws_organizations_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package aws

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/service/organizations"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func testAccAwsOrganizationsUnit_importBasic(t *testing.T) {
resourceName := "aws_organizations_unit.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsOrganizationsUnitDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsUnitConfig("foo"),
},

{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccAwsOrganizationsUnit_basic(t *testing.T) {
var unit organizations.OrganizationalUnit

rInt := acctest.RandInt()
name := fmt.Sprintf("tf_outest_%d", rInt)
resourceName := "aws_organizations_unit.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsOrganizationsUnitDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsUnitConfig(name),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsUnitExists(resourceName, &unit),
resource.TestCheckResourceAttrSet(resourceName, "arn"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can use our helper function testAccMatchResourceAttrRegionalARN() here to verify the ARN contains a value like we are expecting here, e.g.

Suggested change
resource.TestCheckResourceAttrSet(resourceName, "arn"),
testAccMatchResourceAttrGlobalARN(resourceName, "arn", regexp.MustCompile(`ou/o-.+/ou-.+`)),

resource.TestCheckResourceAttr(resourceName, "name", name),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccAwsOrganizationsUnitUpdate(t *testing.T) {
var unit organizations.OrganizationalUnit

rInt := acctest.RandInt()
name1 := fmt.Sprintf("tf_outest_%d", rInt)
name2 := fmt.Sprintf("tf_outest_%d", rInt+1)
resourceName := "aws_organizations_unit.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsOrganizationsUnitDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsUnitConfig(name1),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsUnitExists(resourceName, &unit),
resource.TestCheckResourceAttr(resourceName, "name", name1),
),
},
{
Config: testAccAwsOrganizationsUnitConfig(name2),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsUnitExists(resourceName, &unit),
resource.TestCheckResourceAttr(resourceName, "name", name2),
),
},
},
})
}

func testAccCheckAwsOrganizationsUnitDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).organizationsconn

for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_organizations_unit" {
continue
}

exists, err := existsOrganization(conn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could use just catch and ignore ErrCodeAWSOrganizationsNotInUseException with the DescribeOrganizationalUnit call below instead of making the additional API call here. 👍

if err != nil {
return fmt.Errorf("failed to check for the existance of an AWS Organization: %v", err)
}

if !exists {
continue
}

params := &organizations.DescribeOrganizationalUnitInput{
OrganizationalUnitId: &rs.Primary.ID,
}

resp, err := conn.DescribeOrganizationalUnit(params)

if err != nil {
if isAWSErr(err, organizations.ErrCodeOrganizationalUnitNotFoundException, "") {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use continue here to ensure that if the test state contains multiple aws_organizations_organizational_unit resources that they are all checked appropriately. 😄

Suggested change
return nil
continue

}
return err
}

if resp != nil && resp.OrganizationalUnit != nil {
return fmt.Errorf("Bad: Organizational Unit still exists: %q", rs.Primary.ID)
}
}

return nil

}

func existsOrganization(client *organizations.Organizations) (ok bool, err error) {
_, err = client.DescribeOrganization(&organizations.DescribeOrganizationInput{})
if err != nil {
if isAWSErr(err, organizations.ErrCodeAWSOrganizationsNotInUseException, "") {
err = nil
}
return
}
ok = true
return
}

func testAccCheckAwsOrganizationsUnitExists(n string, ou *organizations.OrganizationalUnit) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

conn := testAccProvider.Meta().(*AWSClient).organizationsconn
params := &organizations.DescribeOrganizationalUnitInput{
OrganizationalUnitId: &rs.Primary.ID,
}

resp, err := conn.DescribeOrganizationalUnit(params)

if err != nil {
if isAWSErr(err, organizations.ErrCodeOrganizationalUnitNotFoundException, "") {
return fmt.Errorf("Organizational Unit %q does not exist", rs.Primary.ID)
}
return err
}

if resp == nil {
return fmt.Errorf("failed to DescribeOrganizationalUnit %q, response was nil", rs.Primary.ID)
}

ou = resp.OrganizationalUnit

return nil
}
}

func testAccAwsOrganizationsUnitConfig(name string) string {
return fmt.Sprintf(`
resource "aws_organizations_organization" "org" {
}

resource "aws_organizations_unit" "test" {
parent_id = "${aws_organizations_organization.org.roots.0.id}"
name = "%s"
}
`, name)
}
5 changes: 4 additions & 1 deletion website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,9 @@
<li>
<a href="/docs/providers/aws/r/organizations_policy_attachment.html">aws_organizations_policy_attachment</a>
</li>
<li>
<a href="/docs/providers/aws/r/organizations_unit.html">aws_organizations_unit</a>
</li>
</ul>
</li>

Expand Down Expand Up @@ -2878,4 +2881,4 @@
<% end %>

<%= yield %>
<% end %>
<% end %>
Loading