Merge pull request #570 from svanharmelen/f-refactor-digitalocean-provider

Refactor to use the schema.Provider approach
This commit is contained in:
Armon Dadgar 2014-11-19 14:15:48 -08:00
commit fc5a13a1c1
13 changed files with 419 additions and 562 deletions

View File

@ -3,13 +3,10 @@ package main
import ( import (
"github.com/hashicorp/terraform/builtin/providers/digitalocean" "github.com/hashicorp/terraform/builtin/providers/digitalocean"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
) )
func main() { func main() {
plugin.Serve(&plugin.ServeOpts{ plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() terraform.ResourceProvider { ProviderFunc: digitalocean.Provider,
return new(digitalocean.ResourceProvider)
},
}) })
} }

View File

@ -2,26 +2,16 @@ package digitalocean
import ( import (
"log" "log"
"os"
"github.com/pearkes/digitalocean" "github.com/pearkes/digitalocean"
) )
type Config struct { type Config struct {
Token string `mapstructure:"token"` Token string
} }
// Client() returns a new client for accessing digital // Client() returns a new client for accessing digital ocean.
// ocean.
//
func (c *Config) Client() (*digitalocean.Client, error) { 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) client, err := digitalocean.NewClient(c.Token)
log.Printf("[INFO] DigitalOcean Client configured for URL: %s", client.URL) log.Printf("[INFO] DigitalOcean Client configured for URL: %s", client.URL)

View File

@ -1,29 +1,48 @@
package digitalocean package digitalocean
import ( import (
"os"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
) )
// Provider returns a schema.Provider for DigitalOcean. // Provider returns a schema.Provider for DigitalOcean.
// func Provider() terraform.ResourceProvider {
// 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
return &schema.Provider{ return &schema.Provider{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"token": &schema.Schema{ "token": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: envDefaultFunc("DIGITALOCEAN_TOKEN"),
Description: "The token key for API operations.",
}, },
}, },
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"digitalocean_domain": resourceDomain(), "digitalocean_domain": resourceDigitalOceanDomain(),
"digitalocean_record": resourceRecord(), "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()
}

View File

@ -1,11 +1,35 @@
package digitalocean package digitalocean
import ( import (
"os"
"testing" "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) { func TestProvider(t *testing.T) {
if err := Provider().InternalValidate(); err != nil { if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err) 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")
}
}

View File

