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