From 48249deed2ac1e60fe9ce669287155d64aaa3a6e Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Fri, 18 Aug 2023 23:24:43 +0530 Subject: [PATCH 1/4] Upgrade metal device resource and data source to use timeouts --- equinix/data_source_metal_device.go | 5 +- equinix/helpers_device.go | 66 +++-------------- equinix/helpers_device_test.go | 3 +- equinix/provider.go | 10 +++ equinix/resource_metal_device.go | 71 ++++++++++++------ equinix/resource_metal_device_acc_test.go | 87 ++++++++++++++++++++++- 6 files changed, 156 insertions(+), 86 deletions(-) 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/helpers_device_test.go b/equinix/helpers_device_test.go index 7e01d8b46..f57c714d3 100644 --- a/equinix/helpers_device_test.go +++ b/equinix/helpers_device_test.go @@ -1,6 +1,7 @@ package equinix import ( + "context" "fmt" "testing" "time" @@ -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) } }) diff --git a/equinix/provider.go b/equinix/provider.go index 347a54eb5..63db867d3 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -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 + } +} 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) } }) From c3f8af4a7fb9fe00349871068ac5f490de4243a2 Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Wed, 23 Aug 2023 19:31:32 +0530 Subject: [PATCH 2/4] Acceptance tests for metal device timeouts --- equinix/provider_test.go | 12 +- equinix/resource_metal_device_acc_test.go | 200 +++++++++++----------- equinix/resource_network_bgp_test.go | 15 ++ go.mod | 2 +- 4 files changed, 129 insertions(+), 100 deletions(-) diff --git a/equinix/provider_test.go b/equinix/provider_test.go index 567ca8a75..27f650dcb 100644 --- a/equinix/provider_test.go +++ b/equinix/provider_test.go @@ -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 { @@ -127,6 +128,11 @@ func init() { testAccProviders = map[string]*schema.Provider{ "equinix": testAccProvider, } + testAccProviderFactories = map[string]func() (*schema.Provider, error){ + "equinix": func() (*schema.Provider, error) { + return testAccProvider, nil + }, + } testExternalProviders = map[string]resource.ExternalProvider{ "random": { Source: "hashicorp/random", diff --git a/equinix/resource_metal_device_acc_test.go b/equinix/resource_metal_device_acc_test.go index 20aac5939..9404dbfc2 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/equinix/resource_metal_device_acc_test.go @@ -2,6 +2,7 @@ package equinix import ( "context" + "errors" "fmt" "log" "net" @@ -11,6 +12,7 @@ import ( "testing" "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -338,28 +340,6 @@ 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) @@ -579,6 +559,23 @@ func testAccMetalDeviceExists(n string, device *packngo.Device) resource.TestChe } } +func testAccWaitForMetalDeviceActive(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + defaultTimeout := 20 * time.Minute + + 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") + } + rd := new(schema.ResourceData) + rd.SetId(rs.Primary.ID) + return waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) + } +} + func testAccMetalSameDevice(t *testing.T, before, after *packngo.Device) resource.TestCheckFunc { return func(s *terraform.State) error { if before.ID != after.ID { @@ -720,38 +717,6 @@ 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 @@ -803,30 +768,6 @@ resource "equinix_metal_device" "test" { `, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, rInt, testDeviceTerminationTime()) } -func testAccMetalDeviceConfig_varname_pxe(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" - description = "test-desc-%d" - plan = local.plan - metro = local.metro - operating_system = local.os - billing_cycle = "hourly" - project_id = "${equinix_metal_project.test.id}" - tags = ["%d"] - always_pxe = true - ipxe_script_url = "http://matchbox.foo.wtf:8080/boot.ipxe" - termination_time = "%s" -} -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, rInt, testDeviceTerminationTime()) -} - func testAccMetalDeviceConfig_metro(projSuffix string) string { return fmt.Sprintf(` %s @@ -994,33 +935,34 @@ resource "equinix_metal_device" "test_ipxe_missing" { always_pxe = true }` -func testAccMetalDeviceConfig_timeout(rInt int, projSuffix string) string { +func testAccMetalDeviceConfig_timeout(projSuffix string) string { return fmt.Sprintf(` -%s - resource "equinix_metal_project" "test" { - name = "tfacc-device-%s" + 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" + hostname = "tfacc-test-device" + description = "test-desc" + plan = "c3.small.x86" + metro = "DA" + operating_system = "ubuntu_20_04" 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()) +} +`, projSuffix, testDeviceTerminationTime()) } type mockDeviceService struct { - GetFn func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) + GetFn func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) + CreateFn func(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) + UpdateFn func(string, *packngo.DeviceUpdateRequest) (*packngo.Device, *packngo.Response, error) + DeleteFn func(string, bool) (*packngo.Response, error) } func (m *mockDeviceService) Get(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) { @@ -1028,19 +970,19 @@ func (m *mockDeviceService) Get(deviceID string, opts *packngo.GetOptions) (*pac } func (m *mockDeviceService) Create(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) { - return nil, nil, mockFuncNotImplemented("Create") + return m.CreateFn(device) } -func (m *mockDeviceService) Delete(string, bool) (*packngo.Response, error) { - return nil, mockFuncNotImplemented("Delete") +func (m *mockDeviceService) Delete(deviceId string, forceDetachVolume bool) (*packngo.Response, error) { + return m.DeleteFn(deviceId, forceDetachVolume) } func (m *mockDeviceService) List(string, *packngo.ListOptions) ([]packngo.Device, *packngo.Response, error) { return nil, nil, mockFuncNotImplemented("List") } -func (m *mockDeviceService) Update(string, *packngo.DeviceUpdateRequest) (*packngo.Device, *packngo.Response, error) { - return nil, nil, mockFuncNotImplemented("Update") +func (m *mockDeviceService) Update(deviceId string, updateReq *packngo.DeviceUpdateRequest) (*packngo.Device, *packngo.Response, error) { + return m.UpdateFn(deviceId, updateReq) } func (m *mockDeviceService) Reboot(string) (*packngo.Response, error) { @@ -1208,3 +1150,69 @@ func TestAccMetalDevice_readErrorHandling(t *testing.T) { }) } } + +func TestAccMetalDevice_timeouts(t *testing.T) { + d1 := &packngo.Device{ + State: "active", + } + projSuffix := acctest.RandString(10) + + mockMetalDeviceService := &mockDeviceService{ + CreateFn: func(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) { + return d1, &packngo.Response{Response: &http.Response{Status: matchErrDeviceReadyTimeout.String(), StatusCode: 500}}, + errors.New(matchErrDeviceReadyTimeout.String()) + }, + GetFn: func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) { + return d1, &packngo.Response{Response: &http.Response{Status: "Success", StatusCode: 200}}, nil + }, + } + mockMetalProjectService := &mockProjectService{ + GetFn: func(projectID string, opts *packngo.GetOptions) (*packngo.Project, *packngo.Response, error) { + project := &packngo.Project{ID: "test"} + httpResp := &http.Response{Status: "Success", StatusCode: 200} + return project, &packngo.Response{Response: httpResp}, nil + }, + CreateFn: func(pcr *packngo.ProjectCreateRequest) (*packngo.Project, *packngo.Response, error) { + project := &packngo.Project{ID: "test"} + httpResp := &http.Response{Status: "Success", StatusCode: 200} + return project, &packngo.Response{Response: httpResp}, nil + }, + DeleteFn: func(projectID string) (*packngo.Response, error) { + httpResp := &http.Response{Status: "Success", StatusCode: 200} + return &packngo.Response{Response: httpResp}, nil + }, + } + mockBgpsvc := &mockBgpConfigService{ + GetFn: func(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) { + return nil, nil, nil + }, + } + + mockEquinix := Provider() + mockEquinix.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { + config := Config{ + metal: &packngo.Client{ + Devices: mockMetalDeviceService, + Projects: mockMetalProjectService, + BGPConfig: mockBgpsvc, + }, + } + return &config, nil + } + + mockProviders := map[string]func() (*schema.Provider, error){ + "equinix": func() (*schema.Provider, error) { + return mockEquinix, nil + }, + } + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: mockProviders, + ExternalProviders: testExternalProviders, + Steps: []resource.TestStep{ + { + Config: testAccMetalDeviceConfig_timeout(projSuffix), + ExpectError: matchErrDeviceReadyTimeout, + }, + }, + }) +} diff --git a/equinix/resource_network_bgp_test.go b/equinix/resource_network_bgp_test.go index b4131f3c1..f40bafb6b 100644 --- a/equinix/resource_network_bgp_test.go +++ b/equinix/resource_network_bgp_test.go @@ -2,6 +2,7 @@ package equinix import ( "context" + "github.com/packethost/packngo" "testing" "time" @@ -100,6 +101,20 @@ func (r *mockedBGPUpdateRequest) Execute() error { return nil } +var _ packngo.BGPConfigService = &mockBgpConfigService{} + +type mockBgpConfigService struct { + GetFn func(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) + CreateFn func(projectID string, request packngo.CreateBGPConfigRequest) (*packngo.Response, error) +} + +func (s *mockBgpConfigService) Get(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) { + return s.GetFn(projectID, getOpt) +} +func (s *mockBgpConfigService) Create(projectID string, request packngo.CreateBGPConfigRequest) (*packngo.Response, error) { + return s.CreateFn(projectID, request) +} + func TestNetworkBGP_createUpdateRequest(t *testing.T) { // given req := &mockedBGPUpdateRequest{data: make(map[string]interface{})} diff --git a/go.mod b/go.mod index f908be4f8..956cb970e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/equinix/oauth2-go v1.0.0 github.com/equinix/rest-go v1.3.0 github.com/gruntwork-io/terratest v0.43.0 - github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.4 @@ -48,6 +47,7 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.11.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter v1.7.1 // indirect From 96a4e69ade502e0211946aea73dc98974cf77a2e Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Thu, 24 Aug 2023 18:49:13 +0530 Subject: [PATCH 3/4] Timeouts tests to be unit test to be able to run without TF_ACC --- equinix/resource_metal_device.go | 6 +- equinix/resource_metal_device_acc_test.go | 132 +++++++++------------- equinix/resource_network_bgp_test.go | 15 --- 3 files changed, 58 insertions(+), 95 deletions(-) diff --git a/equinix/resource_metal_device.go b/equinix/resource_metal_device.go index a9953e5ed..e104334da 100644 --- a/equinix/resource_metal_device.go +++ b/equinix/resource_metal_device.go @@ -597,7 +597,7 @@ func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(newDevice.ID) - createTimeout := d.Timeout(schema.TimeoutCreate) - time.Minute - time.Since(start) + createTimeout := d.Timeout(schema.TimeoutCreate) - 30*time.Second - time.Since(start) if err = waitForActiveDevice(ctx, d, meta, createTimeout); err != nil { return err } @@ -806,7 +806,7 @@ func doReinstall(ctx context.Context, client *packngo.Client, d *schema.Resource return friendlyError(err) } - deleteTimeout := d.Timeout(schema.TimeoutUpdate) - time.Minute - time.Since(start) + deleteTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start) if err := waitForActiveDevice(ctx, d, meta, deleteTimeout); err != nil { return err } @@ -837,7 +837,7 @@ func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta 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(ctx, client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second) if err != nil { diff --git a/equinix/resource_metal_device_acc_test.go b/equinix/resource_metal_device_acc_test.go index 9404dbfc2..02e4c9a3f 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/equinix/resource_metal_device_acc_test.go @@ -2,7 +2,6 @@ package equinix import ( "context" - "errors" "fmt" "log" "net" @@ -12,7 +11,6 @@ import ( "testing" "time" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -559,23 +557,6 @@ func testAccMetalDeviceExists(n string, device *packngo.Device) resource.TestChe } } -func testAccWaitForMetalDeviceActive(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - defaultTimeout := 20 * time.Minute - - 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") - } - rd := new(schema.ResourceData) - rd.SetId(rs.Primary.ID) - return waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) - } -} - func testAccMetalSameDevice(t *testing.T, before, after *packngo.Device) resource.TestCheckFunc { return func(s *terraform.State) error { if before.ID != after.ID { @@ -949,6 +930,7 @@ resource "equinix_metal_device" "test" { operating_system = "ubuntu_20_04" project_id = "${equinix_metal_project.test.id}" termination_time = "%s" + depends_on = [equinix_metal_project.test] timeouts { create = "10s" @@ -959,10 +941,7 @@ resource "equinix_metal_device" "test" { } type mockDeviceService struct { - GetFn func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) - CreateFn func(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) - UpdateFn func(string, *packngo.DeviceUpdateRequest) (*packngo.Device, *packngo.Response, error) - DeleteFn func(string, bool) (*packngo.Response, error) + GetFn func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) } func (m *mockDeviceService) Get(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) { @@ -970,11 +949,11 @@ func (m *mockDeviceService) Get(deviceID string, opts *packngo.GetOptions) (*pac } func (m *mockDeviceService) Create(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) { - return m.CreateFn(device) + return nil, nil, mockFuncNotImplemented("Create") } func (m *mockDeviceService) Delete(deviceId string, forceDetachVolume bool) (*packngo.Response, error) { - return m.DeleteFn(deviceId, forceDetachVolume) + return nil, mockFuncNotImplemented("Delete") } func (m *mockDeviceService) List(string, *packngo.ListOptions) ([]packngo.Device, *packngo.Response, error) { @@ -982,7 +961,7 @@ func (m *mockDeviceService) List(string, *packngo.ListOptions) ([]packngo.Device } func (m *mockDeviceService) Update(deviceId string, updateReq *packngo.DeviceUpdateRequest) (*packngo.Device, *packngo.Response, error) { - return m.UpdateFn(deviceId, updateReq) + return nil, nil, mockFuncNotImplemented("Update") } func (m *mockDeviceService) Reboot(string) (*packngo.Response, error) { @@ -1151,68 +1130,67 @@ func TestAccMetalDevice_readErrorHandling(t *testing.T) { } } -func TestAccMetalDevice_timeouts(t *testing.T) { - d1 := &packngo.Device{ - State: "active", - } - projSuffix := acctest.RandString(10) +func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.ImportStateIdFunc { + return func(state *terraform.State) (string, error) { + defaultTimeout := 20 * time.Minute - mockMetalDeviceService := &mockDeviceService{ - CreateFn: func(device *packngo.DeviceCreateRequest) (*packngo.Device, *packngo.Response, error) { - return d1, &packngo.Response{Response: &http.Response{Status: matchErrDeviceReadyTimeout.String(), StatusCode: 500}}, - errors.New(matchErrDeviceReadyTimeout.String()) - }, - GetFn: func(deviceID string, opts *packngo.GetOptions) (*packngo.Device, *packngo.Response, error) { - return d1, &packngo.Response{Response: &http.Response{Status: "Success", StatusCode: 200}}, nil - }, - } - mockMetalProjectService := &mockProjectService{ - GetFn: func(projectID string, opts *packngo.GetOptions) (*packngo.Project, *packngo.Response, error) { - project := &packngo.Project{ID: "test"} - httpResp := &http.Response{Status: "Success", StatusCode: 200} - return project, &packngo.Response{Response: httpResp}, nil - }, - CreateFn: func(pcr *packngo.ProjectCreateRequest) (*packngo.Project, *packngo.Response, error) { - project := &packngo.Project{ID: "test"} - httpResp := &http.Response{Status: "Success", StatusCode: 200} - return project, &packngo.Response{Response: httpResp}, nil - }, - DeleteFn: func(projectID string) (*packngo.Response, error) { - httpResp := &http.Response{Status: "Success", StatusCode: 200} - return &packngo.Response{Response: httpResp}, nil - }, - } - mockBgpsvc := &mockBgpConfigService{ - GetFn: func(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) { - return nil, nil, nil - }, - } + rs, ok := state.RootModule().Resources[project] + if !ok { + return "", fmt.Errorf("Project Not found in the state: %s", project) + } + if rs.Primary.ID == "" { + return "", fmt.Errorf("No Record ID is set") + } - mockEquinix := Provider() - mockEquinix.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { - config := Config{ - metal: &packngo.Client{ - Devices: mockMetalDeviceService, - Projects: mockMetalProjectService, - BGPConfig: mockBgpsvc, - }, + meta := testAccProvider.Meta() + rd := new(schema.ResourceData) + meta.(*Config).addModuleToMetalUserAgent(rd) + client := meta.(*Config).metal + devices, _, err := client.Devices.List(rs.Primary.ID, &packngo.ListOptions{Search: deviceHostName}) + if err != nil { + return "", fmt.Errorf("error while fetching devices for project [%s], error: %w", rs.Primary.ID, err) + } + if len(devices) == 0 { + return "", fmt.Errorf("Not able to find devices in project [%s]", rs.Primary.ID) + } + if len(devices) > 1 { + return "", fmt.Errorf("Found more than one device with the hostname in project [%s]", rs.Primary.ID) } - return &config, nil - } - mockProviders := map[string]func() (*schema.Provider, error){ - "equinix": func() (*schema.Provider, error) { - return mockEquinix, nil - }, + rd.SetId(devices[0].ID) + return devices[0].ID, waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) } +} + +func TestAccMetalDevice_importBasicTimeout(t *testing.T) { + rs := acctest.RandString(10) + r := "equinix_metal_device.test" + hostname := "tfacc-test-device" + project := "equinix_metal_project.test" + resource.ParallelTest(t, resource.TestCase{ - ProviderFactories: mockProviders, + PreCheck: func() { testAccPreCheck(t) }, ExternalProviders: testExternalProviders, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: testAccMetalDeviceConfig_timeout(projSuffix), + Config: testAccMetalDeviceConfig_timeout(rs), ExpectError: matchErrDeviceReadyTimeout, }, + { + /** + Step 1 errors out, state doesnt have device, need to import that in the state before deleting + */ + ResourceName: r, + ImportState: true, + ImportStateIdFunc: testAccWaitForMetalDeviceActive(project, hostname), + ImportStatePersist: true, + }, + { + Config: testAccMetalDeviceConfig_timeout(rs), + Destroy: true, + }, }, }) } diff --git a/equinix/resource_network_bgp_test.go b/equinix/resource_network_bgp_test.go index f40bafb6b..b4131f3c1 100644 --- a/equinix/resource_network_bgp_test.go +++ b/equinix/resource_network_bgp_test.go @@ -2,7 +2,6 @@ package equinix import ( "context" - "github.com/packethost/packngo" "testing" "time" @@ -101,20 +100,6 @@ func (r *mockedBGPUpdateRequest) Execute() error { return nil } -var _ packngo.BGPConfigService = &mockBgpConfigService{} - -type mockBgpConfigService struct { - GetFn func(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) - CreateFn func(projectID string, request packngo.CreateBGPConfigRequest) (*packngo.Response, error) -} - -func (s *mockBgpConfigService) Get(projectID string, getOpt *packngo.GetOptions) (*packngo.BGPConfig, *packngo.Response, error) { - return s.GetFn(projectID, getOpt) -} -func (s *mockBgpConfigService) Create(projectID string, request packngo.CreateBGPConfigRequest) (*packngo.Response, error) { - return s.CreateFn(projectID, request) -} - func TestNetworkBGP_createUpdateRequest(t *testing.T) { // given req := &mockedBGPUpdateRequest{data: make(map[string]interface{})} From 4ebf3299bf9c4463e9fa89b6b8962a840aca658f Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Wed, 30 Aug 2023 19:11:38 +0530 Subject: [PATCH 4/4] Testing Update and create --- equinix/resource_metal_device.go | 4 +- equinix/resource_metal_device_acc_test.go | 95 ++++++++++++++++++++--- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/equinix/resource_metal_device.go b/equinix/resource_metal_device.go index e104334da..1596ff188 100644 --- a/equinix/resource_metal_device.go +++ b/equinix/resource_metal_device.go @@ -806,8 +806,8 @@ func doReinstall(ctx context.Context, client *packngo.Client, d *schema.Resource return friendlyError(err) } - deleteTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start) - if err := waitForActiveDevice(ctx, d, meta, deleteTimeout); err != nil { + updateTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start) + if err := waitForActiveDevice(ctx, d, meta, updateTimeout); err != nil { return err } } diff --git a/equinix/resource_metal_device_acc_test.go b/equinix/resource_metal_device_acc_test.go index 02e4c9a3f..8f4d31c21 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/equinix/resource_metal_device_acc_test.go @@ -916,28 +916,74 @@ resource "equinix_metal_device" "test_ipxe_missing" { always_pxe = true }` -func testAccMetalDeviceConfig_timeout(projSuffix string) string { +func testAccMetalDeviceConfig_timeout(projSuffix, createTimeout, updateTimeout, deleteTimeout string) string { + if createTimeout == "" { + createTimeout = "20m" + } + if updateTimeout == "" { + updateTimeout = "20m" + } + if deleteTimeout == "" { + deleteTimeout = "20m" + } + + return fmt.Sprintf(` +%s + +resource "equinix_metal_project" "test" { + name = "tfacc-device-%s" +} + +resource "equinix_metal_device" "test" { + hostname = "tfacc-test-device" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" + project_id = "${equinix_metal_project.test.id}" + termination_time = "%s" + + timeouts { + create = "%s" + update = "%s" + delete = "%s" + } +} +`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime(), createTimeout, updateTimeout, deleteTimeout) +} + +func testAccMetalDeviceConfig_reinstall_timeout(projSuffix, updateTimeout string) string { + if updateTimeout == "" { + updateTimeout = "20m" + } + return fmt.Sprintf(` +%s + resource "equinix_metal_project" "test" { name = "tfacc-device-%s" } resource "equinix_metal_device" "test" { hostname = "tfacc-test-device" - description = "test-desc" - plan = "c3.small.x86" - metro = "DA" - operating_system = "ubuntu_20_04" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" project_id = "${equinix_metal_project.test.id}" + user_data = "#!/usr/bin/env sh\necho Reinstall\n" termination_time = "%s" - depends_on = [equinix_metal_project.test] + + reinstall { + enabled = true + deprovision_fast = true + } timeouts { - create = "10s" - update = "10s" + update = "%s" } } -`, projSuffix, testDeviceTerminationTime()) +`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime(), updateTimeout) } type mockDeviceService struct { @@ -1162,7 +1208,7 @@ func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.Im } } -func TestAccMetalDevice_importBasicTimeout(t *testing.T) { +func TestAccMetalDeviceCreate_timeout(t *testing.T) { rs := acctest.RandString(10) r := "equinix_metal_device.test" hostname := "tfacc-test-device" @@ -1175,7 +1221,7 @@ func TestAccMetalDevice_importBasicTimeout(t *testing.T) { CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: testAccMetalDeviceConfig_timeout(rs), + Config: testAccMetalDeviceConfig_timeout(rs, "10s", "", ""), ExpectError: matchErrDeviceReadyTimeout, }, { @@ -1188,9 +1234,34 @@ func TestAccMetalDevice_importBasicTimeout(t *testing.T) { ImportStatePersist: true, }, { - Config: testAccMetalDeviceConfig_timeout(rs), + Config: testAccMetalDeviceConfig_timeout(rs, "", "", ""), Destroy: true, }, }, }) } + +func TestAccMetalDeviceUpdate_timeout(t *testing.T) { + var d1 packngo.Device + rs := acctest.RandString(10) + r := "equinix_metal_device.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ExternalProviders: testExternalProviders, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccMetalDeviceCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccMetalDeviceConfig_timeout(rs, "", "", ""), + Check: resource.ComposeTestCheckFunc( + testAccMetalDeviceExists(r, &d1), + ), + }, + { + Config: testAccMetalDeviceConfig_reinstall_timeout(rs, "10s"), + ExpectError: matchErrDeviceReadyTimeout, + }, + }, + }) +}