diff --git a/builtin/bins/provider-dns/main.go b/builtin/bins/provider-dns/main.go new file mode 100644 index 000000000..2f3e9e1a8 --- /dev/null +++ b/builtin/bins/provider-dns/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/dns" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: dns.Provider, + }) +} diff --git a/builtin/bins/provider-dns/main_test.go b/builtin/bins/provider-dns/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-dns/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/dns/acceptance.sh b/builtin/providers/dns/acceptance.sh new file mode 100755 index 000000000..a86aea43e --- /dev/null +++ b/builtin/providers/dns/acceptance.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -eu +set -x + +# Test domains +export DNS_DOMAIN_FORWARD="example.com." +export DNS_DOMAIN_REVERSE="1.168.192.in-addr.arpa." + +# Run with no authentication + +export DNS_UPDATE_SERVER=127.0.0.1 +docker run -d -p 53:53/udp \ + -e BIND_DOMAIN_FORWARD=${DNS_DOMAIN_FORWARD} \ + -e BIND_DOMAIN_REVERSE=${DNS_DOMAIN_REVERSE} \ + -e BIND_INSECURE=true \ + --name bind_insecure drebes/bind +make testacc TEST=./builtin/providers/dns +docker stop bind_insecure +docker rm bind_insecure + +# Run with authentication + +export DNS_UPDATE_KEYNAME=${DNS_DOMAIN_FORWARD} +export DNS_UPDATE_KEYALGORITHM="hmac-md5" +export DNS_UPDATE_KEYSECRET="c3VwZXJzZWNyZXQ=" +docker run -d -p 53:53/udp \ + -e BIND_DOMAIN_FORWARD=${DNS_DOMAIN_FORWARD} \ + -e BIND_DOMAIN_REVERSE=${DNS_DOMAIN_REVERSE} \ + -e BIND_KEY_NAME=${DNS_UPDATE_KEYNAME} \ + -e BIND_KEY_ALGORITHM=${DNS_UPDATE_KEYALGORITHM} \ + -e BIND_KEY_SECRET=${DNS_UPDATE_KEYSECRET} \ + --name bind_secure drebes/bind +make testacc TEST=./builtin/providers/dns +docker stop bind_secure +docker rm bind_secure diff --git a/builtin/providers/dns/config.go b/builtin/providers/dns/config.go new file mode 100644 index 000000000..8921ebb84 --- /dev/null +++ b/builtin/providers/dns/config.go @@ -0,0 +1,67 @@ +package dns + +import ( + "fmt" + "github.com/miekg/dns" + "log" +) + +type Config struct { + server string + port int + keyname string + keyalgo string + keysecret string +} + +type DNSClient struct { + c *dns.Client + srv_addr string + keyname string + keysecret string + keyalgo string +} + +// Configures and returns a fully initialized DNSClient +func (c *Config) Client() (interface{}, error) { + log.Println("[INFO] Building DNSClient config structure") + + var client DNSClient + client.srv_addr = fmt.Sprintf("%s:%d", c.server, c.port) + authCfgOk := false + if (c.keyname == "" && c.keysecret == "" && c.keyalgo == "") || + (c.keyname != "" && c.keysecret != "" && c.keyalgo != "") { + authCfgOk = true + } + if !authCfgOk { + return nil, fmt.Errorf("Error configuring provider: when using authentication, \"key_name\", \"key_secret\" and \"key_algorithm\" should be non empty") + } + client.c = new(dns.Client) + if c.keyname != "" { + client.keyname = c.keyname + client.keysecret = c.keysecret + keyalgo, err := convertHMACAlgorithm(c.keyalgo) + if err != nil { + return nil, fmt.Errorf("Error configuring provider: %s", err) + } + client.keyalgo = keyalgo + client.c.TsigSecret = map[string]string{c.keyname: c.keysecret} + } + return &client, nil +} + +// Validates and converts HMAC algorithm +func convertHMACAlgorithm(name string) (string, error) { + switch name { + case "hmac-md5": + return dns.HmacMD5, nil + case "hmac-sha1": + return dns.HmacSHA1, nil + case "hmac-sha256": + return dns.HmacSHA256, nil + case "hmac-sha512": + return dns.HmacSHA512, nil + default: + return "", fmt.Errorf("Unknown HMAC algorithm: %s", name) + } +} diff --git a/builtin/providers/dns/provider.go b/builtin/providers/dns/provider.go new file mode 100644 index 000000000..785621782 --- /dev/null +++ b/builtin/providers/dns/provider.go @@ -0,0 +1,165 @@ +package dns + +import ( + "fmt" + "os" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/miekg/dns" +) + +// Provider returns a schema.Provider for DNS dynamic updates. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "update": &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("DNS_UPDATE_SERVER", nil), + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 53, + }, + "key_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DNS_UPDATE_KEYNAME", nil), + }, + "key_algorithm": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DNS_UPDATE_KEYALGORITHM", nil), + }, + "key_secret": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DNS_UPDATE_KEYSECRET", nil), + }, + }, + }, + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "dns_a_record_set": resourceDnsARecordSet(), + "dns_aaaa_record_set": resourceDnsAAAARecordSet(), + "dns_cname_record": resourceDnsCnameRecord(), + "dns_ptr_record": resourceDnsPtrRecord(), + }, + + ConfigureFunc: configureProvider, + } +} + +func configureProvider(d *schema.ResourceData) (interface{}, error) { + + var server, keyname, keyalgo, keysecret string + var port int + + // if the update block is missing, schema.EnvDefaultFunc is not called + if v, ok := d.GetOk("update"); ok { + update := v.([]interface{})[0].(map[string]interface{}) + if val, ok := update["port"]; ok { + port = int(val.(int)) + } + if val, ok := update["server"]; ok { + server = val.(string) + } + if val, ok := update["key_name"]; ok { + keyname = val.(string) + } + if val, ok := update["key_algorithm"]; ok { + keyalgo = val.(string) + } + if val, ok := update["key_secret"]; ok { + keysecret = val.(string) + } + } else { + if len(os.Getenv("DNS_UPDATE_SERVER")) > 0 { + server = os.Getenv("DNS_UPDATE_SERVER") + } else { + return nil, nil + } + port = 53 + if len(os.Getenv("DNS_UPDATE_KEYNAME")) > 0 { + keyname = os.Getenv("DNS_UPDATE_KEYNAME") + } + if len(os.Getenv("DNS_UPDATE_KEYALGORITHM")) > 0 { + keyalgo = os.Getenv("DNS_UPDATE_KEYALGORITHM") + } + if len(os.Getenv("DNS_UPDATE_KEYSECRET")) > 0 { + keysecret = os.Getenv("DNS_UPDATE_KEYSECRET") + } + } + + config := Config{ + server: server, + port: port, + keyname: keyname, + keyalgo: keyalgo, + keysecret: keysecret, + } + + return config.Client() +} + +func getAVal(record interface{}) (string, error) { + + recstr := record.(*dns.A).String() + var name, ttl, class, typ, addr string + + _, err := fmt.Sscanf(recstr, "%s\t%s\t%s\t%s\t%s", &name, &ttl, &class, &typ, &addr) + if err != nil { + return "", fmt.Errorf("Error parsing record: %s", err) + } + + return addr, nil +} + +func getAAAAVal(record interface{}) (string, error) { + + recstr := record.(*dns.AAAA).String() + var name, ttl, class, typ, addr string + + _, err := fmt.Sscanf(recstr, "%s\t%s\t%s\t%s\t%s", &name, &ttl, &class, &typ, &addr) + if err != nil { + return "", fmt.Errorf("Error parsing record: %s", err) + } + + return addr, nil +} + +func getCnameVal(record interface{}) (string, error) { + + recstr := record.(*dns.CNAME).String() + var name, ttl, class, typ, cname string + + _, err := fmt.Sscanf(recstr, "%s\t%s\t%s\t%s\t%s", &name, &ttl, &class, &typ, &cname) + if err != nil { + return "", fmt.Errorf("Error parsing record: %s", err) + } + + return cname, nil +} + +func getPtrVal(record interface{}) (string, error) { + + recstr := record.(*dns.PTR).String() + var name, ttl, class, typ, ptr string + + _, err := fmt.Sscanf(recstr, "%s\t%s\t%s\t%s\t%s", &name, &ttl, &class, &typ, &ptr) + if err != nil { + return "", fmt.Errorf("Error parsing record: %s", err) + } + + return ptr, nil +} diff --git a/builtin/providers/dns/provider_test.go b/builtin/providers/dns/provider_test.go new file mode 100644 index 000000000..52e1c6c26 --- /dev/null +++ b/builtin/providers/dns/provider_test.go @@ -0,0 +1,36 @@ +package dns + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "dns": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + v := os.Getenv("DNS_UPDATE_SERVER") + if v == "" { + t.Fatal("DNS_UPDATE_SERVER must be set for acceptance tests") + } +} diff --git a/builtin/providers/dns/resource_dns_a_record_set.go b/builtin/providers/dns/resource_dns_a_record_set.go new file mode 100644 index 000000000..75991edfd --- /dev/null +++ b/builtin/providers/dns/resource_dns_a_record_set.go @@ -0,0 +1,212 @@ +package dns + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/miekg/dns" +) + +func resourceDnsARecordSet() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsARecordSetCreate, + Read: resourceDnsARecordSetRead, + Update: resourceDnsARecordSetUpdate, + Delete: resourceDnsARecordSetDelete, + + Schema: map[string]*schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "addresses": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 3600, + }, + }, + } +} + +func resourceDnsARecordSetCreate(d *schema.ResourceData, meta interface{}) error { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error creating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + d.SetId(rec_fqdn) + + return resourceDnsARecordSetUpdate(d, meta) +} + +func resourceDnsARecordSetRead(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeA) + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record: %v", r.Rcode) + } + + addresses := schema.NewSet(schema.HashString, nil) + for _, record := range r.Answer { + addr, err := getAVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + addresses.Add(addr) + } + if !addresses.Equal(d.Get("addresses")) { + d.SetId("") + return fmt.Errorf("DNS record differs") + } + return nil + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsARecordSetUpdate(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + ttl := d.Get("ttl").(int) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + if d.HasChange("addresses") { + o, n := d.GetChange("addresses") + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := os.Difference(ns).List() + add := ns.Difference(os).List() + + // Loop through all the old addresses and remove them + for _, addr := range remove { + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s %d A %s", rec_fqdn, ttl, addr.(string))) + msg.Remove([]dns.RR{rr_remove}) + } + // Loop through all the new addresses and insert them + for _, addr := range add { + rr_insert, _ := dns.NewRR(fmt.Sprintf("%s %d A %s", rec_fqdn, ttl, addr.(string))) + msg.Insert([]dns.RR{rr_insert}) + } + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %v", r.Rcode) + } + + addresses := ns + d.Set("addresses", addresses) + } + + return resourceDnsARecordSetRead(d, meta) + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsARecordSetDelete(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s 0 A", rec_fqdn)) + msg.RemoveRRset([]dns.RR{rr_remove}) + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error deleting DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error deleting DNS record: %v", r.Rcode) + } + + return nil + } else { + return fmt.Errorf("update server is not set") + } +} diff --git a/builtin/providers/dns/resource_dns_a_record_set_test.go b/builtin/providers/dns/resource_dns_a_record_set_test.go new file mode 100644 index 000000000..5eb632e42 --- /dev/null +++ b/builtin/providers/dns/resource_dns_a_record_set_test.go @@ -0,0 +1,133 @@ +package dns + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/miekg/dns" +) + +func TestAccDnsARecordSet_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsARecordSetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsARecordSet_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("dns_a_record_set.foo", "addresses.#", "2"), + testAccCheckDnsARecordSetExists(t, "dns_a_record_set.foo", []interface{}{"192.168.0.2", "192.168.0.1"}), + ), + }, + resource.TestStep{ + Config: testAccDnsARecordSet_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("dns_a_record_set.foo", "addresses.#", "3"), + testAccCheckDnsARecordSetExists(t, "dns_a_record_set.foo", []interface{}{"10.0.0.3", "10.0.0.2", "10.0.0.1"}), + ), + }, + }, + }) +} + +func testAccCheckDnsARecordSetDestroy(s *terraform.State) error { + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + for _, rs := range s.RootModule().Resources { + if rs.Type != "dns_a_record_set" { + continue + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeA) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeNameError { + return fmt.Errorf("DNS record still exists: %v", r.Rcode) + } + } + + return nil +} + +func testAccCheckDnsARecordSetExists(t *testing.T, n string, addr []interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeA) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record") + } + + addresses := schema.NewSet(schema.HashString, nil) + expected := schema.NewSet(schema.HashString, addr) + for _, record := range r.Answer { + addr, err := getAVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + addresses.Add(addr) + } + if !addresses.Equal(expected) { + return fmt.Errorf("DNS record differs: expected %v, found %v", expected, addresses) + } + return nil + } +} + +var testAccDnsARecordSet_basic = fmt.Sprintf(` + resource "dns_a_record_set" "foo" { + zone = "example.com." + name = "foo" + addresses = ["192.168.0.1", "192.168.0.2"] + ttl = 300 + }`) + +var testAccDnsARecordSet_update = fmt.Sprintf(` + resource "dns_a_record_set" "foo" { + zone = "example.com." + name = "foo" + addresses = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + ttl = 300 + }`) diff --git a/builtin/providers/dns/resource_dns_aaaa_record_set.go b/builtin/providers/dns/resource_dns_aaaa_record_set.go new file mode 100644 index 000000000..96bcc4c30 --- /dev/null +++ b/builtin/providers/dns/resource_dns_aaaa_record_set.go @@ -0,0 +1,212 @@ +package dns + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/miekg/dns" +) + +func resourceDnsAAAARecordSet() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsAAAARecordSetCreate, + Read: resourceDnsAAAARecordSetRead, + Update: resourceDnsAAAARecordSetUpdate, + Delete: resourceDnsAAAARecordSetDelete, + + Schema: map[string]*schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "addresses": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 3600, + }, + }, + } +} + +func resourceDnsAAAARecordSetCreate(d *schema.ResourceData, meta interface{}) error { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error creating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + d.SetId(rec_fqdn) + + return resourceDnsAAAARecordSetUpdate(d, meta) +} + +func resourceDnsAAAARecordSetRead(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeAAAA) + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record: %v", r.Rcode) + } + + addresses := schema.NewSet(schema.HashString, nil) + for _, record := range r.Answer { + addr, err := getAAAAVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + addresses.Add(addr) + } + if !addresses.Equal(d.Get("addresses")) { + d.SetId("") + return fmt.Errorf("DNS record differs") + } + return nil + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsAAAARecordSetUpdate(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + ttl := d.Get("ttl").(int) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + if d.HasChange("addresses") { + o, n := d.GetChange("addresses") + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := os.Difference(ns).List() + add := ns.Difference(os).List() + + // Loop through all the old addresses and remove them + for _, addr := range remove { + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s %d AAAA %s", rec_fqdn, ttl, addr.(string))) + msg.Remove([]dns.RR{rr_remove}) + } + // Loop through all the new addresses and insert them + for _, addr := range add { + rr_insert, _ := dns.NewRR(fmt.Sprintf("%s %d AAAA %s", rec_fqdn, ttl, addr.(string))) + msg.Insert([]dns.RR{rr_insert}) + } + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %v", r.Rcode) + } + + addresses := ns + d.Set("addresses", addresses) + } + + return resourceDnsAAAARecordSetRead(d, meta) + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsAAAARecordSetDelete(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s 0 AAAA", rec_fqdn)) + msg.RemoveRRset([]dns.RR{rr_remove}) + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error deleting DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error deleting DNS record: %v", r.Rcode) + } + + return nil + } else { + return fmt.Errorf("update server is not set") + } +} diff --git a/builtin/providers/dns/resource_dns_aaaa_record_set_test.go b/builtin/providers/dns/resource_dns_aaaa_record_set_test.go new file mode 100644 index 000000000..ec8ea1571 --- /dev/null +++ b/builtin/providers/dns/resource_dns_aaaa_record_set_test.go @@ -0,0 +1,133 @@ +package dns + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/miekg/dns" +) + +func TestAccDnsAAAARecordSet_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsAAAARecordSetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsAAAARecordSet_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("dns_aaaa_record_set.bar", "addresses.#", "2"), + testAccCheckDnsAAAARecordSetExists(t, "dns_aaaa_record_set.bar", []interface{}{"fdd5:e282:43b8:5303:dead:beef:cafe:babe", "fdd5:e282:43b8:5303:cafe:babe:dead:beef"}), + ), + }, + resource.TestStep{ + Config: testAccDnsAAAARecordSet_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("dns_aaaa_record_set.bar", "addresses.#", "2"), + testAccCheckDnsAAAARecordSetExists(t, "dns_aaaa_record_set.bar", []interface{}{"fdd5:e282:43b8:5303:beef:dead:babe:cafe", "fdd5:e282:43b8:5303:babe:cafe:beef:dead"}), + ), + }, + }, + }) +} + +func testAccCheckDnsAAAARecordSetDestroy(s *terraform.State) error { + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + for _, rs := range s.RootModule().Resources { + if rs.Type != "dns_aaaa_record_set" { + continue + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeAAAA) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeNameError { + return fmt.Errorf("DNS record still exists: %v", r.Rcode) + } + } + + return nil +} + +func testAccCheckDnsAAAARecordSetExists(t *testing.T, n string, addr []interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeAAAA) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record") + } + + addresses := schema.NewSet(schema.HashString, nil) + expected := schema.NewSet(schema.HashString, addr) + for _, record := range r.Answer { + addr, err := getAAAAVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + addresses.Add(addr) + } + if !addresses.Equal(expected) { + return fmt.Errorf("DNS record differs: expected %v, found %v", expected, addresses) + } + return nil + } +} + +var testAccDnsAAAARecordSet_basic = fmt.Sprintf(` + resource "dns_aaaa_record_set" "bar" { + zone = "example.com." + name = "bar" + addresses = ["fdd5:e282:43b8:5303:dead:beef:cafe:babe", "fdd5:e282:43b8:5303:cafe:babe:dead:beef"] + ttl = 300 + }`) + +var testAccDnsAAAARecordSet_update = fmt.Sprintf(` + resource "dns_aaaa_record_set" "bar" { + zone = "example.com." + name = "bar" + addresses = ["fdd5:e282:43b8:5303:beef:dead:babe:cafe", "fdd5:e282:43b8:5303:babe:cafe:beef:dead"] + ttl = 300 + }`) diff --git a/builtin/providers/dns/resource_dns_cname_record.go b/builtin/providers/dns/resource_dns_cname_record.go new file mode 100644 index 000000000..25851ec1a --- /dev/null +++ b/builtin/providers/dns/resource_dns_cname_record.go @@ -0,0 +1,219 @@ +package dns + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/miekg/dns" +) + +func resourceDnsCnameRecord() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsCnameRecordCreate, + Read: resourceDnsCnameRecordRead, + Update: resourceDnsCnameRecordUpdate, + Delete: resourceDnsCnameRecordDelete, + + Schema: map[string]*schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cname": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 3600, + }, + }, + } +} + +func resourceDnsCnameRecordCreate(d *schema.ResourceData, meta interface{}) error { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_cname := d.Get("cname").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error creating DNS record: \"zone\" should be an FQDN") + } + + if rec_cname != dns.Fqdn(rec_cname) { + return fmt.Errorf("Error creating DNS record: \"cname\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + d.SetId(rec_fqdn) + + return resourceDnsCnameRecordUpdate(d, meta) +} + +func resourceDnsCnameRecordRead(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_cname := d.Get("cname").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + if rec_cname != dns.Fqdn(rec_cname) { + return fmt.Errorf("Error reading DNS record: \"cname\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeCNAME) + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record: %v", r.Rcode) + } + + if len(r.Answer) > 1 { + return fmt.Errorf("Error querying DNS record: multiple responses received") + } + record := r.Answer[0] + cname, err := getCnameVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if rec_cname != cname { + d.SetId("") + return fmt.Errorf("DNS record differs") + } + return nil + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsCnameRecordUpdate(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_cname := d.Get("cname").(string) + ttl := d.Get("ttl").(int) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + if rec_cname != dns.Fqdn(rec_cname) { + return fmt.Errorf("Error updating DNS record: \"cname\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + if d.HasChange("cname") { + o, n := d.GetChange("cname") + + if o != "" { + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s %d CNAME %s", rec_fqdn, ttl, o)) + msg.Remove([]dns.RR{rr_remove}) + } + if n != "" { + rr_insert, _ := dns.NewRR(fmt.Sprintf("%s %d CNAME %s", rec_fqdn, ttl, n)) + msg.Insert([]dns.RR{rr_insert}) + } + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %v", r.Rcode) + } + + cname := n + d.Set("cname", cname) + } + + return resourceDnsCnameRecordRead(d, meta) + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsCnameRecordDelete(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s 0 CNAME", rec_fqdn)) + msg.RemoveRRset([]dns.RR{rr_remove}) + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error deleting DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error deleting DNS record: %v", r.Rcode) + } + + return nil + } else { + return fmt.Errorf("update server is not set") + } +} diff --git a/builtin/providers/dns/resource_dns_cname_record_test.go b/builtin/providers/dns/resource_dns_cname_record_test.go new file mode 100644 index 000000000..ef000933d --- /dev/null +++ b/builtin/providers/dns/resource_dns_cname_record_test.go @@ -0,0 +1,129 @@ +package dns + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/miekg/dns" +) + +func TestAccDnsCnameRecord_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsCnameRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsCnameRecord_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsCnameRecordExists(t, "dns_cname_record.foo", "bar.example.com."), + ), + }, + resource.TestStep{ + Config: testAccDnsCnameRecord_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsCnameRecordExists(t, "dns_cname_record.foo", "baz.example.com."), + ), + }, + }, + }) +} + +func testAccCheckDnsCnameRecordDestroy(s *terraform.State) error { + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + for _, rs := range s.RootModule().Resources { + if rs.Type != "dns_cname_record" { + continue + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeCNAME) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeNameError { + return fmt.Errorf("DNS record still exists: %v", r.Rcode) + } + } + + return nil +} + +func testAccCheckDnsCnameRecordExists(t *testing.T, n string, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypeCNAME) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record") + } + + if len(r.Answer) > 1 { + return fmt.Errorf("Error querying DNS record: multiple responses received") + } + record := r.Answer[0] + cname, err := getCnameVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if expected != cname { + return fmt.Errorf("DNS record differs: expected %v, found %v", expected, cname) + } + return nil + } +} + +var testAccDnsCnameRecord_basic = fmt.Sprintf(` + resource "dns_cname_record" "foo" { + zone = "example.com." + name = "foo" + cname = "bar.example.com." + ttl = 300 + }`) + +var testAccDnsCnameRecord_update = fmt.Sprintf(` + resource "dns_cname_record" "foo" { + zone = "example.com." + name = "baz" + cname = "baz.example.com." + ttl = 300 + }`) diff --git a/builtin/providers/dns/resource_dns_ptr_record.go b/builtin/providers/dns/resource_dns_ptr_record.go new file mode 100644 index 000000000..7515bd2f8 --- /dev/null +++ b/builtin/providers/dns/resource_dns_ptr_record.go @@ -0,0 +1,219 @@ +package dns + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/miekg/dns" +) + +func resourceDnsPtrRecord() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsPtrRecordCreate, + Read: resourceDnsPtrRecordRead, + Update: resourceDnsPtrRecordUpdate, + Delete: resourceDnsPtrRecordDelete, + + Schema: map[string]*schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ptr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 3600, + }, + }, + } +} + +func resourceDnsPtrRecordCreate(d *schema.ResourceData, meta interface{}) error { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_ptr := d.Get("ptr").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error creating DNS record: \"zone\" should be an FQDN") + } + + if rec_ptr != dns.Fqdn(rec_ptr) { + return fmt.Errorf("Error creating DNS record: \"ptr\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + d.SetId(rec_fqdn) + + return resourceDnsPtrRecordUpdate(d, meta) +} + +func resourceDnsPtrRecordRead(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_ptr := d.Get("ptr").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + if rec_ptr != dns.Fqdn(rec_ptr) { + return fmt.Errorf("Error reading DNS record: \"ptr\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypePTR) + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record: %v", r.Rcode) + } + + if len(r.Answer) > 1 { + return fmt.Errorf("Error querying DNS record: multiple responses received") + } + record := r.Answer[0] + ptr, err := getPtrVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if rec_ptr != ptr { + d.SetId("") + return fmt.Errorf("DNS record differs") + } + return nil + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsPtrRecordUpdate(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + rec_ptr := d.Get("ptr").(string) + ttl := d.Get("ttl").(int) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + if rec_ptr != dns.Fqdn(rec_ptr) { + return fmt.Errorf("Error updating DNS record: \"ptr\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + if d.HasChange("ptr") { + o, n := d.GetChange("ptr") + + if o != "" { + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s %d PTR %s", rec_fqdn, ttl, o)) + msg.Remove([]dns.RR{rr_remove}) + } + if n != "" { + rr_insert, _ := dns.NewRR(fmt.Sprintf("%s %d PTR %s", rec_fqdn, ttl, n)) + msg.Insert([]dns.RR{rr_insert}) + } + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + d.SetId("") + return fmt.Errorf("Error updating DNS record: %v", r.Rcode) + } + + ptr := n + d.Set("ptr", ptr) + } + + return resourceDnsPtrRecordRead(d, meta) + } else { + return fmt.Errorf("update server is not set") + } +} + +func resourceDnsPtrRecordDelete(d *schema.ResourceData, meta interface{}) error { + + if meta != nil { + + rec_name := d.Get("name").(string) + rec_zone := d.Get("zone").(string) + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error updating DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + keyname := meta.(*DNSClient).keyname + keyalgo := meta.(*DNSClient).keyalgo + + msg := new(dns.Msg) + + msg.SetUpdate(rec_zone) + + rr_remove, _ := dns.NewRR(fmt.Sprintf("%s 0 PTR", rec_fqdn)) + msg.RemoveRRset([]dns.RR{rr_remove}) + + if keyname != "" { + msg.SetTsig(keyname, keyalgo, 300, time.Now().Unix()) + } + + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error deleting DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error deleting DNS record: %v", r.Rcode) + } + + return nil + } else { + return fmt.Errorf("update server is not set") + } +} diff --git a/builtin/providers/dns/resource_dns_ptr_record_test.go b/builtin/providers/dns/resource_dns_ptr_record_test.go new file mode 100644 index 000000000..4729bf1e5 --- /dev/null +++ b/builtin/providers/dns/resource_dns_ptr_record_test.go @@ -0,0 +1,129 @@ +package dns + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/miekg/dns" +) + +func TestAccDnsPtrRecord_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsPtrRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsPtrRecord_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsPtrRecordExists(t, "dns_ptr_record.foo", "bar.example.com."), + ), + }, + resource.TestStep{ + Config: testAccDnsPtrRecord_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsPtrRecordExists(t, "dns_ptr_record.foo", "baz.example.com."), + ), + }, + }, + }) +} + +func testAccCheckDnsPtrRecordDestroy(s *terraform.State) error { + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + for _, rs := range s.RootModule().Resources { + if rs.Type != "dns_ptr_record" { + continue + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypePTR) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeNameError { + return fmt.Errorf("DNS record still exists: %v", r.Rcode) + } + } + + return nil +} + +func testAccCheckDnsPtrRecordExists(t *testing.T, n string, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + rec_name := rs.Primary.Attributes["name"] + rec_zone := rs.Primary.Attributes["zone"] + + if rec_zone != dns.Fqdn(rec_zone) { + return fmt.Errorf("Error reading DNS record: \"zone\" should be an FQDN") + } + + rec_fqdn := fmt.Sprintf("%s.%s", rec_name, rec_zone) + + meta := testAccProvider.Meta() + c := meta.(*DNSClient).c + srv_addr := meta.(*DNSClient).srv_addr + + msg := new(dns.Msg) + msg.SetQuestion(rec_fqdn, dns.TypePTR) + r, _, err := c.Exchange(msg, srv_addr) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + return fmt.Errorf("Error querying DNS record") + } + + if len(r.Answer) > 1 { + return fmt.Errorf("Error querying DNS record: multiple responses received") + } + record := r.Answer[0] + ptr, err := getPtrVal(record) + if err != nil { + return fmt.Errorf("Error querying DNS record: %s", err) + } + if expected != ptr { + return fmt.Errorf("DNS record differs: expected %v, found %v", expected, ptr) + } + return nil + } +} + +var testAccDnsPtrRecord_basic = fmt.Sprintf(` + resource "dns_ptr_record" "foo" { + zone = "example.com." + name = "r._dns-sd._udp" + ptr = "bar.example.com." + ttl = 300 + }`) + +var testAccDnsPtrRecord_update = fmt.Sprintf(` + resource "dns_ptr_record" "foo" { + zone = "example.com." + name = "r._dns-sd._udp" + ptr = "baz.example.com." + ttl = 300 + }`) diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index dab3e3cbc..5c6f4c006 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -22,6 +22,7 @@ import ( datadogprovider "github.com/hashicorp/terraform/builtin/providers/datadog" digitaloceanprovider "github.com/hashicorp/terraform/builtin/providers/digitalocean" dmeprovider "github.com/hashicorp/terraform/builtin/providers/dme" + dnsprovider "github.com/hashicorp/terraform/builtin/providers/dns" dnsimpleprovider "github.com/hashicorp/terraform/builtin/providers/dnsimple" dockerprovider "github.com/hashicorp/terraform/builtin/providers/docker" dynprovider "github.com/hashicorp/terraform/builtin/providers/dyn" @@ -93,6 +94,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "datadog": datadogprovider.Provider, "digitalocean": digitaloceanprovider.Provider, "dme": dmeprovider.Provider, + "dns": dnsprovider.Provider, "dnsimple": dnsimpleprovider.Provider, "docker": dockerprovider.Provider, "dyn": dynprovider.Provider, diff --git a/website/source/docs/providers/dns/index.html.markdown b/website/source/docs/providers/dns/index.html.markdown new file mode 100644 index 000000000..c97a15deb --- /dev/null +++ b/website/source/docs/providers/dns/index.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "dns" +page_title: "Provider: DNS" +sidebar_current: "docs-dns-index" +description: |- + The DNS provider supports DNS updates (RFC 2136). Additionally, the provider can be configured with secret key based transaction authentication (RFC 2845). +--- + +# DNS Provider + +The DNS provider supports DNS updates (RFC 2136). Additionally, the provider can be configured with secret key based transaction authentication (RFC 2845). + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the DNS Provider +provider "dns" { + update { + server = "192.168.0.1" + key_name = "example.com." + key_algorithm = "hmac-md5" + key_secret = "3VwZXJzZWNyZXQ=" + } +} + +# Create a DNS A record set +resource "dns_a_record_set" "www" { + ... +} +``` + +## Configuration Reference + +`update` - (Optional) When the provider is used for DNS updates, this block is required. Structure is documented below. + +The `update` block supports the following attributes: + +* `server` - (Required) The IPv4 address of the DNS server to send updates to. +* `port` - (Optional) The target UDP port on the server where updates are sent to. Defaults to `53`. +* `key_name` - (Optional) The name of the TSIG key used to sign the DNS update messages. +* `key_algorithm` - (Optional; Required if `key_name` is set) When using TSIG authentication, the algorithm to use for HMAC. Valid values are `hmac-md5`, `hmac-sha1`, `hmac-sha256` or `hmac-sha512`. +* `key_secret` - (Optional; Required if `key_name` is set) + A Base64-encoded string containing the shared secret to be used for TSIG. diff --git a/website/source/docs/providers/dns/r/dns_a_record_set.html.markdown b/website/source/docs/providers/dns/r/dns_a_record_set.html.markdown new file mode 100644 index 000000000..f0ae0ffb7 --- /dev/null +++ b/website/source/docs/providers/dns/r/dns_a_record_set.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "dns" +page_title: "DNS: dns_a_record_set" +sidebar_current: "docs-dns-record" +description: |- + Creates a A type DNS record set. +--- + +# dns\_a\_record\_set + +Creates a A type DNS record set. + +## Example Usage + +``` + resource "dns_a_record_set" "www" { + zone = "example.com." + name = "www" + addresses = ["192.168.0.1", "192.168.0.2", "192.168.0.3"] + ttl = 300 + } +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) DNS zone the record set belongs to. It must be an FQDN, that is, include the trailing dot. +* `name` - (Required) The name of the record set. The `zone` argument will be appended to this value to create the full record path. +* `addresses` - (Required) The IPv4 addresses this record set will point to. +* `ttl` - (Optional) The TTL of the record set. Defaults to `3600`. + +## Attributes Reference + +The following attributes are exported: + +* `zone` - See Argument Reference above. +* `name` - See Argument Reference above. +* `addresses` - See Argument Reference above. +* `ttl` - See Argument Reference above. + diff --git a/website/source/docs/providers/dns/r/dns_aaaa_record_set.html.markdown b/website/source/docs/providers/dns/r/dns_aaaa_record_set.html.markdown new file mode 100644 index 000000000..291424771 --- /dev/null +++ b/website/source/docs/providers/dns/r/dns_aaaa_record_set.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "dns" +page_title: "DNS: dns_aaaa_record_set" +sidebar_current: "docs-dns-record" +description: |- + Creates a AAAA type DNS record set. +--- + +# dns\_a\_record\_set + +Creates a AAAA type DNS record set. + +## Example Usage + +``` + resource "dns_aaaa_record_set" "www" { + zone = "example.com." + name = "www" + addresses = ["fdd5:e282:43b8:5303:dead:beef:cafe:babe", "fdd5:e282:43b8:5303:cafe:babe:dead:beef"] + ttl = 300 + } +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) DNS zone the record set belongs to. It must be an FQDN, that is, include the trailing dot. +* `name` - (Required) The name of the record set. The `zone` argument will be appended to this value to create the full record path. +* `addresses` - (Required) The IPv6 addresses this record set will point to. +* `ttl` - (Optional) The TTL of the record set. Defaults to `3600`. + +## Attributes Reference + +The following attributes are exported: + +* `zone` - See Argument Reference above. +* `name` - See Argument Reference above. +* `addresses` - See Argument Reference above. +* `ttl` - See Argument Reference above. + diff --git a/website/source/docs/providers/dns/r/dns_cname_record.html.markdown b/website/source/docs/providers/dns/r/dns_cname_record.html.markdown new file mode 100644 index 000000000..5627497e4 --- /dev/null +++ b/website/source/docs/providers/dns/r/dns_cname_record.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "dns" +page_title: "DNS: dns_cname_record" +sidebar_current: "docs-dns-record" +description: |- + Creates a CNAME type DNS record. +--- + +# dns\_ptr\_record + +Creates a CNAME type DNS record. + +## Example Usage + +``` + resource "dns_cname_record" "foo" { + zone = "example.com." + name = "foo" + cname = "bar.example.com." + ttl = 300 + } +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) DNS zone the record belongs to. It must be an FQDN, that is, include the trailing dot. +* `name` - (Required) The name of the record. The `zone` argument will be appended to this value to create the full record path. +* `cname` - (Required) The canonical name this record will point to. +* `ttl` - (Optional) The TTL of the record set. Defaults to `3600`. + +## Attributes Reference + +The following attributes are exported: + +* `zone` - See Argument Reference above. +* `name` - See Argument Reference above. +* `cname` - See Argument Reference above. +* `ttl` - See Argument Reference above. + diff --git a/website/source/docs/providers/dns/r/dns_ptr_record.html.markdown b/website/source/docs/providers/dns/r/dns_ptr_record.html.markdown new file mode 100644 index 000000000..ff8b92a27 --- /dev/null +++ b/website/source/docs/providers/dns/r/dns_ptr_record.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "dns" +page_title: "DNS: dns_ptr_record" +sidebar_current: "docs-dns-record" +description: |- + Creates a PTR type DNS record. +--- + +# dns\_ptr\_record + +Creates a PTR type DNS record. + +## Example Usage + +``` + resource "dns_ptr_record" "dns-sd" { + zone = "example.com." + name = "r._dns-sd" + ptr = "example.com." + ttl = 300 + } +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) DNS zone the record belongs to. It must be an FQDN, that is, include the trailing dot. +* `name` - (Required) The name of the record. The `zone` argument will be appended to this value to create the full record path. +* `ptr` - (Required) The canonical name this record will point to. +* `ttl` - (Optional) The TTL of the record set. Defaults to `3600`. + +## Attributes Reference + +The following attributes are exported: + +* `zone` - See Argument Reference above. +* `name` - See Argument Reference above. +* `ptr` - See Argument Reference above. +* `ttl` - See Argument Reference above. +