Skip to content
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

resource/aws_organizations_organization: Support managing AWS service access principals and refactor testing #6581

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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