github.com/daveadams/terraform@v0.6.4-0.20160830094355-13ce74975936/builtin/providers/digitalocean/resource_digitalocean_droplet.go (about)

     1  package digitalocean
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/digitalocean/godo"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  )
    14  
    15  func resourceDigitalOceanDroplet() *schema.Resource {
    16  	return &schema.Resource{
    17  		Create: resourceDigitalOceanDropletCreate,
    18  		Read:   resourceDigitalOceanDropletRead,
    19  		Update: resourceDigitalOceanDropletUpdate,
    20  		Delete: resourceDigitalOceanDropletDelete,
    21  		Importer: &schema.ResourceImporter{
    22  			State: schema.ImportStatePassthrough,
    23  		},
    24  
    25  		Schema: map[string]*schema.Schema{
    26  			"image": &schema.Schema{
    27  				Type:     schema.TypeString,
    28  				Required: true,
    29  				ForceNew: true,
    30  			},
    31  
    32  			"name": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Required: true,
    35  			},
    36  
    37  			"region": &schema.Schema{
    38  				Type:     schema.TypeString,
    39  				Required: true,
    40  				ForceNew: true,
    41  				StateFunc: func(val interface{}) string {
    42  					// DO API V2 region slug is always lowercase
    43  					return strings.ToLower(val.(string))
    44  				},
    45  			},
    46  
    47  			"size": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Required: true,
    50  				StateFunc: func(val interface{}) string {
    51  					// DO API V2 size slug is always lowercase
    52  					return strings.ToLower(val.(string))
    53  				},
    54  			},
    55  
    56  			"status": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Computed: true,
    59  			},
    60  
    61  			"locked": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Computed: true,
    64  			},
    65  
    66  			"backups": &schema.Schema{
    67  				Type:     schema.TypeBool,
    68  				Optional: true,
    69  			},
    70  
    71  			"ipv6": &schema.Schema{
    72  				Type:     schema.TypeBool,
    73  				Optional: true,
    74  			},
    75  
    76  			"ipv6_address": &schema.Schema{
    77  				Type:     schema.TypeString,
    78  				Computed: true,
    79  				StateFunc: func(val interface{}) string {
    80  					return strings.ToLower(val.(string))
    81  				},
    82  			},
    83  
    84  			"ipv6_address_private": &schema.Schema{
    85  				Type:     schema.TypeString,
    86  				Computed: true,
    87  			},
    88  
    89  			"private_networking": &schema.Schema{
    90  				Type:     schema.TypeBool,
    91  				Optional: true,
    92  			},
    93  
    94  			"ipv4_address": &schema.Schema{
    95  				Type:     schema.TypeString,
    96  				Computed: true,
    97  			},
    98  
    99  			"ipv4_address_private": &schema.Schema{
   100  				Type:     schema.TypeString,
   101  				Computed: true,
   102  			},
   103  
   104  			"ssh_keys": &schema.Schema{
   105  				Type:     schema.TypeList,
   106  				Optional: true,
   107  				Elem:     &schema.Schema{Type: schema.TypeString},
   108  			},
   109  
   110  			"tags": &schema.Schema{
   111  				Type:     schema.TypeList,
   112  				Optional: true,
   113  				Elem:     &schema.Schema{Type: schema.TypeString},
   114  			},
   115  
   116  			"user_data": &schema.Schema{
   117  				Type:     schema.TypeString,
   118  				Optional: true,
   119  				ForceNew: true,
   120  			},
   121  
   122  			"volume_ids": &schema.Schema{
   123  				Type:     schema.TypeList,
   124  				Elem:     &schema.Schema{Type: schema.TypeString},
   125  				Optional: true,
   126  			},
   127  		},
   128  	}
   129  }
   130  
   131  func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error {
   132  	client := meta.(*godo.Client)
   133  
   134  	// Build up our creation options
   135  	opts := &godo.DropletCreateRequest{
   136  		Image: godo.DropletCreateImage{
   137  			Slug: d.Get("image").(string),
   138  		},
   139  		Name:   d.Get("name").(string),
   140  		Region: d.Get("region").(string),
   141  		Size:   d.Get("size").(string),
   142  	}
   143  
   144  	if attr, ok := d.GetOk("backups"); ok {
   145  		opts.Backups = attr.(bool)
   146  	}
   147  
   148  	if attr, ok := d.GetOk("ipv6"); ok {
   149  		opts.IPv6 = attr.(bool)
   150  	}
   151  
   152  	if attr, ok := d.GetOk("private_networking"); ok {
   153  		opts.PrivateNetworking = attr.(bool)
   154  	}
   155  
   156  	if attr, ok := d.GetOk("user_data"); ok {
   157  		opts.UserData = attr.(string)
   158  	}
   159  
   160  	if attr, ok := d.GetOk("volume_ids"); ok {
   161  		for _, id := range attr.([]interface{}) {
   162  			opts.Volumes = append(opts.Volumes, godo.DropletCreateVolume{
   163  				ID: id.(string),
   164  			})
   165  		}
   166  	}
   167  
   168  	// Get configured ssh_keys
   169  	sshKeys := d.Get("ssh_keys.#").(int)
   170  	if sshKeys > 0 {
   171  		opts.SSHKeys = make([]godo.DropletCreateSSHKey, 0, sshKeys)
   172  		for i := 0; i < sshKeys; i++ {
   173  			key := fmt.Sprintf("ssh_keys.%d", i)
   174  			sshKeyRef := d.Get(key).(string)
   175  
   176  			var sshKey godo.DropletCreateSSHKey
   177  			// sshKeyRef can be either an ID or a fingerprint
   178  			if id, err := strconv.Atoi(sshKeyRef); err == nil {
   179  				sshKey.ID = id
   180  			} else {
   181  				sshKey.Fingerprint = sshKeyRef
   182  			}
   183  
   184  			opts.SSHKeys = append(opts.SSHKeys, sshKey)
   185  		}
   186  	}
   187  
   188  	log.Printf("[DEBUG] Droplet create configuration: %#v", opts)
   189  
   190  	droplet, _, err := client.Droplets.Create(opts)
   191  
   192  	if err != nil {
   193  		return fmt.Errorf("Error creating droplet: %s", err)
   194  	}
   195  
   196  	// Assign the droplets id
   197  	d.SetId(strconv.Itoa(droplet.ID))
   198  
   199  	log.Printf("[INFO] Droplet ID: %s", d.Id())
   200  
   201  	_, err = WaitForDropletAttribute(d, "active", []string{"new"}, "status", meta)
   202  	if err != nil {
   203  		return fmt.Errorf(
   204  			"Error waiting for droplet (%s) to become ready: %s", d.Id(), err)
   205  	}
   206  
   207  	// droplet needs to be active in order to set tags
   208  	err = setTags(client, d)
   209  	if err != nil {
   210  		return fmt.Errorf("Error setting tags: %s", err)
   211  	}
   212  
   213  	return resourceDigitalOceanDropletRead(d, meta)
   214  }
   215  
   216  func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error {
   217  	client := meta.(*godo.Client)
   218  
   219  	id, err := strconv.Atoi(d.Id())
   220  	if err != nil {
   221  		return fmt.Errorf("invalid droplet id: %v", err)
   222  	}
   223  
   224  	// Retrieve the droplet properties for updating the state
   225  	droplet, resp, err := client.Droplets.Get(id)
   226  	if err != nil {
   227  		// check if the droplet no longer exists.
   228  		if resp != nil && resp.StatusCode == 404 {
   229  			log.Printf("[WARN] DigitalOcean Droplet (%s) not found", d.Id())
   230  			d.SetId("")
   231  			return nil
   232  		}
   233  
   234  		return fmt.Errorf("Error retrieving droplet: %s", err)
   235  	}
   236  
   237  	if droplet.Image.Slug != "" {
   238  		d.Set("image", droplet.Image.Slug)
   239  	} else {
   240  		d.Set("image", droplet.Image.ID)
   241  	}
   242  
   243  	d.Set("name", droplet.Name)
   244  	d.Set("region", droplet.Region.Slug)
   245  	d.Set("size", droplet.Size.Slug)
   246  	d.Set("status", droplet.Status)
   247  	d.Set("locked", strconv.FormatBool(droplet.Locked))
   248  
   249  	if len(droplet.VolumeIDs) > 0 {
   250  		vlms := make([]interface{}, 0, len(droplet.VolumeIDs))
   251  		for _, vid := range droplet.VolumeIDs {
   252  			vlms = append(vlms, vid)
   253  		}
   254  		d.Set("volume_ids", vlms)
   255  	}
   256  
   257  	if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" {
   258  		d.Set("ipv6", true)
   259  		d.Set("ipv6_address", strings.ToLower(publicIPv6))
   260  		d.Set("ipv6_address_private", findIPv6AddrByType(droplet, "private"))
   261  	}
   262  
   263  	d.Set("ipv4_address", findIPv4AddrByType(droplet, "public"))
   264  
   265  	if privateIPv4 := findIPv4AddrByType(droplet, "private"); privateIPv4 != "" {
   266  		d.Set("private_networking", true)
   267  		d.Set("ipv4_address_private", privateIPv4)
   268  	}
   269  
   270  	// Initialize the connection info
   271  	d.SetConnInfo(map[string]string{
   272  		"type": "ssh",
   273  		"host": findIPv4AddrByType(droplet, "public"),
   274  	})
   275  
   276  	d.Set("tags", droplet.Tags)
   277  
   278  	return nil
   279  }
   280  
   281  func findIPv6AddrByType(d *godo.Droplet, addrType string) string {
   282  	for _, addr := range d.Networks.V6 {
   283  		if addr.Type == addrType {
   284  			return addr.IPAddress
   285  		}
   286  	}
   287  	return ""
   288  }
   289  
   290  func findIPv4AddrByType(d *godo.Droplet, addrType string) string {
   291  	for _, addr := range d.Networks.V4 {
   292  		if addr.Type == addrType {
   293  			return addr.IPAddress
   294  		}
   295  	}
   296  	return ""
   297  }
   298  
   299  func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error {
   300  	client := meta.(*godo.Client)
   301  
   302  	id, err := strconv.Atoi(d.Id())
   303  	if err != nil {
   304  		return fmt.Errorf("invalid droplet id: %v", err)
   305  	}
   306  
   307  	if d.HasChange("size") {
   308  		oldSize, newSize := d.GetChange("size")
   309  
   310  		_, _, err = client.DropletActions.PowerOff(id)
   311  		if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") {
   312  			return fmt.Errorf(
   313  				"Error powering off droplet (%s): %s", d.Id(), err)
   314  		}
   315  
   316  		// Wait for power off
   317  		_, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client)
   318  		if err != nil {
   319  			return fmt.Errorf(
   320  				"Error waiting for droplet (%s) to become powered off: %s", d.Id(), err)
   321  		}
   322  
   323  		// Resize the droplet
   324  		_, _, err = client.DropletActions.Resize(id, newSize.(string), true)
   325  		if err != nil {
   326  			newErr := powerOnAndWait(d, meta)
   327  			if newErr != nil {
   328  				return fmt.Errorf(
   329  					"Error powering on droplet (%s) after failed resize: %s", d.Id(), err)
   330  			}
   331  			return fmt.Errorf(
   332  				"Error resizing droplet (%s): %s", d.Id(), err)
   333  		}
   334  
   335  		// Wait for the size to change
   336  		_, err = WaitForDropletAttribute(
   337  			d, newSize.(string), []string{"", oldSize.(string)}, "size", meta)
   338  
   339  		if err != nil {
   340  			newErr := powerOnAndWait(d, meta)
   341  			if newErr != nil {
   342  				return fmt.Errorf(
   343  					"Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err)
   344  			}
   345  			return fmt.Errorf(
   346  				"Error waiting for resize droplet (%s) to finish: %s", d.Id(), err)
   347  		}
   348  
   349  		_, _, err = client.DropletActions.PowerOn(id)
   350  
   351  		if err != nil {
   352  			return fmt.Errorf(
   353  				"Error powering on droplet (%s) after resize: %s", d.Id(), err)
   354  		}
   355  
   356  		// Wait for power off
   357  		_, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta)
   358  		if err != nil {
   359  			return err
   360  		}
   361  	}
   362  
   363  	if d.HasChange("name") {
   364  		oldName, newName := d.GetChange("name")
   365  
   366  		// Rename the droplet
   367  		_, _, err = client.DropletActions.Rename(id, newName.(string))
   368  
   369  		if err != nil {
   370  			return fmt.Errorf(
   371  				"Error renaming droplet (%s): %s", d.Id(), err)
   372  		}
   373  
   374  		// Wait for the name to change
   375  		_, err = WaitForDropletAttribute(
   376  			d, newName.(string), []string{"", oldName.(string)}, "name", meta)
   377  
   378  		if err != nil {
   379  			return fmt.Errorf(
   380  				"Error waiting for rename droplet (%s) to finish: %s", d.Id(), err)
   381  		}
   382  	}
   383  
   384  	// As there is no way to disable private networking,
   385  	// we only check if it needs to be enabled
   386  	if d.HasChange("private_networking") && d.Get("private_networking").(bool) {
   387  		_, _, err = client.DropletActions.EnablePrivateNetworking(id)
   388  
   389  		if err != nil {
   390  			return fmt.Errorf(
   391  				"Error enabling private networking for droplet (%s): %s", d.Id(), err)
   392  		}
   393  
   394  		// Wait for the private_networking to turn on
   395  		_, err = WaitForDropletAttribute(
   396  			d, "true", []string{"", "false"}, "private_networking", meta)
   397  
   398  		return fmt.Errorf(
   399  			"Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err)
   400  	}
   401  
   402  	// As there is no way to disable IPv6, we only check if it needs to be enabled
   403  	if d.HasChange("ipv6") && d.Get("ipv6").(bool) {
   404  		_, _, err = client.DropletActions.EnableIPv6(id)
   405  
   406  		if err != nil {
   407  			return fmt.Errorf(
   408  				"Error turning on ipv6 for droplet (%s): %s", d.Id(), err)
   409  		}
   410  
   411  		// Wait for ipv6 to turn on
   412  		_, err = WaitForDropletAttribute(
   413  			d, "true", []string{"", "false"}, "ipv6", meta)
   414  
   415  		if err != nil {
   416  			return fmt.Errorf(
   417  				"Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err)
   418  		}
   419  	}
   420  
   421  	if d.HasChange("tags") {
   422  		err = setTags(client, d)
   423  		if err != nil {
   424  			return fmt.Errorf("Error updating tags: %s", err)
   425  		}
   426  	}
   427  
   428  	if d.HasChange("volume_ids") {
   429  		oldIDs, newIDs := d.GetChange("volume_ids")
   430  		newSet := func(ids []interface{}) map[string]struct{} {
   431  			out := make(map[string]struct{}, len(ids))
   432  			for _, id := range ids {
   433  				out[id.(string)] = struct{}{}
   434  			}
   435  			return out
   436  		}
   437  		// leftDiff returns all elements in Left that are not in Right
   438  		leftDiff := func(left, right map[string]struct{}) map[string]struct{} {
   439  			out := make(map[string]struct{})
   440  			for l := range left {
   441  				if _, ok := right[l]; !ok {
   442  					out[l] = struct{}{}
   443  				}
   444  			}
   445  			return out
   446  		}
   447  		oldIDSet := newSet(oldIDs.([]interface{}))
   448  		newIDSet := newSet(newIDs.([]interface{}))
   449  		for volumeID := range leftDiff(newIDSet, oldIDSet) {
   450  			action, _, err := client.StorageActions.Attach(volumeID, id)
   451  			if err != nil {
   452  				return fmt.Errorf("Error attaching volume %q to droplet (%s): %s", volumeID, d.Id(), err)
   453  			}
   454  			// can't fire >1 action at a time, so waiting for each is OK
   455  			if err := waitForAction(client, action); err != nil {
   456  				return fmt.Errorf("Error waiting for volume %q to attach to droplet (%s): %s", volumeID, d.Id(), err)
   457  			}
   458  		}
   459  		for volumeID := range leftDiff(oldIDSet, newIDSet) {
   460  			action, _, err := client.StorageActions.Detach(volumeID)
   461  			if err != nil {
   462  				return fmt.Errorf("Error detaching volume %q from droplet (%s): %s", volumeID, d.Id(), err)
   463  			}
   464  			// can't fire >1 action at a time, so waiting for each is OK
   465  			if err := waitForAction(client, action); err != nil {
   466  				return fmt.Errorf("Error waiting for volume %q to detach from droplet (%s): %s", volumeID, d.Id(), err)
   467  			}
   468  		}
   469  	}
   470  
   471  	return resourceDigitalOceanDropletRead(d, meta)
   472  }
   473  
   474  func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error {
   475  	client := meta.(*godo.Client)
   476  
   477  	id, err := strconv.Atoi(d.Id())
   478  	if err != nil {
   479  		return fmt.Errorf("invalid droplet id: %v", err)
   480  	}
   481  
   482  	_, err = WaitForDropletAttribute(
   483  		d, "false", []string{"", "true"}, "locked", meta)
   484  
   485  	if err != nil {
   486  		return fmt.Errorf(
   487  			"Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err)
   488  	}
   489  
   490  	log.Printf("[INFO] Deleting droplet: %s", d.Id())
   491  
   492  	// Destroy the droplet
   493  	_, err = client.Droplets.Delete(id)
   494  
   495  	// Handle remotely destroyed droplets
   496  	if err != nil && strings.Contains(err.Error(), "404 Not Found") {
   497  		return nil
   498  	}
   499  
   500  	if err != nil {
   501  		return fmt.Errorf("Error deleting droplet: %s", err)
   502  	}
   503  
   504  	return nil
   505  }
   506  
   507  func WaitForDropletAttribute(
   508  	d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
   509  	// Wait for the droplet so we can get the networking attributes
   510  	// that show up after a while
   511  	log.Printf(
   512  		"[INFO] Waiting for droplet (%s) to have %s of %s",
   513  		d.Id(), attribute, target)
   514  
   515  	stateConf := &resource.StateChangeConf{
   516  		Pending:    pending,
   517  		Target:     []string{target},
   518  		Refresh:    newDropletStateRefreshFunc(d, attribute, meta),
   519  		Timeout:    60 * time.Minute,
   520  		Delay:      10 * time.Second,
   521  		MinTimeout: 3 * time.Second,
   522  
   523  		// This is a hack around DO API strangeness.
   524  		// https://github.com/hashicorp/terraform/issues/481
   525  		//
   526  		NotFoundChecks: 60,
   527  	}
   528  
   529  	return stateConf.WaitForState()
   530  }
   531  
   532  // TODO This function still needs a little more refactoring to make it
   533  // cleaner and more efficient
   534  func newDropletStateRefreshFunc(
   535  	d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
   536  	client := meta.(*godo.Client)
   537  	return func() (interface{}, string, error) {
   538  		id, err := strconv.Atoi(d.Id())
   539  		if err != nil {
   540  			return nil, "", err
   541  		}
   542  
   543  		err = resourceDigitalOceanDropletRead(d, meta)
   544  		if err != nil {
   545  			return nil, "", err
   546  		}
   547  
   548  		// If the droplet is locked, continue waiting. We can
   549  		// only perform actions on unlocked droplets, so it's
   550  		// pointless to look at that status
   551  		if d.Get("locked").(string) == "true" {
   552  			log.Println("[DEBUG] Droplet is locked, skipping status check and retrying")
   553  			return nil, "", nil
   554  		}
   555  
   556  		// See if we can access our attribute
   557  		if attr, ok := d.GetOk(attribute); ok {
   558  			// Retrieve the droplet properties
   559  			droplet, _, err := client.Droplets.Get(id)
   560  			if err != nil {
   561  				return nil, "", fmt.Errorf("Error retrieving droplet: %s", err)
   562  			}
   563  
   564  			return &droplet, attr.(string), nil
   565  		}
   566  
   567  		return nil, "", nil
   568  	}
   569  }
   570  
   571  // Powers on the droplet and waits for it to be active
   572  func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
   573  	id, err := strconv.Atoi(d.Id())
   574  	if err != nil {
   575  		return fmt.Errorf("invalid droplet id: %v", err)
   576  	}
   577  
   578  	client := meta.(*godo.Client)
   579  	_, _, err = client.DropletActions.PowerOn(id)
   580  	if err != nil {
   581  		return err
   582  	}
   583  
   584  	// Wait for power on
   585  	_, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client)
   586  	if err != nil {
   587  		return err
   588  	}
   589  
   590  	return nil
   591  }