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 }