diff --git a/equinix/common_test.go b/equinix/common_test.go new file mode 100644 index 000000000..4f79065ad --- /dev/null +++ b/equinix/common_test.go @@ -0,0 +1,120 @@ +package equinix + +import ( + "fmt" + "strings" + "time" +) + +// list of plans and metros and os used as filter criteria to find available hardware to run tests +var ( + preferable_plans = []string{"x1.small.x86", "t1.small.x86", "c2.medium.x86", "c3.small.x86", "c3.medium.x86", "m3.small.x86"} + preferable_metros = []string{"ch", "ny", "sv", "ty", "am"} + preferable_os = []string{"ubuntu_20_04"} +) + +// Deprecated: use the identical TestDeviceTerminationTime from internal/acceptance instead +func testDeviceTerminationTime() string { + return time.Now().UTC().Add(60 * time.Minute).Format(time.RFC3339) +} + +// This function should be used to find available plans in all test where a metal_device resource is needed. +// +// TODO consider adding a datasource for equinix_metal_operating_system and making the local.os conditional +// +// https://github.com/equinix/terraform-provider-equinix/pull/220#discussion_r915418418equinix_metal_operating_system +// https://github.com/equinix/terraform-provider-equinix/discussions/221 +func confAccMetalDevice_base(plans, metros, os []string) string { + return fmt.Sprintf(` +data "equinix_metal_plans" "test" { + sort { + attribute = "id" + direction = "asc" + } + + filter { + attribute = "name" + values = [%s] + } + filter { + attribute = "available_in_metros" + values = [%s] + } + filter { + attribute = "deployment_types" + values = ["on_demand", "spot_market"] + } +} + +// Select a metal plan randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "plan_idx" { + min = 0 + max = length(data.equinix_metal_plans.test.plans) - 1 +} + +resource "terraform_data" "plan" { + input = data.equinix_metal_plans.test.plans[random_integer.plan_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +resource "terraform_data" "facilities" { + input = sort(tolist(setsubtract(terraform_data.plan.output.available_in, ["nrt1", "dfw2", "ewr1", "ams1", "sjc1", "ld7", "sy4", "ny6"]))) + + lifecycle { + ignore_changes = ["input"] + } +} + +// Select a metal facility randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "facility_idx" { + min = 0 + max = length(local.facilities) - 1 +} + +resource "terraform_data" "facility" { + input = local.facilities[random_integer.facility_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +// Select a metal metro randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "metro_idx" { + min = 0 + max = length(local.metros) - 1 +} + +resource "terraform_data" "metro" { + input = local.metros[random_integer.metro_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +locals { + // Select a random plan + plan = terraform_data.plan.output.slug + + // Select a random facility from the facilities in which the selected plan is available, excluding decommed facilities + facilities = terraform_data.facilities.output + facility = terraform_data.facility.output + + // Select a random metro from the metros in which the selected plan is available + metros = sort(tolist(terraform_data.plan.output.available_in_metros)) + metro = terraform_data.metro.output + + os = [%s][0] +} +`, fmt.Sprintf("\"%s\"", strings.Join(plans[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(metros[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(os[:], `","`))) +} diff --git a/equinix/data_source_metal_device_bgp_neighbors_acc_test.go b/equinix/data_source_metal_device_bgp_neighbors_acc_test.go index 1380c8f77..42ea24627 100644 --- a/equinix/data_source_metal_device_bgp_neighbors_acc_test.go +++ b/equinix/data_source_metal_device_bgp_neighbors_acc_test.go @@ -9,7 +9,7 @@ import ( ) func TestAccDataSourceMetalDeviceBgpNeighbors(t *testing.T) { - projectName := fmt.Sprintf("ds-device-%s", acctest.RandString(10)) + projSuffix := fmt.Sprintf("ds-device-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -17,7 +17,7 @@ func TestAccDataSourceMetalDeviceBgpNeighbors(t *testing.T) { ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDataSourceMetalDeviceBgpNeighborsConfig(projectName), + Config: testAccDataSourceMetalDeviceBgpNeighborsConfig(projSuffix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet( "data.equinix_metal_device_bgp_neighbors.test", "bgp_neighbors.#"), @@ -27,16 +27,34 @@ func TestAccDataSourceMetalDeviceBgpNeighbors(t *testing.T) { }) } -func testAccDataSourceMetalDeviceBgpNeighborsConfig(projectName string) string { +func testAccDataSourceMetalDeviceBgpNeighborsConfig(projSuffix string) string { return fmt.Sprintf(` %s +resource "equinix_metal_project" "test" { + name = "tfacc-project-%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" +} + +data "equinix_metal_device" "test" { + project_id = equinix_metal_project.test.id + hostname = equinix_metal_device.test.hostname +} + data "equinix_metal_device_bgp_neighbors" "test" { device_id = equinix_metal_device.test.id } output "bgp_neighbors_listing" { value = data.equinix_metal_device_bgp_neighbors.test.bgp_neighbors -} -`, testDataSourceMetalDeviceConfig_basic(projectName)) +}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) } diff --git a/equinix/provider.go b/equinix/provider.go index 24cc9645f..6d8dd1117 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -10,6 +10,7 @@ import ( fabric_connection "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/connection" fabric_market_place_subscription "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/marketplace" fabric_network "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/network" + metal_device "github.com/equinix/terraform-provider-equinix/internal/resources/metal/device" metal_port "github.com/equinix/terraform-provider-equinix/internal/resources/metal/port" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/virtual_circuit" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf" @@ -104,8 +105,8 @@ func Provider() *schema.Provider { "equinix_metal_precreated_ip_block": dataSourceMetalPreCreatedIPBlock(), "equinix_metal_operating_system": dataSourceOperatingSystem(), "equinix_metal_spot_market_price": dataSourceSpotMarketPrice(), - "equinix_metal_device": dataSourceMetalDevice(), - "equinix_metal_devices": dataSourceMetalDevices(), + "equinix_metal_device": metal_device.DataSource(), + "equinix_metal_devices": metal_device.ListDataSource(), "equinix_metal_device_bgp_neighbors": dataSourceMetalDeviceBGPNeighbors(), "equinix_metal_plans": dataSourceMetalPlans(), "equinix_metal_port": metal_port.DataSource(), @@ -129,7 +130,7 @@ func Provider() *schema.Provider { "equinix_network_file": resourceNetworkFile(), "equinix_metal_user_api_key": resourceMetalUserAPIKey(), "equinix_metal_project_api_key": resourceMetalProjectAPIKey(), - "equinix_metal_device": resourceMetalDevice(), + "equinix_metal_device": metal_device.Resource(), "equinix_metal_device_network_type": resourceMetalDeviceNetworkType(), "equinix_metal_port": metal_port.Resource(), "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), diff --git a/equinix/resource_metal_spot_market_request.go b/equinix/resource_metal_spot_market_request.go index a1f3fc16f..b44c11017 100644 --- a/equinix/resource_metal_spot_market_request.go +++ b/equinix/resource_metal_spot_market_request.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "reflect" + "regexp" "sort" "strconv" "time" @@ -22,6 +23,10 @@ import ( "github.com/packethost/packngo" ) +var ( + matchIPXEScript = regexp.MustCompile(`(?i)^#![i]?pxe`) +) + func resourceMetalSpotMarketRequest() *schema.Resource { return &schema.Resource{ CreateContext: resourceMetalSpotMarketRequestCreate, @@ -62,10 +67,7 @@ func resourceMetalSpotMarketRequest() *schema.Resource { diffThreshold := .02 priceDiff := oldF / newF - if diffThreshold < priceDiff { - return true - } - return false + return diffThreshold < priceDiff }, }, "facilities": { diff --git a/equinix/data_source_metal_device.go b/internal/resources/metal/device/datasource.go similarity index 85% rename from equinix/data_source_metal_device.go rename to internal/resources/metal/device/datasource.go index d9d14d357..fc7ef75e3 100644 --- a/equinix/data_source_metal_device.go +++ b/internal/resources/metal/device/datasource.go @@ -1,8 +1,9 @@ -package equinix +package device import ( "context" "encoding/json" + "errors" "fmt" "path" "sort" @@ -17,7 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" ) -func dataSourceMetalDevice() *schema.Resource { +func DataSource() *schema.Resource { return &schema.Resource{ Description: `The datasource can be used to fetch a single device. @@ -253,21 +254,23 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta } } - d.Set("hostname", device.GetHostname()) - d.Set("project_id", device.Project.GetId()) - d.Set("device_id", device.GetId()) - d.Set("plan", device.Plan.Slug) - d.Set("facility", device.Facility.Code) + var errs []error + + errs = append(errs, d.Set("hostname", device.GetHostname())) + errs = append(errs, d.Set("project_id", device.Project.GetId())) + errs = append(errs, d.Set("device_id", device.GetId())) + errs = append(errs, d.Set("plan", device.Plan.Slug)) + errs = append(errs, d.Set("facility", device.Facility.Code)) if device.Metro != nil { - d.Set("metro", strings.ToLower(device.Metro.GetCode())) + errs = append(errs, d.Set("metro", strings.ToLower(device.Metro.GetCode()))) } - d.Set("operating_system", device.OperatingSystem.GetSlug()) - d.Set("state", device.GetState()) - d.Set("billing_cycle", device.GetBillingCycle()) - d.Set("ipxe_script_url", device.GetIpxeScriptUrl()) - d.Set("always_pxe", device.GetAlwaysPxe()) - d.Set("root_password", device.GetRootPassword()) - d.Set("sos_hostname", device.GetSos()) + errs = append(errs, d.Set("operating_system", device.OperatingSystem.GetSlug())) + errs = append(errs, d.Set("state", device.GetState())) + errs = append(errs, d.Set("billing_cycle", device.GetBillingCycle())) + errs = append(errs, d.Set("ipxe_script_url", device.GetIpxeScriptUrl())) + errs = append(errs, d.Set("always_pxe", device.GetAlwaysPxe())) + errs = append(errs, d.Set("root_password", device.GetRootPassword())) + errs = append(errs, d.Set("sos_hostname", device.GetSos())) if device.Storage != nil { rawStorageBytes, err := json.Marshal(device.Storage) @@ -279,26 +282,26 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta if err != nil { return diag.Errorf("[ERR] Error normalizing storage JSON string for device (%s): %s", d.Id(), err) } - d.Set("storage", storageString) + errs = append(errs, d.Set("storage", storageString)) } if device.HardwareReservation != nil { - d.Set("hardware_reservation_id", device.HardwareReservation.GetId()) + errs = append(errs, d.Set("hardware_reservation_id", device.HardwareReservation.GetId())) } networkType, err := getNetworkType(device) if err != nil { return diag.Errorf("[ERR] Error computing network type for device (%s): %s", d.Id(), err) } - d.Set("network_type", networkType) + errs = append(errs, d.Set("network_type", networkType)) - d.Set("tags", device.Tags) + errs = append(errs, d.Set("tags", device.Tags)) keyIDs := []string{} for _, k := range device.SshKeys { keyIDs = append(keyIDs, path.Base(k.Href)) } - d.Set("ssh_key_ids", keyIDs) + errs = append(errs, d.Set("ssh_key_ids", keyIDs)) networkInfo := getNetworkInfo(device.IpAddresses) sort.SliceStable(networkInfo.Networks, func(i, j int) bool { @@ -309,15 +312,21 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta return getNetworkRank(int(famI), pubI) < getNetworkRank(int(famJ), pubJ) }) - d.Set("network", networkInfo.Networks) - d.Set("access_public_ipv4", networkInfo.PublicIPv4) - d.Set("access_private_ipv4", networkInfo.PrivateIPv4) - d.Set("access_public_ipv6", networkInfo.PublicIPv6) + errs = append(errs, d.Set("network", networkInfo.Networks)) + errs = append(errs, d.Set("access_public_ipv4", networkInfo.PublicIPv4)) + errs = append(errs, d.Set("access_private_ipv4", networkInfo.PrivateIPv4)) + errs = append(errs, d.Set("access_public_ipv6", networkInfo.PublicIPv6)) ports := getPorts(device.NetworkPorts) - d.Set("ports", ports) + errs = append(errs, d.Set("ports", ports)) d.SetId(device.GetId()) + + err = errors.Join(errs...) + if err != nil { + return diag.FromErr(err) + } + return nil } diff --git a/equinix/data_source_metal_device_acc_test.go b/internal/resources/metal/device/datasource_test.go similarity index 73% rename from equinix/data_source_metal_device_acc_test.go rename to internal/resources/metal/device/datasource_test.go index d68ba3656..98c830507 100644 --- a/equinix/data_source_metal_device_acc_test.go +++ b/internal/resources/metal/device/datasource_test.go @@ -1,24 +1,25 @@ -package equinix +package device_test import ( "fmt" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) func TestAccDataSourceMetalDevice_basic(t *testing.T) { - projectName := fmt.Sprintf("ds-device-%s", acctest.RandString(10)) + projSuffix := fmt.Sprintf("ds-device-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: testDataSourceMetalDeviceConfig_basic(projectName), + Config: testDataSourceMetalDeviceConfig_basic(projSuffix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "data.equinix_metal_device.test", "hostname", "tfacc-test-device"), @@ -59,20 +60,20 @@ resource "equinix_metal_device" "test" { data "equinix_metal_device" "test" { project_id = equinix_metal_project.test.id hostname = equinix_metal_device.test.hostname -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime()) } func TestAccDataSourceMetalDevice_byID(t *testing.T) { - projectName := fmt.Sprintf("ds-device-by-id-%s", acctest.RandString(10)) + projSuffix := fmt.Sprintf("ds-device-by-id-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: testDataSourceMetalDeviceConfig_byID(projectName), + Config: testDataSourceMetalDeviceConfig_byID(projSuffix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "data.equinix_metal_device.test", "hostname", "tfacc-test-device"), @@ -112,5 +113,5 @@ resource "equinix_metal_device" "test" { data "equinix_metal_device" "test" { device_id = equinix_metal_device.test.id -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime()) } diff --git a/equinix/helpers_device.go b/internal/resources/metal/device/helpers.go similarity index 94% rename from equinix/helpers_device.go rename to internal/resources/metal/device/helpers.go index d4749d5fb..a2aeae2b8 100644 --- a/equinix/helpers_device.go +++ b/internal/resources/metal/device/helpers.go @@ -1,4 +1,4 @@ -package equinix +package device import ( "context" @@ -71,6 +71,10 @@ func getNetworkInfo(ips []metalv1.IPAssignment) NetworkInfo { return ni } +// Deprecated: this exists for backwards compatibility with +// packngo-based resources. It relies on the deprecated packngo +// SDK and either the logic from packngo should be pulled in to +// the provider or this functionality should be removed entirely func getNetworkType(device *metalv1.Device) (*string, error) { pgDevice := packngo.Device{} res, err := device.MarshalJSON() @@ -131,7 +135,7 @@ func hwReservationStateRefreshFunc(ctx context.Context, client *metalv1.APIClien } } -func waitUntilReservationProvisionable(ctx context.Context, client *metalv1.APIClient, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error { +func WaitUntilReservationProvisionable(ctx context.Context, client *metalv1.APIClient, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error { stateConf := &retry.StateChangeConf{ Pending: []string{deprovisioning}, Target: []string{provisionable, reprovisioned}, diff --git a/equinix/helpers_device_test.go b/internal/resources/metal/device/helpers_test.go similarity index 82% rename from equinix/helpers_device_test.go rename to internal/resources/metal/device/helpers_test.go index 25054d501..c852a9b52 100644 --- a/equinix/helpers_device_test.go +++ b/internal/resources/metal/device/helpers_test.go @@ -1,7 +1,8 @@ -package equinix +package device_test import ( "context" + "log" "net/http" "net/http/httptest" "strings" @@ -10,9 +11,10 @@ import ( "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/device" ) -func Test_waitUntilReservationProvisionable(t *testing.T) { +func Test_WaitUntilReservationProvisionable(t *testing.T) { type args struct { reservationId string instanceId string @@ -75,7 +77,11 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { w.Header().Add("Content-Type", "application/json") w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") w.WriteHeader(http.StatusOK) - w.Write(body) + _, err = w.Write(body) + if err != nil { + // This should never be reached and indicates a failure in the test itself + panic(err) + } } })(), }, @@ -118,7 +124,11 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { w.Header().Add("Content-Type", "application/json") w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") w.WriteHeader(http.StatusOK) - w.Write(body) + _, err = w.Write(body) + if err != nil { + // This should never be reached and indicates a failure in the test itself + panic(err) + } } })(), }, @@ -143,7 +153,11 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { w.Header().Add("Content-Type", "application/json") w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") w.WriteHeader(http.StatusOK) - w.Write(body) + _, err = w.Write(body) + if err != nil { + // This should never be reached and indicates a failure in the test itself + panic(err) + } }, }, wantErr: true, @@ -159,10 +173,13 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { BaseURL: mockAPI.URL, Token: "fakeTokenForMock", } - meta.Load(ctx) + err := meta.Load(ctx) + if err != nil { + log.Printf("failed to load provider config during test: %v", err) + } client := meta.NewMetalClientForTesting() - if err := waitUntilReservationProvisionable(ctx, client, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr { + if err := device.WaitUntilReservationProvisionable(ctx, client, 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/data_source_metal_devices.go b/internal/resources/metal/device/list_datasource.go similarity index 96% rename from equinix/data_source_metal_devices.go rename to internal/resources/metal/device/list_datasource.go index 476ef2e17..ec6be011b 100644 --- a/equinix/data_source_metal_devices.go +++ b/internal/resources/metal/device/list_datasource.go @@ -1,4 +1,4 @@ -package equinix +package device import ( "context" @@ -11,8 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func dataSourceMetalDevices() *schema.Resource { - dsmd := dataSourceMetalDevice() +func ListDataSource() *schema.Resource { + dsmd := DataSource() sch := dsmd.Schema for _, v := range sch { if v.Optional { diff --git a/equinix/data_source_metal_devices_acc_test.go b/internal/resources/metal/device/list_datasource_test.go similarity index 83% rename from equinix/data_source_metal_devices_acc_test.go rename to internal/resources/metal/device/list_datasource_test.go index 81db9a45b..f76335ccc 100644 --- a/equinix/data_source_metal_devices_acc_test.go +++ b/internal/resources/metal/device/list_datasource_test.go @@ -1,9 +1,10 @@ -package equinix +package device_test import ( "fmt" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -12,9 +13,9 @@ func TestAccDataSourceMetalDevices(t *testing.T) { projectName := fmt.Sprintf("ds-device-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -78,5 +79,5 @@ data "equinix_metal_devices" "test_search" { project_id = equinix_metal_project.test.id search = "unlikelystring" depends_on = [equinix_metal_device.dev_search] -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime()) } diff --git a/equinix/resource_metal_device.go b/internal/resources/metal/device/resource.go similarity index 93% rename from equinix/resource_metal_device.go rename to internal/resources/metal/device/resource.go index a83b30fdd..9405998aa 100644 --- a/equinix/resource_metal_device.go +++ b/internal/resources/metal/device/resource.go @@ -1,4 +1,4 @@ -package equinix +package device import ( "context" @@ -40,7 +40,7 @@ var ( deviceCommonIncludes = []string{"project", "metro", "facility", "hardware_reservation"} ) -func resourceMetalDevice() *schema.Resource { +func Resource() *schema.Resource { return &schema.Resource{ Description: `Provides an Equinix Metal device resource. This can be used to create, modify, and delete devices. @@ -51,7 +51,7 @@ func resourceMetalDevice() *schema.Resource { Delete: schema.DefaultTimeout(30 * time.Minute), }, CreateContext: resourceMetalDeviceCreate, - ReadWithoutTimeout: resourceMetalDeviceRead, + ReadWithoutTimeout: Read, UpdateContext: resourceMetalDeviceUpdate, DeleteContext: resourceMetalDeviceDelete, Importer: &schema.ResourceImporter{ @@ -563,14 +563,14 @@ func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(newDevice.GetId()) createTimeout := d.Timeout(schema.TimeoutCreate) - 30*time.Second - time.Since(start) - if err = waitForActiveDevice(ctx, d, meta, createTimeout); err != nil { + if err = WaitForActiveDevice(ctx, d, meta, createTimeout); err != nil { return diag.FromErr(err) } - return resourceMetalDeviceRead(ctx, d, meta) + return Read(ctx, d, meta) } -func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*config.Config).NewMetalClientForSDK(d) device, resp, err := client.DevicesApi.FindDeviceById(ctx, d.Id()).Include(deviceCommonIncludes).Execute() @@ -589,24 +589,25 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i return diag.FromErr(err) } - d.Set("hostname", device.GetHostname()) - d.Set("plan", device.Plan.GetSlug()) - d.Set("deployed_facility", device.Facility.GetCode()) - d.Set("facilities", []string{device.Facility.GetCode()}) + var errs []error + errs = append(errs, d.Set("hostname", device.GetHostname())) + errs = append(errs, d.Set("plan", device.Plan.GetSlug())) + errs = append(errs, d.Set("deployed_facility", device.Facility.GetCode())) + errs = append(errs, d.Set("facilities", []string{device.Facility.GetCode()})) if device.Metro != nil { - d.Set("metro", device.Metro.GetCode()) + errs = append(errs, d.Set("metro", device.Metro.GetCode())) } - d.Set("operating_system", device.OperatingSystem.GetSlug()) - d.Set("state", device.GetState()) - d.Set("billing_cycle", device.GetBillingCycle()) - d.Set("locked", device.GetLocked()) - d.Set("created", device.GetCreatedAt().Format(time.RFC3339)) - d.Set("updated", device.GetUpdatedAt().Format(time.RFC3339)) - d.Set("ipxe_script_url", device.GetIpxeScriptUrl()) - d.Set("always_pxe", device.GetAlwaysPxe()) - d.Set("root_password", device.GetRootPassword()) - d.Set("project_id", device.Project.GetId()) - d.Set("sos_hostname", device.GetSos()) + errs = append(errs, d.Set("operating_system", device.OperatingSystem.GetSlug())) + errs = append(errs, d.Set("state", device.GetState())) + errs = append(errs, d.Set("billing_cycle", device.GetBillingCycle())) + errs = append(errs, d.Set("locked", device.GetLocked())) + errs = append(errs, d.Set("created", device.GetCreatedAt().Format(time.RFC3339))) + errs = append(errs, d.Set("updated", device.GetUpdatedAt().Format(time.RFC3339))) + errs = append(errs, d.Set("ipxe_script_url", device.GetIpxeScriptUrl())) + errs = append(errs, d.Set("always_pxe", device.GetAlwaysPxe())) + errs = append(errs, d.Set("root_password", device.GetRootPassword())) + errs = append(errs, d.Set("project_id", device.Project.GetId())) + errs = append(errs, d.Set("sos_hostname", device.GetSos())) if device.Storage != nil { rawStorageBytes, err := json.Marshal(device.Storage) if err != nil { @@ -617,37 +618,37 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i if err != nil { return diag.Errorf("[ERR] Error normalizing storage JSON string for device (%s): %s", d.Id(), err) } - d.Set("storage", storageString) + errs = append(errs, d.Set("storage", storageString)) } if device.HardwareReservation != nil { - d.Set("deployed_hardware_reservation_id", device.HardwareReservation.GetId()) + errs = append(errs, d.Set("deployed_hardware_reservation_id", device.HardwareReservation.GetId())) } networkType, err := getNetworkType(device) if err != nil { return diag.Errorf("[ERR] Error computing network type for device (%s): %s", d.Id(), err) } - d.Set("network_type", networkType) + errs = append(errs, d.Set("network_type", networkType)) wfrd := "wait_for_reservation_deprovision" if _, ok := d.GetOk(wfrd); !ok { - d.Set(wfrd, nil) + errs = append(errs, d.Set(wfrd, nil)) } fdv := "force_detach_volumes" if _, ok := d.GetOk(fdv); !ok { - d.Set(fdv, nil) + errs = append(errs, d.Set(fdv, nil)) } tt := "termination_time" if _, ok := d.GetOk(tt); !ok { - d.Set(tt, nil) + errs = append(errs, d.Set(tt, nil)) } - d.Set("tags", device.Tags) + errs = append(errs, d.Set("tags", device.Tags)) keyIDs := []string{} for _, k := range device.SshKeys { keyIDs = append(keyIDs, path.Base(k.Href)) } - d.Set("ssh_key_ids", keyIDs) + errs = append(errs, d.Set("ssh_key_ids", keyIDs)) networkInfo := getNetworkInfo(device.IpAddresses) sort.SliceStable(networkInfo.Networks, func(i, j int) bool { @@ -658,13 +659,13 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i return getNetworkRank(int(famI), pubI) < getNetworkRank(int(famJ), pubJ) }) - d.Set("network", networkInfo.Networks) - d.Set("access_public_ipv4", networkInfo.PublicIPv4) - d.Set("access_private_ipv4", networkInfo.PrivateIPv4) - d.Set("access_public_ipv6", networkInfo.PublicIPv6) + errs = append(errs, d.Set("network", networkInfo.Networks)) + errs = append(errs, d.Set("access_public_ipv4", networkInfo.PublicIPv4)) + errs = append(errs, d.Set("access_private_ipv4", networkInfo.PrivateIPv4)) + errs = append(errs, d.Set("access_public_ipv6", networkInfo.PublicIPv6)) ports := getPorts(device.NetworkPorts) - d.Set("ports", ports) + errs = append(errs, d.Set("ports", ports)) if networkInfo.Host != "" { d.SetConnInfo(map[string]string{ @@ -673,6 +674,11 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i }) } + err = errors.Join(errs...) + if err != nil { + return diag.FromErr(err) + } + return nil } @@ -739,7 +745,7 @@ func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta return diag.FromErr(err) } - return resourceMetalDeviceRead(ctx, d, meta) + return Read(ctx, d, meta) } func doReinstall(ctx context.Context, client *metalv1.APIClient, d *schema.ResourceData, meta interface{}, start time.Time) error { @@ -772,7 +778,7 @@ func doReinstall(ctx context.Context, client *metalv1.APIClient, d *schema.Resou } updateTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start) - if err := waitForActiveDevice(ctx, d, meta, updateTimeout); err != nil { + if err := WaitForActiveDevice(ctx, d, meta, updateTimeout); err != nil { return err } } @@ -803,7 +809,7 @@ func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta // avoid "context: deadline exceeded" 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) + err := WaitUntilReservationProvisionable(ctx, client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second) if err != nil { return diag.FromErr(err) } @@ -812,7 +818,7 @@ func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta return nil } -func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta interface{}, timeout time.Duration) error { +func WaitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta interface{}, timeout time.Duration) error { targets := []string{"active", "failed"} pending := []string{"queued", "provisioning", "reinstalling"} @@ -849,7 +855,7 @@ func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta inter if state != "active" { d.SetId("") - return fmt.Errorf("Device in non-active state \"%s\"", state) + return fmt.Errorf("device in non-active state \"%s\"", state) } return nil diff --git a/equinix/resource_metal_device_acc_test.go b/internal/resources/metal/device/resource_test.go similarity index 77% rename from equinix/resource_metal_device_acc_test.go rename to internal/resources/metal/device/resource_test.go index d075822c2..6f0c37b7f 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/internal/resources/metal/device/resource_test.go @@ -1,33 +1,28 @@ -package equinix +package device_test import ( "context" "fmt" + "log" "net" "net/http" "net/http/httptest" "regexp" - "strings" "testing" "time" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/device" + "github.com/google/uuid" "github.com/equinix/equinix-sdk-go/services/metalv1" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) -// list of plans and metros and os used as filter criteria to find available hardware to run tests -var ( - preferable_plans = []string{"x1.small.x86", "t1.small.x86", "c2.medium.x86", "c3.small.x86", "c3.medium.x86", "m3.small.x86"} - preferable_metros = []string{"ch", "ny", "sv", "ty", "am"} - preferable_os = []string{"ubuntu_20_04"} -) - // Regexp vars for use with resource.ExpectError var ( matchErrMustBeProvided = regexp.MustCompile(".* must be provided when .*") @@ -36,109 +31,107 @@ var ( matchErrDeviceLocked = regexp.MustCompile(".*Cannot delete a locked item.*") ) -// This function should be used to find available plans in all test where a metal_device resource is needed. -// -// TODO consider adding a datasource for equinix_metal_operating_system and making the local.os conditional -// -// https://github.com/equinix/terraform-provider-equinix/pull/220#discussion_r915418418equinix_metal_operating_system -// https://github.com/equinix/terraform-provider-equinix/discussions/221 -func confAccMetalDevice_base(plans, metros, os []string) string { - return fmt.Sprintf(` -data "equinix_metal_plans" "test" { - sort { - attribute = "id" - direction = "asc" - } - - filter { - attribute = "name" - values = [%s] - } - filter { - attribute = "available_in_metros" - values = [%s] - } - filter { - attribute = "deployment_types" - values = ["on_demand", "spot_market"] - } -} - -// Select a metal plan randomly and lock it in -// so that we don't pick a different one for -// every subsequent terraform plan -resource "random_integer" "plan_idx" { - min = 0 - max = length(data.equinix_metal_plans.test.plans) - 1 -} - -resource "terraform_data" "plan" { - input = data.equinix_metal_plans.test.plans[random_integer.plan_idx.result] - - lifecycle { - ignore_changes = ["input"] - } -} - -resource "terraform_data" "facilities" { - input = sort(tolist(setsubtract(terraform_data.plan.output.available_in, ["nrt1", "dfw2", "ewr1", "ams1", "sjc1", "ld7", "sy4", "ny6"]))) - - lifecycle { - ignore_changes = ["input"] - } -} - -// Select a metal facility randomly and lock it in -// so that we don't pick a different one for -// every subsequent terraform plan -resource "random_integer" "facility_idx" { - min = 0 - max = length(local.facilities) - 1 -} - -resource "terraform_data" "facility" { - input = local.facilities[random_integer.facility_idx.result] - - lifecycle { - ignore_changes = ["input"] - } -} - -// Select a metal metro randomly and lock it in -// so that we don't pick a different one for -// every subsequent terraform plan -resource "random_integer" "metro_idx" { - min = 0 - max = length(local.metros) - 1 -} - -resource "terraform_data" "metro" { - input = local.metros[random_integer.metro_idx.result] +func TestMetalDevice_readErrorHandling(t *testing.T) { + type args struct { + newResource bool + handler func(w http.ResponseWriter, r *http.Request) + } - lifecycle { - ignore_changes = ["input"] - } -} + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "forbiddenAfterProvision", + args: args{ + newResource: false, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") + w.WriteHeader(http.StatusForbidden) + }, + }, + wantErr: false, + }, + { + name: "notFoundAfterProvision", + args: args{ + newResource: false, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") + w.WriteHeader(http.StatusNotFound) + }, + }, + wantErr: false, + }, + { + name: "forbiddenWaitForActiveDeviceProvision", + args: args{ + newResource: true, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") + w.WriteHeader(http.StatusForbidden) + }, + }, + wantErr: true, + }, + { + name: "notFoundProvision", + args: args{ + newResource: true, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") + w.WriteHeader(http.StatusNotFound) + }, + }, + wantErr: true, + }, + { + name: "errorProvision", + args: args{ + newResource: true, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") + w.WriteHeader(http.StatusBadRequest) + }, + }, + wantErr: true, + }, + } -locals { - // Select a random plan - plan = terraform_data.plan.output.slug + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + d := new(schema.ResourceData) + if tt.args.newResource { + d.MarkNewResource() + } else { + d.SetId(uuid.New().String()) + } - // Select a random facility from the facilities in which the selected plan is available, excluding decommed facilities - facilities = terraform_data.facilities.output - facility = terraform_data.facility.output + mockAPI := httptest.NewServer(http.HandlerFunc(tt.args.handler)) + meta := &config.Config{ + BaseURL: mockAPI.URL, + Token: "fakeTokenForMock", + } - // Select a random metro from the metros in which the selected plan is available - metros = sort(tolist(terraform_data.plan.output.available_in_metros)) - metro = terraform_data.metro.output + err := meta.Load(ctx) + if err != nil { + log.Printf("failed to load provider config during test: %v", err) + } - os = [%s][0] -} -`, fmt.Sprintf("\"%s\"", strings.Join(plans[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(metros[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(os[:], `","`))) -} + if err := device.Read(ctx, d, meta); (err != nil) != tt.wantErr { + t.Errorf("device.Read() error = %v, wantErr %v", err, tt.wantErr) + } -func testDeviceTerminationTime() string { - return time.Now().UTC().Add(60 * time.Minute).Format(time.RFC3339) + mockAPI.Close() + }) + } } func TestAccMetalDevice_facilityList(t *testing.T) { @@ -147,9 +140,9 @@ func TestAccMetalDevice_facilityList(t *testing.T) { r := "equinix_metal_device.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -174,9 +167,9 @@ func TestAccMetalDevice_sshConfig(t *testing.T) { t.Fatalf("Cannot generate test SSH key pair: %s", err) } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - ExternalProviders: testExternalProviders, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -206,9 +199,9 @@ func TestAccMetalDevice_basic(t *testing.T) { r := "equinix_metal_device.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -252,9 +245,9 @@ func TestAccMetalDevice_update(t *testing.T) { r := "equinix_metal_device.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -308,9 +301,9 @@ func TestAccMetalDevice_IPXEScriptUrl(t *testing.T) { r := "equinix_metal_device.test_ipxe_script_url" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -346,13 +339,13 @@ func TestAccMetalDevice_IPXEConflictingFields(t *testing.T) { r := "equinix_metal_device.test_ipxe_conflict" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccMetalDeviceConfig_ipxe_conflict, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), rs), + Config: fmt.Sprintf(testAccMetalDeviceConfig_ipxe_conflict, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), rs), Check: resource.ComposeTestCheckFunc( testAccMetalDeviceExists(r, &device), ), @@ -368,13 +361,13 @@ func TestAccMetalDevice_IPXEConfigMissing(t *testing.T) { r := "equinix_metal_device.test_ipxe_config_missing" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccMetalDeviceConfig_ipxe_missing, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), rs), + Config: fmt.Sprintf(testAccMetalDeviceConfig_ipxe_missing, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), rs), Check: resource.ComposeTestCheckFunc( testAccMetalDeviceExists(r, &device), ), @@ -394,9 +387,9 @@ func TestAccMetalDevice_allowUserdataChanges(t *testing.T) { userdata2 := fmt.Sprintf("#!/usr/bin/env sh\necho 'Allow userdata changes %d'\n", rInt+1) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -428,9 +421,9 @@ func TestAccMetalDevice_allowCustomdataChanges(t *testing.T) { customdata2 := fmt.Sprintf(`{"message": "Allow customdata changes %d"}`, rInt+1) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -457,8 +450,8 @@ func TestAccMetalDevice_allowChangesErrorOnUnsupportedAttribute(t *testing.T) { rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: testAccMetalDeviceConfig_allowAttributeChanges(rInt, rs, "", "", "project_id"), @@ -469,7 +462,7 @@ func TestAccMetalDevice_allowChangesErrorOnUnsupportedAttribute(t *testing.T) { } func testAccMetalDeviceCheckDestroyed(s *terraform.State) error { - client := testAccProvider.Meta().(*config.Config).Metal + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal for _, rs := range s.RootModule().Resources { if rs.Type != "equinix_metal_device" { @@ -505,7 +498,7 @@ func testAccMetalDeviceExists(n string, device *metalv1.Device) resource.TestChe return fmt.Errorf("No Record ID is set") } - client := testAccProvider.Meta().(*config.Config).NewMetalClientForTesting() + client := acceptance.TestAccProvider.Meta().(*config.Config).NewMetalClientForTesting() foundDevice, _, err := client.DevicesApi.FindDeviceById(context.TODO(), rs.Primary.ID).Execute() if err != nil { @@ -596,9 +589,9 @@ func TestAccMetalDevice_importBasic(t *testing.T) { rs := acctest.RandString(10) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -632,7 +625,7 @@ resource "equinix_metal_device" "test" { tags = ["%d"] termination_time = "%s" } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, testDeviceTerminationTime()) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, rInt, rInt, acceptance.TestDeviceTerminationTime()) } func testAccMetalDeviceConfig_reinstall(rInt int, projSuffix string) string { @@ -659,7 +652,7 @@ resource "equinix_metal_device" "test" { deprovision_fast = true } } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, testDeviceTerminationTime()) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, rInt, rInt, acceptance.TestDeviceTerminationTime()) } func testAccMetalDeviceConfig_allowAttributeChanges(rInt int, projSuffix string, userdata string, customdata string, attributeName string) string { @@ -688,7 +681,7 @@ resource "equinix_metal_device" "test" { ] } } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, userdata, customdata, testDeviceTerminationTime(), attributeName) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, rInt, rInt, userdata, customdata, acceptance.TestDeviceTerminationTime(), attributeName) } func testAccMetalDeviceConfig_varname(rInt int, projSuffix string) string { @@ -710,7 +703,7 @@ resource "equinix_metal_device" "test" { tags = ["%d"] termination_time = "%s" } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, rInt, rInt, rInt, testDeviceTerminationTime()) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, rInt, rInt, rInt, acceptance.TestDeviceTerminationTime()) } func testAccMetalDeviceConfig_minimal(projSuffix string) string { @@ -726,7 +719,7 @@ resource "equinix_metal_device" "test" { metro = local.metro operating_system = local.os project_id = "${equinix_metal_project.test.id}" -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix) } func testAccMetalDeviceConfig_basic(projSuffix string) string { @@ -746,7 +739,7 @@ resource "equinix_metal_device" "test" { billing_cycle = "hourly" project_id = "${equinix_metal_project.test.id}" termination_time = "%s" -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime()) } func testAccMetalDeviceConfig_ssh_key(projSuffix, userSSHKey, projSSHKey string) string { @@ -778,7 +771,7 @@ resource "equinix_metal_device" "test" { user_ssh_key_ids = [equinix_metal_ssh_key.test.owner_id] project_ssh_key_ids = [equinix_metal_project_ssh_key.test.id] } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, projSuffix, userSSHKey, projSSHKey, projSSHKey) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, projSuffix, userSSHKey, projSSHKey, projSSHKey) } func testAccMetalDeviceConfig_facility_list(projSuffix string) string { @@ -798,7 +791,7 @@ resource "equinix_metal_device" "test" { billing_cycle = "hourly" project_id = "${equinix_metal_project.test.id}" termination_time = "%s" -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime()) } func testAccMetalDeviceConfig_ipxe_script_url(projSuffix, url, pxe string) string { @@ -821,7 +814,7 @@ resource "equinix_metal_device" "test_ipxe_script_url" { ipxe_script_url = "%s" always_pxe = "%s" termination_time = "%s" -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, url, pxe, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, url, pxe, acceptance.TestDeviceTerminationTime()) } var testAccMetalDeviceConfig_ipxe_conflict = ` @@ -893,7 +886,7 @@ resource "equinix_metal_device" "test" { delete = "%s" } } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime(), createTimeout, updateTimeout, deleteTimeout) +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime(), createTimeout, updateTimeout, deleteTimeout) } func testAccMetalDeviceConfig_reinstall_timeout(projSuffix, updateTimeout string) string { @@ -927,106 +920,7 @@ resource "equinix_metal_device" "test" { update = "%s" } } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime(), updateTimeout) -} - -func TestAccMetalDevice_readErrorHandling(t *testing.T) { - type args struct { - newResource bool - handler func(w http.ResponseWriter, r *http.Request) - } - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "forbiddenAfterProvision", - args: args{ - newResource: false, - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") - w.WriteHeader(http.StatusForbidden) - }, - }, - wantErr: false, - }, - { - name: "notFoundAfterProvision", - args: args{ - newResource: false, - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") - w.WriteHeader(http.StatusNotFound) - }, - }, - wantErr: false, - }, - { - name: "forbiddenWaitForActiveDeviceProvision", - args: args{ - newResource: true, - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") - w.WriteHeader(http.StatusForbidden) - }, - }, - wantErr: true, - }, - { - name: "notFoundProvision", - args: args{ - newResource: true, - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") - w.WriteHeader(http.StatusNotFound) - }, - }, - wantErr: true, - }, - { - name: "errorProvision", - args: args{ - newResource: true, - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.Header().Add("X-Request-Id", "needed for equinix_errors.FriendlyError") - w.WriteHeader(http.StatusBadRequest) - }, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - d := new(schema.ResourceData) - if tt.args.newResource { - d.MarkNewResource() - } else { - d.SetId(uuid.New().String()) - } - - mockAPI := httptest.NewServer(http.HandlerFunc(tt.args.handler)) - meta := &config.Config{ - BaseURL: mockAPI.URL, - Token: "fakeTokenForMock", - } - meta.Load(ctx) - - if err := resourceMetalDeviceRead(ctx, d, meta); (err != nil) != tt.wantErr { - t.Errorf("resourceMetalDeviceRead() error = %v, wantErr %v", err, tt.wantErr) - } - - mockAPI.Close() - }) - } +`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, acceptance.TestDeviceTerminationTime(), updateTimeout) } func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.ImportStateIdFunc { @@ -1041,7 +935,7 @@ func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.Im return "", fmt.Errorf("No Record ID is set") } - meta := testAccProvider.Meta() + meta := acceptance.TestAccProvider.Meta() rd := new(schema.ResourceData) client := meta.(*config.Config).NewMetalClientForTesting() resp, _, err := client.DevicesApi.FindProjectDevices(context.TODO(), rs.Primary.ID).Search(deviceHostName).Execute() @@ -1057,7 +951,7 @@ func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.Im } rd.SetId(devices[0].GetId()) - return devices[0].GetId(), waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) + return devices[0].GetId(), device.WaitForActiveDevice(context.Background(), rd, acceptance.TestAccProvider.Meta(), defaultTimeout) } } @@ -1068,9 +962,9 @@ func TestAccMetalDeviceCreate_timeout(t *testing.T) { project := "equinix_metal_project.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -1100,9 +994,9 @@ func TestAccMetalDeviceUpdate_timeout(t *testing.T) { r := "equinix_metal_device.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -1125,9 +1019,9 @@ func TestAccMetalDevice_LockingAndUnlocking(t *testing.T) { r := "equinix_metal_device.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDeviceCheckDestroyed, Steps: []resource.TestStep{ { @@ -1173,5 +1067,5 @@ resource "equinix_metal_device" "test" { project_id = "${equinix_metal_project.test.id}" locked = %v termination_time = "%s" -}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, locked, testDeviceTerminationTime()) +}`, acceptance.ConfAccMetalDevice_base(acceptance.Preferable_plans, acceptance.Preferable_metros, acceptance.Preferable_os), projSuffix, locked, acceptance.TestDeviceTerminationTime()) } diff --git a/internal/resources/metal/device/sweeper.go b/internal/resources/metal/device/sweeper.go index d9abde088..cc0b5683a 100644 --- a/internal/resources/metal/device/sweeper.go +++ b/internal/resources/metal/device/sweeper.go @@ -2,6 +2,7 @@ package device import ( "context" + "errors" "fmt" "log" @@ -9,7 +10,6 @@ import ( "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/equinix/terraform-provider-equinix/internal/sweep" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -21,7 +21,7 @@ func AddTestSweeper() { } func testSweepDevices(region string) error { - var errs error + var errs []error log.Printf("[DEBUG] Sweeping devices") ctx := context.Background() config, err := sweep.GetConfigForMetal() @@ -49,12 +49,12 @@ func testSweepDevices(region string) error { for _, d := range ds.Devices { err := sweepDevice(ctx, metal, d) if err != nil { - errs = multierror.Append(errs, fmt.Errorf("Error deleting device %s", err)) + errs = append(errs, fmt.Errorf("error deleting device %s", err)) } } } - return errs + return errors.Join(errs...) } func sweepDevice(ctx context.Context, metal *metalv1.APIClient, d metalv1.Device) error {