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