github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/builtin/providers/digitalocean/resource_digitalocean_droplet.go (about)

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