From daabd138377022ecffd91219fb38235fae8a8e56 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Mon, 13 Sep 2021 11:59:09 -0500 Subject: [PATCH 1/4] Revert `google_dns_record_set` to previous implementation --- mmv1/products/dns/ansible.yaml | 4 +- mmv1/products/dns/api.yaml | 65 --- mmv1/products/dns/inspec.yaml | 2 - mmv1/products/dns/terraform.yaml | 17 - .../resources/resource_dns_record_set.go | 387 ++++++++++++++++++ .../tests/resource_dns_record_set_test.go.erb | 94 +++-- .../terraform/utils/provider.go.erb | 1 + .../docs/r/dns_record_set.html.markdown | 175 ++++++++ 8 files changed, 614 insertions(+), 131 deletions(-) create mode 100644 mmv1/third_party/terraform/resources/resource_dns_record_set.go create mode 100644 mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown diff --git a/mmv1/products/dns/ansible.yaml b/mmv1/products/dns/ansible.yaml index 1e69b4b0b2ce..8d0f8da54260 100644 --- a/mmv1/products/dns/ansible.yaml +++ b/mmv1/products/dns/ansible.yaml @@ -86,9 +86,7 @@ overrides: !ruby/object:Overrides::ResourceOverrides contain_extra_docs: false Project: !ruby/object:Overrides::Ansible::ResourceOverride # TODO(alexstephen): Re-evaluate merging Project into Ansible - exclude: true - ResourceDnsRecordSet: !ruby/object:Overrides::Ansible::ResourceOverride - exclude: true + exclude: true files: !ruby/object:Provider::Config::Files resource: <%= lines(indent(compile('provider/ansible/resource~compile.yaml'), 4)) -%> diff --git a/mmv1/products/dns/api.yaml b/mmv1/products/dns/api.yaml index 6e8faf173fc0..1ab8cfa1e8cd 100644 --- a/mmv1/products/dns/api.yaml +++ b/mmv1/products/dns/api.yaml @@ -497,68 +497,3 @@ objects: description: | As defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1) api_name: rrdatas - - !ruby/object:Api::Resource - name: 'ResourceDnsRecordSet' - kind: 'dns#resourceRecordSet' - description: | - A single DNS record that exists on a domain name (i.e. in a managed zone). - This record defines the information about the domain and where the - domain / subdomains direct to. - - The record will include the domain/subdomain name, a type (i.e. A, AAA, - CAA, MX, CNAME, NS, etc) - base_url: 'projects/{{project}}/managedZones/{{managed_zone}}/rrsets' - self_link: 'projects/{{project}}/managedZones/{{managed_zone}}/rrsets/{{name}}/{{type}}' - update_verb: :PATCH - parameters: - - !ruby/object:Api::Type::ResourceRef - name: 'managed_zone' - input: true - description: | - Identifies the managed zone addressed by this request. - required: true - resource: 'ManagedZone' - imports: 'name' - properties: - - !ruby/object:Api::Type::String - name: 'name' - description: For example, www.example.com. - required: true - input: true - - !ruby/object:Api::Type::Enum - name: 'type' - values: - - :A - - :AAAA - - :CAA - - :CNAME - - :DNSKEY - - :DS - - :IPSECVPNKEY - - :MX - - :NAPTR - - :NS - - :PTR - - :SOA - - :SPF - - :SRV - - :SSHFP - - :TLSA - - :TXT - description: One of valid DNS resource types. - required: true - - !ruby/object:Api::Type::Integer - name: 'ttl' - description: | - Number of seconds that this ResourceRecordSet can be cached by - resolvers. - - !ruby/object:Api::Type::Array - name: rrdatas - input: true - description: | - The string data for the records in this record set whose meaning depends on the DNS type. - For TXT record, if the string data contains spaces, add surrounding \" if you don't want your string to get - split on spaces. To specify a single record value longer than 255 characters such as a TXT record for - DKIM, add \"\" inside the Terraform configuration string (e.g. "first255characters\"\"morecharacters"). - item_type: Api::Type::String - diff --git a/mmv1/products/dns/inspec.yaml b/mmv1/products/dns/inspec.yaml index 7af33fb297b2..bfd87b9dfffd 100644 --- a/mmv1/products/dns/inspec.yaml +++ b/mmv1/products/dns/inspec.yaml @@ -34,6 +34,4 @@ overrides: !ruby/object:Overrides::ResourceOverrides exclude: true Policy: !ruby/object:Overrides::Inspec::ResourceOverride exclude: true - ResourceDnsRecordSet: !ruby/object:Overrides::Inspec::ResourceOverride - exclude: true diff --git a/mmv1/products/dns/terraform.yaml b/mmv1/products/dns/terraform.yaml index 6d3db4ab593d..73f6568391be 100644 --- a/mmv1/products/dns/terraform.yaml +++ b/mmv1/products/dns/terraform.yaml @@ -191,23 +191,6 @@ overrides: !ruby/object:Overrides::ResourceOverrides pre_delete: templates/terraform/pre_delete/detach_network.erb ResourceRecordSet: !ruby/object:Overrides::Terraform::ResourceOverride exclude: true - ResourceDnsRecordSet: !ruby/object:Overrides::Terraform::ResourceOverride - legacy_name: "google_dns_record_set" - import_format: ["projects/{{project}}/managedZones/{{managed_zone}}/rrsets/{{name}}/{{type}}"] - custom_code: !ruby/object:Provider::Terraform::CustomCode - constants: 'templates/terraform/constants/resource_dns_resource_record_set.go.erb' - examples: - - !ruby/object:Provider::Terraform::Examples - skip_test: true - name: "dns_record_set_basic" - primary_resource_id: "resource-recordset" - vars: - zone_name: "my-zone" - properties: - rrdatas: !ruby/object:Overrides::Terraform::PropertyOverride - diff_suppress_func: 'rrdatasDnsDiffSuppress' - managed_zone: !ruby/object:Overrides::Terraform::PropertyOverride - ignore_read: true Project: !ruby/object:Overrides::Terraform::ResourceOverride exclude: true # This is for copying files over diff --git a/mmv1/third_party/terraform/resources/resource_dns_record_set.go b/mmv1/third_party/terraform/resources/resource_dns_record_set.go new file mode 100644 index 000000000000..59220db6c011 --- /dev/null +++ b/mmv1/third_party/terraform/resources/resource_dns_record_set.go @@ -0,0 +1,387 @@ +package google + +import ( + "fmt" + "log" + + "strings" + + "net" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/dns/v1" +) + +func resourceDnsRecordSet() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsRecordSetCreate, + Read: resourceDnsRecordSetRead, + Delete: resourceDnsRecordSetDelete, + Update: resourceDnsRecordSetUpdate, + Importer: &schema.ResourceImporter{ + State: resourceDnsRecordSetImportState, + }, + + Schema: map[string]*schema.Schema{ + "managed_zone": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name of the zone in which this record set will reside.`, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The DNS name this record set will apply to.`, + }, + + "rrdatas": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if d.Get("type") == "AAAA" { + return ipv6AddressDiffSuppress(k, old, new, d) + } + return false + }, + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.ToLower(strings.Trim(old, `"`)) == strings.ToLower(strings.Trim(new, `"`)) + }, + Description: `The string data for the records in this record set whose meaning depends on the DNS type. For TXT record, if the string data contains spaces, add surrounding \" if you don't want your string to get split on spaces. To specify a single record value longer than 255 characters such as a TXT record for DKIM, add \"\" inside the Terraform configuration string (e.g. "first255characters\"\"morecharacters").`, + }, + + "ttl": { + Type: schema.TypeInt, + Optional: true, + Description: `The time-to-live of this record set (seconds).`, + }, + + "type": { + Type: schema.TypeString, + Required: true, + Description: `The DNS record set type.`, + }, + + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: `The ID of the project in which the resource belongs. If it is not provided, the provider project is used.`, + }, + }, + UseJSONNumber: true, + } +} + +func resourceDnsRecordSetCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + name := d.Get("name").(string) + zone := d.Get("managed_zone").(string) + rType := d.Get("type").(string) + + // Build the change + chg := &dns.Change{ + Additions: []*dns.ResourceRecordSet{ + { + Name: name, + Type: rType, + Ttl: int64(d.Get("ttl").(int)), + Rrdatas: rrdata(d), + }, + }, + } + + // The terraform provider is authoritative, so what we do here is check if + // any records that we are trying to create already exist and make sure we + // delete them, before adding in the changes requested. Normally this would + // result in an AlreadyExistsError. + log.Printf("[DEBUG] DNS record list request for %q", zone) + res, err := config.NewDnsClient(userAgent).ResourceRecordSets.List(project, zone).Do() + if err != nil { + return fmt.Errorf("Error retrieving record sets for %q: %s", zone, err) + } + var deletions []*dns.ResourceRecordSet + + for _, record := range res.Rrsets { + if record.Type != rType || record.Name != name { + continue + } + deletions = append(deletions, record) + } + if len(deletions) > 0 { + chg.Deletions = deletions + } + + log.Printf("[DEBUG] DNS Record create request: %#v", chg) + chg, err = config.NewDnsClient(userAgent).Changes.Create(project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Error creating DNS RecordSet: %s", err) + } + + d.SetId(fmt.Sprintf("%s/%s/%s", zone, name, rType)) + + w := &DnsChangeWaiter{ + Service: config.NewDnsClient(userAgent), + Change: chg, + Project: project, + ManagedZone: zone, + } + _, err = w.Conf().WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + + return resourceDnsRecordSetRead(d, meta) +} + +func resourceDnsRecordSetRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + zone := d.Get("managed_zone").(string) + + // name and type are effectively the 'key' + name := d.Get("name").(string) + dnsType := d.Get("type").(string) + + var resp *dns.ResourceRecordSetsListResponse + err = retry(func() error { + var reqErr error + resp, reqErr = config.NewDnsClient(userAgent).ResourceRecordSets.List( + project, zone).Name(name).Type(dnsType).Do() + return reqErr + }) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("DNS Record Set %q", d.Get("name").(string))) + } + if len(resp.Rrsets) == 0 { + // The resource doesn't exist anymore + d.SetId("") + return nil + } + + if len(resp.Rrsets) > 1 { + return fmt.Errorf("Only expected 1 record set, got %d", len(resp.Rrsets)) + } + + if err := d.Set("type", resp.Rrsets[0].Type); err != nil { + return fmt.Errorf("Error setting type: %s", err) + } + if err := d.Set("ttl", resp.Rrsets[0].Ttl); err != nil { + return fmt.Errorf("Error setting ttl: %s", err) + } + if err := d.Set("rrdatas", resp.Rrsets[0].Rrdatas); err != nil { + return fmt.Errorf("Error setting rrdatas: %s", err) + } + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting project: %s", err) + } + + return nil +} + +func resourceDnsRecordSetDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + zone := d.Get("managed_zone").(string) + + // NS records must always have a value, so we short-circuit delete + // this allows terraform delete to work, but may have unexpected + // side-effects when deleting just that record set. + // Unfortunately, you can set NS records on subdomains, and those + // CAN and MUST be deleted, so we need to retrieve the managed zone, + // check if what we're looking at is a subdomain, and only not delete + // if it's not actually a subdomain + if d.Get("type").(string) == "NS" { + mz, err := config.NewDnsClient(userAgent).ManagedZones.Get(project, zone).Do() + if err != nil { + return fmt.Errorf("Error retrieving managed zone %q from %q: %s", zone, project, err) + } + domain := mz.DnsName + + if domain == d.Get("name").(string) { + log.Println("[DEBUG] NS records can't be deleted due to API restrictions, so they're being left in place. See https://www.terraform.io/docs/providers/google/r/dns_record_set.html for more information.") + return nil + } + } + + // Build the change + chg := &dns.Change{ + Deletions: []*dns.ResourceRecordSet{ + { + Name: d.Get("name").(string), + Type: d.Get("type").(string), + Ttl: int64(d.Get("ttl").(int)), + Rrdatas: rrdata(d), + }, + }, + } + + log.Printf("[DEBUG] DNS Record delete request: %#v", chg) + chg, err = config.NewDnsClient(userAgent).Changes.Create(project, zone, chg).Do() + if err != nil { + return handleNotFoundError(err, d, "google_dns_record_set") + } + + w := &DnsChangeWaiter{ + Service: config.NewDnsClient(userAgent), + Change: chg, + Project: project, + ManagedZone: zone, + } + _, err = w.Conf().WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + + d.SetId("") + return nil +} + +func resourceDnsRecordSetUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + zone := d.Get("managed_zone").(string) + recordName := d.Get("name").(string) + + oldTtl, newTtl := d.GetChange("ttl") + oldType, newType := d.GetChange("type") + + oldCountRaw, _ := d.GetChange("rrdatas.#") + oldCount := oldCountRaw.(int) + + chg := &dns.Change{ + Deletions: []*dns.ResourceRecordSet{ + { + Name: recordName, + Type: oldType.(string), + Ttl: int64(oldTtl.(int)), + Rrdatas: make([]string, oldCount), + }, + }, + Additions: []*dns.ResourceRecordSet{ + { + Name: recordName, + Type: newType.(string), + Ttl: int64(newTtl.(int)), + Rrdatas: rrdata(d), + }, + }, + } + + for i := 0; i < oldCount; i++ { + rrKey := fmt.Sprintf("rrdatas.%d", i) + oldRR, _ := d.GetChange(rrKey) + chg.Deletions[0].Rrdatas[i] = oldRR.(string) + } + log.Printf("[DEBUG] DNS Record change request: %#v old: %#v new: %#v", chg, chg.Deletions[0], chg.Additions[0]) + chg, err = config.NewDnsClient(userAgent).Changes.Create(project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Error changing DNS RecordSet: %s", err) + } + + w := &DnsChangeWaiter{ + Service: config.NewDnsClient(userAgent), + Change: chg, + Project: project, + ManagedZone: zone, + } + if _, err = w.Conf().WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + + return resourceDnsRecordSetRead(d, meta) +} + +func resourceDnsRecordSetImportState(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) == 3 { + if err := d.Set("managed_zone", parts[0]); err != nil { + return nil, fmt.Errorf("Error setting managed_zone: %s", err) + } + if err := d.Set("name", parts[1]); err != nil { + return nil, fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("type", parts[2]); err != nil { + return nil, fmt.Errorf("Error setting type: %s", err) + } + } else if len(parts) == 4 { + if err := d.Set("project", parts[0]); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("managed_zone", parts[1]); err != nil { + return nil, fmt.Errorf("Error setting managed_zone: %s", err) + } + if err := d.Set("name", parts[2]); err != nil { + return nil, fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("type", parts[3]); err != nil { + return nil, fmt.Errorf("Error setting type: %s", err) + } + d.SetId(parts[1] + "/" + parts[2] + "/" + parts[3]) + } else { + return nil, fmt.Errorf("Invalid dns record specifier. Expecting {zone-name}/{record-name}/{record-type} or {project}/{zone-name}/{record-name}/{record-type}. The record name must include a trailing '.' at the end.") + } + + return []*schema.ResourceData{d}, nil +} + +func rrdata( + d *schema.ResourceData, +) []string { + rrdatasCount := d.Get("rrdatas.#").(int) + data := make([]string, rrdatasCount) + for i := 0; i < rrdatasCount; i++ { + data[i] = d.Get(fmt.Sprintf("rrdatas.%d", i)).(string) + } + return data +} + +func ipv6AddressDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + oldIp := net.ParseIP(old) + newIp := net.ParseIP(new) + + return oldIp.Equal(newIp) +} diff --git a/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb b/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb index e9e545899b21..3c108b6f0d0d 100644 --- a/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb +++ b/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb @@ -9,49 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestIpv6AddressDiffSuppress(t *testing.T) { - cases := map[string]struct { - Old, New []string - ShouldSuppress bool - }{ - "compact form should suppress diff": { - Old: []string{"2a03:b0c0:1:e0::29b:8001"}, - New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, - ShouldSuppress: true, - }, - "different address should not suppress diff": { - Old: []string{"2a03:b0c0:1:e00::29b:8001"}, - New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, - ShouldSuppress: false, - }, - "increase address should not suppress diff": { - Old: []string{""}, - New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, - ShouldSuppress: false, - }, - "decrease address should not suppress diff": { - Old: []string{"2a03:b0c0:1:e00::29b:8001"}, - New: []string{""}, - ShouldSuppress: false, - }, - "switch address positions should suppress diff": { - Old: []string{"2a03:b0c0:1:e00::28b:8001", "2a03:b0c0:1:e0::29b:8001"}, - New: []string{"2a03:b0c0:1:e0::29b:8001", "2a03:b0c0:1:e00::28b:8001"}, - ShouldSuppress: true, - }, - } - - parseFunc := func(x string) string { - return net.ParseIP(x).String() - } - - for tn, tc := range cases { - shouldSuppress := rrdatasListDiffSuppress(tc.Old, tc.New, parseFunc, nil) - if shouldSuppress != tc.ShouldSuppress { - t.Errorf("%s: expected %t", tn, tc.ShouldSuppress) - } - } -} func TestAccDNSRecordSet_basic(t *testing.T) { t.Parallel() @@ -142,9 +99,40 @@ func TestAccDNSRecordSet_changeType(t *testing.T) { { Config: testAccDnsRecordSet_bigChange(zoneName, 600), }, + { + ResourceName: "google_dns_record_set.foobar", + ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./CNAME", getTestProjectFromEnv(), zoneName, zoneName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccDNSRecordSet_rrdatasUpdate(t *testing.T) { + t.Parallel() + + zoneName := fmt.Sprintf("dnszone-test-%s", randString(t, 10)) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsRecordSetDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDnsRecordSet_basic(zoneName, "127.0.0.10", 300), + }, + { + ResourceName: "google_dns_record_set.foobar", + ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./A", getTestProjectFromEnv(), zoneName, zoneName), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccDnsRecordSet_rrdatasUpdate(zoneName, "127.0.0.10", 300), + }, { ResourceName: "google_dns_record_set.foobar", - ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./CNAME", getTestProjectFromEnv(), zoneName, zoneName), + ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./A", getTestProjectFromEnv(), zoneName, zoneName), ImportState: true, ImportStateVerify: true, }, @@ -270,6 +258,24 @@ resource "google_dns_record_set" "foobar" { `, zoneName, zoneName, zoneName, addr2, ttl) } +func testAccDnsRecordSet_rrdatasUpdate(zoneName string, addr2 string, ttl int) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "parent-zone" { + name = "%s" + dns_name = "%s.hashicorptest.com." + description = "Test Description" +} + +resource "google_dns_record_set" "foobar" { + managed_zone = google_dns_managed_zone.parent-zone.name + name = "test-record.%s.hashicorptest.com." + type = "A" + rrdatas = ["127.0.0.2", "%s"] + ttl = %d +} +`, zoneName, zoneName, zoneName, addr2, ttl) +} + func testAccDnsRecordSet_nestedNS(name string, ttl int) string { return fmt.Sprintf(` resource "google_dns_managed_zone" "parent-zone" { diff --git a/mmv1/third_party/terraform/utils/provider.go.erb b/mmv1/third_party/terraform/utils/provider.go.erb index cf276eacc9f9..f9a23dd3fed9 100644 --- a/mmv1/third_party/terraform/utils/provider.go.erb +++ b/mmv1/third_party/terraform/utils/provider.go.erb @@ -395,6 +395,7 @@ end # products.each do <% end -%> "google_dataproc_cluster": resourceDataprocCluster(), "google_dataproc_job": resourceDataprocJob(), + "google_dns_record_set": resourceDnsRecordSet(), "google_endpoints_service": resourceEndpointsService(), "google_folder": resourceGoogleFolder(), "google_folder_organization_policy": resourceGoogleFolderOrganizationPolicy(), diff --git a/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown new file mode 100644 index 000000000000..336ee302f3c3 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown @@ -0,0 +1,175 @@ +--- +subcategory: "Cloud DNS" +layout: "google" +page_title: "Google: google_dns_record_set" +sidebar_current: "docs-google-dns-record-set" +description: |- + Manages a set of DNS records within Google Cloud DNS. +--- + +# google\_dns\_record\_set + +Manages a set of DNS records within Google Cloud DNS. For more information see [the official documentation](https://cloud.google.com/dns/records/) and +[API](https://cloud.google.com/dns/api/v1/resourceRecordSets). + +~> **Note:** The provider treats this resource as an authoritative record set. This means existing records (including the default records) for the given type will be overwritten when you create this resource in Terraform. In addition, the Google Cloud DNS API requires NS records to be present at all times, so Terraform will not actually remove NS records during destroy but will report that it did. + +## Example Usage + +### Binding a DNS name to the ephemeral IP of a new instance: + +```hcl +resource "google_dns_record_set" "frontend" { + name = "frontend.${google_dns_managed_zone.prod.dns_name}" + type = "A" + ttl = 300 + + managed_zone = google_dns_managed_zone.prod.name + + rrdatas = [google_compute_instance.frontend.network_interface[0].access_config[0].nat_ip] +} + +resource "google_compute_instance" "frontend" { + name = "frontend" + machine_type = "g1-small" + zone = "us-central1-b" + + boot_disk { + initialize_params { + image = "debian-cloud/debian-9" + } + } + + network_interface { + network = "default" + access_config { + } + } +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +### Adding an A record + +```hcl +resource "google_dns_record_set" "a" { + name = "backend.${google_dns_managed_zone.prod.dns_name}" + managed_zone = google_dns_managed_zone.prod.name + type = "A" + ttl = 300 + + rrdatas = ["8.8.8.8"] +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +### Adding an MX record + +```hcl +resource "google_dns_record_set" "mx" { + name = google_dns_managed_zone.prod.dns_name + managed_zone = google_dns_managed_zone.prod.name + type = "MX" + ttl = 3600 + + rrdatas = [ + "1 aspmx.l.google.com.", + "5 alt1.aspmx.l.google.com.", + "5 alt2.aspmx.l.google.com.", + "10 alt3.aspmx.l.google.com.", + "10 alt4.aspmx.l.google.com.", + ] +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +### Adding an SPF record + +Quotes (`""`) must be added around your `rrdatas` for a SPF record. Otherwise `rrdatas` string gets split on spaces. + +```hcl +resource "google_dns_record_set" "spf" { + name = "frontend.${google_dns_managed_zone.prod.dns_name}" + managed_zone = google_dns_managed_zone.prod.name + type = "TXT" + ttl = 300 + + rrdatas = ["\"v=spf1 ip4:111.111.111.111 include:backoff.email-example.com -all\""] +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +### Adding a CNAME record + + The list of `rrdatas` should only contain a single string corresponding to the Canonical Name intended. + +```hcl +resource "google_dns_record_set" "cname" { + name = "frontend.${google_dns_managed_zone.prod.dns_name}" + managed_zone = google_dns_managed_zone.prod.name + type = "CNAME" + ttl = 300 + rrdatas = ["frontend.mydomain.com."] +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +## Argument Reference + +The following arguments are supported: + +* `managed_zone` - (Required) The name of the zone in which this record set will + reside. + +* `name` - (Required) The DNS name this record set will apply to. + +* `rrdatas` - (Required) The string data for the records in this record set + whose meaning depends on the DNS type. For TXT record, if the string data contains spaces, add surrounding `\"` if you don't want your string to get split on spaces. To specify a single record value longer than 255 characters such as a TXT record for DKIM, add `\" \"` inside the Terraform configuration string (e.g. `"first255characters\" \"morecharacters"`). + + +* `type` - (Required) The DNS record set type. + +- - - + +* `ttl` - (Optional) The time-to-live of this record set (seconds). + +* `project` - (Optional) The ID of the project in which the resource belongs. If it + is not provided, the provider project is used. + +## Attributes Reference + +-In addition to the arguments listed above, the following computed attributes are +-exported: + +* `id` - an identifier for the resource with format `{{project}}/{{zone}}/{{name}}/{{type}}` + +## Import + +DNS record sets can be imported using either of these accepted formats: + +``` +$ terraform import google_dns_record_set.frontend {{project}}/{{zone}}/{{name}}/{{type}} +$ terraform import google_dns_record_set.frontend {{zone}}/{{name}}/{{type}} +``` + +Note: The record name must include the trailing dot at the end. \ No newline at end of file From 77de5a336e1c499fdd23e88ea92c70e9bb043386 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Mon, 13 Sep 2021 16:10:22 -0500 Subject: [PATCH 2/4] reset ID on updates --- mmv1/third_party/terraform/resources/resource_dns_record_set.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mmv1/third_party/terraform/resources/resource_dns_record_set.go b/mmv1/third_party/terraform/resources/resource_dns_record_set.go index 59220db6c011..3f6dc96c7407 100644 --- a/mmv1/third_party/terraform/resources/resource_dns_record_set.go +++ b/mmv1/third_party/terraform/resources/resource_dns_record_set.go @@ -332,6 +332,8 @@ func resourceDnsRecordSetUpdate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error waiting for Google DNS change: %s", err) } + d.SetId(fmt.Sprintf("%s/%s/%s", zone, recordName, newType)) + return resourceDnsRecordSetRead(d, meta) } From df66a7fa7bdc6f8b700ed18e7dc05c0233c4a906 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 16 Sep 2021 12:55:12 -0500 Subject: [PATCH 3/4] Changes from review --- .../resources/resource_dns_record_set.go | 120 +++++++++++------- .../tests/resource_dns_record_set_test.go.erb | 94 ++++++++------ .../docs/r/dns_record_set.html.markdown | 1 + 3 files changed, 134 insertions(+), 81 deletions(-) diff --git a/mmv1/third_party/terraform/resources/resource_dns_record_set.go b/mmv1/third_party/terraform/resources/resource_dns_record_set.go index 3f6dc96c7407..40cfa4c1cd37 100644 --- a/mmv1/third_party/terraform/resources/resource_dns_record_set.go +++ b/mmv1/third_party/terraform/resources/resource_dns_record_set.go @@ -12,6 +12,58 @@ import ( "google.golang.org/api/dns/v1" ) +func rrdatasDnsDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + o, n := d.GetChange("rrdatas") + if o == nil || n == nil { + return false + } + + oList := convertStringArr(o.([]interface{})) + nList := convertStringArr(n.([]interface{})) + + parseFunc := func(record string) string { + switch d.Get("type") { + case "AAAA": + // parse ipv6 to a key from one list + return net.ParseIP(record).String() + case "MX", "DS": + return strings.ToLower(record) + case "TXT": + return strings.ToLower(strings.Trim(record, `"`)) + default: + return record + } + } + return rrdatasListDiffSuppress(oList, nList, parseFunc, d) +} + +// suppress on a list when 1) its items have dups that need to be ignored +// and 2) string comparison on the items may need a special parse function +// example of usage can be found ../../../third_party/terraform/tests/resource_dns_record_set_test.go.erb +func rrdatasListDiffSuppress(oldList, newList []string, fun func(x string) string, _ *schema.ResourceData) bool { + // compare two lists of unordered records + diff := make(map[string]bool, len(oldList)) + for _, oldRecord := range oldList { + // set all new IPs to true + diff[fun(oldRecord)] = true + } + for _, newRecord := range newList { + // set matched IPs to false otherwise can't suppress + if diff[fun(newRecord)] { + diff[fun(newRecord)] = false + } else { + return false + } + } + // can't suppress if unmatched records are found + for _, element := range diff { + if element { + return false + } + } + return true +} + func resourceDnsRecordSet() *schema.Resource { return &schema.Resource{ Create: resourceDnsRecordSetCreate, @@ -24,10 +76,11 @@ func resourceDnsRecordSet() *schema.Resource { Schema: map[string]*schema.Schema{ "managed_zone": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: `The name of the zone in which this record set will reside.`, + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `The name of the zone in which this record set will reside.`, }, "name": { @@ -42,17 +95,9 @@ func resourceDnsRecordSet() *schema.Resource { Required: true, Elem: &schema.Schema{ Type: schema.TypeString, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if d.Get("type") == "AAAA" { - return ipv6AddressDiffSuppress(k, old, new, d) - } - return false - }, }, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.ToLower(strings.Trim(old, `"`)) == strings.ToLower(strings.Trim(new, `"`)) - }, - Description: `The string data for the records in this record set whose meaning depends on the DNS type. For TXT record, if the string data contains spaces, add surrounding \" if you don't want your string to get split on spaces. To specify a single record value longer than 255 characters such as a TXT record for DKIM, add \"\" inside the Terraform configuration string (e.g. "first255characters\"\"morecharacters").`, + DiffSuppressFunc: rrdatasDnsDiffSuppress, + Description: `The string data for the records in this record set whose meaning depends on the DNS type. For TXT record, if the string data contains spaces, add surrounding \" if you don't want your string to get split on spaces. To specify a single record value longer than 255 characters such as a TXT record for DKIM, add \"\" inside the Terraform configuration string (e.g. "first255characters\"\"morecharacters").`, }, "ttl": { @@ -134,7 +179,7 @@ func resourceDnsRecordSetCreate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error creating DNS RecordSet: %s", err) } - d.SetId(fmt.Sprintf("%s/%s/%s", zone, name, rType)) + d.SetId(fmt.Sprintf("projects/%s/managedZones/%s/rrsets/%s/%s", project, zone, name, rType)) w := &DnsChangeWaiter{ Service: config.NewDnsClient(userAgent), @@ -332,40 +377,27 @@ func resourceDnsRecordSetUpdate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error waiting for Google DNS change: %s", err) } - d.SetId(fmt.Sprintf("%s/%s/%s", zone, recordName, newType)) + d.SetId(fmt.Sprintf("projects/%s/managedZones/%s/rrsets/%s/%s", project, zone, recordName, newType)) return resourceDnsRecordSetRead(d, meta) } -func resourceDnsRecordSetImportState(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { - parts := strings.Split(d.Id(), "/") - if len(parts) == 3 { - if err := d.Set("managed_zone", parts[0]); err != nil { - return nil, fmt.Errorf("Error setting managed_zone: %s", err) - } - if err := d.Set("name", parts[1]); err != nil { - return nil, fmt.Errorf("Error setting name: %s", err) - } - if err := d.Set("type", parts[2]); err != nil { - return nil, fmt.Errorf("Error setting type: %s", err) - } - } else if len(parts) == 4 { - if err := d.Set("project", parts[0]); err != nil { - return nil, fmt.Errorf("Error setting project: %s", err) - } - if err := d.Set("managed_zone", parts[1]); err != nil { - return nil, fmt.Errorf("Error setting managed_zone: %s", err) - } - if err := d.Set("name", parts[2]); err != nil { - return nil, fmt.Errorf("Error setting name: %s", err) - } - if err := d.Set("type", parts[3]); err != nil { - return nil, fmt.Errorf("Error setting type: %s", err) - } - d.SetId(parts[1] + "/" + parts[2] + "/" + parts[3]) - } else { - return nil, fmt.Errorf("Invalid dns record specifier. Expecting {zone-name}/{record-name}/{record-type} or {project}/{zone-name}/{record-name}/{record-type}. The record name must include a trailing '.' at the end.") +func resourceDnsRecordSetImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/managedZones/(?P[^/]+)/rrsets/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/managedZones/{{managed_zone}}/rrsets/{{name}}/{{type}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) } + d.SetId(id) return []*schema.ResourceData{d}, nil } diff --git a/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb b/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb index 3c108b6f0d0d..55b4fb622574 100644 --- a/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb +++ b/mmv1/third_party/terraform/tests/resource_dns_record_set_test.go.erb @@ -9,6 +9,51 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) + +func TestIpv6AddressDiffSuppress(t *testing.T) { + cases := map[string]struct { + Old, New []string + ShouldSuppress bool + }{ + "compact form should suppress diff": { + Old: []string{"2a03:b0c0:1:e0::29b:8001"}, + New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, + ShouldSuppress: true, + }, + "different address should not suppress diff": { + Old: []string{"2a03:b0c0:1:e00::29b:8001"}, + New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, + ShouldSuppress: false, + }, + "increase address should not suppress diff": { + Old: []string{""}, + New: []string{"2a03:b0c0:0001:00e0:0000:0000:029b:8001"}, + ShouldSuppress: false, + }, + "decrease address should not suppress diff": { + Old: []string{"2a03:b0c0:1:e00::29b:8001"}, + New: []string{""}, + ShouldSuppress: false, + }, + "switch address positions should suppress diff": { + Old: []string{"2a03:b0c0:1:e00::28b:8001", "2a03:b0c0:1:e0::29b:8001"}, + New: []string{"2a03:b0c0:1:e0::29b:8001", "2a03:b0c0:1:e00::28b:8001"}, + ShouldSuppress: true, + }, + } + + parseFunc := func(x string) string { + return net.ParseIP(x).String() + } + + for tn, tc := range cases { + shouldSuppress := rrdatasListDiffSuppress(tc.Old, tc.New, parseFunc, nil) + if shouldSuppress != tc.ShouldSuppress { + t.Errorf("%s: expected %t", tn, tc.ShouldSuppress) + } + } +} + func TestAccDNSRecordSet_basic(t *testing.T) { t.Parallel() @@ -109,30 +154,22 @@ func TestAccDNSRecordSet_changeType(t *testing.T) { }) } -func TestAccDNSRecordSet_rrdatasUpdate(t *testing.T) { +func TestAccDNSRecordSet_nestedNS(t *testing.T) { t.Parallel() - zoneName := fmt.Sprintf("dnszone-test-%s", randString(t, 10)) + zoneName := fmt.Sprintf("dnszone-test-ns-%s", randString(t, 10)) + recordSetName := fmt.Sprintf("\"nested.%s.hashicorptest.com.\"", zoneName) vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDnsRecordSetDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccDnsRecordSet_basic(zoneName, "127.0.0.10", 300), + Config: testAccDnsRecordSet_NS(zoneName, recordSetName, 300), }, { ResourceName: "google_dns_record_set.foobar", - ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./A", getTestProjectFromEnv(), zoneName, zoneName), - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccDnsRecordSet_rrdatasUpdate(zoneName, "127.0.0.10", 300), - }, - { - ResourceName: "google_dns_record_set.foobar", - ImportStateId: fmt.Sprintf("%s/%s/test-record.%s.hashicorptest.com./A", getTestProjectFromEnv(), zoneName, zoneName), + ImportStateId: fmt.Sprintf("%s/nested.%s.hashicorptest.com./NS", zoneName, zoneName), ImportState: true, ImportStateVerify: true, }, @@ -140,21 +177,22 @@ func TestAccDNSRecordSet_rrdatasUpdate(t *testing.T) { }) } -func TestAccDNSRecordSet_nestedNS(t *testing.T) { +func TestAccDNSRecordSet_secondaryNS(t *testing.T) { t.Parallel() zoneName := fmt.Sprintf("dnszone-test-ns-%s", randString(t, 10)) + recordSetName := "google_dns_managed_zone.parent-zone.dns_name" vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDnsRecordSetDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccDnsRecordSet_nestedNS(zoneName, 300), + Config: testAccDnsRecordSet_NS(zoneName, recordSetName, 300), }, { ResourceName: "google_dns_record_set.foobar", - ImportStateId: fmt.Sprintf("%s/nested.%s.hashicorptest.com./NS", zoneName, zoneName), + ImportStateId: fmt.Sprintf("projects/%s/managedZones/%s/rrsets/%s.hashicorptest.com./NS", getTestProjectFromEnv(), zoneName, zoneName), ImportState: true, ImportStateVerify: true, }, @@ -258,7 +296,7 @@ resource "google_dns_record_set" "foobar" { `, zoneName, zoneName, zoneName, addr2, ttl) } -func testAccDnsRecordSet_rrdatasUpdate(zoneName string, addr2 string, ttl int) string { +func testAccDnsRecordSet_NS(name string, recordSetName string, ttl int) string { return fmt.Sprintf(` resource "google_dns_managed_zone" "parent-zone" { name = "%s" @@ -268,30 +306,12 @@ resource "google_dns_managed_zone" "parent-zone" { resource "google_dns_record_set" "foobar" { managed_zone = google_dns_managed_zone.parent-zone.name - name = "test-record.%s.hashicorptest.com." - type = "A" - rrdatas = ["127.0.0.2", "%s"] - ttl = %d -} -`, zoneName, zoneName, zoneName, addr2, ttl) -} - -func testAccDnsRecordSet_nestedNS(name string, ttl int) string { - return fmt.Sprintf(` -resource "google_dns_managed_zone" "parent-zone" { - name = "%s" - dns_name = "%s.hashicorptest.com." - description = "Test Description" -} - -resource "google_dns_record_set" "foobar" { - managed_zone = google_dns_managed_zone.parent-zone.name - name = "nested.%s.hashicorptest.com." + name = %s type = "NS" rrdatas = ["ns.hashicorp.services.", "ns2.hashicorp.services."] ttl = %d } -`, name, name, name, ttl) +`, name, name, recordSetName, ttl) } func testAccDnsRecordSet_bigChange(zoneName string, ttl int) string { diff --git a/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown index 336ee302f3c3..ba43ccde3fce 100644 --- a/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown @@ -168,6 +168,7 @@ The following arguments are supported: DNS record sets can be imported using either of these accepted formats: ``` +$ terraform import google_dns_record_set.frontend projects/{{project}}/managedZones/{{zone}}/rrsets/{{name}}/{{type}} $ terraform import google_dns_record_set.frontend {{project}}/{{zone}}/{{name}}/{{type}} $ terraform import google_dns_record_set.frontend {{zone}}/{{name}}/{{type}} ``` From 360a86a248860c865103388cd0e1b1318fad1733 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 16 Sep 2021 13:02:50 -0500 Subject: [PATCH 4/4] update ID format in docs --- .../terraform/website/docs/r/dns_record_set.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown index ba43ccde3fce..4d62df958c6c 100644 --- a/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/dns_record_set.html.markdown @@ -161,7 +161,7 @@ The following arguments are supported: -In addition to the arguments listed above, the following computed attributes are -exported: -* `id` - an identifier for the resource with format `{{project}}/{{zone}}/{{name}}/{{type}}` +* `id` - an identifier for the resource with format `projects/{{project}}/managedZones/{{zone}}/rrsets/{{name}}/{{type}}` ## Import