diff --git a/azurerm/provider.go b/azurerm/provider.go index f01044a9c91c..4f0af9d4a978 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -374,6 +374,7 @@ func Provider() terraform.ResourceProvider { "azurerm_private_dns_zone": resourceArmPrivateDnsZone(), "azurerm_private_dns_a_record": resourceArmPrivateDnsARecord(), "azurerm_private_dns_cname_record": resourceArmPrivateDnsCNameRecord(), + "azurerm_private_dns_ptr_record": resourceArmPrivateDnsPtrRecord(), "azurerm_private_dns_zone_virtual_network_link": resourceArmPrivateDnsZoneVirtualNetworkLink(), "azurerm_proximity_placement_group": resourceArmProximityPlacementGroup(), "azurerm_public_ip": resourceArmPublicIp(), diff --git a/azurerm/resource_arm_private_dns_ptr_record.go b/azurerm/resource_arm_private_dns_ptr_record.go new file mode 100644 index 000000000000..f2d0ad2534a4 --- /dev/null +++ b/azurerm/resource_arm_private_dns_ptr_record.go @@ -0,0 +1,212 @@ +package azurerm + +import ( + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "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/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmPrivateDnsPtrRecord() *schema.Resource { + return &schema.Resource{ + Create: resourceArmPrivateDnsPtrRecordCreateUpdate, + Read: resourceArmPrivateDnsPtrRecordRead, + Update: resourceArmPrivateDnsPtrRecordCreateUpdate, + Delete: resourceArmPrivateDnsPtrRecordDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + 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.NoEmptyStrings, + }, + + // TODO: make this case sensitive once the API's fixed https://github.com/Azure/azure-rest-api-specs/issues/6641 + "resource_group_name": azure.SchemaResourceGroupNameDiffSuppress(), + + "zone_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "records": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "ttl": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 2147483647), + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmPrivateDnsPtrRecordCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*ArmClient).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + zoneName := d.Get("zone_name").(string) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.Get(ctx, resGroup, zoneName, privatedns.PTR, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Private DNS PTR Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_private_dns_ptr_record", *existing.ID) + } + } + + ttl := int64(d.Get("ttl").(int)) + t := d.Get("tags").(map[string]interface{}) + + parameters := privatedns.RecordSet{ + Name: &name, + RecordSetProperties: &privatedns.RecordSetProperties{ + Metadata: tags.Expand(t), + TTL: &ttl, + PtrRecords: expandAzureRmPrivateDnsPtrRecords(d), + }, + } + + eTag := "" + ifNoneMatch := "" // set to empty to allow updates to records after creation + if _, err := client.CreateOrUpdate(ctx, resGroup, zoneName, privatedns.PTR, name, parameters, eTag, ifNoneMatch); err != nil { + return fmt.Errorf("Error creating/updating Private DNS PTR Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + resp, err := client.Get(ctx, resGroup, zoneName, privatedns.PTR, name) + if err != nil { + return fmt.Errorf("Error retrieving Private DNS PTR Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + if resp.ID == nil { + return fmt.Errorf("Cannot read Private DNS PTR Record %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*resp.ID) + + return resourceArmPrivateDnsPtrRecordRead(d, meta) +} + +func resourceArmPrivateDnsPtrRecordRead(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*ArmClient).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForRead(meta.(*ArmClient).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["PTR"] + zoneName := id.Path["privateDnsZones"] + + resp, err := dnsClient.Get(ctx, resGroup, zoneName, privatedns.PTR, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Private DNS PTR record %s: %+v", name, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resGroup) + d.Set("zone_name", zoneName) + d.Set("ttl", resp.TTL) + + if props := resp.RecordSetProperties; props != nil { + if err := d.Set("records", flattenAzureRmPrivateDnsPtrRecords(resp.PtrRecords)); err != nil { + return fmt.Errorf("Error setting `records`: %+v", err) + } + } + + return tags.FlattenAndSet(d, resp.Metadata) +} + +func resourceArmPrivateDnsPtrRecordDelete(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*ArmClient).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForDelete(meta.(*ArmClient).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["PTR"] + zoneName := id.Path["privateDnsZones"] + + _, err = dnsClient.Delete(ctx, resGroup, zoneName, privatedns.PTR, name, "") + if err != nil { + return fmt.Errorf("Error deleting Private DNS PTR Record %s: %+v", name, err) + } + + return nil +} + +func flattenAzureRmPrivateDnsPtrRecords(records *[]privatedns.PtrRecord) []string { + results := make([]string, 0) + + if records != nil { + for _, record := range *records { + if record.Ptrdname == nil { + continue + } + results = append(results, *record.Ptrdname) + } + } + + return results +} + +func expandAzureRmPrivateDnsPtrRecords(d *schema.ResourceData) *[]privatedns.PtrRecord { + recordStrings := d.Get("records").(*schema.Set).List() + records := make([]privatedns.PtrRecord, len(recordStrings)) + + for i, v := range recordStrings { + fqdn := v.(string) + records[i] = privatedns.PtrRecord{ + Ptrdname: &fqdn, + } + } + + return &records +} diff --git a/azurerm/resource_arm_private_dns_ptr_record_test.go b/azurerm/resource_arm_private_dns_ptr_record_test.go new file mode 100644 index 000000000000..c7250f176cba --- /dev/null +++ b/azurerm/resource_arm_private_dns_ptr_record_test.go @@ -0,0 +1,303 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" +) + +func TestAccAzureRMPrivateDnsPtrRecord_basic(t *testing.T) { + resourceName := "azurerm_private_dns_ptr_record.test" + ri := tf.AccRandTimeInt() + config := testAccAzureRMPrivateDnsPtrRecord_basic(ri, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsPtrRecordDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsPtrRecord_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + resourceName := "azurerm_private_dns_ptr_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsPtrRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsPtrRecord_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + ), + }, + { + Config: testAccAzureRMPrivateDnsPtrRecord_requiresImport(ri, location), + ExpectError: testRequiresImportError("azurerm_private_dns_ptr_record"), + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsPtrRecord_updateRecords(t *testing.T) { + resourceName := "azurerm_private_dns_ptr_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + preConfig := testAccAzureRMPrivateDnsPtrRecord_basic(ri, location) + postConfig := testAccAzureRMPrivateDnsPtrRecord_updateRecords(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsPtrRecordDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "records.#", "2"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "records.#", "3"), + ), + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsPtrRecord_withTags(t *testing.T) { + resourceName := "azurerm_private_dns_ptr_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + preConfig := testAccAzureRMPrivateDnsPtrRecord_withTags(ri, location) + postConfig := testAccAzureRMPrivateDnsPtrRecord_withTagsUpdate(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsPtrRecordDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsPtrRecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMPrivateDnsPtrRecordExists(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) + } + + ptrName := rs.Primary.Attributes["name"] + zoneName := rs.Primary.Attributes["zone_name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Private DNS PTR record: %s", ptrName) + } + + conn := testAccProvider.Meta().(*ArmClient).PrivateDns.RecordSetsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := conn.Get(ctx, resourceGroup, zoneName, privatedns.PTR, ptrName) + if err != nil { + return fmt.Errorf("Bad: Get PTR RecordSet: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Private DNS PTR record %s (resource group: %s) does not exist", ptrName, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMPrivateDnsPtrRecordDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).PrivateDns.RecordSetsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_private_dns_ptr_record" { + continue + } + + ptrName := rs.Primary.Attributes["name"] + zoneName := rs.Primary.Attributes["zone_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(ctx, resourceGroup, zoneName, privatedns.PTR, ptrName) + + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return nil + } + + return err + } + + return fmt.Errorf("Private DNS PTR record still exists:\n%#v", resp.RecordSetProperties) + } + + return nil +} + +func testAccAzureRMPrivateDnsPtrRecord_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "%d.0.10.in-addr.arpa" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_ptr_record" "test" { + name = "%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["test.contoso.com", "test2.contoso.com"] +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsPtrRecord_requiresImport(rInt int, location string) string { + template := testAccAzureRMPrivateDnsPtrRecord_basic(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_private_dns_ptr_record" "import" { + name = "${azurerm_private_dns_ptr_record.test.name}" + resource_group_name = "${azurerm_private_dns_ptr_record.test.resource_group_name}" + zone_name = "${azurerm_private_dns_ptr_record.test.zone_name}" + ttl = 300 + records = ["test.contoso.com", "test2.contoso.com"] +} +`, template) +} + +func testAccAzureRMPrivateDnsPtrRecord_updateRecords(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "%d.0.10.in-addr.arpa" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_ptr_record" "test" { + name = "%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["test.contoso.com", "test2.contoso.com", "test3.contoso.com"] +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsPtrRecord_withTags(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "%d.0.10.in-addr.arpa" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_ptr_record" "test" { + name = "%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["test.contoso.com", "test2.contoso.com"] + + tags = { + environment = "Production" + cost_center = "MSFT" + } +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsPtrRecord_withTagsUpdate(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "%d.0.10.in-addr.arpa" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_ptr_record" "test" { + name = "%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["test.contoso.com", "test2.contoso.com"] + + tags = { + environment = "staging" + } +} +`, rInt, location, rInt, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index feab96257f32..482a7b8478f0 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1632,6 +1632,9 @@