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 }