Merge pull request #4676 from betawaffle/packet-failure-handling

Handle external state changes for Packet resources gracefully.
This commit is contained in:
Paul Hinze 2016-01-14 10:33:21 -06:00
commit 6bc93ba6e7
8 changed files with 119 additions and 120 deletions

View File

@ -13,7 +13,7 @@ type Config struct {
AuthToken string AuthToken string
} }
// Client() returns a new client for accessing packet. // Client() returns a new client for accessing Packet's API.
func (c *Config) Client() *packngo.Client { func (c *Config) Client() *packngo.Client {
return packngo.NewClient(consumerToken, c.AuthToken, cleanhttp.DefaultClient()) return packngo.NewClient(consumerToken, c.AuthToken, cleanhttp.DefaultClient())
} }

View File

@ -0,0 +1,43 @@
package packet
import (
"net/http"
"strings"
"github.com/packethost/packngo"
)
func friendlyError(err error) error {
if e, ok := err.(*packngo.ErrorResponse); ok {
return &ErrorResponse{
StatusCode: e.Response.StatusCode,
Errors: Errors(e.Errors),
}
}
return err
}
func isForbidden(err error) bool {
if r, ok := err.(*ErrorResponse); ok {
return r.StatusCode == http.StatusForbidden
}
return false
}
func isNotFound(err error) bool {
if r, ok := err.(*ErrorResponse); ok {
return r.StatusCode == http.StatusNotFound
}
return false
}
type Errors []string
func (e Errors) Error() string {
return strings.Join(e, "; ")
}
type ErrorResponse struct {
StatusCode int
Errors
}

View File

