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
}
// Client() returns a new client for accessing packet.
// Client() returns a new client for accessing Packet's API.
func (c *Config) Client() *packngo.Client {
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"
)
// Provider returns a schema.Provider for Packet.
// Provider returns a schema.Provider for managing Packet infrastructure.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
@ -31,6 +31,5 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
AuthToken: d.Get("auth_token").(string),
}
return config.Client(), nil
}

View File

@ -1,8 +1,8 @@
package packet
import (
"errors"
"fmt"
"log"
"time"
"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)
if err != nil {
return fmt.Errorf("Error creating device: %s", err)
return friendlyError(err)
}
// Assign the device id
d.SetId(newDevice.ID)
log.Printf("[INFO] Device ID: %s", d.Id())
_, err = WaitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
// 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)
if err != nil {
return fmt.Errorf(
"Error waiting for device (%s) to become ready: %s", d.Id(), err)
if isForbidden(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)
@ -170,10 +171,17 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)
// Retrieve the device properties for updating the state
device, _, err := client.Devices.Get(d.Id())
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)
@ -186,35 +194,36 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("created", device.Created)
d.Set("updated", device.Updated)
tags := make([]string, 0)
tags := make([]string, 0, len(device.Tags))
for _, tag := range device.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)
provisionerAddress := ""
networks := make([]map[string]interface{}, 0, 1)
var (
host string
networks = make([]map[string]interface{}, 0, 1)
)
for _, ip := range device.Network {
network := make(map[string]interface{})
network["address"] = ip.Address
network["gateway"] = ip.Gateway
network["family"] = ip.Family
network["cidr"] = ip.Cidr
network["public"] = ip.Public
network := map[string]interface{}{
"address": ip.Address,
"gateway": ip.Gateway,
"family": ip.Family,
"cidr": ip.Cidr,
"public": ip.Public,
}
networks = append(networks, network)
if ip.Family == 4 && ip.Public == true {
provisionerAddress = ip.Address
host = ip.Address
}
}
d.Set("network", networks)
log.Printf("[DEBUG] Provisioner Address set to %v", provisionerAddress)
if provisionerAddress != "" {
if host != "" {
d.SetConnInfo(map[string]string{
"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 {
client := meta.(*packngo.Client)
if d.HasChange("locked") && d.Get("locked").(bool) {
_, err := client.Devices.Lock(d.Id())
if err != nil {
return fmt.Errorf(
"Error locking device (%s): %s", d.Id(), err)
if d.HasChange("locked") {
var action func(string) (*packngo.Response, error)
if d.Get("locked").(bool) {
action = client.Devices.Lock
} else {
action = client.Devices.Unlock
}
} else if d.HasChange("locked") {
_, err := client.Devices.Unlock(d.Id())
if err != nil {
return fmt.Errorf(
"Error unlocking device (%s): %s", d.Id(), err)
if _, err := action(d.Id()); err != nil {
return friendlyError(err)
}
}
@ -246,22 +251,14 @@ func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)
log.Printf("[INFO] Deleting device: %s", d.Id())
if _, err := client.Devices.Delete(d.Id()); err != nil {
return fmt.Errorf("Error deleting device: %s", err)
return friendlyError(err)
}
return nil
}
func WaitForDeviceAttribute(
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)
func waitForDeviceAttribute(d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: target,
@ -270,27 +267,22 @@ func WaitForDeviceAttribute(
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
return stateConf.WaitForState()
}
func newDeviceStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
func newDeviceStateRefreshFunc(d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
client := meta.(*packngo.Client)
return func() (interface{}, string, error) {
err := resourcePacketDeviceRead(d, meta)
if err != nil {
if err := resourcePacketDeviceRead(d, meta); err != nil {
return nil, "", err
}
// See if we can access our attribute
if attr, ok := d.GetOk(attribute); ok {
// Retrieve the device properties
device, _, err := client.Devices.Get(d.Id())
if err != nil {
return nil, "", fmt.Errorf("Error retrieving device: %s", err)
return nil, "", friendlyError(err)
}
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 {
client := meta.(*packngo.Client)
_, err := client.Devices.PowerOn(d.Id())
if err != nil {
return err
return friendlyError(err)
}
// Wait for power on
_, err = WaitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
if err != nil {
return err
}
return nil
_, err = waitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
return err
}

View File

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

View File

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

View File

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

View File

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