diff --git a/builtin/providers/openstack/config.go b/builtin/providers/openstack/config.go index 0c6cb8995..142672af5 100644 --- a/builtin/providers/openstack/config.go +++ b/builtin/providers/openstack/config.go @@ -152,6 +152,13 @@ func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, err }) } +func (c *Config) dnsV2Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewDNSV2(c.osClient, gophercloud.EndpointOpts{ + Region: region, + Availability: c.getEndpointType(), + }) +} + func (c *Config) imageV2Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewImageServiceV2(c.osClient, gophercloud.EndpointOpts{ Region: region, diff --git a/builtin/providers/openstack/import_openstack_dns_zone_v2_test.go b/builtin/providers/openstack/import_openstack_dns_zone_v2_test.go new file mode 100644 index 000000000..ca7ec7a37 --- /dev/null +++ b/builtin/providers/openstack/import_openstack_dns_zone_v2_test.go @@ -0,0 +1,28 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDNSV2Zone_importBasic(t *testing.T) { + resourceName := "openstack_dns_zone_v2.zone_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDNSZoneV2(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSV2ZoneDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDNSV2Zone_basic, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index 294f207e9..1290ee266 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -150,6 +150,7 @@ func Provider() terraform.ResourceProvider { "openstack_compute_floatingip_v2": resourceComputeFloatingIPV2(), "openstack_compute_floatingip_associate_v2": resourceComputeFloatingIPAssociateV2(), "openstack_compute_volume_attach_v2": resourceComputeVolumeAttachV2(), + "openstack_dns_zone_v2": resourceDNSZoneV2(), "openstack_fw_firewall_v1": resourceFWFirewallV1(), "openstack_fw_policy_v1": resourceFWPolicyV1(), "openstack_fw_rule_v1": resourceFWRuleV1(), diff --git a/builtin/providers/openstack/resource_openstack_dns_zone_v2.go b/builtin/providers/openstack/resource_openstack_dns_zone_v2.go new file mode 100644 index 000000000..1e2a6a676 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_dns_zone_v2.go @@ -0,0 +1,195 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDNSZoneV2() *schema.Resource { + return &schema.Resource{ + Create: resourceDNSZoneV2Create, + Read: resourceDNSZoneV2Read, + Update: resourceDNSZoneV2Update, + Delete: resourceDNSZoneV2Delete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "email": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "attributes": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: false, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "masters": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: false, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "value_specs": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceDNSZoneV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + dnsClient, err := config.dnsV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + mastersraw := d.Get("masters").(*schema.Set).List() + masters := make([]string, len(mastersraw)) + for i, masterraw := range mastersraw { + masters[i] = masterraw.(string) + } + + attrsraw := d.Get("attributes").(map[string]interface{}) + attrs := make(map[string]string, len(attrsraw)) + for k, v := range attrsraw { + attrs[k] = v.(string) + } + + createOpts := ZoneCreateOpts{ + zones.CreateOpts{ + Name: d.Get("name").(string), + Type: d.Get("type").(string), + Attributes: attrs, + TTL: d.Get("ttl").(int), + Email: d.Get("email").(string), + Description: d.Get("description").(string), + Masters: masters, + }, + MapValueSpecs(d), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + n, err := zones.Create(dnsClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS zone: %s", err) + } + log.Printf("[INFO] Zone ID: %s", n.ID) + + d.SetId(n.ID) + + return resourceDNSZoneV2Read(d, meta) +} + +func resourceDNSZoneV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + dnsClient, err := config.dnsV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + n, err := zones.Get(dnsClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "zone") + } + + log.Printf("[DEBUG] Retrieved Zone %s: %+v", d.Id(), n) + + d.Set("name", n.Name) + d.Set("email", n.Email) + d.Set("description", n.Description) + d.Set("ttl", strconv.Itoa(n.TTL)) + d.Set("type", n.Type) + d.Set("attributes", n.Attributes) + d.Set("masters", n.Masters) + d.Set("region", GetRegion(d)) + + return nil +} + +func resourceDNSZoneV2Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + dnsClient, err := config.dnsV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + var updateOpts zones.UpdateOpts + if d.HasChange("email") { + updateOpts.Email = d.Get("email").(string) + } + if d.HasChange("ttl") { + updateOpts.TTL = d.Get("ttl").(int) + } + if d.HasChange("masters") { + updateOpts.Masters = d.Get("masters").([]string) + } + if d.HasChange("description") { + updateOpts.Description = d.Get("description").(string) + } + + log.Printf("[DEBUG] Updating Zone %s with options: %+v", d.Id(), updateOpts) + + _, err = zones.Update(dnsClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack DNS Zone: %s", err) + } + + return resourceDNSZoneV2Read(d, meta) +} + +func resourceDNSZoneV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + dnsClient, err := config.dnsV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + _, err = zones.Delete(dnsClient, d.Id()).Extract() + if err != nil { + return fmt.Errorf("Error deleting OpenStack DNS Zone: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_dns_zone_v2_test.go b/builtin/providers/openstack/resource_openstack_dns_zone_v2_test.go new file mode 100644 index 000000000..f656321d0 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_dns_zone_v2_test.go @@ -0,0 +1,145 @@ +package openstack + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" +) + +func TestAccDNSV2Zone_basic(t *testing.T) { + var zone zones.Zone + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDNSZoneV2(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSV2ZoneDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDNSV2Zone_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSV2ZoneExists("openstack_dns_zone_v2.zone_1", &zone), + ), + }, + resource.TestStep{ + Config: testAccDNSV2Zone_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_dns_zone_v2.zone_1", "name", "example.com"), + resource.TestCheckResourceAttr("openstack_dns_zone_v2.zone_1", "email", "email2@example.com"), + resource.TestCheckResourceAttr("openstack_dns_zone_v2.zone_1", "ttl", "6000"), + ), + }, + }, + }) +} + +func TestAccDNSV2Zone_timeout(t *testing.T) { + var zone zones.Zone + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDNSZoneV2(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSV2ZoneDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDNSV2Zone_timeout, + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSV2ZoneExists("openstack_dns_zone_v2.zone_1", &zone), + ), + }, + }, + }) +} + +func testAccCheckDNSV2ZoneDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + dnsClient, err := config.dnsV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_dns_zone_v2" { + continue + } + + _, err := zones.Get(dnsClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Zone still exists") + } + } + + return nil +} + +func testAccCheckDNSV2ZoneExists(n string, zone *zones.Zone) 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) + dnsClient, err := config.dnsV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack DNS client: %s", err) + } + + found, err := zones.Get(dnsClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Zone not found") + } + + *zone = *found + + return nil + } +} + +func testAccPreCheckDNSZoneV2(t *testing.T) { + v := os.Getenv("OS_AUTH_URL") + if v == "" { + t.Fatal("OS_AUTH_URL must be set for acceptance tests") + } +} + +const testAccDNSV2Zone_basic = ` +resource "openstack_dns_zone_v2" "zone_1" { + name = "example.com." + email = "email1@example.com" + ttl = 3000 +} +` + +const testAccDNSV2Zone_update = ` +resource "openstack_dns_zone_v2" "zone_1" { + name = "example.com." + email = "email2@example.com" + ttl = 6000 +} +` + +const testAccDNSV2Zone_timeout = ` +resource "openstack_dns_zone_v2" "zone_1" { + name = "example.com." + email = "email@example.com" + ttl = 3000 + + timeouts { + create = "5m" + delete = "5m" + } +} +` diff --git a/builtin/providers/openstack/types.go b/builtin/providers/openstack/types.go index c6c6a268a..b586e10b0 100644 --- a/builtin/providers/openstack/types.go +++ b/builtin/providers/openstack/types.go @@ -3,6 +3,7 @@ package openstack import ( "bytes" "encoding/json" + "fmt" "io" "io/ioutil" "log" @@ -11,6 +12,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules" @@ -288,3 +290,24 @@ func (opts SubnetCreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) return b, nil } + +// ZoneCreateOpts represents the attributes used when creating a new DNS zone. +type ZoneCreateOpts struct { + zones.CreateOpts + ValueSpecs map[string]string `json:"value_specs,omitempty"` +} + +// ToZoneCreateMap casts a CreateOpts struct to a map. +// It overrides zones.ToZoneCreateMap to add the ValueSpecs field. +func (opts ZoneCreateOpts) ToZoneCreateMap() (map[string]interface{}, error) { + b, err := BuildRequest(opts, "") + if err != nil { + return nil, err + } + + if m, ok := b[""].(map[string]interface{}); ok { + return m, nil + } + + return nil, fmt.Errorf("Expected map but got %T", b[""]) +}