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 }