@ -9,11 +9,11 @@ import (
"github.com/pearkes/digitalocean" "github.com/pearkes/digitalocean"
) )
func resourceDomain() *schema.Resource { func resourceDigitalOceanDomain() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceDomainCreate, Create: resourceDigitalOceanDomainCreate,
Read: resourceDomainRead, Read: resourceDigitalOceanDomainRead,
Delete: resourceDomainDelete, Delete: resourceDigitalOceanDomainDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"name": &schema.Schema{ "name": &schema.Schema{
@ -31,18 +31,17 @@ func resourceDomain() *schema.Resource {
} }
} }
func resourceDomainCreate(d *schema.ResourceData, meta interface{}) error { func resourceDigitalOceanDomainCreate(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider) client := meta.(*digitalocean.Client)
client := p.client
// Build up our creation options // Build up our creation options
opts := digitalocean.CreateDomain{ opts := &digitalocean.CreateDomain{
Name: d.Get("name").(string), Name: d.Get("name").(string),
IPAddress: d.Get("ip_address").(string), IPAddress: d.Get("ip_address").(string),
} }
log.Printf("[DEBUG] Domain create configuration: %#v", opts) log.Printf("[DEBUG] Domain create configuration: %#v", opts)
name, err := client.CreateDomain(&opts) name, err := client.CreateDomain(opts)
if err != nil { if err != nil {
return fmt.Errorf("Error creating Domain: %s", err) return fmt.Errorf("Error creating Domain: %s", err)
} }
@ -50,26 +49,11 @@ func resourceDomainCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(name) d.SetId(name)
log.Printf("[INFO] Domain Name: %s", name) log.Printf("[INFO] Domain Name: %s", name)
return nil return resourceDigitalOceanDomainRead(d, meta)
} }
func resourceDomainDelete(d *schema.ResourceData, meta interface{}) error { func resourceDigitalOceanDomainRead(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider) client := meta.(*digitalocean.Client)
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
domain, err := client.RetrieveDomain(d.Id()) domain, err := client.RetrieveDomain(d.Id())
if err != nil { if err != nil {
@ -87,3 +71,16 @@ func resourceDomainRead(d *schema.ResourceData, meta interface{}) error {
return nil 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
}

View File

@ -33,7 +33,7 @@ func TestAccDigitalOceanDomain_Basic(t *testing.T) {
} }
func testAccCheckDigitalOceanDomainDestroy(s *terraform.State) error { func testAccCheckDigitalOceanDomainDestroy(s *terraform.State) error {
client := testAccProvider.client client := testAccProvider.Meta().(*digitalocean.Client)
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_domain" { if rs.Type != "digitalocean_domain" {
@ -74,7 +74,7 @@ func testAccCheckDigitalOceanDomainExists(n string, domain *digitalocean.Domain)
return fmt.Errorf("No Record ID is set") return fmt.Errorf("No Record ID is set")
} }
client := testAccProvider.client client := testAccProvider.Meta().(*digitalocean.Client)
foundDomain, err := client.RetrieveDomain(rs.Primary.ID) foundDomain, err := client.RetrieveDomain(rs.Primary.ID)

View File

@ -6,202 +6,335 @@ import (
"strings" "strings"
"time" "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/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/helper/schema"
"github.com/pearkes/digitalocean" "github.com/pearkes/digitalocean"
) )
func resource_digitalocean_droplet_create( func resourceDigitalOceanDroplet() *schema.Resource {
s *terraform.InstanceState, return &schema.Resource{
d *terraform.InstanceDiff, Create: resourceDigitalOceanDropletCreate,
meta interface{}) (*terraform.InstanceState, error) { Read: resourceDigitalOceanDropletRead,
p := meta.(*ResourceProvider) Update: resourceDigitalOceanDropletUpdate,
client := p.client Delete: resourceDigitalOceanDropletDelete,
// Merge the diff into the state so that we have all the attributes Schema: map[string]*schema.Schema{
// properly. "image": &schema.Schema{
rs := s.MergeDiff(d) 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 // Build up our creation options
opts := digitalocean.CreateDroplet{ opts := &digitalocean.CreateDroplet{
Backups: rs.Attributes["backups"], Image: d.Get("image").(string),
Image: rs.Attributes["image"], Name: d.Get("name").(string),
IPV6: rs.Attributes["ipv6"], Region: d.Get("region").(string),
Name: rs.Attributes["name"], Size: d.Get("size").(string),
PrivateNetworking: rs.Attributes["private_networking"],
Region: rs.Attributes["region"],
Size: rs.Attributes["size"],
UserData: rs.Attributes["user_data"],
} }
// Only expand ssh_keys if we have them if attr, ok := d.GetOk("backups"); ok {
if _, ok := rs.Attributes["ssh_keys.#"]; ok { opts.Backups = attr.(string)
v := flatmap.Expand(rs.Attributes, "ssh_keys").([]interface{}) }
if len(v) > 0 {
vs := make([]string, 0, len(v))
// here we special case the * expanded lists. For example: if attr, ok := d.GetOk("ipv6"); ok && attr.(bool) {
// opts.IPV6 = "true"
// ssh_keys = ["${digitalocean_key.foo.*.id}"] }
//
if len(v) == 1 && strings.Contains(v[0].(string), ",") {
vs = strings.Split(v[0].(string), ",")
}
for _, v := range v { if attr, ok := d.GetOk("private_networking"); ok && attr.(bool) {
vs = append(vs, v.(string)) 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) log.Printf("[DEBUG] Droplet create configuration: %#v", opts)
id, err := client.CreateDroplet(&opts) id, err := client.CreateDroplet(opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error creating Droplet: %s", err) return fmt.Errorf("Error creating droplet: %s", err)
} }
// Assign the droplets id // 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 { if err != nil {
return rs, fmt.Errorf( return fmt.Errorf(
"Error waiting for droplet (%s) to become ready: %s", "Error waiting for droplet (%s) to become ready: %s", d.Id(), err)
id, err)
} }
droplet := dropletRaw.(*digitalocean.Droplet) return resourceDigitalOceanDropletRead(d, meta)
// Initialize the connection info
rs.Ephemeral.ConnInfo["type"] = "ssh"
rs.Ephemeral.ConnInfo["host"] = droplet.IPV4Address("public")
return resource_digitalocean_droplet_update_state(rs, droplet)
} }
func resource_digitalocean_droplet_update( func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error {
s *terraform.InstanceState, client := meta.(*digitalocean.Client)
d *terraform.InstanceDiff,
meta interface{}) (*terraform.InstanceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rs := s.MergeDiff(d)
var err error // Retrieve the droplet properties for updating the state
droplet, err := client.RetrieveDroplet(d.Id())
if attr, ok := d.Attributes["size"]; ok { if err != nil {
err = client.PowerOff(rs.ID) 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") { 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 // Wait for power off
_, err = WaitForDropletAttribute( _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client)
rs.ID, "off", []string{"active"}, "status", client)
err = client.Resize(rs.ID, attr.New)
if err != nil { 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 { 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 // Wait for the size to change
_, err = WaitForDropletAttribute( _, err = WaitForDropletAttribute(
rs.ID, attr.New, []string{"", attr.Old}, "size", client) d, newSize.(string), []string{"", oldSize.(string)}, "size", meta)
if err != nil { if err != nil {
newErr := power_on_and_wait(rs.ID, client) newErr := power_on_and_wait(d, meta)
if newErr != nil { 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 { if err != nil {
return s, err return fmt.Errorf(
"Error powering on droplet (%s) after resize: %s", d.Id(), err)
} }
// Wait for power off // Wait for power off
_, err = WaitForDropletAttribute( _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta)
rs.ID, "active", []string{"off"}, "status", client)
if err != nil { if err != nil {
return s, err return err
} }
} }
if attr, ok := d.Attributes["name"]; ok { if d.HasChange("name") {
err = client.Rename(rs.ID, attr.New) oldName, newName := d.GetChange("name")
// Rename the droplet
err := client.Rename(d.Id(), newName.(string))
if err != nil { if err != nil {
return s, err return fmt.Errorf(
"Error renaming droplet (%s): %s", d.Id(), err)
} }
// Wait for the name to change // Wait for the name to change
_, err = WaitForDropletAttribute( _, err = WaitForDropletAttribute(
rs.ID, attr.New, []string{"", attr.Old}, "name", client) d, newName.(string), []string{"", oldName.(string)}, "name", meta)
}
if attr, ok := d.Attributes["private_networking"]; ok {
err = client.Rename(rs.ID, attr.New)
if err != nil { 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 { // As there is no way to disable private networking,
err = client.Rename(rs.ID, attr.New) // 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 { 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( _, 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 { if err != nil {
return s, err 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( func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error {
s *terraform.InstanceState, client := meta.(*digitalocean.Client)
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting Droplet: %s", s.ID) log.Printf("[INFO] Deleting droplet: %s", d.Id())
// Destroy the droplet // Destroy the droplet
err := client.DestroyDroplet(s.ID) err := client.DestroyDroplet(d.Id())
// Handle remotely destroyed droplets // Handle remotely destroyed droplets
if err != nil && strings.Contains(err.Error(), "404 Not Found") { if err != nil && strings.Contains(err.Error(), "404 Not Found") {
@ -209,140 +342,24 @@ func resource_digitalocean_droplet_destroy(
} }
if err != nil { if err != nil {
return fmt.Errorf("Error deleting Droplet: %s", err) return fmt.Errorf("Error deleting droplet: %s", err)
} }
return nil return nil
} }
func resource_digitalocean_droplet_refresh( func WaitForDropletAttribute(
s *terraform.InstanceState, d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
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) {
// Wait for the droplet so we can get the networking attributes // Wait for the droplet so we can get the networking attributes
// that show up after a while // that show up after a while
log.Printf( log.Printf(
"[INFO] Waiting for Droplet (%s) to have %s of %s", "[INFO] Waiting for Droplet (%s) to have %s of %s",
id, attribute, target) d.Id(), attribute, target)
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: pending, Pending: pending,
Target: target, Target: target,
Refresh: new_droplet_state_refresh_func(id, attribute, client), Refresh: new_droplet_state_refresh_func(d, attribute, meta),
Timeout: 10 * time.Minute, Timeout: 10 * time.Minute,
Delay: 10 * time.Second, Delay: 10 * time.Second,
MinTimeout: 3 * time.Second, MinTimeout: 3 * time.Second,
@ -351,37 +368,36 @@ func WaitForDropletAttribute(id string, target string, pending []string, attribu
return stateConf.WaitForState() 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) { return func() (interface{}, string, error) {
// Retrieve the ELB properties for updating the state err := resourceDigitalOceanDropletRead(d, meta)
droplet, err := client.RetrieveDroplet(id)
if err != nil { if err != nil {
log.Printf("Error on retrieving droplet when waiting: %s", err)
return nil, "", err return nil, "", err
} }
// If the droplet is locked, continue waiting. We can // If the droplet is locked, continue waiting. We can
// only perform actions on unlocked droplets, so it's // only perform actions on unlocked droplets, so it's
// pointless to look at that status // 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") log.Println("[DEBUG] Droplet is locked, skipping status check and retrying")
return nil, "", nil 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 // See if we can access our attribute
if attr, ok := resourceMap.Attributes[attribute]; ok { if attr, ok := d.GetOk(attribute); ok {
return &droplet, attr, nil // 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 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 // Powers on the droplet and waits for it to be active
func power_on_and_wait(id string, client *digitalocean.Client) error { func power_on_and_wait(d *schema.ResourceData, meta interface{}) error {
err := client.PowerOn(id) client := meta.(*digitalocean.Client)
err := client.PowerOn(d.Id())
if err != nil { if err != nil {
return err return err
} }
// Wait for power on // Wait for power on
_, err = WaitForDropletAttribute( _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client)
id, "active", []string{"off"}, "status", client)
if err != nil { if err != nil {
return err return err

View File

@ -94,7 +94,7 @@ func TestAccDigitalOceanDroplet_PrivateNetworkingIpv6(t *testing.T) {
} }
func testAccCheckDigitalOceanDropletDestroy(s *terraform.State) error { func testAccCheckDigitalOceanDropletDestroy(s *terraform.State) error {
client := testAccProvider.client client := testAccProvider.Meta().(*digitalocean.Client)
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_droplet" { if rs.Type != "digitalocean_droplet" {
@ -207,7 +207,7 @@ func testAccCheckDigitalOceanDropletExists(n string, droplet *digitalocean.Dropl
return fmt.Errorf("No Droplet ID is set") return fmt.Errorf("No Droplet ID is set")
} }
client := testAccProvider.client client := testAccProvider.Meta().(*digitalocean.Client)
retrieveDroplet, err := client.RetrieveDroplet(rs.Primary.ID) 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) { // Not sure if this check should remain here as the underlaying
droplet := digitalocean.Droplet{ // function is changed and is tested indirectly by almost all
Name: "foobar", // other test already
} //
resourceMap, _ := resource_digitalocean_droplet_update_state( //func Test_new_droplet_state_refresh_func(t *testing.T) {
&terraform.InstanceState{Attributes: map[string]string{}}, &droplet) // droplet := digitalocean.Droplet{
// Name: "foobar",
// See if we can access our attribute // }
if _, ok := resourceMap.Attributes["name"]; !ok { // resourceMap, _ := resource_digitalocean_droplet_update_state(
t.Fatalf("bad name: %s", resourceMap.Attributes) // &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 = ` const testAccCheckDigitalOceanDropletConfig_basic = `
resource "digitalocean_droplet" "foobar" { resource "digitalocean_droplet" "foobar" {

View File

@ -9,12 +9,12 @@ import (
"github.com/pearkes/digitalocean" "github.com/pearkes/digitalocean"
) )
func resourceRecord() *schema.Resource { func resourceDigitalOceanRecord() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceRecordCreate, Create: resourceDigitalOceanRecordCreate,
Read: resourceRecordRead, Read: resourceDigitalOceanRecordRead,
Update: resourceRecordUpdate, Update: resourceDigitalOceanRecordUpdate,
Delete: resourceRecordDelete, Delete: resourceDigitalOceanRecordDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"type": &schema.Schema{ "type": &schema.Schema{
@ -65,9 +65,8 @@ func resourceRecord() *schema.Resource {
} }
} }
func resourceRecordCreate(d *schema.ResourceData, meta interface{}) error { func resourceDigitalOceanRecordCreate(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider) client := meta.(*digitalocean.Client)
client := p.client
newRecord := digitalocean.CreateRecord{ newRecord := digitalocean.CreateRecord{
Type: d.Get("type").(string), Type: d.Get("type").(string),
@ -87,50 +86,11 @@ func resourceRecordCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(recId) d.SetId(recId)
log.Printf("[INFO] Record ID: %s", d.Id()) log.Printf("[INFO] Record ID: %s", d.Id())
return resourceRecordRead(d, meta) return resourceDigitalOceanRecordRead(d, meta)
} }
func resourceRecordUpdate(d *schema.ResourceData, meta interface{}) error { func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider) client := meta.(*digitalocean.Client)
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
rec, err := client.RetrieveRecord(d.Get("domain").(string), d.Id()) rec, err := client.RetrieveRecord(d.Get("domain").(string), d.Id())
if err != nil { if err != nil {
@ -153,3 +113,39 @@ func resourceRecordRead(d *schema.ResourceData, meta interface{}) error {
return nil 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
}

View File

@ -77,7 +77,7 @@ func TestAccDigitalOceanRecord_Updated(t *testing.T) {
} }
func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error { func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error {
client := testAccProvider.client client := testAccProvider.Meta().(*digitalocean.Client)
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_record" { if rs.Type != "digitalocean_record" {
@ -128,7 +128,7 @@ func testAccCheckDigitalOceanRecordExists(n string, record *digitalocean.Record)
return fmt.Errorf("No Record ID is set") 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) foundRecord, err := client.RetrieveRecord(rs.Primary.Attributes["domain"], rs.Primary.ID)

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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,
},
},
}
}