From 12f6ccb731dccf9a78265f5ccdaedef7fc161891 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Mon, 17 Nov 2014 18:55:45 +0100 Subject: [PATCH] Refactor the DigitalOcean provider With this refactor the DigitalOcean provider is updated to use the schema.Provider approach released with TF 0.2. --- builtin/bins/provider-digitalocean/main.go | 5 +- builtin/providers/digitalocean/config.go | 14 +- builtin/providers/digitalocean/provider.go | 43 +- .../providers/digitalocean/provider_test.go | 26 +- .../resource_digitalocean_domain.go | 51 +- .../resource_digitalocean_domain_test.go | 4 +- .../resource_digitalocean_droplet.go | 518 +++++++++--------- .../resource_digitalocean_droplet_test.go | 34 +- .../resource_digitalocean_record.go | 96 ++-- .../resource_digitalocean_record_test.go | 4 +- .../digitalocean/resource_provider.go | 99 ---- .../digitalocean/resource_provider_test.go | 63 --- builtin/providers/digitalocean/resources.go | 24 - 13 files changed, 419 insertions(+), 562 deletions(-) delete mode 100644 builtin/providers/digitalocean/resource_provider.go delete mode 100644 builtin/providers/digitalocean/resource_provider_test.go delete mode 100644 builtin/providers/digitalocean/resources.go diff --git a/builtin/bins/provider-digitalocean/main.go b/builtin/bins/provider-digitalocean/main.go index 86d2acf7a..7b43c053a 100644 --- a/builtin/bins/provider-digitalocean/main.go +++ b/builtin/bins/provider-digitalocean/main.go @@ -3,13 +3,10 @@ package main import ( "github.com/hashicorp/terraform/builtin/providers/digitalocean" "github.com/hashicorp/terraform/plugin" - "github.com/hashicorp/terraform/terraform" ) func main() { plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: func() terraform.ResourceProvider { - return new(digitalocean.ResourceProvider) - }, + ProviderFunc: digitalocean.Provider, }) } diff --git a/builtin/providers/digitalocean/config.go b/builtin/providers/digitalocean/config.go index a81ff7a48..c9a43bc09 100644 --- a/builtin/providers/digitalocean/config.go +++ b/builtin/providers/digitalocean/config.go @@ -2,26 +2,16 @@ package digitalocean import ( "log" - "os" "github.com/pearkes/digitalocean" ) type Config struct { - Token string `mapstructure:"token"` + Token string } -// Client() returns a new client for accessing digital -// ocean. -// +// Client() returns a new client for accessing digital ocean. func (c *Config) Client() (*digitalocean.Client, error) { - - // If we have env vars set (like in the acc) tests, - // we need to override the values passed in here. - if v := os.Getenv("DIGITALOCEAN_TOKEN"); v != "" { - c.Token = v - } - client, err := digitalocean.NewClient(c.Token) log.Printf("[INFO] DigitalOcean Client configured for URL: %s", client.URL) diff --git a/builtin/providers/digitalocean/provider.go b/builtin/providers/digitalocean/provider.go index 19629c487..a2dc7651c 100644 --- a/builtin/providers/digitalocean/provider.go +++ b/builtin/providers/digitalocean/provider.go @@ -1,29 +1,48 @@ package digitalocean import ( + "os" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" ) // Provider returns a schema.Provider for DigitalOcean. -// -// NOTE: schema.Provider became available long after the DO provider -// was started, so resources may not be converted to this new structure -// yet. This is a WIP. To assist with the migration, make sure any resources -// you migrate are acceptance tested, then perform the migration. -func Provider() *schema.Provider { - // TODO: Move the configuration to this - +func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "token": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("DIGITALOCEAN_TOKEN"), + Description: "The token key for API operations.", }, }, ResourcesMap: map[string]*schema.Resource{ - "digitalocean_domain": resourceDomain(), - "digitalocean_record": resourceRecord(), + "digitalocean_domain": resourceDigitalOceanDomain(), + "digitalocean_droplet": resourceDigitalOceanDroplet(), + "digitalocean_record": resourceDigitalOceanRecord(), }, + + ConfigureFunc: providerConfigure, } } + +func envDefaultFunc(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return nil, nil + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + Token: d.Get("token").(string), + } + + return config.Client() +} diff --git a/builtin/providers/digitalocean/provider_test.go b/builtin/providers/digitalocean/provider_test.go index b1751e54f..fc5f78a2b 100644 --- a/builtin/providers/digitalocean/provider_test.go +++ b/builtin/providers/digitalocean/provider_test.go @@ -1,11 +1,35 @@ package digitalocean 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{ + "digitalocean": testAccProvider, + } +} + func TestProvider(t *testing.T) { - if err := Provider().InternalValidate(); err != nil { + 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) { + if v := os.Getenv("DIGITALOCEAN_TOKEN"); v == "" { + t.Fatal("DIGITALOCEAN_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_domain.go b/builtin/providers/digitalocean/resource_digitalocean_domain.go index 865f6167e..eecdcd7dc 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_domain.go +++ b/builtin/providers/digitalocean/resource_digitalocean_domain.go @@ -9,11 +9,11 @@ import ( "github.com/pearkes/digitalocean" ) -func resourceDomain() *schema.Resource { +func resourceDigitalOceanDomain() *schema.Resource { return &schema.Resource{ - Create: resourceDomainCreate, - Read: resourceDomainRead, - Delete: resourceDomainDelete, + Create: resourceDigitalOceanDomainCreate, + Read: resourceDigitalOceanDomainRead, + Delete: resourceDigitalOceanDomainDelete, Schema: map[string]*schema.Schema{ "name": &schema.Schema{ @@ -31,18 +31,17 @@ func resourceDomain() *schema.Resource { } } -func resourceDomainCreate(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanDomainCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) // Build up our creation options - opts := digitalocean.CreateDomain{ + opts := &digitalocean.CreateDomain{ Name: d.Get("name").(string), IPAddress: d.Get("ip_address").(string), } log.Printf("[DEBUG] Domain create configuration: %#v", opts) - name, err := client.CreateDomain(&opts) + name, err := client.CreateDomain(opts) if err != nil { return fmt.Errorf("Error creating Domain: %s", err) } @@ -50,26 +49,11 @@ func resourceDomainCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(name) log.Printf("[INFO] Domain Name: %s", name) - return nil + return resourceDigitalOceanDomainRead(d, meta) } -func resourceDomainDelete(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client - - log.Printf("[INFO] Deleting Domain: %s", d.Id()) - err := client.DestroyDomain(d.Id()) - if err != nil { - return fmt.Errorf("Error deleting Domain: %s", err) - } - - d.SetId("") - return nil -} - -func resourceDomainRead(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanDomainRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) domain, err := client.RetrieveDomain(d.Id()) if err != nil { @@ -87,3 +71,16 @@ func resourceDomainRead(d *schema.ResourceData, meta interface{}) error { return nil } + +func resourceDigitalOceanDomainDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + log.Printf("[INFO] Deleting Domain: %s", d.Id()) + err := client.DestroyDomain(d.Id()) + if err != nil { + return fmt.Errorf("Error deleting Domain: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_domain_test.go b/builtin/providers/digitalocean/resource_digitalocean_domain_test.go index ffd8be696..918eea155 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_domain_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_domain_test.go @@ -33,7 +33,7 @@ func TestAccDigitalOceanDomain_Basic(t *testing.T) { } func testAccCheckDigitalOceanDomainDestroy(s *terraform.State) error { - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "digitalocean_domain" { @@ -74,7 +74,7 @@ func testAccCheckDigitalOceanDomainExists(n string, domain *digitalocean.Domain) return fmt.Errorf("No Record ID is set") } - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) foundDomain, err := client.RetrieveDomain(rs.Primary.ID) diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet.go b/builtin/providers/digitalocean/resource_digitalocean_droplet.go index 9c1965669..162828ff4 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet.go @@ -6,202 +6,335 @@ import ( "strings" "time" - "github.com/hashicorp/terraform/flatmap" - "github.com/hashicorp/terraform/helper/config" - "github.com/hashicorp/terraform/helper/diff" "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/helper/schema" "github.com/pearkes/digitalocean" ) -func resource_digitalocean_droplet_create( - s *terraform.InstanceState, - d *terraform.InstanceDiff, - meta interface{}) (*terraform.InstanceState, error) { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanDroplet() *schema.Resource { + return &schema.Resource{ + Create: resourceDigitalOceanDropletCreate, + Read: resourceDigitalOceanDropletRead, + Update: resourceDigitalOceanDropletUpdate, + Delete: resourceDigitalOceanDropletDelete, - // Merge the diff into the state so that we have all the attributes - // properly. - rs := s.MergeDiff(d) + Schema: map[string]*schema.Schema{ + "image": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "status": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "locked": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "backups": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "ipv6": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "ipv6_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "ipv6_address_private": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "private_networking": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "ipv4_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "ipv4_address_private": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "ssh_keys": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "user_data": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) // Build up our creation options - opts := digitalocean.CreateDroplet{ - Backups: rs.Attributes["backups"], - Image: rs.Attributes["image"], - IPV6: rs.Attributes["ipv6"], - Name: rs.Attributes["name"], - PrivateNetworking: rs.Attributes["private_networking"], - Region: rs.Attributes["region"], - Size: rs.Attributes["size"], - UserData: rs.Attributes["user_data"], + opts := &digitalocean.CreateDroplet{ + Image: d.Get("image").(string), + Name: d.Get("name").(string), + Region: d.Get("region").(string), + Size: d.Get("size").(string), } - // Only expand ssh_keys if we have them - if _, ok := rs.Attributes["ssh_keys.#"]; ok { - v := flatmap.Expand(rs.Attributes, "ssh_keys").([]interface{}) - if len(v) > 0 { - vs := make([]string, 0, len(v)) + if attr, ok := d.GetOk("backups"); ok { + opts.Backups = attr.(string) + } - // here we special case the * expanded lists. For example: - // - // ssh_keys = ["${digitalocean_key.foo.*.id}"] - // - if len(v) == 1 && strings.Contains(v[0].(string), ",") { - vs = strings.Split(v[0].(string), ",") - } + if attr, ok := d.GetOk("ipv6"); ok && attr.(bool) { + opts.IPV6 = "true" + } - for _, v := range v { - vs = append(vs, v.(string)) - } + if attr, ok := d.GetOk("private_networking"); ok && attr.(bool) { + opts.PrivateNetworking = "true" + } - opts.SSHKeys = vs + if attr, ok := d.GetOk("user_data"); ok { + opts.UserData = attr.(string) + } + + // Get configured ssh_keys + ssh_keys := d.Get("ssh_keys.#").(int) + if ssh_keys > 0 { + opts.SSHKeys = make([]string, 0, ssh_keys) + for i := 0; i < ssh_keys; i++ { + key := fmt.Sprintf("ssh_keys.%d", i) + opts.SSHKeys = append(opts.SSHKeys, d.Get(key).(string)) } } log.Printf("[DEBUG] Droplet create configuration: %#v", opts) - id, err := client.CreateDroplet(&opts) + id, err := client.CreateDroplet(opts) if err != nil { - return nil, fmt.Errorf("Error creating Droplet: %s", err) + return fmt.Errorf("Error creating droplet: %s", err) } // Assign the droplets id - rs.ID = id + d.SetId(id) - log.Printf("[INFO] Droplet ID: %s", id) + log.Printf("[INFO] Droplet ID: %s", d.Id()) - dropletRaw, err := WaitForDropletAttribute(id, "active", []string{"new"}, "status", client) + _, err = WaitForDropletAttribute(d, "active", []string{"new"}, "status", meta) if err != nil { - return rs, fmt.Errorf( - "Error waiting for droplet (%s) to become ready: %s", - id, err) + return fmt.Errorf( + "Error waiting for droplet (%s) to become ready: %s", d.Id(), err) } - droplet := dropletRaw.(*digitalocean.Droplet) - - // Initialize the connection info - rs.Ephemeral.ConnInfo["type"] = "ssh" - rs.Ephemeral.ConnInfo["host"] = droplet.IPV4Address("public") - - return resource_digitalocean_droplet_update_state(rs, droplet) + return resourceDigitalOceanDropletRead(d, meta) } -func resource_digitalocean_droplet_update( - s *terraform.InstanceState, - d *terraform.InstanceDiff, - meta interface{}) (*terraform.InstanceState, error) { - p := meta.(*ResourceProvider) - client := p.client - rs := s.MergeDiff(d) +func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) - var err error + // Retrieve the droplet properties for updating the state + droplet, err := client.RetrieveDroplet(d.Id()) - if attr, ok := d.Attributes["size"]; ok { - err = client.PowerOff(rs.ID) + if err != nil { + return fmt.Errorf("Error retrieving droplet: %s", err) + } + + if droplet.ImageSlug() == "" && droplet.ImageId() != "" { + d.Set("image", droplet.ImageId()) + } else { + d.Set("image", droplet.ImageSlug()) + } + + d.Set("name", droplet.Name) + d.Set("region", droplet.RegionSlug()) + d.Set("size", droplet.SizeSlug) + d.Set("status", droplet.Status) + d.Set("locked", droplet.IsLocked()) + + if droplet.IPV6Address("public") != "" { + d.Set("ipv6", true) + d.Set("ipv6_address", droplet.IPV6Address("public")) + d.Set("ipv6_address_private", droplet.IPV6Address("private")) + } + + d.Set("ipv4_address", droplet.IPV4Address("public")) + + if droplet.NetworkingType() == "private" { + d.Set("private_networking", true) + d.Set("ipv4_address_private", droplet.IPV4Address("private")) + } + + // Initialize the connection info + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": droplet.IPV4Address("public"), + }) + + return nil +} + +func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + if d.HasChange("size") { + oldSize, newSize := d.GetChange("size") + + err := client.PowerOff(d.Id()) if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { - return s, err + return fmt.Errorf( + "Error powering off droplet (%s): %s", d.Id(), err) } // Wait for power off - _, err = WaitForDropletAttribute( - rs.ID, "off", []string{"active"}, "status", client) - - err = client.Resize(rs.ID, attr.New) + _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client) if err != nil { - newErr := power_on_and_wait(rs.ID, client) + return fmt.Errorf( + "Error waiting for droplet (%s) to become powered off: %s", d.Id(), err) + } + + // Resize the droplet + err = client.Resize(d.Id(), newSize.(string)) + + if err != nil { + newErr := power_on_and_wait(d, meta) if newErr != nil { - return rs, newErr + return fmt.Errorf( + "Error powering on droplet (%s) after failed resize: %s", d.Id(), err) } - return rs, err + return fmt.Errorf( + "Error resizing droplet (%s): %s", d.Id(), err) } // Wait for the size to change _, err = WaitForDropletAttribute( - rs.ID, attr.New, []string{"", attr.Old}, "size", client) + d, newSize.(string), []string{"", oldSize.(string)}, "size", meta) if err != nil { - newErr := power_on_and_wait(rs.ID, client) + newErr := power_on_and_wait(d, meta) if newErr != nil { - return rs, newErr + return fmt.Errorf( + "Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err) } - return s, err + return fmt.Errorf( + "Error waiting for resize droplet (%s) to finish: %s", d.Id(), err) } - err = client.PowerOn(rs.ID) + err = client.PowerOn(d.Id()) if err != nil { - return s, err + return fmt.Errorf( + "Error powering on droplet (%s) after resize: %s", d.Id(), err) } // Wait for power off - _, err = WaitForDropletAttribute( - rs.ID, "active", []string{"off"}, "status", client) + _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta) if err != nil { - return s, err + return err } } - if attr, ok := d.Attributes["name"]; ok { - err = client.Rename(rs.ID, attr.New) + if d.HasChange("name") { + oldName, newName := d.GetChange("name") + + // Rename the droplet + err := client.Rename(d.Id(), newName.(string)) if err != nil { - return s, err + return fmt.Errorf( + "Error renaming droplet (%s): %s", d.Id(), err) } // Wait for the name to change _, err = WaitForDropletAttribute( - rs.ID, attr.New, []string{"", attr.Old}, "name", client) - } - - if attr, ok := d.Attributes["private_networking"]; ok { - err = client.Rename(rs.ID, attr.New) + d, newName.(string), []string{"", oldName.(string)}, "name", meta) if err != nil { - return s, err + return fmt.Errorf( + "Error waiting for rename droplet (%s) to finish: %s", d.Id(), err) } - - // Wait for the private_networking to turn on/off - _, err = WaitForDropletAttribute( - rs.ID, attr.New, []string{"", attr.Old}, "private_networking", client) } - if attr, ok := d.Attributes["ipv6"]; ok { - err = client.Rename(rs.ID, attr.New) + // As there is no way to disable private networking, + // we only check if it needs to be enabled + if d.HasChange("private_networking") && d.Get("private_networking").(bool) { + err := client.EnablePrivateNetworking(d.Id()) if err != nil { - return s, err + return fmt.Errorf( + "Error enabling private networking for droplet (%s): %s", d.Id(), err) } - // Wait for ipv6 to turn on/off + // Wait for the private_networking to turn on _, err = WaitForDropletAttribute( - rs.ID, attr.New, []string{"", attr.Old}, "ipv6", client) + d, "true", []string{"", "false"}, "private_networking", meta) + + return fmt.Errorf( + "Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err) } - droplet, err := resource_digitalocean_droplet_retrieve(rs.ID, client) + // As there is no way to disable IPv6, we only check if it needs to be enabled + if d.HasChange("ipv6") && d.Get("ipv6").(bool) { + err := client.EnableIPV6s(d.Id()) - if err != nil { - return s, err + if err != nil { + return fmt.Errorf( + "Error turning on ipv6 for droplet (%s): %s", d.Id(), err) + } + + // Wait for ipv6 to turn on + _, err = WaitForDropletAttribute( + d, "true", []string{"", "false"}, "ipv6", meta) + + if err != nil { + return fmt.Errorf( + "Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err) + } } - return resource_digitalocean_droplet_update_state(rs, droplet) + return resourceDigitalOceanDropletRead(d, meta) } -func resource_digitalocean_droplet_destroy( - s *terraform.InstanceState, - meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) - log.Printf("[INFO] Deleting Droplet: %s", s.ID) + log.Printf("[INFO] Deleting droplet: %s", d.Id()) // Destroy the droplet - err := client.DestroyDroplet(s.ID) + err := client.DestroyDroplet(d.Id()) // Handle remotely destroyed droplets if err != nil && strings.Contains(err.Error(), "404 Not Found") { @@ -209,140 +342,24 @@ func resource_digitalocean_droplet_destroy( } if err != nil { - return fmt.Errorf("Error deleting Droplet: %s", err) + return fmt.Errorf("Error deleting droplet: %s", err) } return nil } -func resource_digitalocean_droplet_refresh( - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - p := meta.(*ResourceProvider) - client := p.client - - droplet, err := resource_digitalocean_droplet_retrieve(s.ID, client) - - // Handle remotely destroyed droplets - if err != nil && strings.Contains(err.Error(), "404 Not Found") { - return nil, nil - } - - if err != nil { - return nil, err - } - - return resource_digitalocean_droplet_update_state(s, droplet) -} - -func resource_digitalocean_droplet_diff( - s *terraform.InstanceState, - c *terraform.ResourceConfig, - meta interface{}) (*terraform.InstanceDiff, error) { - - b := &diff.ResourceBuilder{ - Attrs: map[string]diff.AttrType{ - "backups": diff.AttrTypeUpdate, - "image": diff.AttrTypeCreate, - "ipv6": diff.AttrTypeUpdate, - "name": diff.AttrTypeUpdate, - "private_networking": diff.AttrTypeUpdate, - "region": diff.AttrTypeCreate, - "size": diff.AttrTypeUpdate, - "ssh_keys": diff.AttrTypeCreate, - "user_data": diff.AttrTypeCreate, - }, - - ComputedAttrs: []string{ - "backups", - "ipv4_address", - "ipv4_address_private", - "ipv6", - "ipv6_address", - "ipv6_address_private", - "locked", - "private_networking", - "status", - }, - } - - return b.Diff(s, c) -} - -func resource_digitalocean_droplet_update_state( - s *terraform.InstanceState, - droplet *digitalocean.Droplet) (*terraform.InstanceState, error) { - - s.Attributes["name"] = droplet.Name - s.Attributes["region"] = droplet.RegionSlug() - - if droplet.ImageSlug() == "" && droplet.ImageId() != "" { - s.Attributes["image"] = droplet.ImageId() - } else { - s.Attributes["image"] = droplet.ImageSlug() - } - - if droplet.IPV6Address("public") != "" { - s.Attributes["ipv6"] = "true" - s.Attributes["ipv6_address"] = droplet.IPV6Address("public") - s.Attributes["ipv6_address_private"] = droplet.IPV6Address("private") - } - - s.Attributes["ipv4_address"] = droplet.IPV4Address("public") - s.Attributes["locked"] = droplet.IsLocked() - - if droplet.NetworkingType() == "private" { - s.Attributes["private_networking"] = "true" - s.Attributes["ipv4_address_private"] = droplet.IPV4Address("private") - } - - s.Attributes["size"] = droplet.SizeSlug - s.Attributes["status"] = droplet.Status - - return s, nil -} - -// retrieves an ELB by its ID -func resource_digitalocean_droplet_retrieve(id string, client *digitalocean.Client) (*digitalocean.Droplet, error) { - // Retrieve the ELB properties for updating the state - droplet, err := client.RetrieveDroplet(id) - - if err != nil { - return nil, fmt.Errorf("Error retrieving droplet: %s", err) - } - - return &droplet, nil -} - -func resource_digitalocean_droplet_validation() *config.Validator { - return &config.Validator{ - Required: []string{ - "image", - "name", - "region", - "size", - }, - Optional: []string{ - "backups", - "user_data", - "ipv6", - "private_networking", - "ssh_keys.*", - }, - } -} - -func WaitForDropletAttribute(id string, target string, pending []string, attribute string, client *digitalocean.Client) (interface{}, error) { +func WaitForDropletAttribute( + d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) { // Wait for the droplet so we can get the networking attributes // that show up after a while log.Printf( "[INFO] Waiting for Droplet (%s) to have %s of %s", - id, attribute, target) + d.Id(), attribute, target) stateConf := &resource.StateChangeConf{ Pending: pending, Target: target, - Refresh: new_droplet_state_refresh_func(id, attribute, client), + Refresh: new_droplet_state_refresh_func(d, attribute, meta), Timeout: 10 * time.Minute, Delay: 10 * time.Second, MinTimeout: 3 * time.Second, @@ -351,37 +368,36 @@ func WaitForDropletAttribute(id string, target string, pending []string, attribu return stateConf.WaitForState() } -func new_droplet_state_refresh_func(id string, attribute string, client *digitalocean.Client) resource.StateRefreshFunc { +// TODO This function still needs a little more refactoring to make it +// cleaner and more efficient +func new_droplet_state_refresh_func( + d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc { + client := meta.(*digitalocean.Client) return func() (interface{}, string, error) { - // Retrieve the ELB properties for updating the state - droplet, err := client.RetrieveDroplet(id) + err := resourceDigitalOceanDropletRead(d, meta) if err != nil { - log.Printf("Error on retrieving droplet when waiting: %s", err) return nil, "", err } // If the droplet is locked, continue waiting. We can // only perform actions on unlocked droplets, so it's // pointless to look at that status - if droplet.IsLocked() == "true" { + if d.Get("locked").(string) == "true" { log.Println("[DEBUG] Droplet is locked, skipping status check and retrying") return nil, "", nil } - // Use our mapping to get back a map of the - // droplet properties - resourceMap, err := resource_digitalocean_droplet_update_state( - &terraform.InstanceState{Attributes: map[string]string{}}, &droplet) - - if err != nil { - log.Printf("Error creating map from droplet: %s", err) - return nil, "", err - } - // See if we can access our attribute - if attr, ok := resourceMap.Attributes[attribute]; ok { - return &droplet, attr, nil + if attr, ok := d.GetOk(attribute); ok { + // Retrieve the droplet properties + droplet, err := client.RetrieveDroplet(d.Id()) + + if err != nil { + return nil, "", fmt.Errorf("Error retrieving droplet: %s", err) + } + + return &droplet, attr.(string), nil } return nil, "", nil @@ -389,16 +405,16 @@ func new_droplet_state_refresh_func(id string, attribute string, client *digital } // Powers on the droplet and waits for it to be active -func power_on_and_wait(id string, client *digitalocean.Client) error { - err := client.PowerOn(id) +func power_on_and_wait(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + err := client.PowerOn(d.Id()) if err != nil { return err } // Wait for power on - _, err = WaitForDropletAttribute( - id, "active", []string{"off"}, "status", client) + _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client) if err != nil { return err diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go index 6666576dd..587612e01 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go @@ -94,7 +94,7 @@ func TestAccDigitalOceanDroplet_PrivateNetworkingIpv6(t *testing.T) { } func testAccCheckDigitalOceanDropletDestroy(s *terraform.State) error { - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "digitalocean_droplet" { @@ -207,7 +207,7 @@ func testAccCheckDigitalOceanDropletExists(n string, droplet *digitalocean.Dropl return fmt.Errorf("No Droplet ID is set") } - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) retrieveDroplet, err := client.RetrieveDroplet(rs.Primary.ID) @@ -225,19 +225,23 @@ func testAccCheckDigitalOceanDropletExists(n string, droplet *digitalocean.Dropl } } -func Test_new_droplet_state_refresh_func(t *testing.T) { - droplet := digitalocean.Droplet{ - Name: "foobar", - } - resourceMap, _ := resource_digitalocean_droplet_update_state( - &terraform.InstanceState{Attributes: map[string]string{}}, &droplet) - - // See if we can access our attribute - if _, ok := resourceMap.Attributes["name"]; !ok { - t.Fatalf("bad name: %s", resourceMap.Attributes) - } - -} +// Not sure if this check should remain here as the underlaying +// function is changed and is tested indirectly by almost all +// other test already +// +//func Test_new_droplet_state_refresh_func(t *testing.T) { +// droplet := digitalocean.Droplet{ +// Name: "foobar", +// } +// resourceMap, _ := resource_digitalocean_droplet_update_state( +// &terraform.InstanceState{Attributes: map[string]string{}}, &droplet) +// +// // See if we can access our attribute +// if _, ok := resourceMap.Attributes["name"]; !ok { +// t.Fatalf("bad name: %s", resourceMap.Attributes) +// } +// +//} const testAccCheckDigitalOceanDropletConfig_basic = ` resource "digitalocean_droplet" "foobar" { diff --git a/builtin/providers/digitalocean/resource_digitalocean_record.go b/builtin/providers/digitalocean/resource_digitalocean_record.go index 0ad08265e..d365e4706 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_record.go +++ b/builtin/providers/digitalocean/resource_digitalocean_record.go @@ -9,12 +9,12 @@ import ( "github.com/pearkes/digitalocean" ) -func resourceRecord() *schema.Resource { +func resourceDigitalOceanRecord() *schema.Resource { return &schema.Resource{ - Create: resourceRecordCreate, - Read: resourceRecordRead, - Update: resourceRecordUpdate, - Delete: resourceRecordDelete, + Create: resourceDigitalOceanRecordCreate, + Read: resourceDigitalOceanRecordRead, + Update: resourceDigitalOceanRecordUpdate, + Delete: resourceDigitalOceanRecordDelete, Schema: map[string]*schema.Schema{ "type": &schema.Schema{ @@ -65,9 +65,8 @@ func resourceRecord() *schema.Resource { } } -func resourceRecordCreate(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanRecordCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) newRecord := digitalocean.CreateRecord{ Type: d.Get("type").(string), @@ -87,50 +86,11 @@ func resourceRecordCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(recId) log.Printf("[INFO] Record ID: %s", d.Id()) - return resourceRecordRead(d, meta) + return resourceDigitalOceanRecordRead(d, meta) } -func resourceRecordUpdate(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client - - var updateRecord digitalocean.UpdateRecord - if v, ok := d.GetOk("name"); ok { - updateRecord.Name = v.(string) - } - - log.Printf("[DEBUG] record update configuration: %#v", updateRecord) - err := client.UpdateRecord(d.Get("domain").(string), d.Id(), &updateRecord) - if err != nil { - return fmt.Errorf("Failed to update record: %s", err) - } - - return resourceRecordRead(d, meta) -} - -func resourceRecordDelete(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client - - log.Printf( - "[INFO] Deleting record: %s, %s", d.Get("domain").(string), d.Id()) - err := client.DestroyRecord(d.Get("domain").(string), d.Id()) - if err != nil { - // If the record is somehow already destroyed, mark as - // succesfully gone - if strings.Contains(err.Error(), "404 Not Found") { - return nil - } - - return fmt.Errorf("Error deleting record: %s", err) - } - - return nil -} - -func resourceRecordRead(d *schema.ResourceData, meta interface{}) error { - p := meta.(*ResourceProvider) - client := p.client +func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) rec, err := client.RetrieveRecord(d.Get("domain").(string), d.Id()) if err != nil { @@ -153,3 +113,39 @@ func resourceRecordRead(d *schema.ResourceData, meta interface{}) error { return nil } + +func resourceDigitalOceanRecordUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + var updateRecord digitalocean.UpdateRecord + if v, ok := d.GetOk("name"); ok { + updateRecord.Name = v.(string) + } + + log.Printf("[DEBUG] record update configuration: %#v", updateRecord) + err := client.UpdateRecord(d.Get("domain").(string), d.Id(), &updateRecord) + if err != nil { + return fmt.Errorf("Failed to update record: %s", err) + } + + return resourceDigitalOceanRecordRead(d, meta) +} + +func resourceDigitalOceanRecordDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + log.Printf( + "[INFO] Deleting record: %s, %s", d.Get("domain").(string), d.Id()) + err := client.DestroyRecord(d.Get("domain").(string), d.Id()) + if err != nil { + // If the record is somehow already destroyed, mark as + // succesfully gone + if strings.Contains(err.Error(), "404 Not Found") { + return nil + } + + return fmt.Errorf("Error deleting record: %s", err) + } + + return nil +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_record_test.go b/builtin/providers/digitalocean/resource_digitalocean_record_test.go index 59a0bb4a4..66ac2bb5f 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_record_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_record_test.go @@ -77,7 +77,7 @@ func TestAccDigitalOceanRecord_Updated(t *testing.T) { } func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error { - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "digitalocean_record" { @@ -128,7 +128,7 @@ func testAccCheckDigitalOceanRecordExists(n string, record *digitalocean.Record) return fmt.Errorf("No Record ID is set") } - client := testAccProvider.client + client := testAccProvider.Meta().(*digitalocean.Client) foundRecord, err := client.RetrieveRecord(rs.Primary.Attributes["domain"], rs.Primary.ID) diff --git a/builtin/providers/digitalocean/resource_provider.go b/builtin/providers/digitalocean/resource_provider.go deleted file mode 100644 index 6ded1018b..000000000 --- a/builtin/providers/digitalocean/resource_provider.go +++ /dev/null @@ -1,99 +0,0 @@ -package digitalocean - -import ( - "log" - - "github.com/hashicorp/terraform/helper/config" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/terraform" - "github.com/pearkes/digitalocean" -) - -type ResourceProvider struct { - Config Config - - client *digitalocean.Client - - // This is the schema.Provider. Eventually this will replace much - // of this structure. For now it is an element of it for compatiblity. - p *schema.Provider -} - -func (p *ResourceProvider) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - return Provider().Input(input, c) -} - -func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { - prov := Provider() - return prov.Validate(c) -} - -func (p *ResourceProvider) ValidateResource( - t string, c *terraform.ResourceConfig) ([]string, []error) { - prov := Provider() - if _, ok := prov.ResourcesMap[t]; ok { - return prov.ValidateResource(t, c) - } - - return resourceMap.Validate(t, c) -} - -func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { - if _, err := config.Decode(&p.Config, c.Config); err != nil { - return err - } - - log.Println("[INFO] Initializing DigitalOcean client") - var err error - p.client, err = p.Config.Client() - - if err != nil { - return err - } - - // Create the provider, set the meta - p.p = Provider() - p.p.SetMeta(p) - - return nil -} - -func (p *ResourceProvider) Apply( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if _, ok := p.p.ResourcesMap[info.Type]; ok { - return p.p.Apply(info, s, d) - } - - return resourceMap.Apply(info, s, d, p) -} - -func (p *ResourceProvider) Diff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - if _, ok := p.p.ResourcesMap[info.Type]; ok { - return p.p.Diff(info, s, c) - } - - return resourceMap.Diff(info, s, c, p) -} - -func (p *ResourceProvider) Refresh( - info *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - if _, ok := p.p.ResourcesMap[info.Type]; ok { - return p.p.Refresh(info, s) - } - - return resourceMap.Refresh(info, s, p) -} - -func (p *ResourceProvider) Resources() []terraform.ResourceType { - result := resourceMap.Resources() - result = append(result, Provider().Resources()...) - return result -} diff --git a/builtin/providers/digitalocean/resource_provider_test.go b/builtin/providers/digitalocean/resource_provider_test.go deleted file mode 100644 index 836c1b244..000000000 --- a/builtin/providers/digitalocean/resource_provider_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package digitalocean - -import ( - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" -) - -var testAccProviders map[string]terraform.ResourceProvider -var testAccProvider *ResourceProvider - -func init() { - testAccProvider = new(ResourceProvider) - testAccProviders = map[string]terraform.ResourceProvider{ - "digitalocean": testAccProvider, - } -} - -func TestResourceProvider_impl(t *testing.T) { - var _ terraform.ResourceProvider = new(ResourceProvider) -} - -func TestResourceProvider_Configure(t *testing.T) { - rp := new(ResourceProvider) - var expectedToken string - - if v := os.Getenv("DIGITALOCEAN_TOKEN"); v != "foo" { - expectedToken = v - } else { - expectedToken = "foo" - } - - raw := map[string]interface{}{ - "token": expectedToken, - } - - rawConfig, err := config.NewRawConfig(raw) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = rp.Configure(terraform.NewResourceConfig(rawConfig)) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := Config{ - Token: expectedToken, - } - - if !reflect.DeepEqual(rp.Config, expected) { - t.Fatalf("bad: %#v", rp.Config) - } -} - -func testAccPreCheck(t *testing.T) { - if v := os.Getenv("DIGITALOCEAN_TOKEN"); v == "" { - t.Fatal("DIGITALOCEAN_TOKEN must be set for acceptance tests") - } -} diff --git a/builtin/providers/digitalocean/resources.go b/builtin/providers/digitalocean/resources.go deleted file mode 100644 index 75b396c52..000000000 --- a/builtin/providers/digitalocean/resources.go +++ /dev/null @@ -1,24 +0,0 @@ -package digitalocean - -import ( - "github.com/hashicorp/terraform/helper/resource" -) - -// resourceMap is the mapping of resources we support to their basic -// operations. This makes it easy to implement new resource types. -var resourceMap *resource.Map - -func init() { - resourceMap = &resource.Map{ - Mapping: map[string]resource.Resource{ - "digitalocean_droplet": resource.Resource{ - ConfigValidator: resource_digitalocean_droplet_validation(), - Create: resource_digitalocean_droplet_create, - Destroy: resource_digitalocean_droplet_destroy, - Diff: resource_digitalocean_droplet_diff, - Refresh: resource_digitalocean_droplet_refresh, - Update: resource_digitalocean_droplet_update, - }, - }, - } -}