diff --git a/azurerm/internal/services/network/client.go b/azurerm/internal/services/network/client.go index d8de802b5e43..afdcbe722f07 100644 --- a/azurerm/internal/services/network/client.go +++ b/azurerm/internal/services/network/client.go @@ -30,6 +30,7 @@ type Client struct { VnetGatewayClient *network.VirtualNetworkGatewaysClient VnetClient *network.VirtualNetworksClient VnetPeeringsClient *network.VirtualNetworkPeeringsClient + VirtualWanClient *network.VirtualWansClient WatcherClient *network.WatchersClient } @@ -107,6 +108,9 @@ func BuildClient(o *common.ClientOptions) *Client { VnetGatewayConnectionsClient := network.NewVirtualNetworkGatewayConnectionsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&VnetGatewayConnectionsClient.Client, o.ResourceManagerAuthorizer) + VirtualWanClient := network.NewVirtualWansClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&VirtualWanClient.Client, o.ResourceManagerAuthorizer) + WatcherClient := network.NewWatchersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&WatcherClient.Client, o.ResourceManagerAuthorizer) @@ -135,6 +139,7 @@ func BuildClient(o *common.ClientOptions) *Client { VnetGatewayClient: &VnetGatewayClient, VnetClient: &VnetClient, VnetPeeringsClient: &VnetPeeringsClient, + VirtualWanClient: &VirtualWanClient, WatcherClient: &WatcherClient, } } diff --git a/azurerm/provider.go b/azurerm/provider.go index 58c2eba37936..6021f8972d2d 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -394,6 +394,7 @@ func Provider() terraform.ResourceProvider { "azurerm_virtual_network_gateway": resourceArmVirtualNetworkGateway(), "azurerm_virtual_network_peering": resourceArmVirtualNetworkPeering(), "azurerm_virtual_network": resourceArmVirtualNetwork(), + "azurerm_virtual_wan": resourceArmVirtualWan(), } for _, service := range supportedServices { diff --git a/azurerm/resource_arm_virtual_wan.go b/azurerm/resource_arm_virtual_wan.go new file mode 100644 index 000000000000..84d55625e687 --- /dev/null +++ b/azurerm/resource_arm_virtual_wan.go @@ -0,0 +1,214 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-12-01/network" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmVirtualWan() *schema.Resource { + return &schema.Resource{ + Create: resourceArmVirtualWanCreateUpdate, + Read: resourceArmVirtualWanRead, + Update: resourceArmVirtualWanCreateUpdate, + Delete: resourceArmVirtualWanDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": azure.SchemaLocation(), + + "disable_vpn_encryption": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "security_provider_name": { + Type: schema.TypeString, + Optional: true, + }, + + "allow_branch_to_branch_traffic": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "allow_vnet_to_vnet_traffic": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "office365_local_breakout_category": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.OfficeTrafficCategoryAll), + string(network.OfficeTrafficCategoryNone), + string(network.OfficeTrafficCategoryOptimize), + string(network.OfficeTrafficCategoryOptimizeAndAllow), + }, false), + Default: string(network.OfficeTrafficCategoryNone), + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmVirtualWanCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).network.VirtualWanClient + ctx := meta.(*ArmClient).StopContext + + log.Printf("[INFO] preparing arguments for Virtual WAN creation.") + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + location := azure.NormalizeLocation(d.Get("location").(string)) + disableVpnEncryption := d.Get("disable_vpn_encryption").(bool) + securityProviderName := d.Get("security_provider_name").(string) + allowBranchToBranchTraffic := d.Get("allow_branch_to_branch_traffic").(bool) + allowVnetToVnetTraffic := d.Get("allow_vnet_to_vnet_traffic").(bool) + office365LocalBreakoutCategory := d.Get("office365_local_breakout_category").(string) + tags := d.Get("tags").(map[string]interface{}) + + if requireResourcesToBeImported && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_virtual_wan", *existing.ID) + } + } + + wan := network.VirtualWAN{ + Location: utils.String(location), + Tags: expandTags(tags), + VirtualWanProperties: &network.VirtualWanProperties{ + DisableVpnEncryption: utils.Bool(disableVpnEncryption), + SecurityProviderName: utils.String(securityProviderName), + AllowBranchToBranchTraffic: utils.Bool(allowBranchToBranchTraffic), + AllowVnetToVnetTraffic: utils.Bool(allowVnetToVnetTraffic), + Office365LocalBreakoutCategory: network.OfficeTrafficCategory(office365LocalBreakoutCategory), + }, + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, wan) + if err != nil { + return fmt.Errorf("Error creating/updating Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for creation/update of Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + read, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error retrieving Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if read.ID == nil { + return fmt.Errorf("Cannot read Virtual WAN %q (Resource Group %q) ID", name, resourceGroup) + } + + d.SetId(*read.ID) + + return resourceArmVirtualWanRead(d, meta) +} + +func resourceArmVirtualWanRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).network.VirtualWanClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Path["virtualWans"] + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Virtual WAN %q (Resource Group %q) was not found - removing from state", name, resourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("Error making Read request on Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := resp.VirtualWanProperties; props != nil { + d.Set("disable_vpn_encryption", props.DisableVpnEncryption) + d.Set("security_provider_name", props.SecurityProviderName) + d.Set("allow_branch_to_branch_traffic", props.AllowBranchToBranchTraffic) + d.Set("allow_vnet_to_vnet_traffic", props.AllowVnetToVnetTraffic) + d.Set("office365_local_breakout_category", props.Office365LocalBreakoutCategory) + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmVirtualWanDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).network.VirtualWanClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Path["virtualWans"] + + future, err := client.Delete(ctx, resourceGroup, name) + if err != nil { + // deleted outside of Terraform + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("Error waiting for the deletion of Virtual WAN %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + + return nil +} diff --git a/azurerm/resource_arm_virtual_wan_test.go b/azurerm/resource_arm_virtual_wan_test.go new file mode 100644 index 000000000000..834b83c37610 --- /dev/null +++ b/azurerm/resource_arm_virtual_wan_test.go @@ -0,0 +1,218 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMVirtualWan_basic(t *testing.T) { + resourceName := "azurerm_virtual_wan.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualWanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVirtualWan_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualWanExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "disable_vpn_encryption"), + resource.TestCheckResourceAttrSet(resourceName, "allow_branch_to_branch_traffic"), + resource.TestCheckResourceAttrSet(resourceName, "allow_vnet_to_vnet_traffic"), + resource.TestCheckResourceAttrSet(resourceName, "office365_local_breakout_category"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} +func TestAccAzureRMVirtualWan_requiresImport(t *testing.T) { + if !requireResourcesToBeImported { + t.Skip("Skipping since resources aren't required to be imported") + return + } + resourceName := "azurerm_virtual_wan.test" + ri := tf.AccRandTimeInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualWanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVirtualWan_basic(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualWanExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "disable_vpn_encryption"), + resource.TestCheckResourceAttrSet(resourceName, "security_provider_name"), + resource.TestCheckResourceAttrSet(resourceName, "allow_branch_to_branch_traffic"), + resource.TestCheckResourceAttrSet(resourceName, "allow_vnet_to_vnet_traffic"), + resource.TestCheckResourceAttrSet(resourceName, "office365_local_breakout_category"), + ), + }, + { + Config: testAccAzureRMVirtualWan_requiresImport(ri, testLocation()), + ExpectError: testRequiresImportError("azurerm_virtual_wan"), + }, + }, + }) +} + +func TestAccAzureRMVirtualWan_complete(t *testing.T) { + resourceName := "azurerm_virtual_wan.test" + ri := tf.AccRandTimeInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualWanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVirtualWan_complete(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualWanExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "disable_vpn_encryption", "false"), + resource.TestCheckResourceAttr(resourceName, "security_provider_name", ""), + resource.TestCheckResourceAttr(resourceName, "allow_branch_to_branch_traffic", "true"), + resource.TestCheckResourceAttr(resourceName, "allow_vnet_to_vnet_traffic", "true"), + resource.TestCheckResourceAttr(resourceName, "office365_local_breakout_category", "All"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Hello", "There"), + resource.TestCheckResourceAttr(resourceName, "tags.World", "Example"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMVirtualWanDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).network.VirtualWanClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_virtual_wan" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Virtual WAN still exists:\n%+v", resp) + } + } + + return nil +} + +func testCheckAzureRMVirtualWanExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + virtualWanName := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Virtual WAN: %s", virtualWanName) + } + + client := testAccProvider.Meta().(*ArmClient).network.VirtualWanClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + resp, err := client.Get(ctx, resourceGroup, virtualWanName) + if err != nil { + return fmt.Errorf("Bad: Get on virtualWanClient: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Virtual WAN %q (resource group: %q) does not exist", virtualWanName, resourceGroup) + } + + return nil + } +} + +func testAccAzureRMVirtualWan_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_wan" "test" { + name = "acctestvwan%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" +} +`, rInt, location, rInt) +} + +func testAccAzureRMVirtualWan_requiresImport(rInt int, location string) string { + template := testAccAzureRMVirtualWan_basic(rInt, location) + + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_wan" "import" { + name = "${azurerm_virtual_wan.test.name}" + resource_group_name = "${azurerm_virtual_wan.test.resource_group_name}" + location = "${azurerm_virtual_wan.test.location}" +} +`, template) +} + +func testAccAzureRMVirtualWan_complete(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_wan" "test" { + name = "acctestvwan%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + + disable_vpn_encryption = false + + allow_branch_to_branch_traffic = true + allow_vnet_to_vnet_traffic = true + + office365_local_breakout_category = "All" + + tags = { + Hello = "There" + World = "Example" + } +} +`, rInt, location, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 8248333614e6..885644d65f76 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1454,6 +1454,10 @@
  • azurerm_virtual_network_peering
  • + + > + azurerm_virtual_wan + diff --git a/website/docs/r/virtual_wan.html.markdown b/website/docs/r/virtual_wan.html.markdown new file mode 100644 index 000000000000..a8440db27597 --- /dev/null +++ b/website/docs/r/virtual_wan.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_virtual_wan" +sidebar_current: "docs-azurerm-resource-network-virtual-wan" +description: |- + Manages a Virtual WAN. + +--- + +# azurerm_virtual_wan + +Manages a Virtual WAN. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "test" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_virtual_wan" "test" { + name = "example-vwan" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the Virtual WAN. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to create the Virtual WAN. Changing this forces a new resource to be created. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `disable_vpn_encryption` - (Optional) Boolean flag to specify whether VPN encryption is disabled. Defaults to `false`. + +* `security_provider_name` - (Optional) The name of the Security Provider. + +* `allow_branch_to_branch_traffic` - (Optional) Boolean flag to specify whether branch to branch traffic is allowed. Defaults to `true`. + +* `allow_vnet_to_vnet_traffic` - (Optional) Boolean flag to specify whether VNet to VNet traffic is allowed. Defaults to `false`. + +* `office365_local_breakout_category` - (Optional) Specifies the Office365 local breakout category. Possible values include: `Optimize`, `OptimizeAndAllow`, `All`, `None`. Defaults to `None`. + +* `tags` - (Optional) A mapping of tags to assign to the Virtual WAN. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Virtual WAN. + +## Import + +Virtual WAN can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_virtual_wan.test /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/virtualWans/testvwan +```