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