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