-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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
New Resource: aws_organizations_account #3524
Changes from all commits
823388a
046ecef
ecbf076
158741f
df173f8
187ed10
911290d
c381d4c
0c58762
ea7573f
e607c48
9a434eb
d132b6c
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 |
---|---|---|
@@ -0,0 +1,243 @@ | ||
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 resourceAwsOrganizationsAccount() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceAwsOrganizationsAccountCreate, | ||
Read: resourceAwsOrganizationsAccountRead, | ||
Delete: resourceAwsOrganizationsAccountDelete, | ||
Importer: &schema.ResourceImporter{ | ||
State: schema.ImportStatePassthrough, | ||
}, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"arn": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"joined_method": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"joined_timestamp": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"status": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"name": { | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
Required: true, | ||
ValidateFunc: validation.StringLenBetween(1, 50), | ||
}, | ||
"email": { | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
Required: true, | ||
ValidateFunc: validateAwsOrganizationsAccountEmail, | ||
}, | ||
"iam_user_access_to_billing": { | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
Optional: true, | ||
ValidateFunc: validation.StringInSlice([]string{organizations.IAMUserAccessToBillingAllow, organizations.IAMUserAccessToBillingDeny}, true), | ||
}, | ||
"role_name": { | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
Optional: true, | ||
ValidateFunc: validateAwsOrganizationsAccountRoleName, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceAwsOrganizationsAccountCreate(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).organizationsconn | ||
|
||
// Create the account | ||
createOpts := &organizations.CreateAccountInput{ | ||
AccountName: aws.String(d.Get("name").(string)), | ||
Email: aws.String(d.Get("email").(string)), | ||
} | ||
if role, ok := d.GetOk("role_name"); ok { | ||
createOpts.RoleName = aws.String(role.(string)) | ||
} | ||
|
||
if iam_user, ok := d.GetOk("iam_user_access_to_billing"); ok { | ||
createOpts.IamUserAccessToBilling = aws.String(iam_user.(string)) | ||
} | ||
|
||
log.Printf("[DEBUG] Account create config: %#v", createOpts) | ||
|
||
var err error | ||
var resp *organizations.CreateAccountOutput | ||
err = resource.Retry(4*time.Minute, func() *resource.RetryError { | ||
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) | ||
} | ||
|
||
return resource.NonRetryableError(err) | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
if err != nil { | ||
return fmt.Errorf("Error creating account: %s", err) | ||
} | ||
log.Printf("[DEBUG] Account create response: %#v", resp) | ||
|
||
requestId := *resp.CreateAccountStatus.Id | ||
|
||
// Wait for the account to become available | ||
log.Printf("[DEBUG] Waiting for account request (%s) to succeed", requestId) | ||
|
||
stateConf := &resource.StateChangeConf{ | ||
Pending: []string{organizations.CreateAccountStateInProgress}, | ||
Target: []string{organizations.CreateAccountStateSucceeded}, | ||
Refresh: resourceAwsOrganizationsAccountStateRefreshFunc(conn, requestId), | ||
PollInterval: 10 * time.Second, | ||
Timeout: 5 * time.Minute, | ||
} | ||
stateResp, stateErr := stateConf.WaitForState() | ||
if stateErr != nil { | ||
return fmt.Errorf( | ||
"Error waiting for account request (%s) to become available: %s", | ||
requestId, stateErr) | ||
} | ||
|
||
// Store the ID | ||
accountId := stateResp.(*organizations.CreateAccountStatus).AccountId | ||
d.SetId(*accountId) | ||
|
||
return resourceAwsOrganizationsAccountRead(d, meta) | ||
} | ||
|
||
func resourceAwsOrganizationsAccountRead(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).organizationsconn | ||
describeOpts := &organizations.DescribeAccountInput{ | ||
AccountId: aws.String(d.Id()), | ||
} | ||
resp, err := conn.DescribeAccount(describeOpts) | ||
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 | ||
} | ||
|
||
account := resp.Account | ||
if account == nil { | ||
log.Printf("[WARN] Account does not exist, removing from state: %s", d.Id()) | ||
d.SetId("") | ||
return nil | ||
} | ||
|
||
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("status", account.Status) | ||
return nil | ||
} | ||
|
||
func resourceAwsOrganizationsAccountDelete(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).organizationsconn | ||
|
||
input := &organizations.RemoveAccountFromOrganizationInput{ | ||
AccountId: aws.String(d.Id()), | ||
} | ||
log.Printf("[DEBUG] Removing AWS account from organization: %s", input) | ||
_, err := conn.RemoveAccountFromOrganization(input) | ||
if err != nil { | ||
if isAWSErr(err, organizations.ErrCodeAccountNotFoundException, "") { | ||
return nil | ||
} | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// resourceAwsOrganizationsAccountStateRefreshFunc returns a resource.StateRefreshFunc | ||
// that is used to watch a CreateAccount request | ||
func resourceAwsOrganizationsAccountStateRefreshFunc(conn *organizations.Organizations, id string) resource.StateRefreshFunc { | ||
return func() (interface{}, string, error) { | ||
opts := &organizations.DescribeCreateAccountStatusInput{ | ||
CreateAccountRequestId: aws.String(id), | ||
} | ||
resp, err := conn.DescribeCreateAccountStatus(opts) | ||
if err != nil { | ||
if isAWSErr(err, organizations.ErrCodeCreateAccountStatusNotFoundException, "") { | ||
resp = nil | ||
} else { | ||
log.Printf("Error on OrganizationAccountStateRefresh: %s", err) | ||
return nil, "", err | ||
} | ||
} | ||
|
||
if resp == nil { | ||
// Sometimes AWS just has consistency issues and doesn't see | ||
// our account yet. Return an empty state. | ||
return nil, "", nil | ||
} | ||
|
||
accountStatus := resp.CreateAccountStatus | ||
if *accountStatus.State == organizations.CreateAccountStateFailed { | ||
return nil, *accountStatus.State, fmt.Errorf(*accountStatus.FailureReason) | ||
} | ||
return accountStatus, *accountStatus.State, nil | ||
} | ||
} | ||
|
||
func validateAwsOrganizationsAccountEmail(v interface{}, k string) (ws []string, errors []error) { | ||
value := v.(string) | ||
if !regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`).MatchString(value) { | ||
errors = append(errors, fmt.Errorf( | ||
"%q must be a valid email address", value)) | ||
} | ||
|
||
if len(value) < 6 { | ||
errors = append(errors, fmt.Errorf( | ||
"%q cannot be less than 6 characters", value)) | ||
} | ||
|
||
if len(value) > 64 { | ||
errors = append(errors, fmt.Errorf( | ||
"%q cannot be greater than 64 characters", value)) | ||
} | ||
|
||
return | ||
} | ||
|
||
func validateAwsOrganizationsAccountRoleName(v interface{}, k string) (ws []string, errors []error) { | ||
value := v.(string) | ||
if !regexp.MustCompile(`^[\w+=,.@-]{1,64}$`).MatchString(value) { | ||
errors = append(errors, fmt.Errorf( | ||
"%q must consist of uppercase letters, lowercase letters, digits with no spaces, and any of the following characters: =,.@-", value)) | ||
} | ||
|
||
return | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package aws | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"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 testAccAwsOrganizationsAccount_basic(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_%s", 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: testAccAwsOrganizationsAccountConfig(name, email), | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckAwsOrganizationsAccountExists("aws_organizations_account.test", &account), | ||
resource.TestCheckResourceAttrSet("aws_organizations_account.test", "arn"), | ||
resource.TestCheckResourceAttrSet("aws_organizations_account.test", "joined_method"), | ||
resource.TestCheckResourceAttrSet("aws_organizations_account.test", "joined_timestamp"), | ||
resource.TestCheckResourceAttr("aws_organizations_account.test", "name", name), | ||
resource.TestCheckResourceAttr("aws_organizations_account.test", "email", email), | ||
resource.TestCheckResourceAttrSet("aws_organizations_account.test", "status"), | ||
), | ||
}, | ||
{ | ||
ResourceName: "aws_organizations_account.test", | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckAwsOrganizationsAccountDestroy(s *terraform.State) error { | ||
conn := testAccProvider.Meta().(*AWSClient).organizationsconn | ||
|
||
for _, rs := range s.RootModule().Resources { | ||
if rs.Type != "aws_organizations_account" { | ||
continue | ||
} | ||
|
||
params := &organizations.DescribeAccountInput{ | ||
AccountId: &rs.Primary.ID, | ||
} | ||
|
||
resp, err := conn.DescribeAccount(params) | ||
|
||
if err != nil { | ||
if isAWSErr(err, organizations.ErrCodeAccountNotFoundException, "") { | ||
return nil | ||
} | ||
return err | ||
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 might need to check for
I believe in our testing at this point the testing account will be removed from the organization again, so this would be the case: if isAWSErr(err, organizations.ErrCodeAccountNotFoundException, "") {
return nil
} |
||
} | ||
|
||
if resp == nil && resp.Account != nil { | ||
return fmt.Errorf("Bad: Account still exists: %q", rs.Primary.ID) | ||
} | ||
} | ||
|
||
return nil | ||
|
||
} | ||
|
||
func testAccCheckAwsOrganizationsAccountExists(n string, a *organizations.Account) 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.DescribeAccountInput{ | ||
AccountId: &rs.Primary.ID, | ||
} | ||
|
||
resp, err := conn.DescribeAccount(params) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
if resp == nil || resp.Account == nil { | ||
return fmt.Errorf("Account %q does not exist", rs.Primary.ID) | ||
} | ||
|
||
a = resp.Account | ||
|
||
return nil | ||
} | ||
} | ||
|
||
func testAccAwsOrganizationsAccountConfig(name, email string) string { | ||
return fmt.Sprintf(` | ||
resource "aws_organizations_account" "test" { | ||
name = "%s" | ||
email = "%s" | ||
} | ||
`, name, email) | ||
} |
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.
Out of curiosity, how was 4 minutes decided here?
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.
I don't remember 100% because this code is nearly a year old. After looking at it for a little bit I think the issue was that there's no API action to check the status of
CreateOrganization
. I didn't realize this could be a problem until I wrote this resource, because I was creating an organization and immediately adding an account, so I put the poll here. Allowingorganizations.ErrCodeFinalizingOrganizationException
as aRetryableError
seems to back that up.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.
Awesome information! If that's the case, maybe we (more like I 😄 ) should setup the organization resource to wait until the organization is finalized before returning that its done on creation. I'll make a note to check that myself.
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.
If you create an issue and assign it to me, I will get it done.