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