diff --git a/aws/resource_aws_organizations_account.go b/aws/resource_aws_organizations_account.go index 51cdafa7ebe4..2708be07a51b 100644 --- a/aws/resource_aws_organizations_account.go +++ b/aws/resource_aws_organizations_account.go @@ -17,6 +17,7 @@ func resourceAwsOrganizationsAccount() *schema.Resource { return &schema.Resource{ Create: resourceAwsOrganizationsAccountCreate, Read: resourceAwsOrganizationsAccountRead, + Update: resourceAwsOrganizationsAccountUpdate, Delete: resourceAwsOrganizationsAccountDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, @@ -36,7 +37,6 @@ func resourceAwsOrganizationsAccount() *schema.Resource { Computed: true, }, "parent_id": { - ForceNew: true, Type: schema.TypeString, Computed: true, Optional: true, @@ -90,29 +90,32 @@ func resourceAwsOrganizationsAccountCreate(d *schema.ResourceData, meta interfac createOpts.IamUserAccessToBilling = aws.String(iam_user.(string)) } - log.Printf("[DEBUG] Account create config: %#v", createOpts) + log.Printf("[DEBUG] Creating AWS Organizations Account: %s", createOpts) - var err error var resp *organizations.CreateAccountOutput - err = resource.Retry(4*time.Minute, func() *resource.RetryError { + err := resource.Retry(4*time.Minute, func() *resource.RetryError { + var err error + resp, err = conn.CreateAccount(createOpts) - if err != nil { - if isAWSErr(err, organizations.ErrCodeFinalizingOrganizationException, "") { - log.Printf("[DEBUG] Trying to create account again: %q", err.Error()) - return resource.RetryableError(err) - } + if isAWSErr(err, organizations.ErrCodeFinalizingOrganizationException, "") { + return resource.RetryableError(err) + } + if err != nil { return resource.NonRetryableError(err) } return nil }) + if isResourceTimeoutError(err) { + resp, err = conn.CreateAccount(createOpts) + } + if err != nil { return fmt.Errorf("Error creating account: %s", err) } - log.Printf("[DEBUG] Account create response: %#v", resp) requestId := *resp.CreateAccountStatus.Id @@ -137,28 +140,24 @@ func resourceAwsOrganizationsAccountCreate(d *schema.ResourceData, meta interfac accountId := stateResp.(*organizations.CreateAccountStatus).AccountId d.SetId(*accountId) - if newParentID, ok := d.GetOk("parent_id"); ok { - // move under an explicit parent + if v, ok := d.GetOk("parent_id"); ok { + newParentID := v.(string) + + existingParentID, err := resourceAwsOrganizationsAccountGetParentId(conn, d.Id()) - // this will be the root ID - existingParentID, err := resourceAwsOrganizationsGetParentID(conn, d.Id()) if err != nil { - return err + return fmt.Errorf("error getting AWS Organizations Account (%s) parent: %s", d.Id(), err) } - newParentIdStr := newParentID.(string) - if newParentIdStr != existingParentID { - // TODO partial - - moveOpts := &organizations.MoveAccountInput{ + if newParentID != existingParentID { + input := &organizations.MoveAccountInput{ AccountId: accountId, SourceParentId: aws.String(existingParentID), - DestinationParentId: aws.String(newParentIdStr), + DestinationParentId: aws.String(newParentID), } - _, err := conn.MoveAccount(moveOpts) - if err != nil { - return err + if _, err := conn.MoveAccount(input); err != nil { + return fmt.Errorf("error moving AWS Organizations Account (%s): %s", d.Id(), err) } } } @@ -172,13 +171,15 @@ func resourceAwsOrganizationsAccountRead(d *schema.ResourceData, meta interface{ AccountId: aws.String(d.Id()), } resp, err := conn.DescribeAccount(describeOpts) + + if isAWSErr(err, organizations.ErrCodeAccountNotFoundException, "") { + log.Printf("[WARN] Account does not exist, removing from state: %s", d.Id()) + d.SetId("") + return nil + } + if err != nil { - if isAWSErr(err, organizations.ErrCodeAccountNotFoundException, "") { - log.Printf("[WARN] Account does not exist, removing from state: %s", d.Id()) - d.SetId("") - return nil - } - return err + return fmt.Errorf("error describing AWS Organizations Account (%s): %s", d.Id(), err) } account := resp.Account @@ -188,20 +189,40 @@ func resourceAwsOrganizationsAccountRead(d *schema.ResourceData, meta interface{ return nil } + parentId, err := resourceAwsOrganizationsAccountGetParentId(conn, d.Id()) + if err != nil { + return fmt.Errorf("error getting AWS Organizations Account (%s) parent: %s", d.Id(), err) + } + d.Set("arn", account.Arn) d.Set("email", account.Email) d.Set("joined_method", account.JoinedMethod) d.Set("joined_timestamp", account.JoinedTimestamp) d.Set("name", account.Name) + d.Set("parent_id", parentId) d.Set("status", account.Status) - parentId, err := resourceAwsOrganizationsGetParentID(conn, d.Id()) - if err != nil { - return err + return nil +} + +func resourceAwsOrganizationsAccountUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).organizationsconn + + if d.HasChange("parent_id") { + o, n := d.GetChange("parent_id") + + input := &organizations.MoveAccountInput{ + AccountId: aws.String(d.Id()), + SourceParentId: aws.String(o.(string)), + DestinationParentId: aws.String(n.(string)), + } + + if _, err := conn.MoveAccount(input); err != nil { + return fmt.Errorf("error moving AWS Organizations Account (%s): %s", d.Id(), err) + } } - d.Set("parent_id", parentId) - return nil + return resourceAwsOrganizationsAccountRead(d, meta) } func resourceAwsOrganizationsAccountDelete(d *schema.ResourceData, meta interface{}) error { @@ -281,3 +302,29 @@ func validateAwsOrganizationsAccountRoleName(v interface{}, k string) (ws []stri return } + +func resourceAwsOrganizationsAccountGetParentId(conn *organizations.Organizations, childId string) (string, error) { + input := &organizations.ListParentsInput{ + ChildId: aws.String(childId), + } + var parents []*organizations.Parent + + err := conn.ListParentsPages(input, func(page *organizations.ListParentsOutput, lastPage bool) bool { + parents = append(parents, page.Parents...) + + return !lastPage + }) + + if err != nil { + return "", err + } + + if len(parents) == 0 { + return "", nil + } + + // assume there is only a single parent + // https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListParents.html + parent := parents[0] + return aws.StringValue(parent.Id), nil +} diff --git a/aws/resource_aws_organizations_account_test.go b/aws/resource_aws_organizations_account_test.go index 5ae9ada591bc..72794ef49aff 100644 --- a/aws/resource_aws_organizations_account_test.go +++ b/aws/resource_aws_organizations_account_test.go @@ -12,6 +12,8 @@ import ( ) func testAccAwsOrganizationsAccount_basic(t *testing.T) { + t.Skip("AWS Organizations Account testing is not currently automated due to manual account deletion steps.") + var account organizations.Account orgsEmailDomain, ok := os.LookupEnv("TEST_AWS_ORGANIZATION_ACCOUNT_EMAIL_DOMAIN") @@ -51,7 +53,9 @@ func testAccAwsOrganizationsAccount_basic(t *testing.T) { }) } -func testAccAwsOrganizationsAccount_parentRoot(t *testing.T) { +func testAccAwsOrganizationsAccount_ParentId(t *testing.T) { + t.Skip("AWS Organizations Account testing is not currently automated due to manual account deletion steps.") + var account organizations.Account orgsEmailDomain, ok := os.LookupEnv("TEST_AWS_ORGANIZATION_ACCOUNT_EMAIL_DOMAIN") @@ -63,6 +67,9 @@ func testAccAwsOrganizationsAccount_parentRoot(t *testing.T) { rInt := acctest.RandInt() name := fmt.Sprintf("tf_acctest_%d", rInt) email := fmt.Sprintf("tf-acctest+%d@%s", rInt, orgsEmailDomain) + resourceName := "aws_organizations_account.test" + parentIdResourceName1 := "aws_organizations_organizational_unit.test1" + parentIdResourceName2 := "aws_organizations_organizational_unit.test2" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -70,40 +77,22 @@ func testAccAwsOrganizationsAccount_parentRoot(t *testing.T) { CheckDestroy: testAccCheckAwsOrganizationsAccountDestroy, Steps: []resource.TestStep{ { - Config: testAccAwsOrganizationsAccountConfigUnderRoot(name, email), + Config: testAccAwsOrganizationsAccountConfigParentId1(name, email), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsOrganizationsAccountExists("aws_organizations_account.test", &account), - resource.TestCheckResourceAttrSet("aws_organizations_account.test", "parent_id"), + testAccCheckAwsOrganizationsAccountExists(resourceName, &account), + resource.TestCheckResourceAttrPair(resourceName, "parent_id", parentIdResourceName1, "id"), ), }, - }, - }) -} - -func testAccAwsOrganizationsAccount_parentOU(t *testing.T) { - var account organizations.Account - - orgsEmailDomain, ok := os.LookupEnv("TEST_AWS_ORGANIZATION_ACCOUNT_EMAIL_DOMAIN") - - if !ok { - t.Skip("'TEST_AWS_ORGANIZATION_ACCOUNT_EMAIL_DOMAIN' not set, skipping test.") - } - - rInt := acctest.RandInt() - name := fmt.Sprintf("tf_acctest_%d", rInt) - email := fmt.Sprintf("tf-acctest+%d@%s", rInt, orgsEmailDomain) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckAwsOrganizationsAccountDestroy, - Steps: []resource.TestStep{ { - Config: testAccAwsOrganizationsAccountConfigUnderOU(name, email), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsOrganizationsAccountConfigParentId2(name, email), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsOrganizationsAccountExists("aws_organizations_account.test", &account), - resource.TestCheckResourceAttrSet("aws_organizations_account.test", "parent_id"), - // TODO actually check that it lives under the parent + testAccCheckAwsOrganizationsAccountExists(resourceName, &account), + resource.TestCheckResourceAttrPair(resourceName, "parent_id", parentIdResourceName2, "id"), ), }, }, @@ -178,35 +167,46 @@ resource "aws_organizations_account" "test" { `, name, email) } -func testAccAwsOrganizationsAccountConfigUnderRoot(name, email string) string { +func testAccAwsOrganizationsAccountConfigParentId1(name, email string) string { return fmt.Sprintf(` -data "aws_organizations_unit" "root" { - root = true +resource "aws_organizations_organization" "test" {} + +resource "aws_organizations_organizational_unit" "test1" { + name = "test1" + parent_id = "${aws_organizations_organization.test.roots.0.id}" +} + +resource "aws_organizations_organizational_unit" "test2" { + name = "test2" + parent_id = "${aws_organizations_organization.test.roots.0.id}" } resource "aws_organizations_account" "test" { - name = "%s" - email = "%s" - parent_id = "${data.aws_organizations_unit.root.id}" + name = %[1]q + email = %[2]q + parent_id = "${aws_organizations_organizational_unit.test1.id}" } `, name, email) } -func testAccAwsOrganizationsAccountConfigUnderOU(name, email string) string { +func testAccAwsOrganizationsAccountConfigParentId2(name, email string) string { return fmt.Sprintf(` -data "aws_organizations_unit" "root" { - root = true +resource "aws_organizations_organization" "test" {} + +resource "aws_organizations_organizational_unit" "test1" { + name = "test1" + parent_id = "${aws_organizations_organization.test.roots.0.id}" } -resource "aws_organizations_unit" "test" { - parent_id = "${data.aws_organizations_unit.root.id}" - name = "%s" +resource "aws_organizations_organizational_unit" "test2" { + name = "test2" + parent_id = "${aws_organizations_organization.test.roots.0.id}" } resource "aws_organizations_account" "test" { - name = "%s" - email = "%s" - parent_id = "${aws_organizations_unit.test.id}" + name = %[1]q + email = %[2]q + parent_id = "${aws_organizations_organizational_unit.test2.id}" } -`, name, name, email) +`, name, email) } diff --git a/aws/resource_aws_organizations_test.go b/aws/resource_aws_organizations_test.go index 75a6e7b3c516..d9ca20240517 100644 --- a/aws/resource_aws_organizations_test.go +++ b/aws/resource_aws_organizations_test.go @@ -12,9 +12,8 @@ func TestAccAWSOrganizations(t *testing.T) { "FeatureSet": testAccAwsOrganizationsOrganization_FeatureSet, }, "Account": { - "basic": testAccAwsOrganizationsAccount_basic, - "parentRoot": testAccAwsOrganizationsAccount_parentRoot, - "parentOU": testAccAwsOrganizationsAccount_parentOU, + "basic": testAccAwsOrganizationsAccount_basic, + "ParentId": testAccAwsOrganizationsAccount_ParentId, }, "OrganizationalUnit": { "basic": testAccAwsOrganizationsOrganizationalUnit_basic, diff --git a/website/docs/r/organizations_account.html.markdown b/website/docs/r/organizations_account.html.markdown index 87b2a8ee28f1..e46db7503c2a 100644 --- a/website/docs/r/organizations_account.html.markdown +++ b/website/docs/r/organizations_account.html.markdown @@ -30,7 +30,7 @@ The following arguments are supported: * `name` - (Required) A friendly name for the member account. * `email` - (Required) The email address of the owner to assign to the new member account. This email address must not already be associated with another AWS account. * `iam_user_access_to_billing` - (Optional) If set to `ALLOW`, the new account enables IAM users to access account billing information if they have the required permissions. If set to `DENY`, then only the root user of the new account can access account billing information. -* `parent_id` - (Optional) The ID of the Organizational Unit or "root" the account should be under. Defaults to the root ID. +* `parent_id` - (Optional) Parent Organizational Unit ID or Root ID for the account. Defaults to the Organization default Root ID. A configuration must be present for this argument to perform drift detection. * `role_name` - (Optional) The name of an IAM role that Organizations automatically preconfigures in the new member account. This role trusts the master account, allowing users in the master account to assume the role, as permitted by the master account administrator. The role has administrator permissions in the new member account. ## Attributes Reference @@ -38,7 +38,6 @@ The following arguments are supported: In addition to all arguments above, the following attributes are exported: * `arn` - The ARN for this account. - * `id` - The AWS account id ## Import