diff --git a/azurerm/internal/services/privatedns/client.go b/azurerm/internal/services/privatedns/client.go index d09ed5c570e9..afb147c6d699 100644 --- a/azurerm/internal/services/privatedns/client.go +++ b/azurerm/internal/services/privatedns/client.go @@ -6,12 +6,16 @@ import ( ) type Client struct { + RecordSetsClient privatedns.RecordSetsClient PrivateZonesClient privatedns.PrivateZonesClient } func BuildClient(o *common.ClientOptions) *Client { c := Client{} + c.RecordSetsClient = privatedns.NewRecordSetsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&c.RecordSetsClient.Client, o.ResourceManagerAuthorizer) + c.PrivateZonesClient = privatedns.NewPrivateZonesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&c.PrivateZonesClient.Client, o.ResourceManagerAuthorizer) diff --git a/azurerm/provider.go b/azurerm/provider.go index 79bd6ee496d7..27a4a7b53539 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -379,6 +379,7 @@ func Provider() terraform.ResourceProvider { "azurerm_postgresql_server": resourceArmPostgreSQLServer(), "azurerm_postgresql_virtual_network_rule": resourceArmPostgreSQLVirtualNetworkRule(), "azurerm_private_dns_zone": resourceArmPrivateDnsZone(), + "azurerm_private_dns_a_record": resourceArmPrivateDnsARecord(), "azurerm_public_ip": resourceArmPublicIp(), "azurerm_public_ip_prefix": resourceArmPublicIpPrefix(), "azurerm_recovery_services_protected_vm": resourceArmRecoveryServicesProtectedVm(), diff --git a/azurerm/resource_arm_private_dns_a_record.go b/azurerm/resource_arm_private_dns_a_record.go new file mode 100644 index 000000000000..9b7a0533afb4 --- /dev/null +++ b/azurerm/resource_arm_private_dns_a_record.go @@ -0,0 +1,194 @@ +package azurerm + +import ( + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/hashicorp/terraform/helper/schema" + "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/utils" +) + +func resourceArmPrivateDnsARecord() *schema.Resource { + return &schema.Resource{ + Create: resourceArmPrivateDnsARecordCreateUpdate, + Read: resourceArmPrivateDnsARecordRead, + Update: resourceArmPrivateDnsARecordCreateUpdate, + Delete: resourceArmPrivateDnsARecordDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + // 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, + }, + + "records": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "ttl": { + Type: schema.TypeInt, + Required: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmPrivateDnsARecordCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).privateDns.RecordSetsClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + zoneName := d.Get("zone_name").(string) + + if requireResourcesToBeImported && d.IsNewResource() { + existing, err := client.Get(ctx, resGroup, zoneName, privatedns.A, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Private DNS A Record %q (Private Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_private_dns_a_record", *existing.ID) + } + } + + ttl := int64(d.Get("ttl").(int)) + tags := d.Get("tags").(map[string]interface{}) + + parameters := privatedns.RecordSet{ + Name: &name, + RecordSetProperties: &privatedns.RecordSetProperties{ + Metadata: expandTags(tags), + TTL: &ttl, + ARecords: expandAzureRmPrivateDnsARecords(d), + }, + } + + eTag := "" + ifNoneMatch := "" // set to empty to allow updates to records after creation + if _, err := client.CreateOrUpdate(ctx, resGroup, zoneName, privatedns.A, name, parameters, eTag, ifNoneMatch); err != nil { + return fmt.Errorf("Error creating/updating Private DNS A Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + resp, err := client.Get(ctx, resGroup, zoneName, privatedns.A, name) + if err != nil { + return fmt.Errorf("Error retrieving Private DNS A Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + if resp.ID == nil { + return fmt.Errorf("Cannot read Private DNS A Record %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*resp.ID) + + return resourceArmPrivateDnsARecordRead(d, meta) +} + +func resourceArmPrivateDnsARecordRead(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*ArmClient).privateDns.RecordSetsClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["A"] + zoneName := id.Path["privateDnsZones"] + + resp, err := dnsClient.Get(ctx, resGroup, zoneName, privatedns.A, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Private DNS A 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 err := d.Set("records", flattenAzureRmPrivateDnsARecords(resp.ARecords)); err != nil { + return err + } + flattenAndSetTags(d, resp.Metadata) + + return nil +} + +func resourceArmPrivateDnsARecordDelete(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*ArmClient).privateDns.RecordSetsClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["A"] + zoneName := id.Path["privateDnsZones"] + + resp, err := dnsClient.Delete(ctx, resGroup, zoneName, privatedns.A, name, "") + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Error deleting Private DNS A Record %s: %+v", name, err) + } + + return nil +} + +func flattenAzureRmPrivateDnsARecords(records *[]privatedns.ARecord) []string { + results := make([]string, 0) + if records == nil { + return results + } + + for _, record := range *records { + if record.Ipv4Address == nil { + continue + } + + results = append(results, *record.Ipv4Address) + } + + return results +} + +func expandAzureRmPrivateDnsARecords(d *schema.ResourceData) *[]privatedns.ARecord { + recordStrings := d.Get("records").(*schema.Set).List() + records := make([]privatedns.ARecord, len(recordStrings)) + + for i, v := range recordStrings { + ipv4 := v.(string) + records[i] = privatedns.ARecord{ + Ipv4Address: &ipv4, + } + } + + return &records +} diff --git a/azurerm/resource_arm_private_dns_a_record_test.go b/azurerm/resource_arm_private_dns_a_record_test.go new file mode 100644 index 000000000000..b679c1d3374a --- /dev/null +++ b/azurerm/resource_arm_private_dns_a_record_test.go @@ -0,0 +1,302 @@ +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/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" +) + +func TestAccAzureRMPrivateDnsARecord_basic(t *testing.T) { + resourceName := "azurerm_private_dns_a_record.test" + ri := tf.AccRandTimeInt() + config := testAccAzureRMPrivateDnsARecord_basic(ri, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsARecordDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsARecord_requiresImport(t *testing.T) { + if !requireResourcesToBeImported { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + resourceName := "azurerm_private_dns_a_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsARecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsARecord_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + ), + }, + { + Config: testAccAzureRMPrivateDnsARecord_requiresImport(ri, location), + ExpectError: testRequiresImportError("azurerm_private_dns_a_record"), + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsARecord_updateRecords(t *testing.T) { + resourceName := "azurerm_private_dns_a_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + preConfig := testAccAzureRMPrivateDnsARecord_basic(ri, location) + postConfig := testAccAzureRMPrivateDnsARecord_updateRecords(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsARecordDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "records.#", "2"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "records.#", "3"), + ), + }, + }, + }) +} + +func TestAccAzureRMPrivateDnsARecord_withTags(t *testing.T) { + resourceName := "azurerm_private_dns_a_record.test" + ri := tf.AccRandTimeInt() + location := testLocation() + preConfig := testAccAzureRMPrivateDnsARecord_withTags(ri, location) + postConfig := testAccAzureRMPrivateDnsARecord_withTagsUpdate(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateDnsARecordDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsARecordExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMPrivateDnsARecordExists(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) + } + + aName := 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 A record: %s", aName) + } + + conn := testAccProvider.Meta().(*ArmClient).privateDns.RecordSetsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := conn.Get(ctx, resourceGroup, zoneName, privatedns.A, aName) + if err != nil { + return fmt.Errorf("Bad: Get A RecordSet: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Private DNS A record %s (resource group: %s) does not exist", aName, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMPrivateDnsARecordDestroy(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_a_record" { + continue + } + + aName := 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.A, aName) + + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return nil + } + + return err + } + + return fmt.Errorf("Private DNS A record still exists:\n%#v", resp.RecordSetProperties) + } + + return nil +} + +func testAccAzureRMPrivateDnsARecord_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 = "acctestzone%d.com" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_a_record" "test" { + name = "myarecord%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["1.2.3.4", "1.2.4.5"] +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsARecord_requiresImport(rInt int, location string) string { + template := testAccAzureRMPrivateDnsARecord_basic(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_private_dns_a_record" "import" { + name = "${azurerm_private_dns_a_record.test.name}" + resource_group_name = "${azurerm_private_dns_a_record.test.resource_group_name}" + zone_name = "${azurerm_private_dns_a_record.test.zone_name}" + ttl = 300 + records = ["1.2.3.4", "1.2.4.5"] +} +`, template) +} + +func testAccAzureRMPrivateDnsARecord_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 = "acctestzone%d.com" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_a_record" "test" { + name = "myarecord%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["1.2.3.4", "1.2.4.5", "1.2.3.7"] +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsARecord_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 = "acctestzone%d.com" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_a_record" "test" { + name = "myarecord%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["1.2.3.4", "1.2.4.5"] + + tags = { + environment = "Production" + cost_center = "MSFT" + } +} +`, rInt, location, rInt, rInt) +} + +func testAccAzureRMPrivateDnsARecord_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 = "acctestzone%d.com" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_private_dns_a_record" "test" { + name = "myarecord%d" + resource_group_name = "${azurerm_resource_group.test.name}" + zone_name = "${azurerm_private_dns_zone.test.name}" + ttl = 300 + records = ["1.2.3.4", "1.2.4.5"] + + tags = { + environment = "staging" + } +} +`, rInt, location, rInt, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index e57332efe5eb..b91cc7357cdb 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1460,6 +1460,9 @@