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