Skip to content

Commit

Permalink
azurerm_dns_zone & azurerm_private_dns_zone - support for the `soa_re…
Browse files Browse the repository at this point in the history
…cord` block (#9319)

API doc for DNS SOA Record: docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate#soarecord
API doc for Private DNS SOA Record: docs.microsoft.com/en-us/rest/api/dns/privatedns/recordsets/createorupdate#soarecord

DNS SOA Record and Private DNS SOA Record cannot be made as separate resource because they aren't allowed to be created and deleted by API. So they have to be embedded into dns zone resource and private dns zone resource.
See more details from below links:

docs.microsoft.com/en-us/rest/api/dns/privatedns/recordsets/delete#recordtype
docs.microsoft.com/en-us/rest/api/dns/privatedns/recordsets/createorupdate#recordtype
docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate#recordtype
docs.microsoft.com/en-us/rest/api/dns/recordsets/delete#recordtype
  • Loading branch information
Neil Ye authored Nov 19, 2020
1 parent 3edbbc1 commit a0155e2
Show file tree
Hide file tree
Showing 10 changed files with 829 additions and 0 deletions.
190 changes: 190 additions & 0 deletions azurerm/internal/services/dns/dns_zone_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package dns

import (
"fmt"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
"github.com/hashicorp/go-azure-helpers/response"
"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/azure"
"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/services/dns/parse"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/dns/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"
Expand Down Expand Up @@ -61,13 +64,86 @@ func resourceArmDnsZone() *schema.Resource {
Set: schema.HashString,
},

"soa_record": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.DnsZoneSOARecordEmail,
},

"host_name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},

"expire_time": {
Type: schema.TypeInt,
Optional: true,
Default: 2419200,
ValidateFunc: validation.IntAtLeast(0),
},

"minimum_ttl": {
Type: schema.TypeInt,
Optional: true,
Default: 300,
ValidateFunc: validation.IntAtLeast(0),
},

"refresh_time": {
Type: schema.TypeInt,
Optional: true,
Default: 3600,
ValidateFunc: validation.IntAtLeast(0),
},

"retry_time": {
Type: schema.TypeInt,
Optional: true,
Default: 300,
ValidateFunc: validation.IntAtLeast(0),
},

"serial_number": {
Type: schema.TypeInt,
Optional: true,
Default: 1,
ValidateFunc: validation.IntAtLeast(0),
},

"ttl": {
Type: schema.TypeInt,
Optional: true,
Default: 3600,
ValidateFunc: validation.IntBetween(0, 2147483647),
},

"tags": tags.Schema(),

"fqdn": {
Type: schema.TypeString,
Computed: true,
},
},
},
},

"tags": tags.Schema(),
},
}
}

func resourceArmDnsZoneCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Dns.ZonesClient
recordSetsClient := meta.(*clients.Client).Dns.RecordSetsClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand Down Expand Up @@ -106,6 +182,25 @@ func resourceArmDnsZoneCreateUpdate(d *schema.ResourceData, meta interface{}) er
return fmt.Errorf("Error retrieving DNS Zone %q (Resource Group %q): %s", name, resGroup, err)
}

if v, ok := d.GetOk("soa_record"); ok {
soaRecord := v.([]interface{})[0].(map[string]interface{})
rsParameters := dns.RecordSet{
RecordSetProperties: &dns.RecordSetProperties{
TTL: utils.Int64(int64(soaRecord["ttl"].(int))),
Metadata: tags.Expand(soaRecord["tags"].(map[string]interface{})),
SoaRecord: expandArmDNSZoneSOARecord(soaRecord),
},
}

if len(name+strings.TrimSuffix(*rsParameters.RecordSetProperties.SoaRecord.Email, ".")) > 253 {
return fmt.Errorf("`email` which is concatenated with DNS Zone `name` cannot exceed 253 characters excluding a trailing period")
}

if _, err := recordSetsClient.CreateOrUpdate(ctx, resGroup, name, "@", dns.SOA, rsParameters, etag, ifNoneMatch); err != nil {
return fmt.Errorf("creating/updating DNS SOA Record @ (Zone %q / Resource Group %q): %s", name, resGroup, err)
}
}

if resp.ID == nil {
return fmt.Errorf("Cannot read DNS Zone %q (Resource Group %q) ID", name, resGroup)
}
Expand All @@ -117,6 +212,7 @@ func resourceArmDnsZoneCreateUpdate(d *schema.ResourceData, meta interface{}) er

func resourceArmDnsZoneRead(d *schema.ResourceData, meta interface{}) error {
zonesClient := meta.(*clients.Client).Dns.ZonesClient
recordSetsClient := meta.(*clients.Client).Dns.RecordSetsClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand Down Expand Up @@ -148,6 +244,15 @@ func resourceArmDnsZoneRead(d *schema.ResourceData, meta interface{}) error {
return err
}

rsResp, err := recordSetsClient.Get(ctx, id.ResourceGroup, id.Name, "@", dns.SOA)
if err != nil {
return fmt.Errorf("reading DNS SOA record @: %v", err)
}

if err := d.Set("soa_record", flattenArmDNSZoneSOARecord(&rsResp)); err != nil {
return fmt.Errorf("setting `soa_record`: %+v", err)
}

return tags.FlattenAndSet(d, resp.Tags)
}

Expand Down Expand Up @@ -179,3 +284,88 @@ func resourceArmDnsZoneDelete(d *schema.ResourceData, meta interface{}) error {

return nil
}

