diff --git a/azurerm/internal/services/privatedns/registration.go b/azurerm/internal/services/privatedns/registration.go index 37b46c37e956..2cc589a51100 100644 --- a/azurerm/internal/services/privatedns/registration.go +++ b/azurerm/internal/services/privatedns/registration.go @@ -33,6 +33,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_private_dns_mx_record": resourceArmPrivateDnsMxRecord(), "azurerm_private_dns_ptr_record": resourceArmPrivateDnsPtrRecord(), "azurerm_private_dns_srv_record": resourceArmPrivateDnsSrvRecord(), + "azurerm_private_dns_txt_record": resourceArmPrivateDnsTxtRecord(), "azurerm_private_dns_zone_virtual_network_link": resourceArmPrivateDnsZoneVirtualNetworkLink(), } } diff --git a/azurerm/internal/services/privatedns/resource_arm_private_dns_txt_record.go b/azurerm/internal/services/privatedns/resource_arm_private_dns_txt_record.go new file mode 100644 index 000000000000..1aa7a69e6216 --- /dev/null +++ b/azurerm/internal/services/privatedns/resource_arm_private_dns_txt_record.go @@ -0,0 +1,241 @@ +package privatedns + +import ( + "fmt" + "strings" + "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/clients" + "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 resourceArmPrivateDnsTxtRecord() *schema.Resource { + return &schema.Resource{ + Create: resourceArmPrivateDnsTxtRecordCreateUpdate, + Read: resourceArmPrivateDnsTxtRecordRead, + Update: resourceArmPrivateDnsTxtRecordCreateUpdate, + Delete: resourceArmPrivateDnsTxtRecordDelete, + 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, + // lower-cased due to the broken API https://github.com/Azure/azure-rest-api-specs/issues/6641 + ValidateFunc: validate.LowerCasedString, + }, + + // 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: validation.StringIsNotEmpty, + }, + + "record": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + }, + }, + }, + }, + + "ttl": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 2147483647), + }, + + "fqdn": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmPrivateDnsTxtRecordCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).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.TXT, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Private DNS TXT Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_private_dns_txt_record", *existing.ID) + } + } + + parameters := privatedns.RecordSet{ + Name: &name, + RecordSetProperties: &privatedns.RecordSetProperties{ + Metadata: tags.Expand(d.Get("tags").(map[string]interface{})), + TTL: utils.Int64(int64(d.Get("ttl").(int))), + TxtRecords: expandAzureRmPrivateDnsTxtRecords(d), + }, + } + + if _, err := client.CreateOrUpdate(ctx, resGroup, zoneName, privatedns.TXT, name, parameters, "", ""); err != nil { + return fmt.Errorf("Error creating/updating Private DNS TXT Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + resp, err := client.Get(ctx, resGroup, zoneName, privatedns.TXT, name) + if err != nil { + return fmt.Errorf("Error retrieving Private DNS TXT Record %q (Zone %q / Resource Group %q): %s", name, zoneName, resGroup, err) + } + + if resp.ID == nil { + return fmt.Errorf("Cannot read Private DNS TXT Record %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*resp.ID) + + return resourceArmPrivateDnsTxtRecordRead(d, meta) +} + +func resourceArmPrivateDnsTxtRecordRead(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*clients.Client).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["TXT"] + zoneName := id.Path["privateDnsZones"] + + resp, err := dnsClient.Get(ctx, resGroup, zoneName, privatedns.TXT, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Private DNS TXT 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) + d.Set("fqdn", resp.Fqdn) + + if err := d.Set("record", flattenAzureRmPrivateDnsTxtRecords(resp.TxtRecords)); err != nil { + return fmt.Errorf("setting `record`: %s", err) + } + + return tags.FlattenAndSet(d, resp.Metadata) +} + +func resourceArmPrivateDnsTxtRecordDelete(d *schema.ResourceData, meta interface{}) error { + dnsClient := meta.(*clients.Client).PrivateDns.RecordSetsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["TXT"] + zoneName := id.Path["privateDnsZones"] + + _, err = dnsClient.Delete(ctx, resGroup, zoneName, privatedns.TXT, name, "") + if err != nil { + return fmt.Errorf("Error deleting Private DNS TXT Record %s: %+v", name, err) + } + + return nil +} + +func flattenAzureRmPrivateDnsTxtRecords(records *[]privatedns.TxtRecord) []map[string]interface{} { + results := make([]map[string]interface{}, 0) + + if records != nil { + for _, record := range *records { + txtRecord := make(map[string]interface{}) + + if v := record.Value; v != nil { + value := strings.Join(*v, "") + txtRecord["value"] = value + } + + results = append(results, txtRecord) + } + } + + return results +} + +func expandAzureRmPrivateDnsTxtRecords(d *schema.ResourceData) *[]privatedns.TxtRecord { + recordStrings := d.Get("record").(*schema.Set).List() + records := make([]privatedns.TxtRecord, len(recordStrings)) + + segmentLen := 254 + for i, v := range recordStrings { + if v == nil { + continue + } + + record := v.(map[string]interface{}) + v := record["value"].(string) + + var value []string + for len(v) > segmentLen { + value = append(value, v[:segmentLen]) + v = v[segmentLen:] + } + value = append(value, v) + + txtRecord := privatedns.TxtRecord{ + Value: &value, + } + + records[i] = txtRecord + } + + return &records +} diff --git a/azurerm/internal/services/privatedns/tests/resource_arm_private_dns_txt_record_test.go b/azurerm/internal/services/privatedns/tests/resource_arm_private_dns_txt_record_test.go new file mode 100644 index 000000000000..e62c9330f603 --- /dev/null +++ b/azurerm/internal/services/privatedns/tests/resource_arm_private_dns_txt_record_test.go @@ -0,0 +1,345 @@ +package tests + +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/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" +) + +func TestAccAzureRMPrivateDnsTxtRecord_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_dns_txt_record", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMPrivateDnsTxtRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsTxtRecord_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttrSet(data.ResourceName, "fqdn"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMPrivateDnsTxtRecord_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + data := acceptance.BuildTestData(t, "azurerm_private_dns_txt_record", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMPrivateDnsTxtRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsTxtRecord_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMPrivateDnsTxtRecord_requiresImport), + }, + }) +} + +func TestAccAzureRMPrivateDnsTxtRecord_updateRecords(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_dns_txt_record", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMPrivateDnsTxtRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsTxtRecord_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "record.#", "2"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMPrivateDnsTxtRecord_updateRecords(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "record.#", "3"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMPrivateDnsTxtRecord_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "record.#", "2"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMPrivateDnsTxtRecord_withTags(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_dns_txt_record", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMPrivateDnsTxtRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateDnsTxtRecord_withTags(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "2"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMPrivateDnsTxtRecord_withTagsUpdate(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateDnsTxtRecordExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "1"), + ), + }, + data.ImportStep(), + }, + }) +} + +func testCheckAzureRMPrivateDnsTxtRecordExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).PrivateDns.RecordSetsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + // 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) + } + + txtName := 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 TXT record: %s", txtName) + } + + resp, err := conn.Get(ctx, resourceGroup, zoneName, privatedns.TXT, txtName) + if err != nil { + return fmt.Errorf("Bad: Get TXT RecordSet: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Private DNS TXT record %s (resource group: %s) does not exist", txtName, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMPrivateDnsTxtRecordDestroy(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).PrivateDns.RecordSetsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_private_dns_txt_record" { + continue + } + + txtName := 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.TXT, txtName) + + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return nil + } + + return err + } + + return fmt.Errorf("Private DNS TXT record still exists:\n%#v", resp.RecordSetProperties) + } + + return nil +} + +func testAccAzureRMPrivateDnsTxtRecord_basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-prvdns-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "testzone%d.com" + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_private_dns_txt_record" "test" { + name = "testacctxt%d" + resource_group_name = azurerm_resource_group.test.name + zone_name = azurerm_private_dns_zone.test.name + ttl = 300 + + record { + value = "Quick brown fox" + } + + record { + value = "A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMPrivateDnsTxtRecord_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMPrivateDnsTxtRecord_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_private_dns_txt_record" "import" { + name = azurerm_private_dns_txt_record.test.name + resource_group_name = azurerm_private_dns_txt_record.test.resource_group_name + zone_name = azurerm_private_dns_txt_record.test.zone_name + ttl = 300 + + record { + value = "Quick brown fox" + } + + record { + value = "A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......" + } +} +`, template) +} + +func testAccAzureRMPrivateDnsTxtRecord_updateRecords(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-prvdns-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "testzone%d.com" + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_private_dns_txt_record" "test" { + name = "test%d" + resource_group_name = azurerm_resource_group.test.name + zone_name = azurerm_private_dns_zone.test.name + ttl = 300 + + record { + value = "Quick brown fox" + } + + record { + value = "A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......" + } + + record { + value = "I'm a record too'" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMPrivateDnsTxtRecord_withTags(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-prvdns-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "testzone%d.com" + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_private_dns_txt_record" "test" { + name = "test%d" + resource_group_name = azurerm_resource_group.test.name + zone_name = azurerm_private_dns_zone.test.name + ttl = 300 + + record { + value = "Quick brown fox" + } + + record { + value = "A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......" + } + + tags = { + environment = "Production" + cost_center = "MSFT" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMPrivateDnsTxtRecord_withTagsUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-prvdns-%d" + location = "%s" +} + +resource "azurerm_private_dns_zone" "test" { + name = "testzone%d.com" + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_private_dns_txt_record" "test" { + name = "test%d" + resource_group_name = azurerm_resource_group.test.name + zone_name = azurerm_private_dns_zone.test.name + ttl = 300 + + record { + value = "Quick brown fox" + } + + record { + value = "A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......A long text......" + } + + tags = { + environment = "staging" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 82495facfd3a..0145ba90d2d5 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -2138,6 +2138,9 @@