diff --git a/aws/data_source_aws_dx_gateway.go b/aws/data_source_aws_dx_gateway.go index f80025c9e3a8..b4ebc9b075ef 100644 --- a/aws/data_source_aws_dx_gateway.go +++ b/aws/data_source_aws_dx_gateway.go @@ -22,6 +22,10 @@ func dataSourceAwsDxGateway() *schema.Resource { Type: schema.TypeString, Required: true, }, + "owner_account_id": { + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -61,6 +65,7 @@ func dataSourceAwsDxGatewayRead(d *schema.ResourceData, meta interface{}) error d.SetId(aws.StringValue(gateway.DirectConnectGatewayId)) d.Set("amazon_side_asn", strconv.FormatInt(aws.Int64Value(gateway.AmazonSideAsn), 10)) + d.Set("owner_account_id", aws.StringValue(gateway.OwnerAccount)) return nil } diff --git a/aws/data_source_aws_dx_gateway_test.go b/aws/data_source_aws_dx_gateway_test.go index b73bca3b56be..9b1530d77165 100644 --- a/aws/data_source_aws_dx_gateway_test.go +++ b/aws/data_source_aws_dx_gateway_test.go @@ -28,6 +28,7 @@ func TestAccDataSourceAwsDxGateway_Basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasourceName, "amazon_side_asn", resourceName, "amazon_side_asn"), resource.TestCheckResourceAttrPair(datasourceName, "id", resourceName, "id"), resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "owner_account_id", resourceName, "owner_account_id"), ), }, }, diff --git a/aws/provider.go b/aws/provider.go index 0ae1f6ef992c..2a272e04cb1d 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -410,6 +410,7 @@ func Provider() terraform.ResourceProvider { "aws_dx_connection_association": resourceAwsDxConnectionAssociation(), "aws_dx_gateway": resourceAwsDxGateway(), "aws_dx_gateway_association": resourceAwsDxGatewayAssociation(), + "aws_dx_gateway_association_proposal": resourceAwsDxGatewayAssociationProposal(), "aws_dx_hosted_private_virtual_interface": resourceAwsDxHostedPrivateVirtualInterface(), "aws_dx_hosted_private_virtual_interface_accepter": resourceAwsDxHostedPrivateVirtualInterfaceAccepter(), "aws_dx_hosted_public_virtual_interface": resourceAwsDxHostedPublicVirtualInterface(), diff --git a/aws/resource_aws_dx_gateway.go b/aws/resource_aws_dx_gateway.go index c98a3bbb6a98..b0693639560f 100644 --- a/aws/resource_aws_dx_gateway.go +++ b/aws/resource_aws_dx_gateway.go @@ -33,6 +33,10 @@ func resourceAwsDxGateway() *schema.Resource { ForceNew: true, ValidateFunc: validateAmazonSideAsn, }, + "owner_account_id": { + Type: schema.TypeString, + Computed: true, + }, }, Timeouts: &schema.ResourceTimeout{ @@ -96,6 +100,7 @@ func resourceAwsDxGatewayRead(d *schema.ResourceData, meta interface{}) error { dxGw := dxGwRaw.(*directconnect.Gateway) d.Set("name", aws.StringValue(dxGw.DirectConnectGatewayName)) d.Set("amazon_side_asn", strconv.FormatInt(aws.Int64Value(dxGw.AmazonSideAsn), 10)) + d.Set("owner_account_id", aws.StringValue(dxGw.OwnerAccount)) return nil } diff --git a/aws/resource_aws_dx_gateway_association_proposal.go b/aws/resource_aws_dx_gateway_association_proposal.go new file mode 100644 index 000000000000..6bd3d3d36421 --- /dev/null +++ b/aws/resource_aws_dx_gateway_association_proposal.go @@ -0,0 +1,197 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsDxGatewayAssociationProposal() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDxGatewayAssociationProposalCreate, + Read: resourceAwsDxGatewayAssociationProposalRead, + Delete: resourceAwsDxGatewayAssociationProposalDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "allowed_prefixes": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dx_gateway_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "dx_gateway_owner_account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "vpn_gateway_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsDxGatewayAssociationProposalCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + + input := &directconnect.CreateDirectConnectGatewayAssociationProposalInput{ + AddAllowedPrefixesToDirectConnectGateway: expandDirectConnectGatewayAssociationProposalAllowedPrefixes(d.Get("allowed_prefixes").(*schema.Set).List()), + DirectConnectGatewayId: aws.String(d.Get("dx_gateway_id").(string)), + DirectConnectGatewayOwnerAccount: aws.String(d.Get("dx_gateway_owner_account_id").(string)), + GatewayId: aws.String(d.Get("vpn_gateway_id").(string)), + } + + log.Printf("[DEBUG] Creating Direct Connect Gateway Association Proposal: %s", input) + output, err := conn.CreateDirectConnectGatewayAssociationProposal(input) + + if err != nil { + return fmt.Errorf("error creating Direct Connect Gateway Association Proposal: %s", err) + } + + d.SetId(aws.StringValue(output.DirectConnectGatewayAssociationProposal.ProposalId)) + + return resourceAwsDxGatewayAssociationProposalRead(d, meta) +} + +func resourceAwsDxGatewayAssociationProposalRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + + proposal, err := describeDirectConnectGatewayAssociationProposal(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error reading Direct Connect Gateway Association Proposal (%s): %s", d.Id(), err) + } + + if proposal == nil { + log.Printf("[WARN] Direct Connect Gateway Association Proposal (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if aws.StringValue(proposal.ProposalState) == directconnect.GatewayAssociationProposalStateDeleted { + log.Printf("[WARN] Direct Connect Gateway Association Proposal (%s) deleted, removing from state", d.Id()) + d.SetId("") + return nil + } + + if proposal.AssociatedGateway == nil { + return fmt.Errorf("error reading Direct Connect Gateway Association Proposal (%s): missing associated gateway information", d.Id()) + } + + if err := d.Set("allowed_prefixes", flattenDirectConnectGatewayAssociationProposalAllowedPrefixes(proposal.RequestedAllowedPrefixesToDirectConnectGateway)); err != nil { + return fmt.Errorf("error setting allowed_prefixes: %s", err) + } + + d.Set("dx_gateway_id", aws.StringValue(proposal.DirectConnectGatewayId)) + d.Set("dx_gateway_owner_account_id", aws.StringValue(proposal.DirectConnectGatewayOwnerAccount)) + d.Set("vpn_gateway_id", aws.StringValue(proposal.AssociatedGateway.Id)) + + return nil +} + +func resourceAwsDxGatewayAssociationProposalDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + + input := &directconnect.DeleteDirectConnectGatewayAssociationProposalInput{ + ProposalId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting Direct Connect Gateway Association Proposal: %s", d.Id()) + + _, err := conn.DeleteDirectConnectGatewayAssociationProposal(input) + + if err != nil { + return fmt.Errorf("error deleting Direct Connect Gateway Association Proposal (%s): %s", d.Id(), err) + } + + return nil +} + +func describeDirectConnectGatewayAssociationProposal(conn *directconnect.DirectConnect, proposalID string) (*directconnect.GatewayAssociationProposal, error) { + input := &directconnect.DescribeDirectConnectGatewayAssociationProposalsInput{ + ProposalId: aws.String(proposalID), + } + + for { + output, err := conn.DescribeDirectConnectGatewayAssociationProposals(input) + + if err != nil { + return nil, err + } + + if output == nil { + continue + } + + for _, proposal := range output.DirectConnectGatewayAssociationProposals { + if aws.StringValue(proposal.ProposalId) == proposalID { + return proposal, nil + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + return nil, nil +} + +func expandDirectConnectGatewayAssociationProposalAllowedPrefixes(allowedPrefixes []interface{}) []*directconnect.RouteFilterPrefix { + if len(allowedPrefixes) == 0 { + return nil + } + + var routeFilterPrefixes []*directconnect.RouteFilterPrefix + + for _, allowedPrefixRaw := range allowedPrefixes { + if allowedPrefixRaw == nil { + continue + } + + routeFilterPrefix := &directconnect.RouteFilterPrefix{ + Cidr: aws.String(allowedPrefixRaw.(string)), + } + + routeFilterPrefixes = append(routeFilterPrefixes, routeFilterPrefix) + } + + return routeFilterPrefixes +} + +func flattenDirectConnectGatewayAssociationProposalAllowedPrefixes(routeFilterPrefixes []*directconnect.RouteFilterPrefix) []interface{} { + if len(routeFilterPrefixes) == 0 { + return []interface{}{} + } + + var allowedPrefixes []interface{} + + for _, routeFilterPrefix := range routeFilterPrefixes { + if routeFilterPrefix == nil { + continue + } + + allowedPrefix := aws.StringValue(routeFilterPrefix.Cidr) + + allowedPrefixes = append(allowedPrefixes, allowedPrefix) + } + + return allowedPrefixes +} diff --git a/aws/resource_aws_dx_gateway_association_proposal_test.go b/aws/resource_aws_dx_gateway_association_proposal_test.go new file mode 100644 index 000000000000..4cb2ffa6bfa9 --- /dev/null +++ b/aws/resource_aws_dx_gateway_association_proposal_test.go @@ -0,0 +1,244 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsDxGatewayAssociationProposal_basic(t *testing.T) { + var proposal1 directconnect.GatewayAssociationProposal + var providers []*schema.Provider + rBgpAsn := randIntRange(64512, 65534) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_dx_gateway_association_proposal.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckAwsDxGatewayAssociationProposalDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDxGatewayAssociationProposalConfig(rName, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxGatewayAssociationProposalExists(resourceName, &proposal1), + resource.TestCheckResourceAttr(resourceName, "allowed_prefixes.#", "1"), + ), + }, + { + Config: testAccDxGatewayAssociationProposalConfig(rName, rBgpAsn), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsDxGatewayAssociationProposal_disappears(t *testing.T) { + var proposal1 directconnect.GatewayAssociationProposal + var providers []*schema.Provider + rBgpAsn := randIntRange(64512, 65534) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_dx_gateway_association_proposal.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckAwsDxGatewayAssociationProposalDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDxGatewayAssociationProposalConfig(rName, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxGatewayAssociationProposalExists(resourceName, &proposal1), + testAccCheckAwsDxGatewayAssociationProposalDisappears(&proposal1), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsDxGatewayAssociationProposal_AllowedPrefixes(t *testing.T) { + var proposal1, proposal2 directconnect.GatewayAssociationProposal + var providers []*schema.Provider + rBgpAsn := randIntRange(64512, 65534) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_dx_gateway_association_proposal.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckAwsDxGatewayAssociationProposalDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDxGatewayAssociationProposalConfigAllowedPrefixes1(rName, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxGatewayAssociationProposalExists(resourceName, &proposal1), + resource.TestCheckResourceAttr(resourceName, "allowed_prefixes.#", "1"), + ), + }, + { + Config: testAccDxGatewayAssociationProposalConfigAllowedPrefixes1(rName, rBgpAsn), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccDxGatewayAssociationProposalConfigAllowedPrefixes2(rName, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxGatewayAssociationProposalExists(resourceName, &proposal2), + testAccCheckAwsDxGatewayAssociationProposalRecreated(&proposal1, &proposal2), + resource.TestCheckResourceAttr(resourceName, "allowed_prefixes.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckAwsDxGatewayAssociationProposalDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).dxconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_dx_gateway_association_proposal" { + continue + } + + proposal, err := describeDirectConnectGatewayAssociationProposal(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if proposal == nil { + continue + } + + return fmt.Errorf("Direct Connect Gateway Association Proposal (%s) still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAwsDxGatewayAssociationProposalExists(resourceName string, gatewayAssociationProposal *directconnect.GatewayAssociationProposal) 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).dxconn + + proposal, err := describeDirectConnectGatewayAssociationProposal(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if proposal == nil { + return fmt.Errorf("Direct Connect Gateway Association Proposal (%s) not found", rs.Primary.ID) + } + + *gatewayAssociationProposal = *proposal + + return nil + } +} + +func testAccCheckAwsDxGatewayAssociationProposalDisappears(proposal *directconnect.GatewayAssociationProposal) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).dxconn + + input := &directconnect.DeleteDirectConnectGatewayAssociationProposalInput{ + ProposalId: proposal.ProposalId, + } + + _, err := conn.DeleteDirectConnectGatewayAssociationProposal(input) + + return err + } +} + +func testAccCheckAwsDxGatewayAssociationProposalRecreated(i, j *directconnect.GatewayAssociationProposal) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(i.ProposalId) == aws.StringValue(j.ProposalId) { + return fmt.Errorf("Direct Connect Gateway Association Proposal not recreated") + } + + return nil + } +} + +func testAccDxGatewayAssociationProposalConfigBase(rName string, rBgpAsn int) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +resource "aws_dx_gateway" "test" { + provider = "aws.alternate" + + amazon_side_asn = %[2]d + name = %[1]q +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "tf-acc-test-dx-gateway-association-proposal" + } +} + +resource "aws_vpn_gateway" "test" { + vpc_id = "${aws_vpc.test.id}" + + tags = { + Name = "tf-acc-test-dx-gateway-association-proposal" + } +} +`, rName, rBgpAsn) +} + +func testAccDxGatewayAssociationProposalConfig(rName string, rBgpAsn int) string { + return testAccDxGatewayAssociationProposalConfigBase(rName, rBgpAsn) + fmt.Sprintf(` +resource "aws_dx_gateway_association_proposal" "test" { + dx_gateway_id = "${aws_dx_gateway.test.id}" + dx_gateway_owner_account_id = "${aws_dx_gateway.test.owner_account_id}" + vpn_gateway_id = "${aws_vpn_gateway.test.id}" +} +`) +} + +func testAccDxGatewayAssociationProposalConfigAllowedPrefixes1(rName string, rBgpAsn int) string { + return testAccDxGatewayAssociationProposalConfigBase(rName, rBgpAsn) + fmt.Sprintf(` +resource "aws_dx_gateway_association_proposal" "test" { + allowed_prefixes = ["10.0.0.0/16"] + dx_gateway_id = "${aws_dx_gateway.test.id}" + dx_gateway_owner_account_id = "${aws_dx_gateway.test.owner_account_id}" + vpn_gateway_id = "${aws_vpn_gateway.test.id}" +} +`) +} + +func testAccDxGatewayAssociationProposalConfigAllowedPrefixes2(rName string, rBgpAsn int) string { + return testAccDxGatewayAssociationProposalConfigBase(rName, rBgpAsn) + fmt.Sprintf(` +resource "aws_dx_gateway_association_proposal" "test" { + allowed_prefixes = ["10.0.0.0/24", "10.0.1.0/24"] + dx_gateway_id = "${aws_dx_gateway.test.id}" + dx_gateway_owner_account_id = "${aws_dx_gateway.test.owner_account_id}" + vpn_gateway_id = "${aws_vpn_gateway.test.id}" +} +`) +} diff --git a/aws/resource_aws_dx_gateway_test.go b/aws/resource_aws_dx_gateway_test.go index 43c1512d2210..59cec32d40ea 100644 --- a/aws/resource_aws_dx_gateway_test.go +++ b/aws/resource_aws_dx_gateway_test.go @@ -140,6 +140,7 @@ func TestAccAwsDxGateway_basic(t *testing.T) { Config: testAccDxGatewayConfig(acctest.RandString(5), randIntRange(64512, 65534)), Check: resource.ComposeTestCheckFunc( testAccCheckAwsDxGatewayExists("aws_dx_gateway.test"), + testAccCheckResourceAttrAccountID("aws_dx_gateway.test", "owner_account_id"), ), }, }, diff --git a/website/aws.erb b/website/aws.erb index 93adc9b69a39..33c096031b7c 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1006,6 +1006,9 @@