github.com/gabrielperezs/terraform@v0.7.0-rc2.0.20160715084931-f7da2612946f/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": &schema.Schema{ 27 Type: schema.TypeString, 28 Required: true, 29 ForceNew: true, 30 }, 31 32 "name": &schema.Schema{ 33 Type: schema.TypeString, 34 Required: true, 35 }, 36 37 "region": &schema.Schema{ 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": &schema.Schema{ 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 "status": &schema.Schema{ 57 Type: schema.TypeString, 58 Computed: true, 59 }, 60 61 "locked": &schema.Schema{ 62 Type: schema.TypeString, 63 Computed: true, 64 }, 65 66 "backups": &schema.Schema{ 67 Type: schema.TypeBool, 68 Optional: true, 69 }, 70 71 "ipv6": &schema.Schema{ 72 Type: schema.TypeBool, 73 Optional: true, 74 }, 75 76 "ipv6_address": &schema.Schema{ 77 Type: schema.TypeString, 78 Computed: true, 79 }, 80 81 "ipv6_address_private": &schema.Schema{ 82 Type: schema.TypeString, 83 Computed: true, 84 }, 85 86 "private_networking": &schema.Schema{ 87 Type: schema.TypeBool, 88 Optional: true, 89 }, 90 91 "ipv4_address": &schema.Schema{ 92 Type: schema.TypeString, 93 Computed: true, 94 }, 95 96 "ipv4_address_private": &schema.Schema{ 97 Type: schema.TypeString, 98 Computed: true, 99 }, 100 101 "ssh_keys": &schema.Schema{ 102 Type: schema.TypeList, 103 Optional: true, 104 Elem: &schema.Schema{Type: schema.TypeString}, 105 }, 106 107 "tags": &schema.Schema{ 108 Type: schema.TypeList, 109 Optional: true, 110 Elem: &schema.Schema{Type: schema.TypeString}, 111 }, 112 113 "user_data": &schema.Schema{ 114 Type: schema.TypeString, 115 Optional: true, 116 ForceNew: true, 117 }, 118 119 "volume_ids": &schema.Schema{ 120 Type: schema.TypeList, 121 Elem: &schema.Schema{Type: schema.TypeString}, 122 Optional: true, 123 }, 124 }, 125 } 126 } 127 128 func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error { 129 client := meta.(*godo.Client) 130 131 // Build up our creation options 132 opts := &godo.DropletCreateRequest{ 133 Image: godo.DropletCreateImage{ 134 Slug: d.Get("image").(string), 135 }, 136 Name: d.Get("name").(string), 137 Region: d.Get("region").(string), 138 Size: d.Get("size").(string), 139 } 140 141 if attr, ok := d.GetOk("backups"); ok { 142 opts.Backups = attr.(bool) 143 } 144 145 if attr, ok := d.GetOk("ipv6"); ok { 146 opts.IPv6 = attr.(bool) 147 } 148 149 if attr, ok := d.GetOk("private_networking"); ok { 150 opts.PrivateNetworking = attr.(bool) 151 } 152 153 if attr, ok := d.GetOk("user_data"); ok { 154 opts.UserData = attr.(string) 155 } 156 157 if attr, ok := d.GetOk("volume_ids"); ok { 158 for _, id := range attr.([]interface{}) { 159 opts.Volumes = append(opts.Volumes, godo.DropletCreateVolume{ 160 ID: id.(string), 161 }) 162 } 163 } 164 165 // Get configured ssh_keys 166 sshKeys := d.Get("ssh_keys.#").(int) 167 if sshKeys > 0 { 168 opts.SSHKeys = make([]godo.DropletCreateSSHKey, 0, sshKeys) 169 for i := 0; i < sshKeys; i++ { 170 key := fmt.Sprintf("ssh_keys.%d", i) 171 sshKeyRef := d.Get(key).(string) 172 173 var sshKey godo.DropletCreateSSHKey 174 // sshKeyRef can be either an ID or a fingerprint 175 if id, err := strconv.Atoi(sshKeyRef); err == nil { 176 sshKey.ID = id 177 } else { 178 sshKey.Fingerprint = sshKeyRef 179 } 180 181 opts.SSHKeys = append(opts.SSHKeys, sshKey) 182 } 183 } 184 185 log.Printf("[DEBUG] Droplet create configuration: %#v", opts) 186 187 droplet, _, err := client.Droplets.Create(opts) 188 189 if err != nil { 190 return fmt.Errorf("Error creating droplet: %s", err) 191 } 192 193 // Assign the droplets id 194 d.SetId(strconv.Itoa(droplet.ID)) 195 196 log.Printf("[INFO] Droplet ID: %s", d.Id()) 197 198 _, err = WaitForDropletAttribute(d, "active", []string{"new"}, "status", meta) 199 if err != nil { 200 return fmt.Errorf( 201 "Error waiting for droplet (%s) to become ready: %s", d.Id(), err) 202 } 203 204 // droplet needs to be active in order to set tags 205 err = setTags(client, d) 206 if err != nil { 207 return fmt.Errorf("Error setting tags: %s", err) 208 } 209 210 return resourceDigitalOceanDropletRead(d, meta) 211 } 212 213 func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) error { 214 client := meta.(*godo.Client) 215 216 id, err := strconv.Atoi(d.Id()) 217 if err != nil { 218 return fmt.Errorf("invalid droplet id: %v", err) 219 } 220 221 // Retrieve the droplet properties for updating the state 222 droplet, resp, err := client.Droplets.Get(id) 223 if err != nil { 224 // check if the droplet no longer exists. 225 if resp != nil && resp.StatusCode == 404 { 226 log.Printf("[WARN] DigitalOcean Droplet (%s) not found", d.Id()) 227 d.SetId("") 228 return nil 229 } 230 231 return fmt.Errorf("Error retrieving droplet: %s", err) 232 } 233 234 if droplet.Image.Slug != "" { 235 d.Set("image", droplet.Image.Slug) 236 } else { 237 d.Set("image", droplet.Image.ID) 238 } 239 240 d.Set("name", droplet.Name) 241 d.Set("region", droplet.Region.Slug) 242 d.Set("size", droplet.Size.Slug) 243 d.Set("status", droplet.Status) 244 d.Set("locked", strconv.FormatBool(droplet.Locked)) 245 246 if len(droplet.VolumeIDs) > 0 { 247 vlms := make([]interface{}, 0, len(droplet.VolumeIDs)) 248 for _, vid := range droplet.VolumeIDs { 249 vlms = append(vlms, vid) 250 } 251 d.Set("volume_ids", vlms) 252 } 253 254 if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" { 255 d.Set("ipv6", true) 256 d.Set("ipv6_address", publicIPv6) 257 d.Set("ipv6_address_private", findIPv6AddrByType(droplet, "private")) 258 } 259 260 d.Set("ipv4_address", findIPv4AddrByType(droplet, "public")) 261 262 if privateIPv4 := findIPv4AddrByType(droplet, "private"); privateIPv4 != "" { 263 d.Set("private_networking", true) 264 d.Set("ipv4_address_private", privateIPv4) 265 } 266 267 // Initialize the connection info 268 d.SetConnInfo(map[string]string{ 269 "type": "ssh", 270 "host": findIPv4AddrByType(droplet, "public"), 271 }) 272 273 d.Set("tags", droplet.Tags) 274 275 return nil 276 } 277 278 func findIPv6AddrByType(d *godo.Droplet, addrType string) string { 279 for _, addr := range d.Networks.V6 { 280 if addr.Type == addrType { 281 return addr.IPAddress 282 } 283 } 284 return "" 285 } 286 287 func findIPv4AddrByType(d *godo.Droplet, addrType string) string { 288 for _, addr := range d.Networks.V4 { 289 if addr.Type == addrType { 290 return addr.IPAddress 291 } 292 } 293 return "" 294 } 295 296 func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) error { 297 client := meta.(*godo.Client) 298 299 id, err := strconv.Atoi(d.Id()) 300 if err != nil { 301 return fmt.Errorf("invalid droplet id: %v", err) 302 } 303 304 if d.HasChange("size") { 305 oldSize, newSize := d.GetChange("size") 306 307 _, _, err = client.DropletActions.PowerOff(id) 308 if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { 309 return fmt.Errorf( 310 "Error powering off droplet (%s): %s", d.Id(), err) 311 } 312 313 // Wait for power off 314 _, err = WaitForDropletAttribute(d, "off", []string{"active"}, "status", client) 315 if err != nil { 316 return fmt.Errorf( 317 "Error waiting for droplet (%s) to become powered off: %s", d.Id(), err) 318 } 319 320 // Resize the droplet 321 _, _, err = client.DropletActions.Resize(id, newSize.(string), true) 322 if err != nil { 323 newErr := powerOnAndWait(d, meta) 324 if newErr != nil { 325 return fmt.Errorf( 326 "Error powering on droplet (%s) after failed resize: %s", d.Id(), err) 327 } 328 return fmt.Errorf( 329 "Error resizing droplet (%s): %s", d.Id(), err) 330 } 331 332 // Wait for the size to change 333 _, err = WaitForDropletAttribute( 334 d, newSize.(string), []string{"", oldSize.(string)}, "size", meta) 335 336 if err != nil { 337 newErr := powerOnAndWait(d, meta) 338 if newErr != nil { 339 return fmt.Errorf( 340 "Error powering on droplet (%s) after waiting for resize to finish: %s", d.Id(), err) 341 } 342 return fmt.Errorf( 343 "Error waiting for resize droplet (%s) to finish: %s", d.Id(), err) 344 } 345 346 _, _, err = client.DropletActions.PowerOn(id) 347 348 if err != nil { 349 return fmt.Errorf( 350 "Error powering on droplet (%s) after resize: %s", d.Id(), err) 351 } 352 353 // Wait for power off 354 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", meta) 355 if err != nil { 356 return err 357 } 358 } 359 360 if d.HasChange("name") { 361 oldName, newName := d.GetChange("name") 362 363 // Rename the droplet 364 _, _, err = client.DropletActions.Rename(id, newName.(string)) 365 366 if err != nil { 367 return fmt.Errorf( 368 "Error renaming droplet (%s): %s", d.Id(), err) 369 } 370 371 // Wait for the name to change 372 _, err = WaitForDropletAttribute( 373 d, newName.(string), []string{"", oldName.(string)}, "name", meta) 374 375 if err != nil { 376 return fmt.Errorf( 377 "Error waiting for rename droplet (%s) to finish: %s", d.Id(), err) 378 } 379 } 380 381 // As there is no way to disable private networking, 382 // we only check if it needs to be enabled 383 if d.HasChange("private_networking") && d.Get("private_networking").(bool) { 384 _, _, err = client.DropletActions.EnablePrivateNetworking(id) 385 386 if err != nil { 387 return fmt.Errorf( 388 "Error enabling private networking for droplet (%s): %s", d.Id(), err) 389 } 390 391 // Wait for the private_networking to turn on 392 _, err = WaitForDropletAttribute( 393 d, "true", []string{"", "false"}, "private_networking", meta) 394 395 return fmt.Errorf( 396 "Error waiting for private networking to be enabled on for droplet (%s): %s", d.Id(), err) 397 } 398 399 // As there is no way to disable IPv6, we only check if it needs to be enabled 400 if d.HasChange("ipv6") && d.Get("ipv6").(bool) { 401 _, _, err = client.DropletActions.EnableIPv6(id) 402 403 if err != nil { 404 return fmt.Errorf( 405 "Error turning on ipv6 for droplet (%s): %s", d.Id(), err) 406 } 407 408 // Wait for ipv6 to turn on 409 _, err = WaitForDropletAttribute( 410 d, "true", []string{"", "false"}, "ipv6", meta) 411 412 if err != nil { 413 return fmt.Errorf( 414 "Error waiting for ipv6 to be turned on for droplet (%s): %s", d.Id(), err) 415 } 416 } 417 418 if d.HasChange("tags") { 419 err = setTags(client, d) 420 if err != nil { 421 return fmt.Errorf("Error updating tags: %s", err) 422 } 423 } 424 425 if d.HasChange("volume_ids") { 426 oldIDs, newIDs := d.GetChange("volume_ids") 427 newSet := func(ids []interface{}) map[string]struct{} { 428 out := make(map[string]struct{}, len(ids)) 429 for _, id := range ids { 430 out[id.(string)] = struct{}{} 431 } 432 return out 433 } 434 // leftDiff returns all elements in Left that are not in Right 435 leftDiff := func(left, right map[string]struct{}) map[string]struct{} { 436 out := make(map[string]struct{}) 437 for l := range left { 438 if _, ok := right[l]; !ok { 439 out[l] = struct{}{} 440 } 441 } 442 return out 443 } 444 oldIDSet := newSet(oldIDs.([]interface{})) 445 newIDSet := newSet(newIDs.([]interface{})) 446 for volumeID := range leftDiff(newIDSet, oldIDSet) { 447 action, _, err := client.StorageActions.Attach(volumeID, id) 448 if err != nil { 449 return fmt.Errorf("Error attaching volume %q to droplet (%s): %s", volumeID, d.Id(), err) 450 } 451 // can't fire >1 action at a time, so waiting for each is OK 452 if err := waitForAction(client, action); err != nil { 453 return fmt.Errorf("Error waiting for volume %q to attach to droplet (%s): %s", volumeID, d.Id(), err) 454 } 455 } 456 for volumeID := range leftDiff(oldIDSet, newIDSet) { 457 action, _, err := client.StorageActions.Detach(volumeID) 458 if err != nil { 459 return fmt.Errorf("Error detaching volume %q from droplet (%s): %s", volumeID, d.Id(), err) 460 } 461 // can't fire >1 action at a time, so waiting for each is OK 462 if err := waitForAction(client, action); err != nil { 463 return fmt.Errorf("Error waiting for volume %q to detach from droplet (%s): %s", volumeID, d.Id(), err) 464 } 465 } 466 } 467 468 return resourceDigitalOceanDropletRead(d, meta) 469 } 470 471 func resourceDigitalOceanDropletDelete(d *schema.ResourceData, meta interface{}) error { 472 client := meta.(*godo.Client) 473 474 id, err := strconv.Atoi(d.Id()) 475 if err != nil { 476 return fmt.Errorf("invalid droplet id: %v", err) 477 } 478 479 _, err = WaitForDropletAttribute( 480 d, "false", []string{"", "true"}, "locked", meta) 481 482 if err != nil { 483 return fmt.Errorf( 484 "Error waiting for droplet to be unlocked for destroy (%s): %s", d.Id(), err) 485 } 486 487 log.Printf("[INFO] Deleting droplet: %s", d.Id()) 488 489 // Destroy the droplet 490 _, err = client.Droplets.Delete(id) 491 492 // Handle remotely destroyed droplets 493 if err != nil && strings.Contains(err.Error(), "404 Not Found") { 494 return nil 495 } 496 497 if err != nil { 498 return fmt.Errorf("Error deleting droplet: %s", err) 499 } 500 501 return nil 502 } 503 504 func WaitForDropletAttribute( 505 d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) { 506 // Wait for the droplet so we can get the networking attributes 507 // that show up after a while 508 log.Printf( 509 "[INFO] Waiting for droplet (%s) to have %s of %s", 510 d.Id(), attribute, target) 511 512 stateConf := &resource.StateChangeConf{ 513 Pending: pending, 514 Target: []string{target}, 515 Refresh: newDropletStateRefreshFunc(d, attribute, meta), 516 Timeout: 60 * time.Minute, 517 Delay: 10 * time.Second, 518 MinTimeout: 3 * time.Second, 519 520 // This is a hack around DO API strangeness. 521 // https://github.com/hashicorp/terraform/issues/481 522 // 523 NotFoundChecks: 60, 524 } 525 526 return stateConf.WaitForState() 527 } 528 529 // TODO This function still needs a little more refactoring to make it 530 // cleaner and more efficient 531 func newDropletStateRefreshFunc( 532 d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc { 533 client := meta.(*godo.Client) 534 return func() (interface{}, string, error) { 535 id, err := strconv.Atoi(d.Id()) 536 if err != nil { 537 return nil, "", err 538 } 539 540 err = resourceDigitalOceanDropletRead(d, meta) 541 if err != nil { 542 return nil, "", err 543 } 544 545 // If the droplet is locked, continue waiting. We can 546 // only perform actions on unlocked droplets, so it's 547 // pointless to look at that status 548 if d.Get("locked").(string) == "true" { 549 log.Println("[DEBUG] Droplet is locked, skipping status check and retrying") 550 return nil, "", nil 551 } 552 553 // See if we can access our attribute 554 if attr, ok := d.GetOk(attribute); ok { 555 // Retrieve the droplet properties 556 droplet, _, err := client.Droplets.Get(id) 557 if err != nil { 558 return nil, "", fmt.Errorf("Error retrieving droplet: %s", err) 559 } 560 561 return &droplet, attr.(string), nil 562 } 563 564 return nil, "", nil 565 } 566 } 567 568 // Powers on the droplet and waits for it to be active 569 func powerOnAndWait(d *schema.ResourceData, meta interface{}) error { 570 id, err := strconv.Atoi(d.Id()) 571 if err != nil { 572 return fmt.Errorf("invalid droplet id: %v", err) 573 } 574 575 client := meta.(*godo.Client) 576 _, _, err = client.DropletActions.PowerOn(id) 577 if err != nil { 578 return err 579 } 580 581 // Wait for power on 582 _, err = WaitForDropletAttribute(d, "active", []string{"off"}, "status", client) 583 if err != nil { 584 return err 585 } 586 587 return nil 588 }