diff --git a/builtin/providers/google/config.go b/builtin/providers/google/config.go index f9a58485f..de97df603 100644 --- a/builtin/providers/google/config.go +++ b/builtin/providers/google/config.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" "google.golang.org/api/compute/v1" + "google.golang.org/api/dns/v1" ) // Config is the configuration structure used to instantiate the Google @@ -24,6 +25,7 @@ type Config struct { Region string clientCompute *compute.Service + clientDns *dns.Service } func (c *Config) loadAndValidate() error { @@ -50,7 +52,10 @@ func (c *Config) loadAndValidate() error { err) } - clientScopes := []string{"https://www.googleapis.com/auth/compute"} + clientScopes := []string{ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite", + } // Get the token for use in our requests log.Printf("[INFO] Requesting Google token...") @@ -83,23 +88,31 @@ func (c *Config) loadAndValidate() error { } - log.Printf("[INFO] Instantiating GCE client...") - var err error - c.clientCompute, err = compute.New(client) - - // Set UserAgent + // Build UserAgent versionString := "0.0.0" // TODO(dcunnin): Use Terraform's version code from version.go // versionString := main.Version // if main.VersionPrerelease != "" { // versionString = fmt.Sprintf("%s-%s", versionString, main.VersionPrerelease) // } - c.clientCompute.UserAgent = fmt.Sprintf( + userAgent := fmt.Sprintf( "(%s %s) Terraform/%s", runtime.GOOS, runtime.GOARCH, versionString) + var err error + + log.Printf("[INFO] Instantiating GCE client...") + c.clientCompute, err = compute.New(client) if err != nil { return err } + c.clientCompute.UserAgent = userAgent + + log.Printf("[INFO] Instantiating Google Cloud DNS client...") + c.clientDns, err = dns.New(client) + if err != nil { + return err + } + c.clientDns.UserAgent = userAgent return nil } diff --git a/builtin/providers/google/dns_change.go b/builtin/providers/google/dns_change.go new file mode 100644 index 000000000..d8cb73cdf --- /dev/null +++ b/builtin/providers/google/dns_change.go @@ -0,0 +1,38 @@ +package google + +import ( + "google.golang.org/api/dns/v1" + + "github.com/hashicorp/terraform/helper/resource" +) + +type DnsChangeWaiter struct { + Service *dns.Service + Change *dns.Change + Project string + ManagedZone string +} + +func (w *DnsChangeWaiter) RefreshFunc() resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var chg *dns.Change + var err error + + chg, err = w.Service.Changes.Get( + w.Project, w.ManagedZone, w.Change.Id).Do() + + if err != nil { + return nil, "", err + } + + return chg, chg.Status, nil + } +} + +func (w *DnsChangeWaiter) Conf() *resource.StateChangeConf { + return &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: "done", + Refresh: w.RefreshFunc(), + } +} diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index c63b29402..09687a770 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -39,6 +39,8 @@ func Provider() terraform.ResourceProvider { "google_compute_network": resourceComputeNetwork(), "google_compute_route": resourceComputeRoute(), "google_compute_target_pool": resourceComputeTargetPool(), + "google_dns_managed_zone": resourceDnsManagedZone(), + "google_dns_record_set": resourceDnsRecordSet(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/google/resource_dns_managed_zone.go b/builtin/providers/google/resource_dns_managed_zone.go new file mode 100644 index 000000000..0e10d3b0b --- /dev/null +++ b/builtin/providers/google/resource_dns_managed_zone.go @@ -0,0 +1,108 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/dns/v1" + "google.golang.org/api/googleapi" +) + +func resourceDnsManagedZone() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsManagedZoneCreate, + Read: resourceDnsManagedZoneRead, + Delete: resourceDnsManagedZoneDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "dns_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "name_servers": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + // Google Cloud DNS ManagedZone resources do not have a SelfLink attribute. + }, + } +} + +func resourceDnsManagedZoneCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Build the parameter + zone := &dns.ManagedZone{ + Name: d.Get("name").(string), + DnsName: d.Get("dns_name").(string), + } + // Optional things + if v, ok := d.GetOk("description"); ok { + zone.Description = v.(string) + } + if v, ok := d.GetOk("dns_name"); ok { + zone.DnsName = v.(string) + } + + log.Printf("[DEBUG] DNS ManagedZone create request: %#v", zone) + zone, err := config.clientDns.ManagedZones.Create(config.Project, zone).Do() + if err != nil { + return fmt.Errorf("Error creating DNS ManagedZone: %s", err) + } + + d.SetId(zone.Name) + + return resourceDnsManagedZoneRead(d, meta) +} + +func resourceDnsManagedZoneRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + zone, err := config.clientDns.ManagedZones.Get( + config.Project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading DNS ManagedZone: %#v", err) + } + + d.Set("name_servers", zone.NameServers) + + return nil +} + +func resourceDnsManagedZoneDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + err := config.clientDns.ManagedZones.Delete(config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting DNS ManagedZone: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_dns_managed_zone_test.go b/builtin/providers/google/resource_dns_managed_zone_test.go new file mode 100644 index 000000000..2f91dfcc8 --- /dev/null +++ b/builtin/providers/google/resource_dns_managed_zone_test.go @@ -0,0 +1,83 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/dns/v1" +) + +func TestAccDnsManagedZone_basic(t *testing.T) { + var zone dns.ManagedZone + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsManagedZoneDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsManagedZone_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsManagedZoneExists( + "google_dns_managed_zone.foobar", &zone), + ), + }, + }, + }) +} + +func testAccCheckDnsManagedZoneDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_dns_zone" { + continue + } + + _, err := config.clientDns.ManagedZones.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("DNS ManagedZone still exists") + } + } + + return nil +} + +func testAccCheckDnsManagedZoneExists(n string, zone *dns.ManagedZone) 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") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientDns.ManagedZones.Get( + config.Project, rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("DNS Zone not found") + } + + *zone = *found + + return nil + } +} + +const testAccDnsManagedZone_basic = ` +resource "google_dns_managed_zone" "foobar" { + name = "terraform-test" + dns_name = "terraform.test." + description = "Test Description" +}` diff --git a/builtin/providers/google/resource_dns_record_set.go b/builtin/providers/google/resource_dns_record_set.go new file mode 100644 index 000000000..795d49985 --- /dev/null +++ b/builtin/providers/google/resource_dns_record_set.go @@ -0,0 +1,182 @@ +package google + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/dns/v1" +) + +func resourceDnsRecordSet() *schema.Resource { + return &schema.Resource{ + Create: resourceDnsRecordSetCreate, + Read: resourceDnsRecordSetRead, + Delete: resourceDnsRecordSetDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "managed_zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "rrdatas": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceDnsRecordSetCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + zone := d.Get("managed_zone").(string) + + rrdatasCount := d.Get("rrdatas.#").(int) + + // Build the change + chg := &dns.Change{ + Additions: []*dns.ResourceRecordSet { + &dns.ResourceRecordSet { + Name: d.Get("name").(string), + Type: d.Get("type").(string), + Ttl: int64(d.Get("ttl").(int)), + Rrdatas: make([]string, rrdatasCount), + }, + }, + } + + for i := 0; i < rrdatasCount ; i++ { + rrdata := fmt.Sprintf("rrdatas.%d", i) + chg.Additions[0].Rrdatas[i] = d.Get(rrdata).(string) + } + + log.Printf("[DEBUG] DNS Record create request: %#v", chg) + chg, err := config.clientDns.Changes.Create(config.Project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Error creating DNS RecordSet: %s", err) + } + + d.SetId(chg.Id) + + w := &DnsChangeWaiter{ + Service: config.clientDns, + Change: chg, + Project: config.Project, + ManagedZone: zone, + } + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = 10 * time.Minute + state.MinTimeout = 2 * time.Second + _, err = state.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) + + zone := d.Get("managed_zone").(string) + + // name and type are effectively the 'key' + name := d.Get("name").(string) + dnsType := d.Get("type").(string) + + resp, err := config.clientDns.ResourceRecordSets.List( + config.Project, zone).Name(name).Type(dnsType).Do() + if err != nil { + return fmt.Errorf("Error reading DNS RecordSet: %#v", err) + } + 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)) + } + + + d.Set("ttl", resp.Rrsets[0].Ttl) + d.Set("rrdatas", resp.Rrsets[0].Rrdatas) + + return nil +} + +func resourceDnsRecordSetDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + zone := d.Get("managed_zone").(string) + + rrdatasCount := d.Get("rrdatas.#").(int) + + // Build the change + chg := &dns.Change{ + Deletions: []*dns.ResourceRecordSet { + &dns.ResourceRecordSet { + Name: d.Get("name").(string), + Type: d.Get("type").(string), + Ttl: int64(d.Get("ttl").(int)), + Rrdatas: make([]string, rrdatasCount), + }, + }, + } + + for i := 0; i < rrdatasCount ; i++ { + rrdata := fmt.Sprintf("rrdatas.%d", i) + chg.Deletions[0].Rrdatas[i] = d.Get(rrdata).(string) + } + log.Printf("[DEBUG] DNS Record delete request: %#v", chg) + chg, err := config.clientDns.Changes.Create(config.Project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Error deleting DNS RecordSet: %s", err) + } + + w := &DnsChangeWaiter{ + Service: config.clientDns, + Change: chg, + Project: config.Project, + ManagedZone: zone, + } + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = 10 * time.Minute + state.MinTimeout = 2 * time.Second + _, err = state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_dns_record_set_test.go b/builtin/providers/google/resource_dns_record_set_test.go new file mode 100644 index 000000000..5ff123388 --- /dev/null +++ b/builtin/providers/google/resource_dns_record_set_test.go @@ -0,0 +1,92 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDnsRecordSet_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDnsRecordSetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDnsRecordSet_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDnsRecordSetExists( + "google_dns_record_set.foobar"), + ), + }, + }, + }) +} + +func testAccCheckDnsRecordSetDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + // Deletion of the managed_zone implies everything is gone + if rs.Type == "google_dns_managed_zone" { + _, err := config.clientDns.ManagedZones.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("DNS ManagedZone still exists") + } + } + } + + return nil +} + +func testAccCheckDnsRecordSetExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + dnsName := rs.Primary.Attributes["name"] + dnsType := rs.Primary.Attributes["type"] + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + resp, err := config.clientDns.ResourceRecordSets.List( + config.Project, "terraform-test-zone").Name(dnsName).Type(dnsType).Do() + if err != nil { + return fmt.Errorf("Error confirming DNS RecordSet existence: %#v", err) + } + if len(resp.Rrsets) == 0 { + // The resource doesn't exist anymore + return fmt.Errorf("DNS RecordSet not found") + } + + if len(resp.Rrsets) > 1 { + return fmt.Errorf("Only expected 1 record set, got %d", len(resp.Rrsets)) + } + + return nil + } +} + +const testAccDnsRecordSet_basic = ` +resource "google_dns_managed_zone" "parent-zone" { + name = "terraform-test-zone" + dns_name = "terraform.test." + description = "Test Description" +} +resource "google_dns_record_set" "foobar" { + managed_zone = "${google_dns_managed_zone.parent-zone.name}" + name = "test-record.terraform.test." + type = "A" + rrdatas = ["127.0.0.1", "127.0.0.10"] + ttl = 600 +} +` diff --git a/website/source/docs/providers/google/r/dns_managed_zone.markdown b/website/source/docs/providers/google/r/dns_managed_zone.markdown new file mode 100644 index 000000000..f3e650a27 --- /dev/null +++ b/website/source/docs/providers/google/r/dns_managed_zone.markdown @@ -0,0 +1,42 @@ +--- +layout: "google" +page_title: "Google: google_dns_managed_zone" +sidebar_current: "docs-google-resource-dns-managed-zone" +description: |- + Manages a zone within Google Cloud DNS. +--- + +# google\_dns\_managed_zone + +Manages a zone within Google Cloud DNS. + +## Example Usage + +``` +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." + description = "Production DNS zone" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `dns_name` - (Required) The DNS name of this zone, e.g. "terraform.io". + +* `description` - (Optional) A textual description field. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `dns_name` - The DNS name of this zone. +* `name_servers` - The list of nameservers that will be authoritative for this + domain. Use NS records to redirect from your DNS provider to these names, +thus making Google Cloud DNS authoritative for this zone. diff --git a/website/source/docs/providers/google/r/dns_record_set.markdown b/website/source/docs/providers/google/r/dns_record_set.markdown new file mode 100644 index 000000000..79ad2eb30 --- /dev/null +++ b/website/source/docs/providers/google/r/dns_record_set.markdown @@ -0,0 +1,64 @@ +--- +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. + +## Example Usage + +This example is the common case of binding a DNS name to the ephemeral IP of a new instance: + +``` +resource "google_compute_instance" "frontend" { + name = "frontend" + machine_type = "g1-small" + zone = "us-central1-b" + + disk { + image = "debian-7-wheezy-v20140814" + } + + network_interface { + network = "default" + access_config { + } + } +} +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} + +resource "google_dns_record_set" "frontend" { + managed_zone = "${google_dns_managed_zone.prod.name}" + name = "frontend.${google_dns_managed_zone.prod.dns_name}" + type = "A" + ttl = 300 + rrdatas = ["${google_compute_instance.frontend.network_interface.0.access_config.0.nat_ip}"] +} +``` + +## 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. + +* `type` - (Required) The DNS record set type. + +* `ttl` - (Required) The time-to-live of this record set (seconds). + +* `rrdatas` - (Required) The string data for the records in this record set + whose meaning depends on the DNS type. + +## Attributes Reference + +All arguments are available as attributes. diff --git a/website/source/layouts/google.erb b/website/source/layouts/google.erb index a6eb170e6..fbf05b201 100644 --- a/website/source/layouts/google.erb +++ b/website/source/layouts/google.erb @@ -52,6 +52,14 @@