@ -5,7 +5,7 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// Provider returns a schema.Provider for Packet. // Provider returns a schema.Provider for managing Packet infrastructure.
func Provider() terraform.ResourceProvider { func Provider() terraform.ResourceProvider {
return &schema.Provider{ return &schema.Provider{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -31,6 +31,5 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{ config := Config{
AuthToken: d.Get("auth_token").(string), AuthToken: d.Get("auth_token").(string),
} }
return config.Client(), nil return config.Client(), nil
} }

View File

@ -1,8 +1,8 @@
package packet package packet
import ( import (
"errors"
"fmt" "fmt"
"log"
"time" "time"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
@ -146,22 +146,23 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
} }
} }
log.Printf("[DEBUG] Device create configuration: %#v", createRequest)
newDevice, _, err := client.Devices.Create(createRequest) newDevice, _, err := client.Devices.Create(createRequest)
if err != nil { if err != nil {
return fmt.Errorf("Error creating device: %s", err) return friendlyError(err)
} }
// Assign the device id
d.SetId(newDevice.ID) d.SetId(newDevice.ID)
log.Printf("[INFO] Device ID: %s", d.Id()) // Wait for the device so we can get the networking attributes that show up after a while.
_, err = waitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
_, err = WaitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
if err != nil { if err != nil {
return fmt.Errorf( if isForbidden(err) {
"Error waiting for device (%s) to become ready: %s", d.Id(), err) // If the device doesn't get to the active state, we can't recover it from here.
d.SetId("")
return errors.New("provisioning time limit exceeded; the Packet team will investigate")
}
return err
} }
return resourcePacketDeviceRead(d, meta) return resourcePacketDeviceRead(d, meta)
@ -170,10 +171,17 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error { func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
// Retrieve the device properties for updating the state
device, _, err := client.Devices.Get(d.Id()) device, _, err := client.Devices.Get(d.Id())
if err != nil { if err != nil {
return fmt.Errorf("Error retrieving device: %s", err) err = friendlyError(err)
// If the device somehow already destroyed, mark as succesfully gone.
if isNotFound(err) {
d.SetId("")
return nil
}
return err
} }
d.Set("name", device.Hostname) d.Set("name", device.Hostname)
@ -186,35 +194,36 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("created", device.Created) d.Set("created", device.Created)
d.Set("updated", device.Updated) d.Set("updated", device.Updated)
tags := make([]string, 0) tags := make([]string, 0, len(device.Tags))
for _, tag := range device.Tags { for _, tag := range device.Tags {
tags = append(tags, tag) tags = append(tags, tag)
} }
d.Set("tags", tags) d.Set("tags", tags)
provisionerAddress := "" var (
host string
networks := make([]map[string]interface{}, 0, 1) networks = make([]map[string]interface{}, 0, 1)
)
for _, ip := range device.Network { for _, ip := range device.Network {
network := make(map[string]interface{}) network := map[string]interface{}{
network["address"] = ip.Address "address": ip.Address,
network["gateway"] = ip.Gateway "gateway": ip.Gateway,
network["family"] = ip.Family "family": ip.Family,
network["cidr"] = ip.Cidr "cidr": ip.Cidr,
network["public"] = ip.Public "public": ip.Public,
}
networks = append(networks, network) networks = append(networks, network)
if ip.Family == 4 && ip.Public == true { if ip.Family == 4 && ip.Public == true {
provisionerAddress = ip.Address host = ip.Address
} }
} }
d.Set("network", networks) d.Set("network", networks)
log.Printf("[DEBUG] Provisioner Address set to %v", provisionerAddress) if host != "" {
if provisionerAddress != "" {
d.SetConnInfo(map[string]string{ d.SetConnInfo(map[string]string{
"type": "ssh", "type": "ssh",
"host": provisionerAddress, "host": host,
}) })
} }
@ -224,19 +233,15 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error { func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
if d.HasChange("locked") && d.Get("locked").(bool) { if d.HasChange("locked") {
_, err := client.Devices.Lock(d.Id()) var action func(string) (*packngo.Response, error)
if d.Get("locked").(bool) {
if err != nil { action = client.Devices.Lock
return fmt.Errorf( } else {
"Error locking device (%s): %s", d.Id(), err) action = client.Devices.Unlock
} }
} else if d.HasChange("locked") { if _, err := action(d.Id()); err != nil {
_, err := client.Devices.Unlock(d.Id()) return friendlyError(err)
if err != nil {
return fmt.Errorf(
"Error unlocking device (%s): %s", d.Id(), err)
} }
} }
@ -246,22 +251,14 @@ func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceDelete(d *schema.ResourceData, meta interface{}) error { func resourcePacketDeviceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
log.Printf("[INFO] Deleting device: %s", d.Id())
if _, err := client.Devices.Delete(d.Id()); err != nil { if _, err := client.Devices.Delete(d.Id()); err != nil {
return fmt.Errorf("Error deleting device: %s", err) return friendlyError(err)
} }
return nil return nil
} }
func WaitForDeviceAttribute( func waitForDeviceAttribute(d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
// Wait for the device so we can get the networking attributes
// that show up after a while
log.Printf(
"[INFO] Waiting for device (%s) to have %s of %s",
d.Id(), attribute, target)
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: pending, Pending: pending,
Target: target, Target: target,
@ -270,27 +267,22 @@ func WaitForDeviceAttribute(
Delay: 10 * time.Second, Delay: 10 * time.Second,
MinTimeout: 3 * time.Second, MinTimeout: 3 * time.Second,
} }
return stateConf.WaitForState() return stateConf.WaitForState()
} }
func newDeviceStateRefreshFunc( func newDeviceStateRefreshFunc(d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
err := resourcePacketDeviceRead(d, meta) if err := resourcePacketDeviceRead(d, meta); err != nil {
if err != nil {
return nil, "", err return nil, "", err
} }
// See if we can access our attribute
if attr, ok := d.GetOk(attribute); ok { if attr, ok := d.GetOk(attribute); ok {
// Retrieve the device properties
device, _, err := client.Devices.Get(d.Id()) device, _, err := client.Devices.Get(d.Id())
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Error retrieving device: %s", err) return nil, "", friendlyError(err)
} }
return &device, attr.(string), nil return &device, attr.(string), nil
} }
@ -298,19 +290,14 @@ func newDeviceStateRefreshFunc(
} }
} }
// Powers on the device and waits for it to be active // powerOnAndWait Powers on the device and waits for it to be active.
func powerOnAndWait(d *schema.ResourceData, meta interface{}) error { func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
_, err := client.Devices.PowerOn(d.Id()) _, err := client.Devices.PowerOn(d.Id())
if err != nil { if err != nil {
return err return friendlyError(err)
} }
// Wait for power on _, err = waitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
_, err = WaitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
if err != nil {
return err return err
}
return nil
} }

View File

