Skip to content

Commit

Permalink
Merge pull request #6581 from terraform-providers/f-aws_organizations…
Browse files Browse the repository at this point in the history
…_organization-root-and-service-access

 resource/aws_organizations_organization: Support managing AWS service access principals and refactor testing
  • Loading branch information
bflad authored Nov 26, 2018
2 parents 9e64e80 + 1e33be8 commit b1d8963
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 32 deletions.
29 changes: 28 additions & 1 deletion aws/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func testAccAwsProviderAccountID(provider *schema.Provider) string {
return client.accountid
}

// testAccCheckResourceAttrAccountID ensures the Terraform state exactly matches the account ID
func testAccCheckResourceAttrAccountID(resourceName, attributeName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
return resource.TestCheckResourceAttr(resourceName, attributeName, testAccGetAccountID())(s)
}
}

// testAccCheckResourceAttrRegionalARN ensures the Terraform state exactly matches a formatted ARN with region
func testAccCheckResourceAttrRegionalARN(resourceName, attributeName, arnService, arnResource string) resource.TestCheckFunc {
return func(s *terraform.State) error {
Expand All @@ -113,7 +120,7 @@ func testAccCheckResourceAttrRegionalARN(resourceName, attributeName, arnService
}
}

// testAccCheckResourceAttrRegionalARN ensures the Terraform state regexp matches a formatted ARN with region
// testAccMatchResourceAttrRegionalARN ensures the Terraform state regexp matches a formatted ARN with region
func testAccMatchResourceAttrRegionalARN(resourceName, attributeName, arnService string, arnResourceRegexp *regexp.Regexp) resource.TestCheckFunc {
return func(s *terraform.State) error {
arnRegexp := arn.ARN{
Expand Down Expand Up @@ -147,6 +154,26 @@ func testAccCheckResourceAttrGlobalARN(resourceName, attributeName, arnService,
}
}

// testAccMatchResourceAttrGlobalARN ensures the Terraform state regexp matches a formatted ARN without region
func testAccMatchResourceAttrGlobalARN(resourceName, attributeName, arnService string, arnResourceRegexp *regexp.Regexp) resource.TestCheckFunc {
return func(s *terraform.State) error {
arnRegexp := arn.ARN{
AccountID: testAccGetAccountID(),
Partition: testAccGetPartition(),
Resource: arnResourceRegexp.String(),
Service: arnService,
}.String()

attributeMatch, err := regexp.Compile(arnRegexp)

if err != nil {
return fmt.Errorf("Unable to compile ARN regexp (%s): %s", arnRegexp, err)
}

return resource.TestMatchResourceAttr(resourceName, attributeName, attributeMatch)(s)
}
}

// testAccGetAccountID returns the account ID of testAccProvider
// Must be used returned within a resource.TestCheckFunc
func testAccGetAccountID() string {
Expand Down
96 changes: 90 additions & 6 deletions aws/resource_aws_organizations_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func resourceAwsOrganizationsOrganization() *schema.Resource {
return &schema.Resource{
Create: resourceAwsOrganizationsOrganizationCreate,
Read: resourceAwsOrganizationsOrganizationRead,
Update: resourceAwsOrganizationsOrganizationUpdate,
Delete: resourceAwsOrganizationsOrganizationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
Expand All @@ -36,6 +37,11 @@ func resourceAwsOrganizationsOrganization() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"aws_service_access_principals": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"feature_set": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -66,6 +72,21 @@ func resourceAwsOrganizationsOrganizationCreate(d *schema.ResourceData, meta int
org := resp.Organization
d.SetId(*org.Id)

awsServiceAccessPrincipals := d.Get("aws_service_access_principals").(*schema.Set).List()
for _, principalRaw := range awsServiceAccessPrincipals {
principal := principalRaw.(string)
input := &organizations.EnableAWSServiceAccessInput{
ServicePrincipal: aws.String(principal),
}

log.Printf("[DEBUG] Enabling AWS Service Access in Organization: %s", input)
_, err := conn.EnableAWSServiceAccess(input)

if err != nil {
return fmt.Errorf("error enabling AWS Service Access (%s) in Organization: %s", principal, err)
}
}

return resourceAwsOrganizationsOrganizationRead(d, meta)
}

Expand All @@ -74,23 +95,86 @@ func resourceAwsOrganizationsOrganizationRead(d *schema.ResourceData, meta inter

log.Printf("[INFO] Reading Organization: %s", d.Id())
org, err := conn.DescribeOrganization(&organizations.DescribeOrganizationInput{})

if isAWSErr(err, organizations.ErrCodeAWSOrganizationsNotInUseException, "") {
log.Printf("[WARN] Organization does not exist, removing from state: %s", d.Id())
d.SetId("")
return nil
}

if err != nil {
if isAWSErr(err, organizations.ErrCodeAWSOrganizationsNotInUseException, "") {
log.Printf("[WARN] Organization does not exist, removing from state: %s", d.Id())
d.SetId("")
return nil
}
return err
return fmt.Errorf("error describing Organization: %s", err)
}

d.Set("arn", org.Organization.Arn)
d.Set("feature_set", org.Organization.FeatureSet)
d.Set("master_account_arn", org.Organization.MasterAccountArn)
d.Set("master_account_email", org.Organization.MasterAccountEmail)
d.Set("master_account_id", org.Organization.MasterAccountId)

awsServiceAccessPrincipals := make([]string, 0)

// ConstraintViolationException: The request failed because the organization does not have all features enabled. Please enable all features in your organization and then retry.
if aws.StringValue(org.Organization.FeatureSet) == organizations.OrganizationFeatureSetAll {
err = conn.ListAWSServiceAccessForOrganizationPages(&organizations.ListAWSServiceAccessForOrganizationInput{}, func(page *organizations.ListAWSServiceAccessForOrganizationOutput, lastPage bool) bool {
for _, enabledServicePrincipal := range page.EnabledServicePrincipals {
awsServiceAccessPrincipals = append(awsServiceAccessPrincipals, aws.StringValue(enabledServicePrincipal.ServicePrincipal))
}
return !lastPage
})

if err != nil {
return fmt.Errorf("error listing AWS Service Access for Organization (%s): %s", d.Id(), err)
}
}

if err := d.Set("aws_service_access_principals", awsServiceAccessPrincipals); err != nil {
return fmt.Errorf("error setting aws_service_access_principals: %s", err)
}

return nil
}

func resourceAwsOrganizationsOrganizationUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).organizationsconn

if d.HasChange("aws_service_access_principals") {
oldRaw, newRaw := d.GetChange("aws_service_access_principals")
oldSet := oldRaw.(*schema.Set)
newSet := newRaw.(*schema.Set)

for _, disablePrincipalRaw := range oldSet.Difference(newSet).List() {
principal := disablePrincipalRaw.(string)
input := &organizations.DisableAWSServiceAccessInput{
ServicePrincipal: aws.String(principal),
}

log.Printf("[DEBUG] Disabling AWS Service Access in Organization: %s", input)
_, err := conn.DisableAWSServiceAccess(input)

if err != nil {
return fmt.Errorf("error disabling AWS Service Access (%s) in Organization: %s", principal, err)
}
}

for _, enablePrincipalRaw := range newSet.Difference(oldSet).List() {
principal := enablePrincipalRaw.(string)
input := &organizations.EnableAWSServiceAccessInput{
ServicePrincipal: aws.String(principal),
}

log.Printf("[DEBUG] Enabling AWS Service Access in Organization: %s", input)
_, err := conn.EnableAWSServiceAccess(input)

if err != nil {
return fmt.Errorf("error enabling AWS Service Access (%s) in Organization: %s", principal, err)
}
}
}

return resourceAwsOrganizationsOrganizationRead(d, meta)
}

func resourceAwsOrganizationsOrganizationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).organizationsconn

Expand Down
95 changes: 73 additions & 22 deletions aws/resource_aws_organizations_organization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package aws

import (
"fmt"
"regexp"
"testing"

"github.com/aws/aws-sdk-go/service/organizations"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func testAccAwsOrganizationsOrganization_importBasic(t *testing.T) {
func testAccAwsOrganizationsOrganization_basic(t *testing.T) {
var organization organizations.Organization
resourceName := "aws_organizations_organization.test"

resource.Test(t, resource.TestCase{
Expand All @@ -19,8 +21,16 @@ func testAccAwsOrganizationsOrganization_importBasic(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsOrganizationConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsOrganizationExists(resourceName, &organization),
testAccMatchResourceAttrGlobalARN(resourceName, "arn", "organizations", regexp.MustCompile(`organization/o-.+`)),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.#", "0"),
resource.TestCheckResourceAttr(resourceName, "feature_set", organizations.OrganizationFeatureSetAll),
testAccMatchResourceAttrGlobalARN(resourceName, "master_account_arn", "organizations", regexp.MustCompile(`account/o-.+/.+`)),
resource.TestMatchResourceAttr(resourceName, "master_account_email", regexp.MustCompile(`.+@.+`)),
testAccCheckResourceAttrAccountID(resourceName, "master_account_id"),
),
},

{
ResourceName: resourceName,
ImportState: true,
Expand All @@ -30,46 +40,70 @@ func testAccAwsOrganizationsOrganization_importBasic(t *testing.T) {
})
}

func testAccAwsOrganizationsOrganization_basic(t *testing.T) {
func testAccAwsOrganizationsOrganization_AwsServiceAccessPrincipals(t *testing.T) {
var organization organizations.Organization
resourceName := "aws_organizations_organization.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccOrganizationsAccountPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsOrganizationsOrganizationDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsOrganizationConfig,
Config: testAccAwsOrganizationsOrganizationConfigAwsServiceAccessPrincipals1("config.amazonaws.com"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsOrganizationExists(resourceName, &organization),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.#", "1"),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.553690328", "config.amazonaws.com"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccAwsOrganizationsOrganizationConfigAwsServiceAccessPrincipals2("config.amazonaws.com", "ds.amazonaws.com"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsOrganizationExists(resourceName, &organization),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.#", "2"),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.553690328", "config.amazonaws.com"),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.3567899500", "ds.amazonaws.com"),
),
},
{
Config: testAccAwsOrganizationsOrganizationConfigAwsServiceAccessPrincipals1("fms.amazonaws.com"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsOrganizationExists("aws_organizations_organization.test", &organization),
resource.TestCheckResourceAttr("aws_organizations_organization.test", "feature_set", organizations.OrganizationFeatureSetAll),
resource.TestCheckResourceAttrSet("aws_organizations_organization.test", "arn"),
resource.TestCheckResourceAttrSet("aws_organizations_organization.test", "master_account_arn"),
resource.TestCheckResourceAttrSet("aws_organizations_organization.test", "master_account_email"),
resource.TestCheckResourceAttrSet("aws_organizations_organization.test", "feature_set"),
testAccCheckAwsOrganizationsOrganizationExists(resourceName, &organization),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.#", "1"),
resource.TestCheckResourceAttr(resourceName, "aws_service_access_principals.4066123156", "fms.amazonaws.com"),
),
},
},
})
}

func testAccAwsOrganizationsOrganization_consolidatedBilling(t *testing.T) {
func testAccAwsOrganizationsOrganization_FeatureSet(t *testing.T) {
var organization organizations.Organization

feature_set := organizations.OrganizationFeatureSetConsolidatedBilling
resourceName := "aws_organizations_organization.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccOrganizationsAccountPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsOrganizationsOrganizationDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsOrganizationsOrganizationConfigConsolidatedBilling(feature_set),
Config: testAccAwsOrganizationsOrganizationConfigFeatureSet(organizations.OrganizationFeatureSetConsolidatedBilling),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsOrganizationsOrganizationExists("aws_organizations_organization.test", &organization),
resource.TestCheckResourceAttr("aws_organizations_organization.test", "feature_set", feature_set),
testAccCheckAwsOrganizationsOrganizationExists(resourceName, &organization),
resource.TestCheckResourceAttr(resourceName, "feature_set", organizations.OrganizationFeatureSetConsolidatedBilling),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Expand All @@ -86,10 +120,11 @@ func testAccCheckAwsOrganizationsOrganizationDestroy(s *terraform.State) error {

resp, err := conn.DescribeOrganization(params)

if isAWSErr(err, organizations.ErrCodeAWSOrganizationsNotInUseException, "") {
return nil
}

if err != nil {
if isAWSErr(err, organizations.ErrCodeAWSOrganizationsNotInUseException, "") {
return nil
}
return err
}

Expand Down Expand Up @@ -133,10 +168,26 @@ func testAccCheckAwsOrganizationsOrganizationExists(n string, a *organizations.O

const testAccAwsOrganizationsOrganizationConfig = "resource \"aws_organizations_organization\" \"test\" {}"

func testAccAwsOrganizationsOrganizationConfigConsolidatedBilling(feature_set string) string {
func testAccAwsOrganizationsOrganizationConfigAwsServiceAccessPrincipals1(principal1 string) string {
return fmt.Sprintf(`
resource "aws_organizations_organization" "test" {
aws_service_access_principals = [%q]
}
`, principal1)
}

func testAccAwsOrganizationsOrganizationConfigAwsServiceAccessPrincipals2(principal1, principal2 string) string {
return fmt.Sprintf(`
resource "aws_organizations_organization" "test" {
aws_service_access_principals = [%q, %q]
}
`, principal1, principal2)
}

func testAccAwsOrganizationsOrganizationConfigFeatureSet(featureSet string) string {
return fmt.Sprintf(`
resource "aws_organizations_organization" "test" {
feature_set = "%s"
feature_set = %q
}
`, feature_set)
`, featureSet)
}
6 changes: 3 additions & 3 deletions aws/resource_aws_organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
func TestAccAWSOrganizations(t *testing.T) {
testCases := map[string]map[string]func(t *testing.T){
"Organization": {
"basic": testAccAwsOrganizationsOrganization_basic,
"importBasic": testAccAwsOrganizationsOrganization_importBasic,
"consolidatedBilling": testAccAwsOrganizationsOrganization_consolidatedBilling,
"basic": testAccAwsOrganizationsOrganization_basic,
"AwsServiceAccessPrincipals": testAccAwsOrganizationsOrganization_AwsServiceAccessPrincipals,
"FeatureSet": testAccAwsOrganizationsOrganization_FeatureSet,
},
"Account": {
"basic": testAccAwsOrganizationsAccount_basic,
Expand Down
6 changes: 6 additions & 0 deletions website/docs/r/organizations_organization.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Provides a resource to create an organization.

```hcl
resource "aws_organizations_organization" "org" {
aws_service_access_principals = [
"cloudtrail.amazonaws.com",
"config.amazonaws.com",
]
feature_set = "ALL"
}
```
Expand All @@ -22,6 +27,7 @@ resource "aws_organizations_organization" "org" {

The following arguments are supported:

* `aws_service_access_principals` - (Optional) List of AWS service principal names for which you want to enable integration with your organization. This is typically in the form of a URL, such as service-abbreviation.amazonaws.com. Organization must have `feature_set` set to `ALL`. For additional information, see the [AWS Organizations User Guide](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_integrate_services.html).
* `feature_set` - (Optional) Specify "ALL" (default) or "CONSOLIDATED_BILLING".

## Attributes Reference
Expand Down

0 comments on commit b1d8963

Please sign in to comment.