Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade metal device resource and data source to use timeouts #364

Merged
merged 4 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions equinix/data_source_metal_device.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package equinix

import (
"context"
"encoding/json"
"fmt"
"path"
Expand All @@ -14,7 +15,7 @@ import (

func dataSourceMetalDevice() *schema.Resource {
return &schema.Resource{
Read: dataSourceMetalDeviceRead,
ReadWithoutTimeout: diagnosticsWrapper(dataSourceMetalDeviceRead),
Schema: map[string]*schema.Schema{
"hostname": {
Type: schema.TypeString,
Expand Down Expand Up @@ -201,7 +202,7 @@ func dataSourceMetalDevice() *schema.Resource {
}
}

func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).metal

hostnameRaw, hostnameOK := d.GetOk("hostname")
Expand Down
66 changes: 8 additions & 58 deletions equinix/helpers_device.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package equinix

import (
"context"
"errors"
"fmt"
"log"
"strings"
"sync"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/packethost/packngo"
Expand Down Expand Up @@ -142,7 +143,7 @@ func hwReservationStateRefreshFunc(client *packngo.Client, reservationId, instan
}
}

func waitUntilReservationProvisionable(client *packngo.Client, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error {
func waitUntilReservationProvisionable(ctx context.Context, client *packngo.Client, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error {
stateConf := &retry.StateChangeConf{
Pending: []string{deprovisioning},
Target: []string{provisionable, reprovisioned},
Expand All @@ -151,7 +152,7 @@ func waitUntilReservationProvisionable(client *packngo.Client, reservationId, in
Delay: delay,
MinTimeout: minTimeout,
}
_, err := stateConf.WaitForState()
_, err := stateConf.WaitForStateContext(ctx)
return err
}

Expand All @@ -166,7 +167,7 @@ func getWaitForDeviceLock(deviceID string) *sync.WaitGroup {
return wg
}

func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []string, attribute string, meta interface{}) (string, error) {
func waitForDeviceAttribute(ctx context.Context, d *schema.ResourceData, stateConf *retry.StateChangeConf) (string, error) {
wg := getWaitForDeviceLock(d.Id())
wg.Wait()

Expand All @@ -180,34 +181,11 @@ func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []
wgMutex.Unlock()
}()

if attribute != "state" && attribute != "network_type" {
return "", fmt.Errorf("unsupported attr to wait for: %s", attribute)
if stateConf == nil || stateConf.Refresh == nil {
return "", errors.New("invalid stateconf to wait for")
}

stateConf := &retry.StateChangeConf{
Pending: pending,
Target: targets,
Refresh: func() (interface{}, string, error) {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

device, _, err := client.Devices.Get(d.Id(), &packngo.GetOptions{Includes: []string{"project"}})
if err == nil {
retAttrVal := device.State
if attribute == "network_type" {
networkType := device.GetNetworkType()
retAttrVal = networkType
}
return retAttrVal, retAttrVal, nil
}
return "error", "error", err
},
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

attrValRaw, err := stateConf.WaitForState()
attrValRaw, err := stateConf.WaitForStateContext(ctx)

if v, ok := attrValRaw.(string); ok {
return v, err
Expand All @@ -216,34 +194,6 @@ func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []
return "", err
}

// powerOnAndWait Powers on the device and waits for it to be active.
func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
ctreatma marked this conversation as resolved.
Show resolved Hide resolved
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

_, err := client.Devices.PowerOn(d.Id())
if err != nil {
return friendlyError(err)
}

_, err = waitForDeviceAttribute(d, []string{"active", "failed"}, []string{"off"}, "state", client)
if err != nil {
return err
}
state := d.Get("state").(string)
if state != "active" {
return friendlyError(fmt.Errorf("device in non-active state \"%s\"", state))
}
return nil
}

func validateFacilityForDevice(v interface{}, k string) (ws []string, errors []error) {
if v.(string) == "any" {
errors = append(errors, fmt.Errorf(`cannot use facility: "any"`))
}
return
}

func ipAddressSchema() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down
3 changes: 2 additions & 1 deletion equinix/helpers_device_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package equinix

import (
"context"
"fmt"
"testing"
"time"
Expand Down Expand Up @@ -149,7 +150,7 @@ func Test_waitUntilReservationProvisionable(t *testing.T) {
// timeout * number of tests that reach timeout must be less than 30s (default go test timeout).
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := waitUntilReservationProvisionable(tt.args.meta, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr {
if err := waitUntilReservationProvisionable(context.Background(), tt.args.meta, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr {
t.Errorf("waitUntilReservationProvisionable() error = %v, wantErr %v", err, tt.wantErr)
}
})
Expand Down
10 changes: 10 additions & 0 deletions equinix/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,13 @@ func schemaSetToMap(set *schema.Set) map[int]interface{} {
}
return transformed
}

func diagnosticsWrapper(fn func(ctx context.Context, d *schema.ResourceData, meta interface{}) error) func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if err := fn(ctx, d, meta); err != nil {
return diag.FromErr(err)
}

return nil
}
}
12 changes: 9 additions & 3 deletions equinix/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
)

var (
testAccProviders map[string]*schema.Provider
testAccProvider *schema.Provider
testExternalProviders map[string]resource.ExternalProvider
testAccProviders map[string]*schema.Provider
testAccProviderFactories map[string]func() (*schema.Provider, error)
testAccProvider *schema.Provider
testExternalProviders map[string]resource.ExternalProvider
)

type mockedResourceDataProvider struct {
Expand Down Expand Up @@ -127,6 +128,11 @@ func init() {
testAccProviders = map[string]*schema.Provider{
"equinix": testAccProvider,
}
testAccProviderFactories = map[string]func() (*schema.Provider, error){
ctreatma marked this conversation as resolved.
Show resolved Hide resolved
"equinix": func() (*schema.Provider, error) {
return testAccProvider, nil
},
}
testExternalProviders = map[string]resource.ExternalProvider{
"random": {
Source: "hashicorp/random",
Expand Down
73 changes: 50 additions & 23 deletions equinix/resource_metal_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"sort"
"time"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -37,12 +37,12 @@ func resourceMetalDevice() *schema.Resource {
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},
Create: resourceMetalDeviceCreate,
Read: resourceMetalDeviceRead,
Update: resourceMetalDeviceUpdate,
Delete: resourceMetalDeviceDelete,
CreateContext: diagnosticsWrapper(resourceMetalDeviceCreate),
ReadWithoutTimeout: diagnosticsWrapper(resourceMetalDeviceRead),
UpdateContext: diagnosticsWrapper(resourceMetalDeviceUpdate),
DeleteContext: diagnosticsWrapper(resourceMetalDeviceDelete),
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"project_id": {
Expand Down Expand Up @@ -468,7 +468,7 @@ func reinstallDisabledAndNoChangesAllowed(attribute string) customdiff.ResourceC
}
}

func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand All @@ -492,7 +492,7 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
metroRaw, metroOk := d.GetOk("metro")

if !facsOk && !metroOk {
return friendlyError(errors.New("one of facilies and metro must be configured"))
return errors.New("one of facilies and metro must be configured")
}

if facsOk {
Expand Down Expand Up @@ -575,16 +575,17 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
if attr, ok := d.GetOk("storage"); ok {
s, err := structure.NormalizeJsonString(attr.(string))
if err != nil {
return errwrap.Wrapf("storage param contains invalid JSON: {{err}}", err)
return fmt.Errorf("storage param contains invalid JSON: %s", err)
}
var cpr packngo.CPR
err = json.Unmarshal([]byte(s), &cpr)
if err != nil {
return errwrap.Wrapf("Error parsing Storage string: {{err}}", err)
return fmt.Errorf("error parsing Storage string: %s", err)
}
createRequest.Storage = &cpr
}

start := time.Now()
newDevice, _, err := client.Devices.Create(createRequest)
if err != nil {
retErr := friendlyError(err)
Expand All @@ -596,14 +597,15 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {

d.SetId(newDevice.ID)

if err = waitForActiveDevice(d, meta); err != nil {
createTimeout := d.Timeout(schema.TimeoutCreate) - 30*time.Second - time.Since(start)
if err = waitForActiveDevice(ctx, d, meta, createTimeout); err != nil {
return err
}

return resourceMetalDeviceRead(d, meta)
return resourceMetalDeviceRead(ctx, d, meta)
}

func resourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand Down Expand Up @@ -706,7 +708,7 @@ func resourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

func resourceMetalDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand Down Expand Up @@ -761,20 +763,22 @@ func resourceMetalDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
dPXE := d.Get("always_pxe").(bool)
ur.AlwaysPXE = &dPXE
}

start := time.Now()
if !reflect.DeepEqual(ur, packngo.DeviceUpdateRequest{}) {
if _, _, err := client.Devices.Update(d.Id(), &ur); err != nil {
return friendlyError(err)
}
}

if err := doReinstall(client, d, meta); err != nil {
if err := doReinstall(ctx, client, d, meta, start); err != nil {
return err
}

return resourceMetalDeviceRead(d, meta)
return resourceMetalDeviceRead(ctx, d, meta)
}

func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{}) error {
func doReinstall(ctx context.Context, client *packngo.Client, d *schema.ResourceData, meta interface{}, start time.Time) error {
if d.HasChange("operating_system") || d.HasChange("user_data") || d.HasChange("custom_data") {
reinstall, ok := d.GetOk("reinstall")

Expand Down Expand Up @@ -802,15 +806,16 @@ func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{
return friendlyError(err)
}

if err := waitForActiveDevice(d, meta); err != nil {
updateTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start)
if err := waitForActiveDevice(ctx, d, meta, updateTimeout); err != nil {
return err
}
}

return nil
}

func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand All @@ -832,9 +837,9 @@ func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
wfrd, wfrdOK := d.GetOk("wait_for_reservation_deprovision")
if wfrdOK && wfrd.(bool) {
// avoid "context: deadline exceeded"
timeout := d.Timeout(schema.TimeoutDelete) - time.Minute - time.Since(start)
timeout := d.Timeout(schema.TimeoutDelete) - 30*time.Second - time.Since(start)

err := waitUntilReservationProvisionable(client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second)
err := waitUntilReservationProvisionable(ctx, client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second)
if err != nil {
return err
}
Expand All @@ -843,9 +848,31 @@ func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}

func waitForActiveDevice(d *schema.ResourceData, meta interface{}) error {
func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta interface{}, timeout time.Duration) error {
targets := []string{"active", "failed"}
pending := []string{"queued", "provisioning", "reinstalling"}

stateConf := &retry.StateChangeConf{
Pending: pending,
Target: targets,
Refresh: func() (interface{}, string, error) {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

device, _, err := client.Devices.Get(d.Id(), &packngo.GetOptions{Includes: []string{"project"}})
if err == nil {
retAttrVal := device.State
return retAttrVal, retAttrVal, nil
}
return "error", "error", err
},
Timeout: timeout,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

// Wait for the device so we can get the networking attributes that show up after a while.
state, err := waitForDeviceAttribute(d, []string{"active", "failed"}, []string{"queued", "provisioning", "reinstalling"}, "state", meta)
state, err := waitForDeviceAttribute(ctx, d, stateConf)
if err != nil {
d.SetId("")
fErr := friendlyError(err)
Expand Down
Loading