Handle external state changes for Packet resources gracefully.

When a Packet provision exceeds our time limit, we move the device to an
internal project for Packet staff to investigate. When this happens, the
original user no longer has access to the device, and they get a 403.

These changes make that and other external state changes more pleasant for
users of Terraform.
This commit is contained in:
Andrew Hodges 2016-01-14 10:50:15 -05:00
parent ea4595840c
commit a1935a135d
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) return err
if err != nil {
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)
} }