diff --git a/azurerm/config.go b/azurerm/config.go index 1e031c8600d2..e474549f4525 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -146,6 +146,7 @@ type ArmClient struct { applicationSecurityGroupsClient network.ApplicationSecurityGroupsClient expressRouteAuthsClient network.ExpressRouteCircuitAuthorizationsClient expressRouteCircuitClient network.ExpressRouteCircuitsClient + expressRoutePeeringsClient network.ExpressRouteCircuitPeeringsClient ifaceClient network.InterfacesClient loadBalancerClient network.LoadBalancersClient localNetConnClient network.LocalNetworkGatewaysClient @@ -698,6 +699,10 @@ func (c *ArmClient) registerNetworkingClients(endpoint, subscriptionId string, a c.configureClient(&expressRouteCircuitsClient.Client, auth) c.expressRouteCircuitClient = expressRouteCircuitsClient + expressRoutePeeringsClient := network.NewExpressRouteCircuitPeeringsClientWithBaseURI(endpoint, subscriptionId) + c.configureClient(&expressRoutePeeringsClient.Client, auth) + c.expressRoutePeeringsClient = expressRoutePeeringsClient + interfacesClient := network.NewInterfacesClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&interfacesClient.Client, auth) c.ifaceClient = interfacesClient diff --git a/azurerm/import_arm_express_route_circuit_peering_test.go b/azurerm/import_arm_express_route_circuit_peering_test.go new file mode 100644 index 000000000000..f7f682c71caa --- /dev/null +++ b/azurerm/import_arm_express_route_circuit_peering_test.go @@ -0,0 +1,76 @@ +package azurerm + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func testAccAzureRMExpressRouteCircuitPeering_importAzurePrivatePeering(t *testing.T) { + rInt := acctest.RandInt() + location := testLocation() + resourceName := "azurerm_express_route_circuit_peering.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_privatePeering(rInt, location), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"shared_key"}, + }, + }, + }) +} + +func testAccAzureRMExpressRouteCircuitPeering_importAzurePublicPeering(t *testing.T) { + rInt := acctest.RandInt() + location := testLocation() + resourceName := "azurerm_express_route_circuit_peering.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_publicPeering(rInt, location), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"shared_key"}, + }, + }, + }) +} + +func testAccAzureRMExpressRouteCircuitPeering_importMicrosoftPeering(t *testing.T) { + rInt := acctest.RandInt() + location := testLocation() + resourceName := "azurerm_express_route_circuit_peering.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_msPeering(rInt, location), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 37dee6f77074..a866f0c05dc9 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -140,6 +140,7 @@ func Provider() terraform.ResourceProvider { "azurerm_eventhub_namespace": resourceArmEventHubNamespace(), "azurerm_express_route_circuit": resourceArmExpressRouteCircuit(), "azurerm_express_route_circuit_authorization": resourceArmExpressRouteCircuitAuthorization(), + "azurerm_express_route_circuit_peering": resourceArmExpressRouteCircuitPeering(), "azurerm_function_app": resourceArmFunctionApp(), "azurerm_image": resourceArmImage(), "azurerm_iothub": resourceArmIotHub(), diff --git a/azurerm/resource_arm_express_route_circuit_peering.go b/azurerm/resource_arm_express_route_circuit_peering.go new file mode 100644 index 000000000000..8ea9fc051a4a --- /dev/null +++ b/azurerm/resource_arm_express_route_circuit_peering.go @@ -0,0 +1,275 @@ +package azurerm + +import ( + "fmt" + "log" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2017-09-01/network" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmExpressRouteCircuitPeering() *schema.Resource { + return &schema.Resource{ + Create: resourceArmExpressRouteCircuitPeeringCreateUpdate, + Read: resourceArmExpressRouteCircuitPeeringRead, + Update: resourceArmExpressRouteCircuitPeeringCreateUpdate, + Delete: resourceArmExpressRouteCircuitPeeringDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "peering_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.AzurePrivatePeering), + string(network.AzurePublicPeering), + string(network.MicrosoftPeering), + }, false), + }, + + "express_route_circuit_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_group_name": resourceGroupNameSchema(), + + "primary_peer_address_prefix": { + Type: schema.TypeString, + Required: true, + }, + + "secondary_peer_address_prefix": { + Type: schema.TypeString, + Required: true, + }, + + "vlan_id": { + Type: schema.TypeInt, + Required: true, + }, + + "shared_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 25), + }, + + "peer_asn": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "microsoft_peering_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "advertised_public_prefixes": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + + "azure_asn": { + Type: schema.TypeInt, + Computed: true, + }, + + "primary_azure_port": { + Type: schema.TypeString, + Computed: true, + }, + + "secondary_azure_port": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArmExpressRouteCircuitPeeringCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).expressRoutePeeringsClient + ctx := meta.(*ArmClient).StopContext + + log.Printf("[INFO] preparing arguments for Express Route Peering create/update.") + + peeringType := d.Get("peering_type").(string) + circuitName := d.Get("express_route_circuit_name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + sharedKey := d.Get("shared_key").(string) + primaryPeerAddressPrefix := d.Get("primary_peer_address_prefix").(string) + secondaryPeerAddressPrefix := d.Get("secondary_peer_address_prefix").(string) + vlanId := d.Get("vlan_id").(int) + azureASN := d.Get("azure_asn").(int) + peerASN := d.Get("peer_asn").(int) + + parameters := network.ExpressRouteCircuitPeering{ + ExpressRouteCircuitPeeringPropertiesFormat: &network.ExpressRouteCircuitPeeringPropertiesFormat{ + PeeringType: network.ExpressRouteCircuitPeeringType(peeringType), + SharedKey: utils.String(sharedKey), + PrimaryPeerAddressPrefix: utils.String(primaryPeerAddressPrefix), + SecondaryPeerAddressPrefix: utils.String(secondaryPeerAddressPrefix), + AzureASN: utils.Int32(int32(azureASN)), + PeerASN: utils.Int32(int32(peerASN)), + VlanID: utils.Int32(int32(vlanId)), + }, + } + + if strings.EqualFold(peeringType, string(network.MicrosoftPeering)) { + peerings := d.Get("microsoft_peering_config").([]interface{}) + if len(peerings) == 0 { + return fmt.Errorf("`microsoft_peering_config` must be specified when `peering_type` is set to `MicrosoftPeering`") + } + + peeringConfig := expandExpressRouteCircuitPeeringMicrosoftConfig(peerings) + parameters.ExpressRouteCircuitPeeringPropertiesFormat.MicrosoftPeeringConfig = peeringConfig + } + + azureRMLockByName(circuitName, expressRouteCircuitResourceName) + defer azureRMUnlockByName(circuitName, expressRouteCircuitResourceName) + + future, err := client.CreateOrUpdate(ctx, resourceGroup, circuitName, peeringType, parameters) + if err != nil { + return err + } + + err = future.WaitForCompletion(ctx, client.Client) + if err != nil { + return err + } + + read, err := client.Get(ctx, resourceGroup, circuitName, peeringType) + if err != nil { + return err + } + + d.SetId(*read.ID) + + return resourceArmExpressRouteCircuitPeeringRead(d, meta) +} + +func resourceArmExpressRouteCircuitPeeringRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).expressRoutePeeringsClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + circuitName := id.Path["expressRouteCircuits"] + peeringType := id.Path["peerings"] + + resp, err := client.Get(ctx, resourceGroup, circuitName, peeringType) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error making Read request on Express Route Circuit Peering %q (Circuit %q / Resource Group %q): %+v", peeringType, circuitName, resourceGroup, err) + } + + d.Set("peering_type", peeringType) + d.Set("express_route_circuit_name", circuitName) + d.Set("resource_group_name", resourceGroup) + + if props := resp.ExpressRouteCircuitPeeringPropertiesFormat; props != nil { + d.Set("azure_asn", props.AzureASN) + d.Set("peer_asn", props.PeerASN) + d.Set("primary_azure_port", props.PrimaryAzurePort) + d.Set("secondary_azure_port", props.SecondaryAzurePort) + d.Set("primary_peer_address_prefix", props.PrimaryPeerAddressPrefix) + d.Set("secondary_peer_address_prefix", props.SecondaryPeerAddressPrefix) + d.Set("vlan_id", props.VlanID) + + config := flattenExpressRouteCircuitPeeringMicrosoftConfig(props.MicrosoftPeeringConfig) + if err := d.Set("microsoft_peering_config", config); err != nil { + return fmt.Errorf("Error flattening `microsoft_peering_config`: %+v", err) + } + } + + return nil +} + +func resourceArmExpressRouteCircuitPeeringDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).expressRoutePeeringsClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + circuitName := id.Path["expressRouteCircuits"] + peeringType := id.Path["peerings"] + + azureRMLockByName(circuitName, expressRouteCircuitResourceName) + defer azureRMUnlockByName(circuitName, expressRouteCircuitResourceName) + + future, err := client.Delete(ctx, resourceGroup, circuitName, peeringType) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("Error issuing delete request for Express Route Circuit Peering %q (Circuit %q / Resource Group %q): %+v", peeringType, circuitName, resourceGroup, err) + } + + err = future.WaitForCompletion(ctx, client.Client) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("Error waiting for Express Route Circuit Peering %q (Circuit %q / Resource Group %q) to be deleted: %+v", peeringType, circuitName, resourceGroup, err) + } + + return err +} + +func expandExpressRouteCircuitPeeringMicrosoftConfig(input []interface{}) *network.ExpressRouteCircuitPeeringConfig { + peering := input[0].(map[string]interface{}) + + prefixes := make([]string, 0) + inputPrefixes := peering["advertised_public_prefixes"].([]interface{}) + for _, v := range inputPrefixes { + prefixes = append(prefixes, v.(string)) + } + + return &network.ExpressRouteCircuitPeeringConfig{ + AdvertisedPublicPrefixes: &prefixes, + } +} + +func flattenExpressRouteCircuitPeeringMicrosoftConfig(input *network.ExpressRouteCircuitPeeringConfig) interface{} { + if input == nil { + return []interface{}{} + } + + config := make(map[string]interface{}, 0) + prefixes := make([]string, 0) + if ps := input.AdvertisedPublicPrefixes; ps != nil { + for _, p := range *ps { + prefixes = append(prefixes, p) + } + } + + config["advertised_public_prefixes"] = prefixes + + return []interface{}{config} +} diff --git a/azurerm/resource_arm_express_route_circuit_peering_test.go b/azurerm/resource_arm_express_route_circuit_peering_test.go new file mode 100644 index 000000000000..263ef8225f0a --- /dev/null +++ b/azurerm/resource_arm_express_route_circuit_peering_test.go @@ -0,0 +1,287 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMExpressRouteCircuitPeering(t *testing.T) { + // NOTE: this is a combined test rather than separate split out tests due to + // Azure only being happy about provisioning one at once + // (which our test suite can't easily workaround) + testCases := map[string]map[string]func(t *testing.T){ + "PrivatePeering": { + "azurePrivatePeering": testAccAzureRMExpressRouteCircuitPeering_azurePrivatePeering, + "importPrivatePeering": testAccAzureRMExpressRouteCircuitPeering_importAzurePrivatePeering, + }, + "PublicPeering": { + "azurePublicPeering": testAccAzureRMExpressRouteCircuitPeering_azurePublicPeering, + "importPublicPeering": testAccAzureRMExpressRouteCircuitPeering_importAzurePublicPeering, + }, + "MicrosoftPeering": { + "microsoftPeering": testAccAzureRMExpressRouteCircuitPeering_microsoftPeering, + "importMicrosoftPeering": testAccAzureRMExpressRouteCircuitPeering_importMicrosoftPeering, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } + }) + } +} + +func testAccAzureRMExpressRouteCircuitPeering_azurePrivatePeering(t *testing.T) { + resourceName := "azurerm_express_route_circuit_peering.test" + ri := acctest.RandInt() + location := testLocation() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_privatePeering(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMExpressRouteCircuitPeeringExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "peering_type", "AzurePrivatePeering"), + resource.TestCheckResourceAttr(resourceName, "microsoft_peering_config.#", "0"), + ), + }, + }, + }) +} + +func testAccAzureRMExpressRouteCircuitPeering_azurePublicPeering(t *testing.T) { + resourceName := "azurerm_express_route_circuit_peering.test" + ri := acctest.RandInt() + location := testLocation() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_publicPeering(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMExpressRouteCircuitPeeringExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "peering_type", "AzurePublicPeering"), + resource.TestCheckResourceAttr(resourceName, "microsoft_peering_config.#", "0"), + ), + }, + }, + }) +} + +func testAccAzureRMExpressRouteCircuitPeering_microsoftPeering(t *testing.T) { + resourceName := "azurerm_express_route_circuit_peering.test" + ri := acctest.RandInt() + location := testLocation() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMExpressRouteCircuitPeeringDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMExpressRouteCircuitPeering_msPeering(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMExpressRouteCircuitPeeringExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "peering_type", "MicrosoftPeering"), + resource.TestCheckResourceAttr(resourceName, "microsoft_peering_config.#", "1"), + ), + }, + }, + }) +} + +func testCheckAzureRMExpressRouteCircuitPeeringExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + peeringType := rs.Primary.Attributes["peering_type"] + circuitName := rs.Primary.Attributes["express_route_circuit_name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Express Route Circuit Peering: %s", peeringType) + } + + client := testAccProvider.Meta().(*ArmClient).expressRoutePeeringsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + resp, err := client.Get(ctx, resourceGroup, circuitName, peeringType) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Express Route Circuit Peering %q (Circuit %q / Resource Group %q) does not exist", peeringType, circuitName, resourceGroup) + } + + return fmt.Errorf("Bad: Get on expressRoutePeeringsClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMExpressRouteCircuitPeeringDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).expressRoutePeeringsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_express_route_circuit_peering" { + continue + } + + peeringType := rs.Primary.Attributes["peering_type"] + circuitName := rs.Primary.Attributes["express_route_circuit_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.Get(ctx, resourceGroup, circuitName, peeringType) + + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Express Route Circuit Peering still exists:\n%#v", resp) + } + } + + return nil +} + +func testAccAzureRMExpressRouteCircuitPeering_privatePeering(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestrg-%d" + location = "%s" +} + +resource "azurerm_express_route_circuit" "test" { + name = "acctest-erc-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + service_provider_name = "Equinix" + peering_location = "Silicon Valley" + bandwidth_in_mbps = 50 + + sku { + tier = "Standard" + family = "MeteredData" + } + + tags { + Environment = "production" + Purpose = "AcceptanceTests" + } +} + +resource "azurerm_express_route_circuit_peering" "test" { + peering_type = "AzurePrivatePeering" + express_route_circuit_name = "${azurerm_express_route_circuit.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" + shared_key = "ABCdefGHIJklm@nOPqrsTU!!" + peer_asn = 100 + primary_peer_address_prefix = "192.168.1.0/30" + secondary_peer_address_prefix = "192.168.2.0/30" + vlan_id = 100 +} +`, rInt, location, rInt) +} + +func testAccAzureRMExpressRouteCircuitPeering_publicPeering(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestrg-%d" + location = "%s" +} + +resource "azurerm_express_route_circuit" "test" { + name = "acctest-erc-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + service_provider_name = "Equinix" + peering_location = "Silicon Valley" + bandwidth_in_mbps = 50 + + sku { + tier = "Standard" + family = "MeteredData" + } + + tags { + Environment = "production" + Purpose = "AcceptanceTests" + } +} + +resource "azurerm_express_route_circuit_peering" "test" { + peering_type = "AzurePublicPeering" + express_route_circuit_name = "${azurerm_express_route_circuit.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" + shared_key = "ABCdefGHIJklm@nOPqrsTU!!" + peer_asn = 100 + primary_peer_address_prefix = "123.0.0.0/30" + secondary_peer_address_prefix = "123.0.0.4/30" + vlan_id = 300 +} +`, rInt, location, rInt) +} + +func testAccAzureRMExpressRouteCircuitPeering_msPeering(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestrg-%d" + location = "%s" +} + +resource "azurerm_express_route_circuit" "test" { + name = "acctest-erc-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + service_provider_name = "Equinix" + peering_location = "Silicon Valley" + bandwidth_in_mbps = 50 + + sku { + tier = "Premium" + family = "MeteredData" + } + + tags { + Environment = "production" + Purpose = "AcceptanceTests" + } +} + +resource "azurerm_express_route_circuit_peering" "test" { + peering_type = "MicrosoftPeering" + express_route_circuit_name = "${azurerm_express_route_circuit.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" + peer_asn = 100 + primary_peer_address_prefix = "192.168.1.0/30" + secondary_peer_address_prefix = "192.168.2.0/30" + vlan_id = 300 + + microsoft_peering_config { + advertised_public_prefixes = ["123.1.0.0/24"] + } +} +`, rInt, location, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0139ea74338a..e30c6a28d0b5 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -540,6 +540,10 @@ azurerm_express_route_circuit_authorization +