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