diff --git a/aws/provider.go b/aws/provider.go index 93966053b04..65821f6e1d7 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -633,6 +633,7 @@ func Provider() terraform.ResourceProvider { "aws_ram_principal_association": resourceAwsRamPrincipalAssociation(), "aws_ram_resource_association": resourceAwsRamResourceAssociation(), "aws_ram_resource_share": resourceAwsRamResourceShare(), + "aws_ram_resource_share_accepter": resourceAwsRamResourceShareAccepter(), "aws_rds_cluster": resourceAwsRDSCluster(), "aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), diff --git a/aws/resource_aws_ram_resource_share_accepter.go b/aws/resource_aws_ram_resource_share_accepter.go new file mode 100644 index 00000000000..7065174453c --- /dev/null +++ b/aws/resource_aws_ram_resource_share_accepter.go @@ -0,0 +1,270 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRamResourceShareAccepter() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRamResourceShareAccepterCreate, + Read: resourceAwsRamResourceShareAccepterRead, + Delete: resourceAwsRamResourceShareAccepterDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "share_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + + "invitation_arn": { + Type: schema.TypeString, + Computed: true, + }, + + "share_id": { + Type: schema.TypeString, + Computed: true, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "receiver_account_id": { + Type: schema.TypeString, + Computed: true, + }, + + "sender_account_id": { + Type: schema.TypeString, + Computed: true, + }, + + "share_name": { + Type: schema.TypeString, + Computed: true, + }, + + "resources": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceAwsRamResourceShareAccepterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + shareARN := d.Get("share_arn").(string) + + // need invitation arn + invitation, err := resourceAwsRamResourceShareGetInvitation(conn, shareARN, ram.ResourceShareInvitationStatusPending) + + if err != nil { + return err + } + + if invitation == nil || aws.StringValue(invitation.ResourceShareInvitationArn) == "" { + return fmt.Errorf("No RAM resource share invitation by ARN (%s) found", shareARN) + } + + input := &ram.AcceptResourceShareInvitationInput{ + ClientToken: aws.String(resource.UniqueId()), + ResourceShareInvitationArn: invitation.ResourceShareInvitationArn, + } + + log.Printf("[DEBUG] Accept RAM resource share invitation request: %s", input) + output, err := conn.AcceptResourceShareInvitation(input) + + if err != nil { + return fmt.Errorf("Error accepting RAM resource share invitation: %s", err) + } + + d.SetId(shareARN) + + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareInvitationStatusPending}, + Target: []string{ram.ResourceShareInvitationStatusAccepted}, + Refresh: resourceAwsRamResourceShareAccepterStateRefreshFunc( + conn, + aws.StringValue(output.ResourceShareInvitation.ResourceShareInvitationArn)), + Timeout: d.Timeout(schema.TimeoutCreate), + } + + _, err = stateConf.WaitForState() + + if err != nil { + return fmt.Errorf("Error waiting for RAM resource share (%s) state: %s", d.Id(), err) + } + + return resourceAwsRamResourceShareAccepterRead(d, meta) +} + +func resourceAwsRamResourceShareAccepterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + invitation, err := resourceAwsRamResourceShareGetInvitation(conn, d.Id(), ram.ResourceShareInvitationStatusAccepted) + + if err == nil && invitation == nil { + log.Printf("[WARN] No RAM resource share invitation by ARN (%s) found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return err + } + + d.Set("status", invitation.Status) + d.Set("receiver_account_id", invitation.ReceiverAccountId) + d.Set("sender_account_id", invitation.SenderAccountId) + d.Set("share_arn", invitation.ResourceShareArn) + d.Set("invitation_arn", invitation.ResourceShareInvitationArn) + d.Set("share_id", resourceAwsRamResourceShareGetIDFromARN(d.Id())) + d.Set("share_name", invitation.ResourceShareName) + + listInput := &ram.ListResourcesInput{ + MaxResults: aws.Int64(int64(500)), + ResourceOwner: aws.String(ram.ResourceOwnerOtherAccounts), + ResourceShareArns: aws.StringSlice([]string{d.Id()}), + Principal: invitation.SenderAccountId, + } + + var resourceARNs []*string + err = conn.ListResourcesPages(listInput, func(page *ram.ListResourcesOutput, lastPage bool) bool { + for _, resource := range page.Resources { + resourceARNs = append(resourceARNs, resource.Arn) + } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("Error reading RAM resource share resources %s: %s", d.Id(), err) + } + + if err := d.Set("resources", flattenStringList(resourceARNs)); err != nil { + return fmt.Errorf("unable to set resources: %s", err) + } + + return nil +} + +func resourceAwsRamResourceShareAccepterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + receiverAccountID := d.Get("receiver_account_id").(string) + + if receiverAccountID == "" { + return fmt.Errorf("The receiver account ID is required to leave a resource share") + } + + input := &ram.DisassociateResourceShareInput{ + ClientToken: aws.String(resource.UniqueId()), + ResourceShareArn: aws.String(d.Id()), + Principals: []*string{aws.String(receiverAccountID)}, + } + log.Printf("[DEBUG] Leave RAM resource share request: %s", input) + + _, err := conn.DisassociateResourceShare(input) + + if err != nil { + return fmt.Errorf("Error leaving RAM resource share: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareAssociationStatusAssociated}, + Target: []string{ram.ResourceShareAssociationStatusDisassociated}, + Refresh: resourceAwsRamResourceShareStateRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + } + + if _, err := stateConf.WaitForState(); err != nil { + if isAWSErr(err, ram.ErrCodeUnknownResourceException, "") { + // what we want + return nil + } + return fmt.Errorf("Error waiting for RAM resource share (%s) state: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsRamResourceShareGetInvitation(conn *ram.RAM, resourceShareARN, status string) (*ram.ResourceShareInvitation, error) { + input := &ram.GetResourceShareInvitationsInput{ + ResourceShareArns: []*string{aws.String(resourceShareARN)}, + } + + var invitation *ram.ResourceShareInvitation + err := conn.GetResourceShareInvitationsPages(input, func(page *ram.GetResourceShareInvitationsOutput, lastPage bool) bool { + for _, rsi := range page.ResourceShareInvitations { + if aws.StringValue(rsi.Status) == status { + invitation = rsi + break + } + } + + return !lastPage + }) + + if invitation == nil { + return nil, nil + } + + if err != nil { + return nil, fmt.Errorf("Error reading RAM resource share invitation %s: %s", resourceShareARN, err) + } + + return invitation, nil +} + +func resourceAwsRamResourceShareAccepterStateRefreshFunc(conn *ram.RAM, invitationArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + request := &ram.GetResourceShareInvitationsInput{ + ResourceShareInvitationArns: []*string{aws.String(invitationArn)}, + } + + output, err := conn.GetResourceShareInvitations(request) + + if err != nil { + return nil, "Unable to get resource share invitations", err + } + + if len(output.ResourceShareInvitations) == 0 { + return nil, "Resource share invitation not found", nil + } + + invitation := output.ResourceShareInvitations[0] + + return invitation, aws.StringValue(invitation.Status), nil + } +} + +func resourceAwsRamResourceShareGetIDFromARN(arn string) string { + return strings.Replace(arn[strings.LastIndex(arn, ":")+1:], "resource-share/", "rs-", -1) +} diff --git a/aws/resource_aws_ram_resource_share_accepter_test.go b/aws/resource_aws_ram_resource_share_accepter_test.go new file mode 100644 index 00000000000..0e7c50e8e81 --- /dev/null +++ b/aws/resource_aws_ram_resource_share_accepter_test.go @@ -0,0 +1,132 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" +) + +func TestAccAwsRamResourceShareAccepter_basic(t *testing.T) { + var providers []*schema.Provider + resourceName := "aws_ram_resource_share_accepter.test" + shareName := fmt.Sprintf("tf-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckAwsRamResourceShareAccepterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsRamResourceShareAccepterBasic(shareName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsRamResourceShareAccepterExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "share_arn", regexp.MustCompile(`^arn\:aws\:ram\:.*resource-share/.+$`)), + resource.TestMatchResourceAttr(resourceName, "invitation_arn", regexp.MustCompile(`^arn\:aws\:ram\:.*resource-share-invitation/.+$`)), + resource.TestMatchResourceAttr(resourceName, "share_id", regexp.MustCompile(`^rs-.+$`)), + resource.TestCheckResourceAttr(resourceName, "status", ram.ResourceShareInvitationStatusAccepted), + resource.TestMatchResourceAttr(resourceName, "receiver_account_id", regexp.MustCompile(`\d{12}`)), + resource.TestMatchResourceAttr(resourceName, "sender_account_id", regexp.MustCompile(`\d{12}`)), + resource.TestCheckResourceAttr(resourceName, "share_name", shareName), + resource.TestCheckResourceAttr(resourceName, "resources.%", "0"), + ), + }, + { + Config: testAccAwsRamResourceShareAccepterBasic(shareName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsRamResourceShareAccepterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ramconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ram_resource_share_accepter" { + continue + } + + input := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(rs.Primary.Attributes["share_arn"])}, + ResourceOwner: aws.String(ram.ResourceOwnerOtherAccounts), + } + + output, err := conn.GetResourceShares(input) + if err != nil { + if isAWSErr(err, ram.ErrCodeUnknownResourceException, "") { + return nil + } + return fmt.Errorf("Error deleting RAM resource share: %s", err) + } + + if len(output.ResourceShares) == 0 { + return nil + } + } + return fmt.Errorf("RAM resource share invitation found, should be destroyed") +} + +func testAccCheckAwsRamResourceShareAccepterExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + + if !ok || rs.Type != "aws_ram_resource_share_accepter" { + return fmt.Errorf("RAM resource share invitation not found: %s", name) + } + + conn := testAccProvider.Meta().(*AWSClient).ramconn + + input := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(rs.Primary.Attributes["share_arn"])}, + ResourceOwner: aws.String(ram.ResourceOwnerOtherAccounts), + } + + output, err := conn.GetResourceShares(input) + if err != nil || len(output.ResourceShares) == 0 { + return fmt.Errorf("Error finding RAM resource share: %s", err) + } + + return nil + } +} + +func testAccAwsRamResourceShareAccepterBasic(shareName string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +resource "aws_ram_resource_share" "test" { + provider = "aws.alternate" + + name = %[1]q + allow_external_principals = true + + tags = { + Name = %[1]q + } +} + +resource "aws_ram_principal_association" "test" { + provider = "aws.alternate" + + principal = "${data.aws_caller_identity.receiver.account_id}" + resource_share_arn = "${aws_ram_resource_share.test.arn}" +} + +data "aws_caller_identity" "receiver" {} + +resource "aws_ram_resource_share_accepter" "test" { + share_arn = "${aws_ram_principal_association.test.resource_share_arn}" +} +`, shareName) +} diff --git a/website/aws.erb b/website/aws.erb index f8ae727771d..2bb589fd6ee 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -324,6 +324,9 @@ +
  • + aws_ram_resource_share_accepter +
  • diff --git a/website/docs/r/ram_resource_share_accepter.markdown b/website/docs/r/ram_resource_share_accepter.markdown new file mode 100644 index 00000000000..92b9ce0a476 --- /dev/null +++ b/website/docs/r/ram_resource_share_accepter.markdown @@ -0,0 +1,78 @@ +--- +layout: "aws" +page_title: "AWS: aws_ram_resource_share_accepter" +sidebar_current: "docs-aws-resource-ram-resource-share-accepter" +description: |- + Manages accepting a Resource Access Manager (RAM) Resource Share invitation. +--- + +# Resource: aws_ram_resource_share_accepter + +Manage accepting a Resource Access Manager (RAM) Resource Share invitation. From a _receiver_ AWS account, accept an invitation to share resources that were shared by a _sender_ AWS account. To create a resource share in the _sender_, see the [`aws_ram_resource_share` resource](/docs/providers/aws/r/ram_resource_share.html). + +~> **Note:** You can [`share resources`](https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html) with Organizations, Organizational Units (OUs), AWS accounts, and accounts from outside of your Organization. + +## Example Usage + +This configuration provides an example of using multiple Terraform AWS providers to configure two different AWS accounts. In the _sender_ account, the configuration creates a `aws_ram_resource_share` and uses a data source in the _receiver_ account to create a `aws_ram_principal_association` resource with the _receiver's_ account ID. In the _receiver_ account, the configuration accepts the invitation to share resources with the `aws_ram_resource_share_accepter`. + +```hcl +provider "aws" { + profile = "profile2" +} + +provider "aws" { + alias = "alternate" + profile = "profile1" +} + +resource "aws_ram_resource_share" "sender_share" { + provider = "aws.alternate" + + name = "tf-test-resource-share" + allow_external_principals = true + + tags = { + Name = "tf-test-resource-share" + } +} + +resource "aws_ram_principal_association" "sender_invite" { + provider = "aws.alternate" + + principal = "${data.aws_caller_identity.receiver.account_id}" + resource_share_arn = "${aws_ram_resource_share.test.arn}" +} + +data "aws_caller_identity" "receiver" {} + +resource "aws_ram_resource_share_accepter" "receiver_accept" { + share_arn = "${aws_ram_principal_association.test.resource_share_arn}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `share_arn` - (Required) The ARN of the resource share. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `invitation_arn` - The ARN of the resource share invitation. +* `share_id` - The ID of the resource share as displayed in the console. +* `status` - The status of the invitation (e.g., ACCEPTED, REJECTED). +* `receiver_account_id` - The account ID of the receiver account which accepts the invitation. +* `sender_account_id` - The account ID of the sender account which extends the invitation. +* `share_name` - The name of the resource share. +* `resources` - A list of the resource ARNs shared via the resource share. + +## Import + +Resource share accepters can be imported using the resource share ARN, e.g. + +``` +$ terraform import aws_ram_resource_share_accepter.example arn:aws:ram:us-east-1:123456789012:resource-share/c4b56393-e8d9-89d9-6dc9-883752de4767 +```