Skip to content

Commit a1935a1

Browse files
committed
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.
1 parent ea45958 commit a1935a1

8 files changed

Lines changed: 119 additions & 120 deletions

builtin/providers/packet/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type Config struct {
1313
AuthToken string
1414
}
1515

16-
// Client() returns a new client for accessing packet.
16+
// Client() returns a new client for accessing Packet's API.
1717
func (c *Config) Client() *packngo.Client {
1818
return packngo.NewClient(consumerToken, c.AuthToken, cleanhttp.DefaultClient())
1919
}

builtin/providers/packet/errors.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package packet
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/packethost/packngo"
8+
)
9+
10+
func friendlyError(err error) error {
11+
if e, ok := err.(*packngo.ErrorResponse); ok {
12+
return &ErrorResponse{
13+
StatusCode: e.Response.StatusCode,
14+
Errors: Errors(e.Errors),
15+
}
16+
}
17+
return err
18+
}
19+
20+
func isForbidden(err error) bool {
21+
if r, ok := err.(*ErrorResponse); ok {
22+
return r.StatusCode == http.StatusForbidden
23+
}
24+
return false
25+
}
26+
27+
func isNotFound(err error) bool {
28+
if r, ok := err.(*ErrorResponse); ok {
29+
return r.StatusCode == http.StatusNotFound
30+
}
31+
return false
32+
}
33+
34+
type Errors []string
35+
36+
func (e Errors) Error() string {
37+
return strings.Join(e, "; ")
38+
}
39+
40+
type ErrorResponse struct {
41+
StatusCode int
42+
Errors
43+
}

builtin/providers/packet/provider.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"github.com/hashicorp/terraform/terraform"
66
)
77

