diff --git a/aws/provider.go b/aws/provider.go index 5d519834168..281cbb31fd0 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -476,6 +476,7 @@ func Provider() terraform.ResourceProvider { "aws_organizations_organization": resourceAwsOrganizationsOrganization(), "aws_organizations_account": resourceAwsOrganizationsAccount(), "aws_organizations_policy": resourceAwsOrganizationsPolicy(), + "aws_organizations_policy_attachment": resourceAwsOrganizationsPolicyAttachment(), "aws_placement_group": resourceAwsPlacementGroup(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_rds_cluster": resourceAwsRDSCluster(), diff --git a/aws/resource_aws_organizations_policy_attachment.go b/aws/resource_aws_organizations_policy_attachment.go new file mode 100644 index 00000000000..b51949e8034 --- /dev/null +++ b/aws/resource_aws_organizations_policy_attachment.go @@ -0,0 +1,154 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "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" +) + +func resourceAwsOrganizationsPolicyAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsOrganizationsPolicyAttachmentCreate, + Read: resourceAwsOrganizationsPolicyAttachmentRead, + Delete: resourceAwsOrganizationsPolicyAttachmentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "policy_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "target_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsOrganizationsPolicyAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).organizationsconn + + policyID := d.Get("policy_id").(string) + targetID := d.Get("target_id").(string) + + input := &organizations.AttachPolicyInput{ + PolicyId: aws.String(policyID), + TargetId: aws.String(targetID), + } + + log.Printf("[DEBUG] Creating Organizations Policy Attachment: %s", input) + + err := resource.Retry(4*time.Minute, func() *resource.RetryError { + _, err := conn.AttachPolicy(input) + + if err != nil { + if isAWSErr(err, organizations.ErrCodeFinalizingOrganizationException, "") { + log.Printf("[DEBUG] Trying to create policy attachment again: %q", err.Error()) + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("error creating Organizations Policy Attachment: %s", err) + } + + d.SetId(fmt.Sprintf("%s:%s", targetID, policyID)) + + return resourceAwsOrganizationsPolicyAttachmentRead(d, meta) +} + +func resourceAwsOrganizationsPolicyAttachmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).organizationsconn + + targetID, policyID, err := decodeAwsOrganizationsPolicyAttachmentID(d.Id()) + if err != nil { + return err + } + + input := &organizations.ListPoliciesForTargetInput{ + Filter: aws.String(organizations.PolicyTypeServiceControlPolicy), + TargetId: aws.String(targetID), + } + + log.Printf("[DEBUG] Listing Organizations Policies for Target: %s", input) + var output *organizations.PolicySummary + err = conn.ListPoliciesForTargetPages(input, func(page *organizations.ListPoliciesForTargetOutput, lastPage bool) bool { + for _, policySummary := range page.Policies { + if aws.StringValue(policySummary.Id) == policyID { + output = policySummary + return true + } + } + return !lastPage + }) + + if err != nil { + if isAWSErr(err, organizations.ErrCodeTargetNotFoundException, "") { + log.Printf("[WARN] Target does not exist, removing from state: %s", d.Id()) + d.SetId("") + return nil + } + return err + } + + if output == nil { + log.Printf("[WARN] Attachment does not exist, removing from state: %s", d.Id()) + d.SetId("") + return nil + } + + d.Set("policy_id", policyID) + d.Set("target_id", targetID) + return nil +} + +func resourceAwsOrganizationsPolicyAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).organizationsconn + + targetID, policyID, err := decodeAwsOrganizationsPolicyAttachmentID(d.Id()) + if err != nil { + return err + } + + input := &organizations.DetachPolicyInput{ + PolicyId: aws.String(policyID), + TargetId: aws.String(targetID), + } + + log.Printf("[DEBUG] Detaching Organizations Policy %q from %q", policyID, targetID) + _, err = conn.DetachPolicy(input) + if err != nil { + if isAWSErr(err, organizations.ErrCodePolicyNotFoundException, "") { + return nil + } + if isAWSErr(err, organizations.ErrCodeTargetNotFoundException, "") { + return nil + } + return err + } + return nil +} + +func decodeAwsOrganizationsPolicyAttachmentID(id string) (string, string, error) { + idParts := strings.Split(id, ":") + if len(idParts) != 2 { + return "", "", fmt.Errorf("expected ID in format of TARGETID:POLICYID, received: %s", id) + } + return idParts[0], idParts[1], nil +} diff --git a/aws/resource_aws_organizations_policy_attachment_test.go b/aws/resource_aws_organizations_policy_attachment_test.go new file mode 100644 index 00000000000..c205ee1e11c --- /dev/null +++ b/aws/resource_aws_organizations_policy_attachment_test.go @@ -0,0 +1,146 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "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 TestAccAwsOrganizationsPolicyAttachment_account(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_organizations_policy_attachment.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsOrganizationsPolicyAttachmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsOrganizationsPolicyAttachmentConfig_Account(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsOrganizationsPolicyAttachmentExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + resource.TestCheckResourceAttrSet(resourceName, "target_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsOrganizationsPolicyAttachmentDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).organizationsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_organizations_policy_attachment" { + continue + } + + targetID, policyID, err := decodeAwsOrganizationsPolicyAttachmentID(rs.Primary.ID) + if err != nil { + return err + } + + input := &organizations.ListPoliciesForTargetInput{ + Filter: aws.String(organizations.PolicyTypeServiceControlPolicy), + TargetId: aws.String(targetID), + } + + log.Printf("[DEBUG] Listing Organizations Policies for Target: %s", input) + var output *organizations.PolicySummary + err = conn.ListPoliciesForTargetPages(input, func(page *organizations.ListPoliciesForTargetOutput, lastPage bool) bool { + for _, policySummary := range page.Policies { + if aws.StringValue(policySummary.Id) == policyID { + output = policySummary + return true + } + } + return !lastPage + }) + + if err != nil { + if isAWSErr(err, organizations.ErrCodeTargetNotFoundException, "") { + return nil + } + return err + } + + if output == nil { + return nil + } + + return fmt.Errorf("Policy attachment %q still exists", rs.Primary.ID) + } + + return nil + +} + +func testAccCheckAwsOrganizationsPolicyAttachmentExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).organizationsconn + + targetID, policyID, err := decodeAwsOrganizationsPolicyAttachmentID(rs.Primary.ID) + if err != nil { + return err + } + + input := &organizations.ListPoliciesForTargetInput{ + Filter: aws.String(organizations.PolicyTypeServiceControlPolicy), + TargetId: aws.String(targetID), + } + + log.Printf("[DEBUG] Listing Organizations Policies for Target: %s", input) + var output *organizations.PolicySummary + err = conn.ListPoliciesForTargetPages(input, func(page *organizations.ListPoliciesForTargetOutput, lastPage bool) bool { + for _, policySummary := range page.Policies { + if aws.StringValue(policySummary.Id) == policyID { + output = policySummary + return true + } + } + return !lastPage + }) + + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("Policy attachment %q does not exist", rs.Primary.ID) + } + + return nil + } +} + +func testAccAwsOrganizationsPolicyAttachmentConfig_Account(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_organizations_policy" "test" { + content = "{\"Version\": \"2012-10-17\", \"Statement\": { \"Effect\": \"Allow\", \"Action\": \"*\", \"Resource\": \"*\"}}" + name = "%s" +} + +resource "aws_organizations_policy_attachment" "test" { + policy_id = "${aws_organizations_policy.test.id}" + target_id = "${data.aws_caller_identity.current.account_id}" +} +`, rName) +} diff --git a/website/aws.erb b/website/aws.erb index f06470124b0..4ab8ef7a51b 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1443,6 +1443,9 @@ > aws_organizations_policy + > + aws_organizations_policy_attachment + diff --git a/website/docs/r/organizations_policy_attachment.html.markdown b/website/docs/r/organizations_policy_attachment.html.markdown new file mode 100644 index 00000000000..4242364389f --- /dev/null +++ b/website/docs/r/organizations_policy_attachment.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "aws" +page_title: "AWS: aws_organizations_policy_attachment" +sidebar_current: "docs-aws-resource-organizations-policy-attachment" +description: |- + Provides a resource to attach an AWS Organizations policy to an organization account, root, or unit. +--- + +# aws_organizations_policy_attachment + +Provides a resource to attach an AWS Organizations policy to an organization account, root, or unit. + +## Example Usage + +### Organization Account + +```hcl +resource "aws_organizations_policy_attachment" "account" { + policy_id = "${aws_organizations_policy.example.id}" + target_id = "123456789012" +} +``` + +### Organization Root + +```hcl +resource "aws_organizations_policy_attachment" "root" { + policy_id = "${aws_organizations_policy.example.id}" + target_id = "r-12345678" +} +``` + +### Organization Unit + +```hcl +resource "aws_organizations_policy_attachment" "unit" { + policy_id = "${aws_organizations_policy.example.id}" + target_id = "ou-12345678" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `policy_id` - (Required) The unique identifier (ID) of the policy that you want to attach to the target. +* `target_id` - (Required) The unique identifier (ID) of the root, organizational unit, or account number that you want to attach the policy to. + +## Import + +`aws_organizations_policy_attachment` can be imported by using the target ID and policy ID, e.g. with an account target + +``` +$ terraform import aws_organization_policy_attachment.account 123456789012:p-12345678 +```