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