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