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 @@
  • aws_organizations_policy_attachment
  • +
  • + aws_organizations_unit +
  • @@ -2878,4 +2881,4 @@ <% end %> <%= yield %> -<% end %> +<% end %> \ No newline at end of file diff --git a/website/docs/d/organizations_unit.html.markdown b/website/docs/d/organizations_unit.html.markdown new file mode 100644 index 000000000000..b5aa3510d53b --- /dev/null +++ b/website/docs/d/organizations_unit.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "aws" +page_title: "AWS: aws_organizations_unit" +sidebar_current: "docs-aws-datasource-organizations-unit" +description: |- + Provides details about an organizational unit +--- + +# Data Source: aws_organizations_unit + +`aws_organizations_unit` provides details about an [organizational unit](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_ous.html). + +~> **Note:** Only supports root organizational units at the moment. Also, must be retrieved from the organization's master account. + +Will give an error if Organizations aren't enabled - see `aws_organizations_organization`. + +## Example Usage + +```hcl +data "aws_organizations_unit" "root" { + root = true +} +``` + +## Argument Reference + +* `root` - (Optional) Boolean constraint on whether the desired organizational unit is the [root](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_getting-started_concepts.html#root) for the organization. For now, this is always `true`. + +## Attributes Reference + +The following attributes are exported: + +* `arn` - The ARN of the organizational unit +* `id` - The ID of the organizational unit (`r-...`) diff --git a/website/docs/r/organizations_policy_attachment.html.markdown b/website/docs/r/organizations_policy_attachment.html.markdown index 95b7970a197a..ed9966dd83c9 100644 --- a/website/docs/r/organizations_policy_attachment.html.markdown +++ b/website/docs/r/organizations_policy_attachment.html.markdown @@ -24,9 +24,13 @@ resource "aws_organizations_policy_attachment" "account" { ### Organization Root ```hcl +data "aws_organizations_unit" "root" { + root = true +} + resource "aws_organizations_policy_attachment" "root" { policy_id = "${aws_organizations_policy.example.id}" - target_id = "r-12345678" + target_id = "${data.aws_organizations_unit.root.id}" } ``` diff --git a/website/docs/r/organizations_unit.html.markdown b/website/docs/r/organizations_unit.html.markdown new file mode 100644 index 000000000000..7c7cfb255db8 --- /dev/null +++ b/website/docs/r/organizations_unit.html.markdown @@ -0,0 +1,46 @@ +--- +layout: "aws" +page_title: "AWS: aws_organizations_unit" +sidebar_current: "docs-aws-resource-organizations-unit" +description: |- + Provides a resource to create an organizational unit. +--- + +# aws_organizations_unit + +Provides a resource to create an organizational unit. + +## Example Usage: + +```hcl +resource "aws_organizations_organization" "org" { +} + +resource "aws_organizations_unit" "tenants" { + parent_id = "${aws_organizations_organization.roots.0.id}" + name = "tenants" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - The name for the organizational unit +* `parent_id` - ID of the parent organizational unit, which may be the root + +## Attributes Reference + +The following additional attributes are exported: + +* `arn` - ARN of the organization +* `id` - Identifier of the organization +* `parent_id` - ID of the parent organizational unit + +## Import + +The AWS organization can be imported by using the `id`, e.g. + +``` +$ terraform import aws_organizations_unit.my_unit ou-1234567 +```