diff --git a/equinix/data_source_metal_device.go b/equinix/data_source_metal_device.go index d0a9bc84f..f656068a1 100644 --- a/equinix/data_source_metal_device.go +++ b/equinix/data_source_metal_device.go @@ -1,6 +1,7 @@ package equinix import ( + "context" "encoding/json" "fmt" "path" @@ -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, @@ -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") diff --git a/equinix/helpers_device.go b/equinix/helpers_device.go index 427464c2f..6deef124b 100644 --- a/equinix/helpers_device.go +++ b/equinix/helpers_device.go @@ -1,6 +1,8 @@ package equinix import ( + "context" + "errors" "fmt" "log" "strings" @@ -8,7 +10,6 @@ import ( "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" @@ -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}, @@ -151,7 +152,7 @@ func waitUntilReservationProvisionable(client *packngo.Client, reservationId, in Delay: delay, MinTimeout: minTimeout, } - _, err := stateConf.WaitForState() + _, err := stateConf.WaitForStateContext(ctx) return err } @@ -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() @@ -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 @@ -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 { - 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{ diff --git a/equinix/provider.go b/equinix/provider.go index c6100044f..3869cd0e9 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -444,3 +444,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 + } +} diff --git a/equinix/resource_metal_device.go b/equinix/resource_metal_device.go index 16da4298d..a9953e5ed 100644 --- a/equinix/resource_metal_device.go +++ b/equinix/resource_metal_device.go @@ -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" @@ -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": { @@ -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 @@ -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 { @@ -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) @@ -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) - time.Minute - 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 @@ -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 @@ -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") @@ -802,7 +806,8 @@ func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{ return friendlyError(err) } - if err := waitForActiveDevice(d, meta); err != nil { + deleteTimeout := d.Timeout(schema.TimeoutUpdate) - time.Minute - time.Since(start) + if err := waitForActiveDevice(ctx, d, meta, deleteTimeout); err != nil { return err } } @@ -810,7 +815,7 @@ func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{ 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 @@ -834,7 +839,7 @@ func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error { // avoid "context: deadline exceeded" timeout := d.Timeout(schema.TimeoutDelete) - time.Minute - 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 } @@ -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) diff --git a/equinix/resource_metal_device_acc_test.go b/equinix/resource_metal_device_acc_test.go index ebf892158..20aac5939 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/equinix/resource_metal_device_acc_test.go @@ -1,6 +1,7 @@ package equinix import ( + "context" "fmt" "log" "net" @@ -74,8 +75,9 @@ func testSweepDevices(region string) error { // Regexp vars for use with resource.ExpectError var ( - matchErrMustBeProvided = regexp.MustCompile(".* must be provided when .*") - matchErrShouldNotBeAnIPXE = regexp.MustCompile(`.*"user_data" should not be an iPXE.*`) + matchErrMustBeProvided = regexp.MustCompile(".* must be provided when .*") + matchErrShouldNotBeAnIPXE = regexp.MustCompile(`.*"user_data" should not be an iPXE.*`) + matchErrDeviceReadyTimeout = regexp.MustCompile(".* timeout while waiting for state to become 'active, failed'.*") ) // This function should be used to find available plans in all test where a metal_device resource is needed. @@ -336,6 +338,28 @@ func TestAccMetalDevice_update(t *testing.T) { }) } +func TestAccMetalDevice_timeouts(t *testing.T) { + rs := acctest.RandString(10) + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ExternalProviders: testExternalProviders, + Providers: testAccProviders, + CheckDestroy: testAccMetalDeviceCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccMetalDeviceConfig_timeout(rInt, rs), + ExpectError: matchErrDeviceReadyTimeout, + }, + { + Config: testAccMetalDeviceConfig_reinstall_timeout(rInt+1, rs), + ExpectError: matchErrDeviceReadyTimeout, + }, + }, + }) +} + func TestAccMetalDevice_IPXEScriptUrl(t *testing.T) { var device, d2 packngo.Device rs := acctest.RandString(10) @@ -696,6 +720,38 @@ resource "equinix_metal_device" "test" { `, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, testDeviceTerminationTime()) } +func testAccMetalDeviceConfig_reinstall_timeout(rInt int, projSuffix string) string { + return fmt.Sprintf(` +%s + +resource "equinix_metal_project" "test" { + name = "tfacc-device-%s" +} + +resource "equinix_metal_device" "test" { + hostname = "tfacc-test-device-%d" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" + project_id = "${equinix_metal_project.test.id}" + tags = ["%d"] + user_data = "#!/usr/bin/env sh\necho Reinstall\n" + termination_time = "%s" + + reinstall { + enabled = true + deprovision_fast = true + } + + timeouts { + create = "10s" + update = "10s" + } +} +`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, testDeviceTerminationTime()) +} + func testAccMetalDeviceConfig_allowAttributeChanges(rInt int, projSuffix string, userdata string, customdata string, attributeName string) string { return fmt.Sprintf(` %s @@ -938,6 +994,31 @@ resource "equinix_metal_device" "test_ipxe_missing" { always_pxe = true }` +func testAccMetalDeviceConfig_timeout(rInt int, projSuffix string) string { + return fmt.Sprintf(` +%s + +resource "equinix_metal_project" "test" { + name = "tfacc-device-%s" +} +resource "equinix_metal_device" "test" { + hostname = "tfacc-test-device-%d" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" + project_id = "${equinix_metal_project.test.id}" + tags = ["%d"] + user_data = "#!/usr/bin/env sh\necho Reinstall\n" + termination_time = "%s" + + timeouts { + create = "10s" + update = "10s" + } +}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, testDeviceTerminationTime()) +} + type mockDeviceService struct { GetFn func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) } @@ -1121,7 +1202,7 @@ func TestAccMetalDevice_readErrorHandling(t *testing.T) { if tt.args.newResource { d.MarkNewResource() } - if err := resourceMetalDeviceRead(d, tt.args.meta); (err != nil) != tt.wantErr { + if err := resourceMetalDeviceRead(context.Background(), d, tt.args.meta); (err != nil) != tt.wantErr { t.Errorf("resourceMetalDeviceRead() error = %v, wantErr %v", err, tt.wantErr) } })