diff --git a/azurerm/data_source_public_ip.go b/azurerm/data_source_public_ip.go index 7164e6785d3f..fe469626c1a3 100644 --- a/azurerm/data_source_public_ip.go +++ b/azurerm/data_source_public_ip.go @@ -20,6 +20,11 @@ func dataSourceArmPublicIP() *schema.Resource { "resource_group_name": resourceGroupNameForDataSourceSchema(), + "ip_version": { + Type: schema.TypeString, + Computed: true, + }, + "domain_name_label": { Type: schema.TypeString, Computed: true, @@ -73,6 +78,10 @@ func dataSourceArmPublicIPRead(d *schema.ResourceData, meta interface{}) error { } } + if ipVersion := props.PublicIPAddressVersion; string(ipVersion) != "" { + d.Set("ip_version", string(ipVersion)) + } + if v := props.IPAddress; v != nil && *v != "" { d.Set("ip_address", v) } diff --git a/azurerm/data_source_public_ip_test.go b/azurerm/data_source_public_ip_test.go index 07899ea236b5..c5c2ca929524 100644 --- a/azurerm/data_source_public_ip_test.go +++ b/azurerm/data_source_public_ip_test.go @@ -31,6 +31,7 @@ func TestAccDataSourceAzureRMPublicIP_basic(t *testing.T) { resource.TestCheckResourceAttr(dataSourceName, "idle_timeout_in_minutes", "30"), resource.TestCheckResourceAttrSet(dataSourceName, "fqdn"), resource.TestCheckResourceAttrSet(dataSourceName, "ip_address"), + resource.TestCheckResourceAttr(dataSourceName, "ip_version", "IPv4"), resource.TestCheckResourceAttr(dataSourceName, "tags.%", "1"), resource.TestCheckResourceAttr(dataSourceName, "tags.environment", "test"), ), diff --git a/azurerm/helpers/validate/network.go b/azurerm/helpers/validate/network.go index 609a23a33ae3..3a38c2177e38 100644 --- a/azurerm/helpers/validate/network.go +++ b/azurerm/helpers/validate/network.go @@ -5,6 +5,30 @@ import ( "net" ) +func IPv6Address(i interface{}, k string) (_ []string, errors []error) { + return validateIpv6Address(i, k, false) +} + +func validateIpv6Address(i interface{}, k string, allowEmpty bool) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if v == "" && allowEmpty { + return + } + + ip := net.ParseIP(v) + if six := ip.To16(); six == nil { + errors = append(errors, fmt.Errorf("%q is not a valid IPv6 address: %q", k, v)) + } + + return + +} + func IPv4Address(i interface{}, k string) (_ []string, errors []error) { return validateIpv4Address(i, k, false) } @@ -26,7 +50,7 @@ func validateIpv4Address(i interface{}, k string, allowEmpty bool) (_ []string, ip := net.ParseIP(v) if four := ip.To4(); four == nil { - errors = append(errors, fmt.Errorf("%q is not a valid IP4 address: %q", k, v)) + errors = append(errors, fmt.Errorf("%q is not a valid IPv4 address: %q", k, v)) } return diff --git a/azurerm/helpers/validate/network_test.go b/azurerm/helpers/validate/network_test.go index 3911319b0b77..0c59bb15c756 100644 --- a/azurerm/helpers/validate/network_test.go +++ b/azurerm/helpers/validate/network_test.go @@ -5,6 +5,57 @@ import ( "testing" ) +func TestIPv6Address(t *testing.T) { + cases := []struct { + IP string + Errors int + }{ + { + IP: "", + Errors: 1, + }, + { + IP: "0.0.0.0", + Errors: 0, + }, + { + IP: "not:a:real:address:1:2:3:4", + Errors: 1, + }, + { + IP: "text", + Errors: 1, + }, + { + IP: "::", + Errors: 0, + }, + { + IP: "0:0:0:0:0:0:0:0", + Errors: 0, + }, + { + IP: "2001:0db8:85a3:0:0:8a2e:0370:7334", + Errors: 0, + }, + { + IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Errors: 0, + }, + } + + for _, tc := range cases { + t.Run(tc.IP, func(t *testing.T) { + _, errors := IPv6Address(tc.IP, "test") + + if len(errors) != tc.Errors { + t.Fatalf("Expected IPv6Address to return %d error(s) not %d", tc.Errors, len(errors)) + } + }) + } + +} + func TestIPv4Address(t *testing.T) { cases := []struct { IP string diff --git a/azurerm/resource_arm_public_ip.go b/azurerm/resource_arm_public_ip.go index 846338adaac9..84716ac742b3 100644 --- a/azurerm/resource_arm_public_ip.go +++ b/azurerm/resource_arm_public_ip.go @@ -2,11 +2,12 @@ package azurerm import ( "fmt" - "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" "log" "regexp" "strings" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-04-01/network" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" @@ -60,6 +61,18 @@ func resourceArmPublicIp() *schema.Resource { }, true), }, + "ip_version": { + Type: schema.TypeString, + Optional: true, + Default: string(network.IPv4), + ForceNew: true, + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: validation.StringInSlice([]string{ + string(network.IPv4), + string(network.IPv6), + }, true), + }, + "sku": { Type: schema.TypeString, Optional: true, @@ -122,6 +135,13 @@ func resourceArmPublicIpCreate(d *schema.ResourceData, meta interface{}) error { idleTimeout := d.Get("idle_timeout_in_minutes").(int) ipAllocationMethod := network.IPAllocationMethod(d.Get("public_ip_address_allocation").(string)) + ipVersion := network.IPVersion(d.Get("ip_version").(string)) + + if strings.EqualFold(string(ipVersion), string(network.IPv6)) { + if strings.EqualFold(string(ipAllocationMethod), "static") { + return fmt.Errorf("Cannot specify publicIpAllocationMethod as Static for IPv6 PublicIp") + } + } if strings.ToLower(string(sku.Name)) == "standard" { if strings.ToLower(string(ipAllocationMethod)) != "static" { @@ -135,6 +155,7 @@ func resourceArmPublicIpCreate(d *schema.ResourceData, meta interface{}) error { Sku: &sku, PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ PublicIPAllocationMethod: ipAllocationMethod, + PublicIPAddressVersion: ipVersion, IdleTimeoutInMinutes: utils.Int32(int32(idleTimeout)), }, Tags: expandTags(tags), @@ -218,6 +239,7 @@ func resourceArmPublicIpRead(d *schema.ResourceData, meta interface{}) error { if props := resp.PublicIPAddressPropertiesFormat; props != nil { d.Set("public_ip_address_allocation", strings.ToLower(string(props.PublicIPAllocationMethod))) + d.Set("ip_version", strings.ToLower(string(props.PublicIPAddressVersion))) if settings := props.DNSSettings; settings != nil { d.Set("fqdn", settings.Fqdn) diff --git a/azurerm/resource_arm_public_ip_test.go b/azurerm/resource_arm_public_ip_test.go index 6fc922833be2..05902fdd4781 100644 --- a/azurerm/resource_arm_public_ip_test.go +++ b/azurerm/resource_arm_public_ip_test.go @@ -71,6 +71,34 @@ func TestAccAzureRMPublicIpStatic_basic(t *testing.T) { }) } +func TestAccAzureRMPublicIpStatic_zones(t *testing.T) { + resourceName := "azurerm_public_ip.test" + ri := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPublicIpDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPublicIPStatic_withZone(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPublicIpExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "ip_address"), + resource.TestCheckResourceAttr(resourceName, "public_ip_address_allocation", "static"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "1"), + resource.TestCheckResourceAttr(resourceName, "zones.0", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAzureRMPublicIpStatic_basic_withDNSLabel(t *testing.T) { resourceName := "azurerm_public_ip.test" ri := acctest.RandInt() @@ -100,6 +128,103 @@ func TestAccAzureRMPublicIpStatic_basic_withDNSLabel(t *testing.T) { }) } +func TestAccAzureRMPublicIpStatic_standard_withIPv6_fails(t *testing.T) { + ri := acctest.RandInt() + config := testAccAzureRMPublicIPStatic_standard_withIPVersion(ri, testLocation(), "IPv6") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPublicIpDestroy, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("Cannot specify publicIpAllocationMethod as Static for IPv6 PublicIp"), + }, + }, + }) +} + +func TestAccAzureRMPublicIpDynamic_basic_withIPv6(t *testing.T) { + resourceName := "azurerm_public_ip.test" + ri := acctest.RandInt() + ipVersion := "Ipv6" + config := testAccAzureRMPublicIPDynamic_basic_withIPVersion(ri, testLocation(), ipVersion) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPublicIpDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPublicIpExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ip_version", "IPv6"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + +} + +func TestAccAzureRMPublicIpStatic_basic_defaultsToIPv4(t *testing.T) { + resourceName := "azurerm_public_ip.test" + ri := acctest.RandInt() + config := testAccAzureRMPublicIPStatic_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPublicIpDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPublicIpExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ip_version", "ipv4"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} +func TestAccAzureRMPublicIpStatic_basic_withIPv4(t *testing.T) { + resourceName := "azurerm_public_ip.test" + ri := acctest.RandInt() + ipVersion := "IPv4" + config := testAccAzureRMPublicIPStatic_basic_withIPVersion(ri, testLocation(), ipVersion) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPublicIpDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPublicIpExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ip_version", "ipv4"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAzureRMPublicIpStatic_standard(t *testing.T) { resourceName := "azurerm_public_ip.test" ri := acctest.RandInt() @@ -392,7 +517,7 @@ resource "azurerm_public_ip" "test" { `, rInt, location, rInt) } -func testAccAzureRMPublicIPStatic_basic_withZone(rInt int, location string) string { +func testAccAzureRMPublicIPStatic_withZone(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { name = "acctestRG-%d" @@ -428,6 +553,24 @@ resource "azurerm_public_ip" "test" { `, rInt, location, rInt, dnsNameLabel) } +func testAccAzureRMPublicIPStatic_basic_withIPVersion(rInt int, location string, ipVersion string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_public_ip" "test" { + name = "acctestpublicip-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + public_ip_address_allocation = "static" + + ip_version = "%s" +} +`, rInt, location, rInt, ipVersion) +} + func testAccAzureRMPublicIPStatic_standard(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { @@ -445,6 +588,24 @@ resource "azurerm_public_ip" "test" { `, rInt, location, rInt) } +func testAccAzureRMPublicIPStatic_standard_withIPVersion(rInt int, location string, ipVersion string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_public_ip" "test" { + name = "acctestpublicip-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + public_ip_address_allocation = "static" + ip_version = "%s" + sku = "standard" +} +`, rInt, location, rInt, ipVersion) +} + func testAccAzureRMPublicIPStatic_update(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { @@ -495,6 +656,24 @@ resource "azurerm_public_ip" "test" { `, rInt, location, rInt) } +func testAccAzureRMPublicIPDynamic_basic_withIPVersion(rInt int, location string, ipVersion string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_public_ip" "test" { + name = "acctestpublicip-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + public_ip_address_allocation = "dynamic" + + ip_version = "%s" +} +`, rInt, location, rInt, ipVersion) +} + func testAccAzureRMPublicIPStatic_withTags(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { diff --git a/website/docs/d/public_ip.html.markdown b/website/docs/d/public_ip.html.markdown index 37f244b7c553..c1054f09d011 100644 --- a/website/docs/d/public_ip.html.markdown +++ b/website/docs/d/public_ip.html.markdown @@ -107,4 +107,5 @@ output "public_ip_address" { * `idle_timeout_in_minutes` - Specifies the timeout for the TCP idle connection. * `fqdn` - Fully qualified domain name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone. * `ip_address` - The IP address value that was allocated. +* `ip_version` - The IP version being used, for example `IPv4` or `IPv6`. * `tags` - A mapping of tags to assigned to the resource. diff --git a/website/docs/r/public_ip.html.markdown b/website/docs/r/public_ip.html.markdown index b832c8726bfc..8f5df3f4d488 100644 --- a/website/docs/r/public_ip.html.markdown +++ b/website/docs/r/public_ip.html.markdown @@ -52,6 +52,10 @@ The following arguments are supported: ~> **Note** `Dynamic` Public IP Addresses aren't allocated until they're assigned to a resource (such as a Virtual Machine or a Load Balancer) by design within Azure - [more information is available below](#ip_address). +* `ip_version` - (Optional) The IP Version to use, IPv6 or IPv4. + +-> **Note** Only `dynamic` IP address allocation is supported for IPv6. + * `idle_timeout_in_minutes` - (Optional) Specifies the timeout for the TCP idle connection. The value can be set between 4 and 30 minutes. * `domain_name_label` - (Optional) Label for the Domain Name. Will be used to make up the FQDN. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system.