func expandArmDNSZoneSOARecord(input map[string]interface{}) *dns.SoaRecord {
return &dns.SoaRecord{
Email: utils.String(input["email"].(string)),
Host: utils.String(input["host_name"].(string)),
ExpireTime: utils.Int64(int64(input["expire_time"].(int))),
MinimumTTL: utils.Int64(int64(input["minimum_ttl"].(int))),
RefreshTime: utils.Int64(int64(input["refresh_time"].(int))),
RetryTime: utils.Int64(int64(input["retry_time"].(int))),
SerialNumber: utils.Int64(int64(input["serial_number"].(int))),
}
}

func flattenArmDNSZoneSOARecord(input *dns.RecordSet) []interface{} {
if input == nil {
return make([]interface{}, 0)
}

ttl := 0
if input.TTL != nil {
ttl = int(*input.TTL)
}

metaData := make(map[string]interface{})
if input.Metadata != nil {
metaData = tags.Flatten(input.Metadata)
}

fqdn := ""
if input.Fqdn != nil {
fqdn = *input.Fqdn
}

email := ""
hostName := ""
expireTime := 0
minimumTTL := 0
refreshTime := 0
retryTime := 0
serialNumber := 0
if input.SoaRecord != nil {
if input.SoaRecord.Email != nil {
email = *input.SoaRecord.Email
}

if input.SoaRecord.Host != nil {
hostName = *input.SoaRecord.Host
}

if input.SoaRecord.ExpireTime != nil {
expireTime = int(*input.SoaRecord.ExpireTime)
}

if input.SoaRecord.MinimumTTL != nil {
minimumTTL = int(*input.SoaRecord.MinimumTTL)
}

if input.SoaRecord.RefreshTime != nil {
refreshTime = int(*input.SoaRecord.RefreshTime)
}

if input.SoaRecord.RetryTime != nil {
retryTime = int(*input.SoaRecord.RetryTime)
}

if input.SoaRecord.SerialNumber != nil {
serialNumber = int(*input.SoaRecord.SerialNumber)
}
}

return []interface{}{
map[string]interface{}{
"email": email,
"host_name": hostName,
"expire_time": expireTime,
"minimum_ttl": minimumTTL,
"refresh_time": refreshTime,
"retry_time": retryTime,
"serial_number": serialNumber,
"ttl": ttl,
"tags": metaData,
"fqdn": fqdn,
},
}
}
89 changes: 89 additions & 0 deletions azurerm/internal/services/dns/tests/dns_zone_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,39 @@ func TestAccAzureRMDnsZone_withTags(t *testing.T) {
})
}

func TestAccAzureRMDnsZone_withSOARecord(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_dns_zone", "test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
Providers: acceptance.SupportedProviders,
CheckDestroy: testCheckAzureRMDnsZoneDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureRMDnsZone_withBasicSOARecord(data),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMDnsZoneExists(data.ResourceName),
),
},
data.ImportStep(),
{
Config: testAccAzureRMDnsZone_withCompletedSOARecord(data),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMDnsZoneExists(data.ResourceName),
),
},
data.ImportStep(),
{
Config: testAccAzureRMDnsZone_withBasicSOARecord(data),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMDnsZoneExists(data.ResourceName),
),
},
data.ImportStep(),
},
})
}

func testCheckAzureRMDnsZoneExists(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := acceptance.AzureProvider.Meta().(*clients.Client).Dns.ZonesClient
Expand Down Expand Up @@ -212,3 +245,59 @@ resource "azurerm_dns_zone" "test" {
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func testAccAzureRMDnsZone_withBasicSOARecord(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
name = "acctestRG-dns-%d"
location = "%s"
}
resource "azurerm_dns_zone" "test" {
name = "acctestzone%d.com"
resource_group_name = azurerm_resource_group.test.name
soa_record {
email = "testemail.com"
host_name = "testhost.contoso.com"
}
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func testAccAzureRMDnsZone_withCompletedSOARecord(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
name = "acctestRG-dns-%d"
location = "%s"
}
resource "azurerm_dns_zone" "test" {
name = "acctestzone%d.com"
resource_group_name = azurerm_resource_group.test.name
soa_record {
email = "testemail.com"
host_name = "testhost.contoso.com"
expire_time = 2419200
minimum_ttl = 200
refresh_time = 2600
retry_time = 200
serial_number = 1
ttl = 100
tags = {
ENv = "Test"
}
}
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}
41 changes: 41 additions & 0 deletions azurerm/internal/services/dns/validate/dns_zone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package validate

import (
"fmt"
"regexp"
"strings"
)

func DnsZoneSOARecordEmail(v interface{}, k string) (warnings []string, errors []error) {
value := v.(string)

if len(value) == 0 {
errors = append(errors, fmt.Errorf("%q cannot be an empty string: %q", k, v))
return warnings, errors
}

vSegments := strings.Split(value, ".")
if len(vSegments) < 2 || len(vSegments) > 34 {
errors = append(errors, fmt.Errorf("%q must be between 2 and 34 segments", k))
return warnings, errors
}

for _, segment := range vSegments {
if segment == "" {
errors = append(errors, fmt.Errorf("%q cannot contain consecutive period", k))
return warnings, errors
}

if len(segment) > 63 {
errors = append(errors, fmt.Errorf("the each segment of the `email` must contain between 1 and 63 characters"))
return warnings, errors
}
}

if !regexp.MustCompile(`^[a-zA-Z\d._-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf("%q only contains letters, numbers, underscores, dashes and periods", k))
return warnings, errors
}

return warnings, errors
}
Loading

0 comments on commit a0155e2

Please sign in to comment.