409 lines
9.9 KiB
Go
409 lines
9.9 KiB
Go
package digitalocean
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"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/pearkes/digitalocean"
|
|
)
|
|
|
|
func resource_digitalocean_droplet_create(
|
|
s *terraform.InstanceState,
|
|
d *terraform.ResourceDiff,
|
|
meta interface{}) (*terraform.InstanceState, error) {
|
|
p := meta.(*ResourceProvider)
|
|
client := p.client
|
|
|
|
// Merge the diff into the state so that we have all the attributes
|
|
// properly.
|
|
rs := s.MergeDiff(d)
|
|
|
|
// 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"],
|
|
}
|
|
|
|
// 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))
|
|
|
|
// 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), ",")
|
|
}
|
|
|
|
for _, v := range v {
|
|
vs = append(vs, v.(string))
|
|
}
|
|
|
|
opts.SSHKeys = vs
|
|
}
|
|
}
|
|
|
|
log.Printf("[DEBUG] Droplet create configuration: %#v", opts)
|
|
|
|
id, err := client.CreateDroplet(&opts)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error creating Droplet: %s", err)
|
|
}
|
|
|
|
// Assign the droplets id
|
|
rs.ID = id
|
|
|
|
log.Printf("[INFO] Droplet ID: %s", id)
|
|
|
|
dropletRaw, err := WaitForDropletAttribute(id, "active", []string{"new"}, "status", client)
|
|
|
|
if err != nil {
|
|
return rs, fmt.Errorf(
|
|
"Error waiting for droplet (%s) to become ready: %s",
|
|
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)
|
|
}
|
|
|
|
func resource_digitalocean_droplet_update(
|
|
s *terraform.InstanceState,
|
|
d *terraform.ResourceDiff,
|
|
meta interface{}) (*terraform.InstanceState, error) {
|
|
p := meta.(*ResourceProvider)
|
|
client := p.client
|
|
rs := s.MergeDiff(d)
|
|
|
|
var err error
|
|
|
|
if attr, ok := d.Attributes["size"]; ok {
|
|
err = client.PowerOff(rs.ID)
|
|
|
|
if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") {
|
|
return s, err
|
|
}
|
|
|
|
// Wait for power off
|
|
_, err = WaitForDropletAttribute(
|
|
rs.ID, "off", []string{"active"}, "status", client)
|
|
|
|
err = client.Resize(rs.ID, attr.New)
|
|
|
|
if err != nil {
|
|
newErr := power_on_and_wait(rs.ID, client)
|
|
if newErr != nil {
|
|
return rs, newErr
|
|
}
|
|
return rs, err
|
|
}
|
|
|
|
// Wait for the size to change
|
|
_, err = WaitForDropletAttribute(
|
|
rs.ID, attr.New, []string{"", attr.Old}, "size", client)
|
|
|
|
if err != nil {
|
|
newErr := power_on_and_wait(rs.ID, client)
|
|
if newErr != nil {
|
|
return rs, newErr
|
|
}
|
|
return s, err
|
|
}
|
|
|
|
err = client.PowerOn(rs.ID)
|
|
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
// Wait for power off
|
|
_, err = WaitForDropletAttribute(
|
|
rs.ID, "active", []string{"off"}, "status", client)
|
|
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
}
|
|
|
|
if attr, ok := d.Attributes["name"]; ok {
|
|
err = client.Rename(rs.ID, attr.New)
|
|
|
|
if err != nil {
|
|
return s, 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)
|
|
|
|
if err != nil {
|
|
return s, 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)
|
|
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
// Wait for ipv6 to turn on/off
|
|
_, err = WaitForDropletAttribute(
|
|
rs.ID, attr.New, []string{"", attr.Old}, "ipv6", client)
|
|
}
|
|
|
|
droplet, err := resource_digitalocean_droplet_retrieve(rs.ID, client)
|
|
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
return resource_digitalocean_droplet_update_state(rs, droplet)
|
|
}
|
|
|
|
func resource_digitalocean_droplet_destroy(
|
|
s *terraform.InstanceState,
|
|
meta interface{}) error {
|
|
p := meta.(*ResourceProvider)
|
|
client := p.client
|
|
|
|
log.Printf("[INFO] Deleting Droplet: %s", s.ID)
|
|
|
|
// Destroy the droplet
|
|
err := client.DestroyDroplet(s.ID)
|
|
|
|
// Handle remotely destroyed droplets
|
|
if err != nil && strings.Contains(err.Error(), "404 Not Found") {
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
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.ResourceDiff, 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
|
|
// that show up after a while
|
|
log.Printf(
|
|
"[INFO] Waiting for Droplet (%s) to have %s of %s",
|
|
id, attribute, target)
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: pending,
|
|
Target: target,
|
|
Refresh: new_droplet_state_refresh_func(id, attribute, client),
|
|
Timeout: 10 * time.Minute,
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
return stateConf.WaitForState()
|
|
}
|
|
|
|
func new_droplet_state_refresh_func(id string, attribute string, client *digitalocean.Client) resource.StateRefreshFunc {
|
|
return func() (interface{}, string, error) {
|
|
// Retrieve the ELB properties for updating the state
|
|
droplet, err := client.RetrieveDroplet(id)
|
|
|
|
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" {
|
|
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
|
|
}
|
|
|
|
return nil, "", nil
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait for power on
|
|
_, err = WaitForDropletAttribute(
|
|
id, "active", []string{"off"}, "status", client)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|