@ -1,10 +1,6 @@
package packet package packet
import ( import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/packethost/packngo" "github.com/packethost/packngo"
) )
@ -53,14 +49,12 @@ func resourcePacketProjectCreate(d *schema.ResourceData, meta interface{}) error
PaymentMethod: d.Get("payment_method").(string), PaymentMethod: d.Get("payment_method").(string),
} }
log.Printf("[DEBUG] Project create configuration: %#v", createRequest)
project, _, err := client.Projects.Create(createRequest) project, _, err := client.Projects.Create(createRequest)
if err != nil { if err != nil {
return fmt.Errorf("Error creating Project: %s", err) return friendlyError(err)
} }
d.SetId(project.ID) d.SetId(project.ID)
log.Printf("[INFO] Project created: %s", project.ID)
return resourcePacketProjectRead(d, meta) return resourcePacketProjectRead(d, meta)
} }
@ -70,14 +64,16 @@ func resourcePacketProjectRead(d *schema.ResourceData, meta interface{}) error {
key, _, err := client.Projects.Get(d.Id()) key, _, err := client.Projects.Get(d.Id())
if err != nil { if err != nil {
// If the project somehow already destroyed, mark as err = friendlyError(err)
// succesfully gone
if strings.Contains(err.Error(), "404") { // If the project somehow already destroyed, mark as succesfully gone.
if isNotFound(err) {
d.SetId("") d.SetId("")
return nil return nil
} }
return fmt.Errorf("Error retrieving Project: %s", err) return err
} }
d.Set("id", key.ID) d.Set("id", key.ID)
@ -100,10 +96,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
updateRequest.PaymentMethod = attr.(string) updateRequest.PaymentMethod = attr.(string)
} }
log.Printf("[DEBUG] Project update: %#v", d.Get("id"))
_, _, err := client.Projects.Update(updateRequest) _, _, err := client.Projects.Update(updateRequest)
if err != nil { if err != nil {
return fmt.Errorf("Failed to update Project: %s", err) return friendlyError(err)
} }
return resourcePacketProjectRead(d, meta) return resourcePacketProjectRead(d, meta)
@ -112,10 +107,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketProjectDelete(d *schema.ResourceData, meta interface{}) error { func resourcePacketProjectDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
log.Printf("[INFO] Deleting Project: %s", d.Id())
_, err := client.Projects.Delete(d.Id()) _, err := client.Projects.Delete(d.Id())
if err != nil { if err != nil {
return fmt.Errorf("Error deleting SSH key: %s", err) return friendlyError(err)
} }
d.SetId("") d.SetId("")

View File

@ -37,11 +37,8 @@ func testAccCheckPacketProjectDestroy(s *terraform.State) error {
if rs.Type != "packet_project" { if rs.Type != "packet_project" {
continue continue
} }
if _, _, err := client.Projects.Get(rs.Primary.ID); err == nil {
_, _, err := client.Projects.Get(rs.Primary.ID) return fmt.Errorf("Project cstill exists")
if err == nil {
fmt.Errorf("Project cstill exists")
} }
} }
@ -50,11 +47,9 @@ func testAccCheckPacketProjectDestroy(s *terraform.State) error {
func testAccCheckPacketProjectAttributes(project *packngo.Project) resource.TestCheckFunc { func testAccCheckPacketProjectAttributes(project *packngo.Project) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if project.Name != "foobar" { if project.Name != "foobar" {
return fmt.Errorf("Bad name: %s", project.Name) return fmt.Errorf("Bad name: %s", project.Name)
} }
return nil return nil
} }
} }
@ -62,11 +57,9 @@ func testAccCheckPacketProjectAttributes(project *packngo.Project) resource.Test
func testAccCheckPacketProjectExists(n string, project *packngo.Project) resource.TestCheckFunc { func testAccCheckPacketProjectExists(n string, project *packngo.Project) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
return fmt.Errorf("Not found: %s", n) return fmt.Errorf("Not found: %s", n)
} }
if rs.Primary.ID == "" { if rs.Primary.ID == "" {
return fmt.Errorf("No Record ID is set") return fmt.Errorf("No Record ID is set")
} }
@ -74,11 +67,9 @@ func testAccCheckPacketProjectExists(n string, project *packngo.Project) resourc
client := testAccProvider.Meta().(*packngo.Client) client := testAccProvider.Meta().(*packngo.Client)
foundProject, _, err := client.Projects.Get(rs.Primary.ID) foundProject, _, err := client.Projects.Get(rs.Primary.ID)
if err != nil { if err != nil {
return err return err
} }
if foundProject.ID != rs.Primary.ID { if foundProject.ID != rs.Primary.ID {
return fmt.Errorf("Record not found: %v - %v", rs.Primary.ID, foundProject) return fmt.Errorf("Record not found: %v - %v", rs.Primary.ID, foundProject)
} }

View File

