From b30d119aaf1bd4c3029de87c85215fafb60034a0 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 28 Oct 2020 05:47:36 +0800 Subject: [PATCH] new resource: azurerm_vpn_site (#8896) Co-authored-by: kt --- .../services/network/client/client.go | 5 + .../services/network/parse/virtual_wan.go | 45 ++ .../network/parse/virtual_wan_test.go | 90 ++++ .../services/network/parse/vpn_site.go | 89 ++++ .../services/network/parse/vpn_site_test.go | 183 +++++++ .../internal/services/network/registration.go | 1 + .../network/tests/vpn_site_resource_test.go | 245 ++++++++++ .../services/network/validate/virtual_wan.go | 22 + .../services/network/validate/vpn_site.go | 28 ++ .../services/network/vpn_site_resource.go | 454 ++++++++++++++++++ website/azurerm.erb | 4 + website/docs/r/vpn_site.html.markdown | 123 +++++ 12 files changed, 1289 insertions(+) create mode 100644 azurerm/internal/services/network/parse/virtual_wan.go create mode 100644 azurerm/internal/services/network/parse/virtual_wan_test.go create mode 100644 azurerm/internal/services/network/parse/vpn_site.go create mode 100644 azurerm/internal/services/network/parse/vpn_site_test.go create mode 100644 azurerm/internal/services/network/tests/vpn_site_resource_test.go create mode 100644 azurerm/internal/services/network/validate/virtual_wan.go create mode 100644 azurerm/internal/services/network/validate/vpn_site.go create mode 100644 azurerm/internal/services/network/vpn_site_resource.go create mode 100644 website/docs/r/vpn_site.html.markdown diff --git a/azurerm/internal/services/network/client/client.go b/azurerm/internal/services/network/client/client.go index 375f87b0f7c3..d7012c3406e6 100644 --- a/azurerm/internal/services/network/client/client.go +++ b/azurerm/internal/services/network/client/client.go @@ -46,6 +46,7 @@ type Client struct { VirtualHubClient *network.VirtualHubsClient VpnGatewaysClient *network.VpnGatewaysClient VpnServerConfigurationsClient *network.VpnServerConfigurationsClient + VpnSitesClient *network.VpnSitesClient WatcherClient *network.WatchersClient WebApplicationFirewallPoliciesClient *network.WebApplicationFirewallPoliciesClient PrivateDnsZoneGroupClient *network.PrivateDNSZoneGroupsClient @@ -178,6 +179,9 @@ func NewClient(o *common.ClientOptions) *Client { vpnGatewaysClient := network.NewVpnGatewaysClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&vpnGatewaysClient.Client, o.ResourceManagerAuthorizer) + vpnSitesClient := network.NewVpnSitesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&vpnSitesClient.Client, o.ResourceManagerAuthorizer) + WatcherClient := network.NewWatchersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&WatcherClient.Client, o.ResourceManagerAuthorizer) @@ -230,6 +234,7 @@ func NewClient(o *common.ClientOptions) *Client { VirtualHubClient: &VirtualHubClient, VpnGatewaysClient: &vpnGatewaysClient, VpnServerConfigurationsClient: &vpnServerConfigurationsClient, + VpnSitesClient: &vpnSitesClient, WatcherClient: &WatcherClient, WebApplicationFirewallPoliciesClient: &WebApplicationFirewallPoliciesClient, PrivateDnsZoneGroupClient: &PrivateDnsZoneGroupClient, diff --git a/azurerm/internal/services/network/parse/virtual_wan.go b/azurerm/internal/services/network/parse/virtual_wan.go new file mode 100644 index 000000000000..fed766678a59 --- /dev/null +++ b/azurerm/internal/services/network/parse/virtual_wan.go @@ -0,0 +1,45 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type VirtualWanId struct { + ResourceGroup string + Name string +} + +func (id VirtualWanId) ID(subscriptionId string) string { + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualWans/%s", + subscriptionId, id.ResourceGroup, id.Name) +} + +func NewVirtualWanID(resourceGroup, name string) VirtualWanId { + return VirtualWanId{ + ResourceGroup: resourceGroup, + Name: name, + } +} + +func VirtualWanID(input string) (*VirtualWanId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing Virtual Wan ID %q: %+v", input, err) + } + + vwanId := VirtualWanId{ + ResourceGroup: id.ResourceGroup, + } + + if vwanId.Name, err = id.PopSegment("virtualWans"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &vwanId, nil +} diff --git a/azurerm/internal/services/network/parse/virtual_wan_test.go b/azurerm/internal/services/network/parse/virtual_wan_test.go new file mode 100644 index 000000000000..e5d3af773074 --- /dev/null +++ b/azurerm/internal/services/network/parse/virtual_wan_test.go @@ -0,0 +1,90 @@ +package parse + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = VirtualWanId{} + +func TestVirtualWanIDFormatter(t *testing.T) { + subscriptionId := "12345678-1234-5678-1234-123456789012" + actual := NewVirtualWanID("group1", "wan1").ID(subscriptionId) + expected := "/subscriptions/12345678-1234-5678-1234-123456789012/resourceGroups/group1/providers/Microsoft.Network/virtualWans/wan1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestVirtualWanID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *VirtualWanId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/", + Error: true, + }, + { + Name: "Missing leading slash", + Input: "subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1", + Error: true, + }, + { + Name: "Malformed segments", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/foo/bar", + Error: true, + }, + { + Name: "Missing vwan segment", + Input: "subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network", + Error: true, + }, + { + Name: "Correct", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network/virtualWans/wan1", + Expect: &VirtualWanId{ + ResourceGroup: "group1", + Name: "wan1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := VirtualWanID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get") + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/network/parse/vpn_site.go b/azurerm/internal/services/network/parse/vpn_site.go new file mode 100644 index 000000000000..19e6878540ef --- /dev/null +++ b/azurerm/internal/services/network/parse/vpn_site.go @@ -0,0 +1,89 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type VpnSiteId struct { + ResourceGroup string + Name string +} + +func (id VpnSiteId) ID(subscriptionId string) string { + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/vpnSites/%s", + subscriptionId, id.ResourceGroup, id.Name) +} + +func NewVpnSiteID(resourceGroup, name string) VpnSiteId { + return VpnSiteId{ + ResourceGroup: resourceGroup, + Name: name, + } +} + +func VpnSiteID(input string) (*VpnSiteId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing Vpn Site ID %q: %+v", input, err) + } + + vpnSiteId := VpnSiteId{ + ResourceGroup: id.ResourceGroup, + } + + if vpnSiteId.Name, err = id.PopSegment("vpnSites"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &vpnSiteId, nil +} + +type VpnSiteLinkId struct { + ResourceGroup string + Site string + Name string +} + +func (id VpnSiteLinkId) ID(subscriptionId string) string { + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/vpnSites/%s/vpnSiteLinks/%s", + subscriptionId, id.ResourceGroup, id.Site, id.Name) +} + +func NewVpnSiteLinkID(vpnSiteId VpnSiteId, name string) VpnSiteLinkId { + return VpnSiteLinkId{ + ResourceGroup: vpnSiteId.ResourceGroup, + Site: vpnSiteId.Name, + Name: name, + } +} + +func VpnSiteLinkID(input string) (*VpnSiteLinkId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing Vpn Site Link ID %q: %+v", input, err) + } + + vpnSiteLinkId := VpnSiteLinkId{ + ResourceGroup: id.ResourceGroup, + } + + if vpnSiteLinkId.Site, err = id.PopSegment("vpnSites"); err != nil { + return nil, err + } + + if vpnSiteLinkId.Name, err = id.PopSegment("vpnSiteLinks"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &vpnSiteLinkId, nil +} diff --git a/azurerm/internal/services/network/parse/vpn_site_test.go b/azurerm/internal/services/network/parse/vpn_site_test.go new file mode 100644 index 000000000000..1d24dcfba2e0 --- /dev/null +++ b/azurerm/internal/services/network/parse/vpn_site_test.go @@ -0,0 +1,183 @@ +package parse + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = VpnSiteId{} + +func TestVpnSiteIDFormatter(t *testing.T) { + subscriptionId := "12345678-1234-5678-1234-123456789012" + actual := NewVpnSiteID("group1", "site1").ID(subscriptionId) + expected := "/subscriptions/12345678-1234-5678-1234-123456789012/resourceGroups/group1/providers/Microsoft.Network/vpnSites/site1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestVpnSiteID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *VpnSiteId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/", + Error: true, + }, + { + Name: "Missing leading slash", + Input: "subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1", + Error: true, + }, + { + Name: "Malformed segments", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/foo/bar", + Error: true, + }, + { + Name: "No vpn site segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network", + Error: true, + }, + { + Name: "Correct", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network/vpnSites/site1", + Expect: &VpnSiteId{ + ResourceGroup: "group1", + Name: "site1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := VpnSiteID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get") + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + } +} + +var _ resourceid.Formatter = VpnSiteLinkId{} + +func TestVpnSiteLinkIDFormatter(t *testing.T) { + subscriptionId := "12345678-1234-5678-1234-123456789012" + actual := NewVpnSiteLinkID(NewVpnSiteID("group1", "site1"), "link1").ID(subscriptionId) + expected := "/subscriptions/12345678-1234-5678-1234-123456789012/resourceGroups/group1/providers/Microsoft.Network/vpnSites/site1/vpnSiteLinks/link1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestVpnSiteLinkID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *VpnSiteLinkId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/", + Error: true, + }, + { + Name: "Missing leading slash", + Input: "subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1", + Error: true, + }, + { + Name: "Malformed segments", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/foo/bar", + Error: true, + }, + { + Name: "No vpn site segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network", + Error: true, + }, + { + Name: "No link segment", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network/vpnSites/site1", + Error: true, + }, + { + Name: "Correct", + Input: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/group1/providers/Microsoft.Network/vpnSites/site1/vpnSiteLinks/link1", + Expect: &VpnSiteLinkId{ + ResourceGroup: "group1", + Site: "site1", + Name: "link1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := VpnSiteLinkID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get") + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + + if actual.Site != v.Expect.Site { + t.Fatalf("Expected %q but got %q for Site", v.Expect.Site, actual.Site) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/network/registration.go b/azurerm/internal/services/network/registration.go index 316288e8303b..9d5fbf82c84e 100644 --- a/azurerm/internal/services/network/registration.go +++ b/azurerm/internal/services/network/registration.go @@ -115,6 +115,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_virtual_wan": resourceArmVirtualWan(), "azurerm_vpn_gateway": resourceArmVPNGateway(), "azurerm_vpn_server_configuration": resourceArmVPNServerConfiguration(), + "azurerm_vpn_site": resourceArmVpnSite(), "azurerm_web_application_firewall_policy": resourceArmWebApplicationFirewallPolicy(), } } diff --git a/azurerm/internal/services/network/tests/vpn_site_resource_test.go b/azurerm/internal/services/network/tests/vpn_site_resource_test.go new file mode 100644 index 000000000000..44656ec12c1f --- /dev/null +++ b/azurerm/internal/services/network/tests/vpn_site_resource_test.go @@ -0,0 +1,245 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMVpnSite_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_vpn_site", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMVpnSiteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVpnSite_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMVpnSite_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_vpn_site", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMVpnSiteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVpnSite_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMVpnSite_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_vpn_site", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMVpnSiteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVpnSite_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMVpnSite_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMVpnSite_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMVpnSite_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_vpn_site", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMVpnSiteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMVpnSite_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVpnSiteExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMVpnSite_requiresImport), + }, + }) +} + +func testCheckAzureRMVpnSiteExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Network.VpnSitesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Vpn Site not found: %s", resourceName) + } + + id, err := parse.VpnSiteID(rs.Primary.ID) + if err != nil { + return err + } + + if resp, err := client.Get(ctx, id.ResourceGroup, id.Name); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Vpn Site %q (Resource Group %q) does not exist", id.Name, id.ResourceGroup) + } + return fmt.Errorf("Getting on Network.VpnSites: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMVpnSiteDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Network.VpnSitesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_vpn_site" { + continue + } + + id, err := parse.VpnSiteID(rs.Primary.ID) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err == nil { + return fmt.Errorf("Network.VpnSites still exists") + } + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Getting on Network.VpnSites: %+v", err) + } + return nil + } + + return nil +} + +func testAccAzureRMVpnSite_basic(data acceptance.TestData) string { + template := testAccAzureRMVpnSite_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_vpn_site" "test" { + name = "acctest-VpnSite-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + virtual_wan_id = azurerm_virtual_wan.test.id + link { + name = "link1" + ip_address = "10.0.0.1" + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMVpnSite_complete(data acceptance.TestData) string { + template := testAccAzureRMVpnSite_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_vpn_site" "test" { + name = "acctest-VpnSite-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + virtual_wan_id = azurerm_virtual_wan.test.id + address_cidrs = ["10.0.0.0/24", "10.0.1.0/24"] + + device_vendor = "Cisco" + device_model = "foobar" + + link { + name = "link1" + provider_name = "Verizon" + speed_in_mbps = 50 + ip_address = "10.0.0.1" + bgp { + asn = 12345 + peering_address = "10.0.0.1" + } + } + + link { + name = "link2" + fqdn = "foo.com" + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMVpnSite_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMVpnSite_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_vpn_site" "import" { + name = "acctest-VpnSite-%d" + location = azurerm_vpn_site.test.location + resource_group_name = azurerm_vpn_site.test.resource_group_name + virtual_wan_id = azurerm_vpn_site.test.virtual_wan_id + link { + name = "link1" + ip_address = "10.0.0.1" + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMVpnSite_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctest-rg-%d" + location = "%s" +} + + +resource "azurerm_virtual_wan" "test" { + name = "acctest-vwan-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/network/validate/virtual_wan.go b/azurerm/internal/services/network/validate/virtual_wan.go new file mode 100644 index 000000000000..8f206bfd9093 --- /dev/null +++ b/azurerm/internal/services/network/validate/virtual_wan.go @@ -0,0 +1,22 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" +) + +func VirtualWanID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.VirtualWanID(v); err != nil { + errors = append(errors, fmt.Errorf("parsing %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/network/validate/vpn_site.go b/azurerm/internal/services/network/validate/vpn_site.go new file mode 100644 index 000000000000..bf81d92e9815 --- /dev/null +++ b/azurerm/internal/services/network/validate/vpn_site.go @@ -0,0 +1,28 @@ +package validate + +import ( + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" +) + +func VpnSiteID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.VpnSiteID(v); err != nil { + errors = append(errors, fmt.Errorf("parsing %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} + +func VpnSiteName() func(i interface{}, k string) (warnings []string, errors []error) { + return validation.StringMatch(regexp.MustCompile(`^[^'<>%&:?/+]+$`), "The value must not contain characters from '<>%&:?/+.") +} diff --git a/azurerm/internal/services/network/vpn_site_resource.go b/azurerm/internal/services/network/vpn_site_resource.go new file mode 100644 index 000000000000..44a697840ea5 --- /dev/null +++ b/azurerm/internal/services/network/vpn_site_resource.go @@ -0,0 +1,454 @@ +package network + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/go-azure-helpers/response" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-05-01/network" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmVpnSite() *schema.Resource { + return &schema.Resource{ + Create: resourceArmVpnSiteCreateUpdate, + Read: resourceArmVpnSiteRead, + Update: resourceArmVpnSiteCreateUpdate, + Delete: resourceArmVpnSiteDelete, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.VpnSiteID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.VpnSiteName(), + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": location.Schema(), + + "virtual_wan_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.VirtualWanID, + }, + + "address_cidrs": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsCIDR, + }, + }, + + "device_vendor": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "device_model": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "link": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "provider_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "speed_in_mbps": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Default: 0, + }, + "ip_address": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsIPAddress, + }, + "fqdn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "bgp": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "asn": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 4294967295), + }, + "peering_address": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsIPAddress, + }, + }, + }, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmVpnSiteCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Network.VpnSitesClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + location := azure.NormalizeLocation(d.Get("location").(string)) + + if d.IsNewResource() { + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("checking for existing Vpn Site %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + + if resp.ID != nil && *resp.ID != "" { + return tf.ImportAsExistsError("azurerm_vpn_site", *resp.ID) + } + } + + param := network.VpnSite{ + Name: &name, + Location: &location, + VpnSiteProperties: &network.VpnSiteProperties{ + VirtualWan: &network.SubResource{ID: utils.String(d.Get("virtual_wan_id").(string))}, + DeviceProperties: expandArmVpnSiteDeviceProperties(d), + AddressSpace: expandArmVpnSiteAddressSpace(d.Get("address_cidrs").(*schema.Set).List()), + VpnSiteLinks: expandArmVpnSiteLinks(d.Get("link").([]interface{})), + }, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, param) + if err != nil { + return fmt.Errorf("creating %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for creation of %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("retrieving Vpn Site %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("empty or nil ID returned for Vpn Site %q (Resource Group %q) ID", name, resourceGroup) + } + + id, err := parse.VpnSiteID(*resp.ID) + if err != nil { + return err + } + d.SetId(id.ID(subscriptionId)) + + return resourceArmVpnSiteRead(d, meta) +} + +func resourceArmVpnSiteRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Network.VpnSitesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.VpnSiteID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Vpn Site %q was not found in Resource Group %q - removing from state!", id.Name, id.ResourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("retrieving Vpn Site %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + d.Set("name", id.Name) + d.Set("resource_group_name", id.ResourceGroup) + + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if prop := resp.VpnSiteProperties; prop != nil { + if deviceProp := prop.DeviceProperties; deviceProp != nil { + d.Set("device_vendor", deviceProp.DeviceVendor) + d.Set("device_model", deviceProp.DeviceModel) + } + if prop.VirtualWan != nil { + d.Set("virtual_wan_id", prop.VirtualWan.ID) + } + if err := d.Set("address_cidrs", flattenArmVpnSiteAddressSpace(prop.AddressSpace)); err != nil { + return fmt.Errorf("setting `address_cidrs`") + } + if err := d.Set("link", flattenArmVpnSiteLinks(prop.VpnSiteLinks)); err != nil { + return fmt.Errorf("setting `link`") + } + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmVpnSiteDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Network.VpnSitesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.VpnSiteID(d.Id()) + if err != nil { + return err + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.Name) + if err != nil { + return fmt.Errorf("deleting Vpn Site %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("waiting for deleting Vpn Site %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + } + + return nil +} + +func expandArmVpnSiteDeviceProperties(d *schema.ResourceData) *network.DeviceProperties { + vendor, model := d.Get("device_vendor").(string), d.Get("device_model").(string) + if vendor == "" && model == "" { + return nil + } + output := &network.DeviceProperties{} + if vendor != "" { + output.DeviceVendor = &vendor + } + if model != "" { + output.DeviceModel = &model + } + + return output +} + +func expandArmVpnSiteAddressSpace(input []interface{}) *network.AddressSpace { + if len(input) == 0 { + return nil + } + + addressPrefixes := []string{} + for _, addr := range input { + addressPrefixes = append(addressPrefixes, addr.(string)) + } + + return &network.AddressSpace{ + AddressPrefixes: &addressPrefixes, + } +} + +func flattenArmVpnSiteAddressSpace(input *network.AddressSpace) []interface{} { + if input == nil { + return nil + } + return utils.FlattenStringSlice(input.AddressPrefixes) +} + +func expandArmVpnSiteLinks(input []interface{}) *[]network.VpnSiteLink { + if len(input) == 0 { + return nil + } + + result := make([]network.VpnSiteLink, 0) + for _, e := range input { + if e == nil { + continue + } + e := e.(map[string]interface{}) + link := network.VpnSiteLink{ + Name: utils.String(e["name"].(string)), + VpnSiteLinkProperties: &network.VpnSiteLinkProperties{ + LinkProperties: &network.VpnLinkProviderProperties{ + LinkSpeedInMbps: utils.Int32(int32(e["speed_in_mbps"].(int))), + }, + }, + } + + if v, ok := e["provider_name"]; ok { + link.VpnSiteLinkProperties.LinkProperties.LinkProviderName = utils.String(v.(string)) + } + if v, ok := e["ip_address"]; ok { + link.VpnSiteLinkProperties.IPAddress = utils.String(v.(string)) + } + if v, ok := e["fqdn"]; ok { + link.VpnSiteLinkProperties.Fqdn = utils.String(v.(string)) + } + if v, ok := e["bgp"]; ok { + link.VpnSiteLinkProperties.BgpProperties = expandArmVpnSiteVpnLinkBgpSettings(v.([]interface{})) + } + + result = append(result, link) + } + + return &result +} + +func flattenArmVpnSiteLinks(input *[]network.VpnSiteLink) []interface{} { + if input == nil { + return nil + } + + output := make([]interface{}, 0) + + for _, e := range *input { + var name string + if e.Name != nil { + name = *e.Name + } + + var id string + if e.ID != nil { + id = *e.ID + } + + var ( + ipAddress string + fqdn string + linkProviderName string + linkSpeed int + bgpProperty []interface{} + ) + + if prop := e.VpnSiteLinkProperties; prop != nil { + if prop.IPAddress != nil { + ipAddress = *prop.IPAddress + } + + if prop.Fqdn != nil { + fqdn = *prop.Fqdn + } + + if linkProp := prop.LinkProperties; linkProp != nil { + if linkProp.LinkProviderName != nil { + linkProviderName = *linkProp.LinkProviderName + } + if linkProp.LinkSpeedInMbps != nil { + linkSpeed = int(*linkProp.LinkSpeedInMbps) + } + } + + bgpProperty = flattenArmVpnSiteVpnSiteBgpSettings(prop.BgpProperties) + } + + link := map[string]interface{}{ + "name": name, + "id": id, + "provider_name": linkProviderName, + "speed_in_mbps": linkSpeed, + "ip_address": ipAddress, + "fqdn": fqdn, + "bgp": bgpProperty, + } + + output = append(output, link) + } + + return output +} + +func expandArmVpnSiteVpnLinkBgpSettings(input []interface{}) *network.VpnLinkBgpSettings { + if len(input) == 0 || input[0] == nil { + return nil + } + + v := input[0].(map[string]interface{}) + + return &network.VpnLinkBgpSettings{ + Asn: utils.Int64(int64(v["asn"].(int))), + BgpPeeringAddress: utils.String(v["peering_address"].(string)), + } +} + +func flattenArmVpnSiteVpnSiteBgpSettings(input *network.VpnLinkBgpSettings) []interface{} { + if input == nil { + return nil + } + + var asn int + if input.Asn != nil { + asn = int(*input.Asn) + } + + var peerAddress string + if input.BgpPeeringAddress != nil { + peerAddress = *input.BgpPeeringAddress + } + + return []interface{}{ + map[string]interface{}{ + "asn": asn, + "peering_address": peerAddress, + }, + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 36f5b8fd48ac..ead2ffd74191 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -2509,6 +2509,10 @@ azurerm_vpn_server_configuration +
  • + azurerm_vpn_site +
  • +
  • azurerm_web_application_firewall_policy
  • diff --git a/website/docs/r/vpn_site.html.markdown b/website/docs/r/vpn_site.html.markdown new file mode 100644 index 000000000000..73511f550001 --- /dev/null +++ b/website/docs/r/vpn_site.html.markdown @@ -0,0 +1,123 @@ +--- +subcategory: "Network" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_vpn_site" +description: |- + Manages a VPN Site. +--- + +# azurerm_vpn_site + +Manages a VPN Site. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "West Europe" +} + +resource "azurerm_virtual_wan" "example" { + name = "example-vwan" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location +} + +resource "azurerm_vpn_site" "example" { + name = "site1" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + virtual_wan_id = azurerm_virtual_wan.example.id + + link { + name = "link1" + ip_address = "10.0.0.1" + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `location` - (Required) The Azure Region where the VPN Site should exist. Changing this forces a new VPN Site to be created. + +* `name` - (Required) The name which should be used for this VPN Site. Changing this forces a new VPN Site to be created. + +* `resource_group_name` - (Required) The name of the Resource Group where the VPN Site should exist. Changing this forces a new VPN Site to be created. + +* `virtual_wan_id` - (Required) The ID of the Virtual Wan where this VPN site resides in. Changing this forces a new VPN Site to be created. + +* `link` - (Required) One or more `link` blocks as defined below. + +--- + +* `address_cidrs` - (Optional) Specifies a list of IP address CIDRs that are located on your on-premises site. Traffic destined for these address spaces is routed to your local site. + +* `device_model` - (Optional) The model of the VPN device. + +* `device_vendor` - (Optional) The name of the VPN device vendor. + +* `tags` - (Optional) A mapping of tags which should be assigned to the VPN Site. + +--- + +A `bgp` block supports the following: + +* `asn` - (Required) The BGP speaker's ASN. + +* `peering_address` - (Required) The BGP peering ip address. + +--- + +A `link` block supports the following: + +* `name` - (Required) The name which should be used for this VPN Site Link. + +* `bgp` - (Optional) A `bgp` block as defined above. + +* `fqdn` - (Optional) The FQDN of this VPN Site Link. + +* `ip_address` - (Optional) The IP address of this VPN Site Link. + +-> **NOTE**: Either `fqdn` or `ip_address` should be specified. + +* `provider_name` - (Optional) The name of the physical link at the VPN Site. Example: `ATT`, `Verizon`. + +* `speed_in_mbps` - (Optional) The speed of the VPN device at the branch location in unit of mbps. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the VPN Site. + +* `link` - One or more `link` blocks as defined below. + +--- + +A `link` block supports the following: + +* `id` - The ID of the VPN Site Link. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the VPN Site. +* `read` - (Defaults to 5 minutes) Used when retrieving the VPN Site. +* `update` - (Defaults to 30 minutes) Used when updating the VPN Site. +* `delete` - (Defaults to 30 minutes) Used when deleting the VPN Site. + +## Import + +VPN Sites can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_vpn_site.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/vpnSites/site1 +```