-
Notifications
You must be signed in to change notification settings - Fork 9.1k
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
Changes from all commits
b28e84a
a4286f1
20959bf
3edc42d
80ea22d
9e468dc
68165cd
206cd66
cf2ecb2
fd0b5c2
6ab939b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,11 @@ func TestAccAWSOrganizations(t *testing.T) { | |
"Account": { | ||
"basic": testAccAwsOrganizationsAccount_basic, | ||
}, | ||
"Unit": { | ||
"basic": testAccAwsOrganizationsUnit_basic, | ||
"importBasic": testAccAwsOrganizationsUnit_importBasic, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: We are now preferring to add |
||
"update": testAccAwsOrganizationsUnitUpdate, | ||
}, | ||
} | ||
|
||
for group, m := range testCases { | ||
|
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API documentation for this call notes:
It looks like the AWS Go SDK has a paginated function available 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 | ||
} |
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"), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: We can use our helper function
Suggested change
|
||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we could use just catch and ignore |
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use
Suggested change
|
||||||
} | ||||||
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) | ||||||
} |
There was a problem hiding this comment.
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".