@ -1,10 +1,6 @@
package packet package packet
import ( import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/packethost/packngo" "github.com/packethost/packngo"
) )
@ -59,14 +55,12 @@ func resourcePacketSSHKeyCreate(d *schema.ResourceData, meta interface{}) error
Key: d.Get("public_key").(string), Key: d.Get("public_key").(string),
} }
log.Printf("[DEBUG] SSH Key create configuration: %#v", createRequest)
key, _, err := client.SSHKeys.Create(createRequest) key, _, err := client.SSHKeys.Create(createRequest)
if err != nil { if err != nil {
return fmt.Errorf("Error creating SSH Key: %s", err) return friendlyError(err)
} }
d.SetId(key.ID) d.SetId(key.ID)
log.Printf("[INFO] SSH Key: %s", key.ID)
return resourcePacketSSHKeyRead(d, meta) return resourcePacketSSHKeyRead(d, meta)
} }
@ -76,14 +70,16 @@ func resourcePacketSSHKeyRead(d *schema.ResourceData, meta interface{}) error {
key, _, err := client.SSHKeys.Get(d.Id()) key, _, err := client.SSHKeys.Get(d.Id())
if err != nil { if err != nil {
err = friendlyError(err)
// If the key is somehow already destroyed, mark as // If the key is somehow already destroyed, mark as
// succesfully gone // succesfully gone
if strings.Contains(err.Error(), "404") { if isNotFound(err) {
d.SetId("") d.SetId("")
return nil return nil
} }
return fmt.Errorf("Error retrieving SSH key: %s", err) return err
} }
d.Set("id", key.ID) d.Set("id", key.ID)
@ -105,10 +101,9 @@ func resourcePacketSSHKeyUpdate(d *schema.ResourceData, meta interface{}) error
Key: d.Get("public_key").(string), Key: d.Get("public_key").(string),
} }
log.Printf("[DEBUG] SSH key update: %#v", d.Get("id"))
_, _, err := client.SSHKeys.Update(updateRequest) _, _, err := client.SSHKeys.Update(updateRequest)
if err != nil { if err != nil {
return fmt.Errorf("Failed to update SSH key: %s", err) return friendlyError(err)
} }
return resourcePacketSSHKeyRead(d, meta) return resourcePacketSSHKeyRead(d, meta)
@ -117,10 +112,9 @@ func resourcePacketSSHKeyUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketSSHKeyDelete(d *schema.ResourceData, meta interface{}) error { func resourcePacketSSHKeyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client) client := meta.(*packngo.Client)
log.Printf("[INFO] Deleting SSH key: %s", d.Id())
_, err := client.SSHKeys.Delete(d.Id()) _, err := client.SSHKeys.Delete(d.Id())
if err != nil { if err != nil {
return fmt.Errorf("Error deleting SSH key: %s", err) return friendlyError(err)
} }
d.SetId("") d.SetId("")

View File

@ -40,11 +40,8 @@ func testAccCheckPacketSSHKeyDestroy(s *terraform.State) error {
if rs.Type != "packet_ssh_key" { if rs.Type != "packet_ssh_key" {
continue continue
} }
if _, _, err := client.SSHKeys.Get(rs.Primary.ID); err == nil {
_, _, err := client.SSHKeys.Get(rs.Primary.ID) return fmt.Errorf("SSH key still exists")
if err == nil {
fmt.Errorf("SSH key still exists")
} }
} }
@ -53,11 +50,9 @@ func testAccCheckPacketSSHKeyDestroy(s *terraform.State) error {
func testAccCheckPacketSSHKeyAttributes(key *packngo.SSHKey) resource.TestCheckFunc { func testAccCheckPacketSSHKeyAttributes(key *packngo.SSHKey) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if key.Label != "foobar" { if key.Label != "foobar" {
return fmt.Errorf("Bad name: %s", key.Label) return fmt.Errorf("Bad name: %s", key.Label)
} }
return nil return nil
} }
} }
@ -65,11 +60,9 @@ func testAccCheckPacketSSHKeyAttributes(key *packngo.SSHKey) resource.TestCheckF
func testAccCheckPacketSSHKeyExists(n string, key *packngo.SSHKey) resource.TestCheckFunc { func testAccCheckPacketSSHKeyExists(n string, key *packngo.SSHKey) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
return fmt.Errorf("Not found: %s", n) return fmt.Errorf("Not found: %s", n)
} }
if rs.Primary.ID == "" { if rs.Primary.ID == "" {
return fmt.Errorf("No Record ID is set") return fmt.Errorf("No Record ID is set")
} }
@ -77,11 +70,9 @@ func testAccCheckPacketSSHKeyExists(n string, key *packngo.SSHKey) resource.Test
client := testAccProvider.Meta().(*packngo.Client) client := testAccProvider.Meta().(*packngo.Client)
foundKey, _, err := client.SSHKeys.Get(rs.Primary.ID) foundKey, _, err := client.SSHKeys.Get(rs.Primary.ID)
if err != nil { if err != nil {
return err return err
} }
if foundKey.ID != rs.Primary.ID { if foundKey.ID != rs.Primary.ID {
return fmt.Errorf("SSh Key not found: %v - %v", rs.Primary.ID, foundKey) return fmt.Errorf("SSh Key not found: %v - %v", rs.Primary.ID, foundKey)
} }