8-
// Provider returns a schema.Provider for Packet.
8+
// Provider returns a schema.Provider for managing Packet infrastructure.
99
func Provider() terraform.ResourceProvider {
1010
return &schema.Provider{
1111
Schema: map[string]*schema.Schema{
@@ -31,6 +31,5 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
3131
config := Config{
3232
AuthToken: d.Get("auth_token").(string),
3333
}
34-
3534
return config.Client(), nil
3635
}

builtin/providers/packet/resource_packet_device.go

Lines changed: 54 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package packet
22

33
import (
4+
"errors"
45
"fmt"
5-
"log"
66
"time"
77

88
"github.com/hashicorp/terraform/helper/resource"
@@ -146,22 +146,23 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
146146
}
147147
}
148148

149-
log.Printf("[DEBUG] Device create configuration: %#v", createRequest)
150-
151149
newDevice, _, err := client.Devices.Create(createRequest)
152150
if err != nil {
153-
return fmt.Errorf("Error creating device: %s", err)
151+
return friendlyError(err)
154152
}
155153

156-
// Assign the device id
157154
d.SetId(newDevice.ID)
158155

159-
log.Printf("[INFO] Device ID: %s", d.Id())
160-
161-
_, err = WaitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
156+
// Wait for the device so we can get the networking attributes that show up after a while.
157+
_, err = waitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
162158
if err != nil {
163-
return fmt.Errorf(
164-
"Error waiting for device (%s) to become ready: %s", d.Id(), err)
159+
if isForbidden(err) {
160+
// If the device doesn't get to the active state, we can't recover it from here.
161+
d.SetId("")
162+
163+
return errors.New("provisioning time limit exceeded; the Packet team will investigate")
164+
}
165+
return err
165166
}
166167

167168
return resourcePacketDeviceRead(d, meta)
@@ -170,10 +171,17 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
170171
func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
171172
client := meta.(*packngo.Client)
172173

173-
// Retrieve the device properties for updating the state
174174
device, _, err := client.Devices.Get(d.Id())
175175
if err != nil {
176-
return fmt.Errorf("Error retrieving device: %s", err)
176+
err = friendlyError(err)
177+
178+
// If the device somehow already destroyed, mark as succesfully gone.
179+
if isNotFound(err) {
180+
d.SetId("")
181+
return nil
182+
}
183+
184+
return err
177185
}
178186

179187
d.Set("name", device.Hostname)
@@ -186,35 +194,36 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
186194
d.Set("created", device.Created)
187195
d.Set("updated", device.Updated)
188196

189-
tags := make([]string, 0)
197+
tags := make([]string, 0, len(device.Tags))
190198
for _, tag := range device.Tags {
191199
tags = append(tags, tag)
192200
}
193201
d.Set("tags", tags)
194202

195-
provisionerAddress := ""
196-
197-
networks := make([]map[string]interface{}, 0, 1)
203+
var (
204+
host string
205+
networks = make([]map[string]interface{}, 0, 1)
206+
)
198207
for _, ip := range device.Network {
199-
network := make(map[string]interface{})
200-
network["address"] = ip.Address
201-
network["gateway"] = ip.Gateway
202-
network["family"] = ip.Family
203-
network["cidr"] = ip.Cidr
204-
network["public"] = ip.Public
208+
network := map[string]interface{}{
209+
"address": ip.Address,
210+
"gateway": ip.Gateway,
211+
"family": ip.Family,
212+
"cidr": ip.Cidr,
213+
"public": ip.Public,
214+
}
205215
networks = append(networks, network)
216+
206217
if ip.Family == 4 && ip.Public == true {
207-
provisionerAddress = ip.Address
218+
host = ip.Address
208219
}
209220
}
210221
d.Set("network", networks)
211222

212-
log.Printf("[DEBUG] Provisioner Address set to %v", provisionerAddress)
213-
214-
if provisionerAddress != "" {
223+
if host != "" {
215224
d.SetConnInfo(map[string]string{
216225
"type": "ssh",
217-
"host": provisionerAddress,
226+
"host": host,
218227
})
219228
}
220229

@@ -224,19 +233,15 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
224233
func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
225234
client := meta.(*packngo.Client)
226235

227-
if d.HasChange("locked") && d.Get("locked").(bool) {
228-
_, err := client.Devices.Lock(d.Id())
229-
230-
if err != nil {
231-
return fmt.Errorf(
232-
"Error locking device (%s): %s", d.Id(), err)
236+
if d.HasChange("locked") {
237+
var action func(string) (*packngo.Response, error)
238+
if d.Get("locked").(bool) {
239+
action = client.Devices.Lock
240+
} else {
241+
action = client.Devices.Unlock
233242
}
234-
} else if d.HasChange("locked") {
235-
_, err := client.Devices.Unlock(d.Id())
236-
237-
if err != nil {
238-
return fmt.Errorf(
239-
"Error unlocking device (%s): %s", d.Id(), err)
243+
if _, err := action(d.Id()); err != nil {
244+
return friendlyError(err)
240245
}
241246
}
242247

@@ -246,22 +251,14 @@ func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error
246251
func resourcePacketDeviceDelete(d *schema.ResourceData, meta interface{}) error {
247252
client := meta.(*packngo.Client)
248253

249-
log.Printf("[INFO] Deleting device: %s", d.Id())
250254
if _, err := client.Devices.Delete(d.Id()); err != nil {
251-
return fmt.Errorf("Error deleting device: %s", err)
255+
return friendlyError(err)
252256
}
253257

254258
return nil
255259
}
256260

257-
func WaitForDeviceAttribute(
258-
d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
259-
// Wait for the device so we can get the networking attributes
260-
// that show up after a while
261-
log.Printf(
262-
"[INFO] Waiting for device (%s) to have %s of %s",
263-
d.Id(), attribute, target)
264-
261+
func waitForDeviceAttribute(d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
265262
stateConf := &resource.StateChangeConf{
266263
Pending: pending,
267264
Target: target,
@@ -270,47 +267,37 @@ func WaitForDeviceAttribute(
270267
Delay: 10 * time.Second,
271268
MinTimeout: 3 * time.Second,
272269
}
273-
274270
return stateConf.WaitForState()
275271
}
276272

277-
func newDeviceStateRefreshFunc(
278-
d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
273+
func newDeviceStateRefreshFunc(d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
279274
client := meta.(*packngo.Client)
275+
280276
return func() (interface{}, string, error) {
281-
err := resourcePacketDeviceRead(d, meta)
282-
if err != nil {
277+
if err := resourcePacketDeviceRead(d, meta); err != nil {
283278
return nil, "", err
284279
}
285280

286-
// See if we can access our attribute
287281
if attr, ok := d.GetOk(attribute); ok {
288-
// Retrieve the device properties
289282
device, _, err := client.Devices.Get(d.Id())
290283
if err != nil {
291-
return nil, "", fmt.Errorf("Error retrieving device: %s", err)
284+
return nil, "", friendlyError(err)
292285
}
293-
294286
return &device, attr.(string), nil
295287
}
296288

297289
return nil, "", nil
298290
}
299291
}
300292

301-
// Powers on the device and waits for it to be active
293+
// powerOnAndWait Powers on the device and waits for it to be active.
302294
func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
303295
client := meta.(*packngo.Client)
304296
_, err := client.Devices.PowerOn(d.Id())
305297
if err != nil {
306-
return err
307-
}
308-
309-
// Wait for power on
310-
_, err = WaitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
311-
if err != nil {
312-
return err
298+
return friendlyError(err)
313299
}
314300

315-
return nil
301+
_, err = waitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
302+
return err
316303
}

builtin/providers/packet/resource_packet_project.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package packet
22

33
import (
4-
"fmt"
5-
"log"
6-
"strings"
7-
84
"github.com/hashicorp/terraform/helper/schema"
95
"github.com/packethost/packngo"
106
)
@@ -53,14 +49,12 @@ func resourcePacketProjectCreate(d *schema.ResourceData, meta interface{}) error
5349
PaymentMethod: d.Get("payment_method").(string),
5450
}
5551

56-
log.Printf("[DEBUG] Project create configuration: %#v", createRequest)
5752
project, _, err := client.Projects.Create(createRequest)
5853
if err != nil {
59-
return fmt.Errorf("Error creating Project: %s", err)
54+
return friendlyError(err)
6055
}
6156

6257
d.SetId(project.ID)
63-
log.Printf("[INFO] Project created: %s", project.ID)
6458

6559
return resourcePacketProjectRead(d, meta)
6660
}
@@ -70,14 +64,16 @@ func resourcePacketProjectRead(d *schema.ResourceData, meta interface{}) error {
7064

7165
key, _, err := client.Projects.Get(d.Id())
7266
if err != nil {
73-
// If the project somehow already destroyed, mark as
74-
// succesfully gone
75-
if strings.Contains(err.Error(), "404") {
67+
err = friendlyError(err)
68+
69+
// If the project somehow already destroyed, mark as succesfully gone.
70+
if isNotFound(err) {
7671
d.SetId("")
72+
7773
return nil
7874
}
7975

80-
return fmt.Errorf("Error retrieving Project: %s", err)
76+
return err
8177
}
8278

8379
d.Set("id", key.ID)
@@ -100,10 +96,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
10096
updateRequest.PaymentMethod = attr.(string)
10197
}
10298

103-
log.Printf("[DEBUG] Project update: %#v", d.Get("id"))
10499
_, _, err := client.Projects.Update(updateRequest)
105100
if err != nil {
106-
return fmt.Errorf("Failed to update Project: %s", err)
101+
return friendlyError(err)
107102
}
108103

109104
return resourcePacketProjectRead(d, meta)
@@ -112,10 +107,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
112107
func resourcePacketProjectDelete(d *schema.ResourceData, meta interface{}) error {
113108
client := meta.(*packngo.Client)
114109

115-
log.Printf("[INFO] Deleting Project: %s", d.Id())
116110
_, err := client.Projects.Delete(d.Id())
117111
if err != nil {
118-
return fmt.Errorf("Error deleting SSH key: %s", err)
112+
return friendlyError(err)
119113
}
120114

121115
d.SetId("")

0 commit comments

Comments
 (0)