diff --git a/aws/provider.go b/aws/provider.go index 15f4cc7fe8b5..728a308164aa 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -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(), "aws_placement_group": resourceAwsPlacementGroup(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_ram_principal_association": resourceAwsRamPrincipalAssociation(), diff --git a/aws/resource_aws_organizations_test.go b/aws/resource_aws_organizations_test.go index a437b76bb992..9ecbcf70ad98 100644 --- a/aws/resource_aws_organizations_test.go +++ b/aws/resource_aws_organizations_test.go @@ -14,6 +14,11 @@ func TestAccAWSOrganizations(t *testing.T) { "Account": { "basic": testAccAwsOrganizationsAccount_basic, }, + "Unit": { + "basic": testAccAwsOrganizationsUnit_basic, + "importBasic": testAccAwsOrganizationsUnit_importBasic, + "update": testAccAwsOrganizationsUnitUpdate, + }, } for group, m := range testCases { diff --git a/aws/resource_aws_organizations_unit.go b/aws/resource_aws_organizations_unit.go new file mode 100644 index 000000000000..51f33c1e30e0 --- /dev/null +++ b/aws/resource_aws_organizations_unit.go @@ -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, + 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) + 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 +} diff --git a/aws/resource_aws_organizations_unit_test.go b/aws/resource_aws_organizations_unit_test.go new file mode 100644 index 000000000000..adc904f6619b --- /dev/null +++ b/aws/resource_aws_organizations_unit_test.go @@ -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"), + 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) + 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 + } + 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) +} diff --git a/website/aws.erb b/website/aws.erb index 4ff3f34ed0da..5b06eb6e587e 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1998,6 +1998,9 @@