github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 if droplet.Image.Slug != "" { 264 d.Set("image", droplet.Image.Slug) 265 } else { 266 d.Set("image", droplet.Image.ID) 267 } 268 269 d.Set("name", droplet.Name) 270 d.Set("region", droplet.Region.Slug) 271 d.Set("size", droplet.Size.Slug) 272 d.Set("price_hourly", droplet.Size.PriceHourly) 273 d.Set("price_monthly", droplet.Size.PriceMonthly) 274 d.Set("disk", droplet.Disk) 275 d.Set("vcpus", droplet.Vcpus) 276 d.Set("status", droplet.Status) 277 d.Set("locked", strconv.FormatBool(droplet.Locked)) 278 279 if len(droplet.VolumeIDs) > 0 { 280 vlms := make([]interface{}, 0, len(droplet.VolumeIDs)) 281 for _, vid := range droplet.VolumeIDs { 282 vlms = append(vlms, vid) 283 } 284 d.Set("volume_ids", vlms) 285 } 286 287 if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" { 288 d.Set("ipv6", true) 289 d.Set("ipv6_address", strings.ToLower(publicIPv6)) 290 d.Set("ipv6_address_private", findIPv6AddrByType(droplet, "private")) 291 } 292 293 d.Set("ipv4_address", findIPv4AddrByType(droplet, "public")) 294 295 if privateIPv4 := findIPv4AddrByType(droplet, "private"); privateIPv4 != "" { 296 d.Set("private_networking", true) 297 d.Set("ipv4_address_private", privateIPv4) 298 } 299 300 // Initialize the connection info 301 d.SetConnInfo(map[string]string{ 302 "type": "ssh", 303 "host": findIPv4AddrByType(droplet, "public"), 304 }) 305 306 d.Set("tags", droplet.Tags) 307 308 return nil 309 } 310 311 func findIPv6AddrByType(d *godo.Droplet, addrType string) string { 312 for _, addr := range d.Networks.V6 { 313 if addr.Type == addrType { 314 return addr.IPAddress 315 } 316 } 317 return "" 318 } 319 320 func findIPv4AddrByType(d *godo.Droplet, addrType string) string { 321 for _, addr := range d.Networks.V4 { 322 if addr.Type == addrType { 323 return addr.IPAddress 324 } 325 } 326 return "" 327 } 328 329 func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error { 330 client := meta.(*godo.Client) 331 332 id, err := strconv.Atoi(d.Id()) 333 if err != nil { 334 return fmt.Errorf("invalid droplet id: %v", err) 335 } 336 337 resize_disk := d.Get("resize_disk").(bool) 338 if d.HasChange("size") || d.HasChange("resize_disk") && resize_disk { 339 newSize := d.Get("size") 340 341 _, _, err = client.DropletActions.PowerOff(id) 342 if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { 343 return fmt.Errorf( 344 "Error powering off droplet (%s): %s", d.Id(), err) 345 } 346 347 // Wait for power off 348 _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client) 349 if err != nil { 350 return fmt.Errorf( 351 "Error waiting for droplet (%s) to become powered off: %s", d.Id(), err) 352 } 353 354 // Resize the droplet 355 action, _, err := client.DropletActions.Resize(id, newSize.(string), resize_disk) 356 if err != nil { 357 newErr := powerOnAndWait(d, meta) 358 if newErr != nil { 359 return fmt.Errorf( 360 "Error powering on droplet (%s) after failed resize: %s", d.Id(), err) 361 } 362 return fmt.Errorf( 363 "Error resizing droplet (%s): %s", d.Id(), err) 364 } 365 366 // Wait for the resize action to complete. 367 if err := waitForAction(client, action); err != nil { 368 newErr := powerOnAndWait(d, meta) 369 if newErr != nil { 370 return fmt.Errorf( 371 "Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err) 372 } 373 return fmt.Errorf( 374 "Error waiting for resize droplet (%s) to finish: %s", d.Id(), err) 375 } 376 377 _, _, err = client.DropletActions.PowerOn(id) 378 379 if err != nil { 380 return fmt.Errorf( 381 "Error powering on droplet (%s) after resize: %s", d.Id(), err) 382 } 383 384 // Wait for power off 385 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta) 386 if err != nil { 387 return err 388 } 389 } 390 391 if d.HasChange("name") { 392 oldName, newName := d.GetChange("name") 393 394 // Rename the droplet 395 _, _, err = client.DropletActions.Rename(id, newName.(string)) 396 397 if err != nil { 398 return fmt.Errorf( 399 "Error renaming droplet (%s): %s", d.Id(), err) 400 } 401 402 // Wait for the name to change 403 _, err = WaitForDropletAttribute( 404 d, newName.(string), []string{"", oldName.(string)}, "name", meta) 405 406 if err != nil { 407 return fmt.Errorf( 408 "Error waiting for rename droplet (%s) to finish: %s", d.Id(), err) 409 } 410 } 411 412 // As there is no way to disable private networking, 413 // we only check if it needs to be enabled 414 if d.HasChange("private_networking") && d.Get("private_networking").(bool) { 415 _, _, err = client.DropletActions.EnablePrivateNetworking(id) 416 417 if err != nil { 418 return fmt.Errorf( 419 "Error enabling private networking for droplet (%s): %s", d.Id(), err) 420 } 421 422 // Wait for the private_networking to turn on 423 _, err = WaitForDropletAttribute( 424 d, "true", []string{"", "false"}, "private_networking", meta) 425 426 return fmt.Errorf( 427 "Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err) 428 } 429 430 // As there is no way to disable IPv6, we only check if it needs to be enabled 431 if d.HasChange("ipv6") && d.Get("ipv6").(bool) { 432 _, _, err = client.DropletActions.EnableIPv6(id) 433 434 if err != nil { 435 return fmt.Errorf( 436 "Error turning on ipv6 for droplet (%s): %s", d.Id(), err) 437 } 438 439 // Wait for ipv6 to turn on 440 _, err = WaitForDropletAttribute( 441 d, "true", []string{"", "false"}, "ipv6", meta) 442 443 if err != nil { 444 return fmt.Errorf( 445 "Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err) 446 } 447 } 448 449 if d.HasChange("tags") { 450 err = setTags(client, d) 451 if err != nil { 452 return fmt.Errorf("Error updating tags: %s", err) 453 } 454 } 455 456 if d.HasChange("volume_ids") { 457 oldIDs, newIDs := d.GetChange("volume_ids") 458 newSet := func(ids []interface{}) map[string]struct{} { 459 out := make(map[string]struct{}, len(ids)) 460 for _, id := range ids { 461 out[id.(string)] = struct{}{} 462 } 463 return out 464 } 465 // leftDiff returns all elements in Left that are not in Right 466 leftDiff := func(left, right map[string]struct{}) map[string]struct{} { 467 out := make(map[string]struct{}) 468 for l := range left { 469 if _, ok := right[l]; !ok { 470 out[l] = struct{}{} 471 } 472 } 473 return out 474 } 475 oldIDSet := newSet(oldIDs.([]interface{})) 476 newIDSet := newSet(newIDs.([]interface{})) 477 for volumeID := range leftDiff(newIDSet, oldIDSet) { 478 action, _, err := client.StorageActions.Attach(volumeID, id) 479 if err != nil { 480 return fmt.Errorf("Error attaching volume %q to droplet (%s): %s", volumeID, d.Id(), err) 481 } 482 // can't fire >1 action at a time, so waiting for each is OK 483 if err := waitForAction(client, action); err != nil { 484 return fmt.Errorf("Error waiting for volume %q to attach to droplet (%s): %s", volumeID, d.Id(), err) 485 } 486 } 487 for volumeID := range leftDiff(oldIDSet, newIDSet) { 488 action, _, err := client.StorageActions.Detach(volumeID) 489 if err != nil { 490 return fmt.Errorf("Error detaching volume %q from droplet (%s): %s", volumeID, d.Id(), err) 491 } 492 // can't fire >1 action at a time, so waiting for each is OK 493 if err := waitForAction(client, action); err != nil { 494 return fmt.Errorf("Error waiting for volume %q to detach from droplet (%s): %s", volumeID, d.Id(), err) 495 } 496 } 497 } 498 499 return resourceDigitalOceanDropletRead(d, meta) 500 } 501 502 func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error { 503 client := meta.(*godo.Client) 504 505 id, err := strconv.Atoi(d.Id()) 506 if err != nil { 507 return fmt.Errorf("invalid droplet id: %v", err) 508 } 509 510 _, err = WaitForDropletAttribute( 511 d, "false", []string{"", "true"}, "locked", meta) 512 513 if err != nil { 514 return fmt.Errorf( 515 "Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err) 516 } 517 518 log.Printf("[INFO] Deleting droplet: %s", d.Id()) 519 520 // Destroy the droplet 521 _, err = client.Droplets.Delete(id) 522 523 // Handle remotely destroyed droplets 524 if err != nil && strings.Contains(err.Error(), "404 Not Found") { 525 return nil 526 } 527 528 if err != nil { 529 return fmt.Errorf("Error deleting droplet: %s", err) 530 } 531 532 return nil 533 } 534 535 func WaitForDropletAttribute( 536 d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) { 537 // Wait for the droplet so we can get the networking attributes 538 // that show up after a while 539 log.Printf( 540 "[INFO] Waiting for droplet (%s) to have %s of %s", 541 d.Id(), attribute, target) 542 543 stateConf := &resource.StateChangeConf{ 544 Pending: pending, 545 Target: []string{target}, 546 Refresh: newDropletStateRefreshFunc(d, attribute, meta), 547 Timeout: 60 * time.Minute, 548 Delay: 10 * time.Second, 549 MinTimeout: 3 * time.Second, 550 551 // This is a hack around DO API strangeness. 552 // https://github.com/hashicorp/terraform/issues/481 553 // 554 NotFoundChecks: 60, 555 } 556 557 return stateConf.WaitForState() 558 } 559 560 // TODO This function still needs a little more refactoring to make it 561 // cleaner and more efficient 562 func newDropletStateRefreshFunc( 563 d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc { 564 client := meta.(*godo.Client) 565 return func() (interface{}, string, error) { 566 id, err := strconv.Atoi(d.Id()) 567 if err != nil { 568 return nil, "", err 569 } 570 571 err = resourceDigitalOceanDropletRead(d, meta) 572 if err != nil { 573 return nil, "", err 574 } 575 576 // If the droplet is locked, continue waiting. We can 577 // only perform actions on unlocked droplets, so it's 578 // pointless to look at that status 579 if d.Get("locked").(string) == "true" { 580 log.Println("[DEBUG] Droplet is locked, skipping status check and retrying") 581 return nil, "", nil 582 } 583 584 // See if we can access our attribute 585 if attr, ok := d.GetOk(attribute); ok { 586 // Retrieve the droplet properties 587 droplet, _, err := client.Droplets.Get(id) 588 if err != nil { 589 return nil, "", fmt.Errorf("Error retrieving droplet: %s", err) 590 } 591 592 return &droplet, attr.(string), nil 593 } 594 595 return nil, "", nil 596 } 597 } 598 599 // Powers on the droplet and waits for it to be active 600 func powerOnAndWait(d *schema.ResourceData, meta interface{}) error { 601 id, err := strconv.Atoi(d.Id()) 602 if err != nil { 603 return fmt.Errorf("invalid droplet id: %v", err) 604 } 605 606 client := meta.(*godo.Client) 607 _, _, err = client.DropletActions.PowerOn(id) 608 if err != nil { 609 return err 610 } 611 612 // Wait for power on 613 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client) 614 if err != nil { 615 return err 616 } 617 618 return nil 619 }