From 5865babf4ecd3e5fb3213a5f40b6d81d62d25399 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Thu, 21 Dec 2023 14:33:59 -0600 Subject: [PATCH] refactor: finish moving device resource off of packngo --- equinix/helpers_device.go | 72 +--- equinix/helpers_device_test.go | 192 +++++------ equinix/resource_metal_device.go | 381 ++++++++++++++-------- equinix/resource_metal_device_acc_test.go | 60 ++-- internal/errors/errors.go | 19 ++ 5 files changed, 407 insertions(+), 317 deletions(-) diff --git a/equinix/helpers_device.go b/equinix/helpers_device.go index 0a2932ce2..fc2b9b28f 100644 --- a/equinix/helpers_device.go +++ b/equinix/helpers_device.go @@ -4,11 +4,9 @@ import ( "context" "encoding/json" "errors" - "fmt" "log" "path" "sort" - "strings" "sync" "time" @@ -19,7 +17,6 @@ import ( "github.com/equinix/equinix-sdk-go/services/metalv1" "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" ) @@ -35,35 +32,6 @@ var ( wgMutex = sync.Mutex{} ) -func ifToIPCreateRequest(m interface{}) packngo.IPAddressCreateRequest { - iacr := packngo.IPAddressCreateRequest{} - ia := m.(map[string]interface{}) - at := ia["type"].(string) - switch at { - case "public_ipv4": - iacr.AddressFamily = 4 - iacr.Public = true - case "private_ipv4": - iacr.AddressFamily = 4 - iacr.Public = false - case "public_ipv6": - iacr.AddressFamily = 6 - iacr.Public = true - } - iacr.CIDR = ia["cidr"].(int) - iacr.Reservations = converters.IfArrToStringArr(ia["reservation_ids"].([]interface{})) - return iacr -} - -func getNewIPAddressSlice(arr []interface{}) []packngo.IPAddressCreateRequest { - addressTypesSlice := make([]packngo.IPAddressCreateRequest, len(arr)) - - for i, m := range arr { - addressTypesSlice[i] = ifToIPCreateRequest(m) - } - return addressTypesSlice -} - type NetworkInfo struct { Networks []map[string]interface{} IPv4SubnetSize int @@ -142,18 +110,18 @@ func getPorts(ps []metalv1.Port) []map[string]interface{} { return ret } -func hwReservationStateRefreshFunc(client *packngo.Client, reservationId, instanceId string) retry.StateRefreshFunc { +func hwReservationStateRefreshFunc(client *metalv1.APIClient, reservationId, instanceId string) retry.StateRefreshFunc { return func() (interface{}, string, error) { - r, _, err := client.HardwareReservations.Get(reservationId, &packngo.GetOptions{Includes: []string{"device"}}) + r, _, err := client.HardwareReservationsApi.FindHardwareReservationById(context.TODO(), reservationId).Include([]string{"device"}).Execute() state := deprovisioning switch { case err != nil: err = equinix_errors.FriendlyError(err) state = errstate - case r != nil && r.Provisionable: + case r != nil && r.GetProvisionable(): state = provisionable - case r != nil && r.Device != nil && (r.Device.ID != "" && r.Device.ID != instanceId): - log.Printf("[WARN] Equinix Metal device instance %s (reservation %s) was reprovisioned to a another instance (%s)", instanceId, reservationId, r.Device.ID) + case r != nil && r.Device != nil && (r.Device.GetId() != "" && r.Device.GetId() != instanceId): + log.Printf("[WARN] Equinix Metal device instance %s (reservation %s) was reprovisioned to a another instance (%s)", instanceId, reservationId, r.Device.GetId()) state = reprovisioned default: log.Printf("[DEBUG] Equinix Metal device instance %s (reservation %s) is still deprovisioning", instanceId, reservationId) @@ -163,7 +131,7 @@ func hwReservationStateRefreshFunc(client *packngo.Client, reservationId, instan } } -func waitUntilReservationProvisionable(ctx context.Context, client *packngo.Client, 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}, @@ -214,34 +182,6 @@ func waitForDeviceAttribute(ctx context.Context, d *schema.ResourceData, stateCo return "", err } -func ipAddressSchema() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(ipAddressTypes, false), - Description: fmt.Sprintf("one of %s", strings.Join(ipAddressTypes, ",")), - }, - "cidr": { - Type: schema.TypeInt, - Optional: true, - Description: "CIDR suffix for IP block assigned to this device", - }, - "reservation_ids": { - Type: schema.TypeList, - Optional: true, - Description: "IDs of reservations to pick the blocks from", - MinItems: 1, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.IsUUID, - }, - }, - }, - } -} - func getDeviceMap(device metalv1.Device) map[string]interface{} { networkInfo := getNetworkInfo(device.IpAddresses) sort.SliceStable(networkInfo.Networks, func(i, j int) bool { diff --git a/equinix/helpers_device_test.go b/equinix/helpers_device_test.go index 1f9dc7517..90e8e9a27 100644 --- a/equinix/helpers_device_test.go +++ b/equinix/helpers_device_test.go @@ -2,40 +2,21 @@ package equinix import ( "context" - "fmt" + "net/http" + "net/http/httptest" + "strings" "testing" "time" - "golang.org/x/exp/slices" - - "github.com/packethost/packngo" + "github.com/equinix/equinix-sdk-go/services/metalv1" + "github.com/equinix/terraform-provider-equinix/internal/config" ) -type mockHWService struct { - GetFn func(string, *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) - ListFn func(string, *packngo.ListOptions) ([]packngo.HardwareReservation, *packngo.Response, error) - MoveFn func(string, string) (*packngo.HardwareReservation, *packngo.Response, error) -} - -func (m *mockHWService) Get(id string, opt *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) { - return m.GetFn(id, opt) -} - -func (m *mockHWService) List(project string, opt *packngo.ListOptions) ([]packngo.HardwareReservation, *packngo.Response, error) { - return m.ListFn(project, opt) -} - -func (m *mockHWService) Move(id string, dest string) (*packngo.HardwareReservation, *packngo.Response, error) { - return m.MoveFn(id, dest) -} - -var _ packngo.HardwareReservationService = (*mockHWService)(nil) - func Test_waitUntilReservationProvisionable(t *testing.T) { type args struct { reservationId string instanceId string - meta *packngo.Client + handler func(w http.ResponseWriter, r *http.Request) } tests := []struct { @@ -48,12 +29,10 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { args: args{ reservationId: "reservationId", instanceId: "instanceId", - meta: &packngo.Client{ - HardwareReservations: &mockHWService{ - GetFn: func(_ string, _ *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) { - return nil, nil, fmt.Errorf("boom") - }, - }, + 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.StatusInternalServerError) }, }, wantErr: true, @@ -63,34 +42,41 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { args: args{ reservationId: "reservationId", instanceId: "instanceId", - meta: &packngo.Client{ - HardwareReservations: (func() *mockHWService { - invoked := new(int) - - responses := map[int]struct { - id string - provisionable bool - }{ - 0: {"instanceId", false}, // should retry - 1: {"", true}, // should return success + handler: (func() func(w http.ResponseWriter, r *http.Request) { + invoked := new(int) + + responses := map[int]struct { + id string + provisionable bool + }{ + 0: {"instanceId", false}, // should retry + 1: {"", true}, // should return success + } + + return func(w http.ResponseWriter, r *http.Request) { + response := responses[*invoked] + *invoked++ + + var device *metalv1.Device + include := r.URL.Query().Get("include") + if strings.Contains(include, "device") { + device = &metalv1.Device{Id: &response.id} + } + reservation := metalv1.HardwareReservation{ + Device: device, Provisionable: &response.provisionable, } - return &mockHWService{ - GetFn: func(_ string, opts *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) { - response := responses[*invoked] - *invoked++ - - var device *packngo.Device - if opts != nil && slices.Contains(opts.Includes, "device") { - device = &packngo.Device{ID: response.id} - } - return &packngo.HardwareReservation{ - Device: device, Provisionable: response.provisionable, - }, nil, nil - }, + body, err := reservation.MarshalJSON() + if err != nil { + return // TODO ... ? } - })(), - }, + + 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) + } + })(), }, wantErr: false, }, @@ -99,33 +85,40 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { args: args{ reservationId: "reservationId", instanceId: "instanceId", - meta: &packngo.Client{ - HardwareReservations: (func() *mockHWService { - responses := map[int]struct { - id string - provisionable bool - }{ - 0: {"instanceId", false}, // should retry - 1: {"new instance id", false}, // should return success + handler: (func() func(w http.ResponseWriter, r *http.Request) { + responses := map[int]struct { + id string + provisionable bool + }{ + 0: {"instanceId", false}, // should retry + 1: {"new instance id", false}, // should return success + } + invoked := new(int) + + return func(w http.ResponseWriter, r *http.Request) { + response := responses[*invoked] + *invoked++ + + var device *metalv1.Device + include := r.URL.Query().Get("include") + if strings.Contains(include, "device") { + device = &metalv1.Device{Id: &response.id} } - invoked := new(int) - - return &mockHWService{ - GetFn: func(_ string, opts *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) { - response := responses[*invoked] - *invoked++ - - var device *packngo.Device - if opts != nil && slices.Contains(opts.Includes, "device") { - device = &packngo.Device{ID: response.id} - } - return &packngo.HardwareReservation{ - Device: device, Provisionable: response.provisionable, - }, nil, nil - }, + reservation := metalv1.HardwareReservation{ + Device: device, Provisionable: &response.provisionable, } - })(), - }, + + body, err := reservation.MarshalJSON() + if err != nil { + return // TODO ... ? + } + + 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) + } + })(), }, wantErr: false, }, @@ -134,27 +127,42 @@ func Test_waitUntilReservationProvisionable(t *testing.T) { args: args{ reservationId: "reservationId", instanceId: "instanceId", - meta: &packngo.Client{ - HardwareReservations: &mockHWService{ - GetFn: func(_ string, _ *packngo.GetOptions) (*packngo.HardwareReservation, *packngo.Response, error) { - return &packngo.HardwareReservation{ - Device: nil, Provisionable: false, - }, nil, nil - }, - }, + handler: func(w http.ResponseWriter, r *http.Request) { + reservation := metalv1.HardwareReservation{ + Device: nil, Provisionable: metalv1.PtrBool(false), + } + + body, err := reservation.MarshalJSON() + if err != nil { + return // TODO ... ? + } + + 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) }, }, wantErr: true, }, } - // delay and minTimeout * 2 should be less than timeout for each test. - // 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(context.Background(), tt.args.meta, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr { + ctx := context.Background() + + mockAPI := httptest.NewServer(http.HandlerFunc(tt.args.handler)) + meta := &config.Config{ + BaseURL: mockAPI.URL, + Token: "fakeTokenForMock", + } + meta.Load(ctx) + + if err := waitUntilReservationProvisionable(ctx, meta.Metalgo, 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) } + + mockAPI.Close() }) } } diff --git a/equinix/resource_metal_device.go b/equinix/resource_metal_device.go index 277bcac5e..8eaa1df24 100644 --- a/equinix/resource_metal_device.go +++ b/equinix/resource_metal_device.go @@ -10,6 +10,7 @@ import ( "reflect" "regexp" "sort" + "strings" "time" "golang.org/x/exp/slices" @@ -20,13 +21,13 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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" - "github.com/packethost/packngo" ) var ( @@ -131,8 +132,32 @@ func resourceMetalDevice() *schema.Resource { Type: schema.TypeList, Description: "A list of IP address types for the device (structure is documented below)", Optional: true, - Elem: ipAddressSchema(), - MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(ipAddressTypes, false), + Description: fmt.Sprintf("one of %s", strings.Join(ipAddressTypes, ",")), + }, + "cidr": { + Type: schema.TypeInt, + Optional: true, + Description: "CIDR suffix for IP block assigned to this device", + }, + "reservation_ids": { + Type: schema.TypeList, + Optional: true, + Description: "IDs of reservations to pick the blocks from", + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsUUID, + }, + }, + }, + }, + MinItems: 1, }, "plan": { Type: schema.TypeString, @@ -482,24 +507,18 @@ func reinstallDisabledAndNoChangesAllowed(attribute string) customdiff.ResourceC } func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal + meta.(*config.Config).AddModuleToMetalGoUserAgent(d) + client := meta.(*config.Config).Metalgo - var addressTypesSlice []packngo.IPAddressCreateRequest - _, ok := d.GetOk("ip_address") - if ok { - arr := d.Get("ip_address").([]interface{}) - addressTypesSlice = getNewIPAddressSlice(arr) - } + createRequest := metalv1.CreateDeviceRequest{} - createRequest := &packngo.DeviceCreateRequest{ - Hostname: d.Get("hostname").(string), + /* Hostname: d.Get("hostname").(string), Plan: d.Get("plan").(string), IPAddresses: addressTypesSlice, OS: d.Get("operating_system").(string), BillingCycle: d.Get("billing_cycle").(string), ProjectID: d.Get("project_id").(string), - } + }*/ facsRaw, facsOk := d.GetOk("facilities") metroRaw, metroOk := d.GetOk("metro") @@ -509,106 +528,43 @@ func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta } if facsOk { - createRequest.Facility = converters.IfArrToStringArr(facsRaw.([]interface{})) - } - - if metroOk { - createRequest.Metro = metroRaw.(string) - } - - if attr, ok := d.GetOk("user_data"); ok { - createRequest.UserData = attr.(string) - } - - if attr, ok := d.GetOk("custom_data"); ok { - createRequest.CustomData = attr.(string) - } - - if attr, ok := d.GetOk("ipxe_script_url"); ok { - createRequest.IPXEScriptURL = attr.(string) - } - - if attr, ok := d.GetOk("termination_time"); ok { - tt, err := time.ParseInLocation(time.RFC3339, attr.(string), time.UTC) - if err != nil { - return diag.FromErr(err) + facilityRequest := &metalv1.DeviceCreateInFacilityInput{ + Facility: converters.IfArrToStringArr(facsRaw.([]interface{})), } - createRequest.TerminationTime = &packngo.Timestamp{Time: tt} - } - if attr, ok := d.GetOk("hardware_reservation_id"); ok { - createRequest.HardwareReservationID = attr.(string) - } else { - wfrd := "wait_for_reservation_deprovision" - if d.Get(wfrd).(bool) { - return diag.FromErr(equinix_errors.FriendlyError(fmt.Errorf("You can't set %s when not using a hardware reservation", wfrd))) + diagErr := setupDeviceCreateRequest(d, facilityRequest) + if diagErr != nil { + return diagErr } + + createRequest.DeviceCreateInFacilityInput = facilityRequest } - if createRequest.OS == "custom_ipxe" { - if createRequest.IPXEScriptURL == "" && createRequest.UserData == "" { - return diag.FromErr(equinix_errors.FriendlyError(errors.New("\"ipxe_script_url\" or \"user_data\"" + - " must be provided when \"custom_ipxe\" OS is selected."))) + if metroOk { + metroRequest := &metalv1.DeviceCreateInMetroInput{ + Metro: metroRaw.(string), } - // ipxe_script_url + user_data is OK, unless user_data is an ipxe script in - // which case it's an error. - if createRequest.IPXEScriptURL != "" { - if matchIPXEScript.MatchString(createRequest.UserData) { - return diag.Errorf("\"user_data\" should not be an iPXE " + - "script when \"ipxe_script_url\" is also provided.") - } + diagErr := setupDeviceCreateRequest(d, metroRequest) + if diagErr != nil { + return diagErr } - } - - if createRequest.OS != "custom_ipxe" && createRequest.IPXEScriptURL != "" { - return diag.Errorf("\"ipxe_script_url\" argument provided, but" + - " OS is not \"custom_ipxe\". Please verify and fix device arguments.") - } - - if attr, ok := d.GetOk("always_pxe"); ok { - createRequest.AlwaysPXE = attr.(bool) - } - - projectKeys := d.Get("project_ssh_key_ids.#").(int) - if projectKeys > 0 { - createRequest.ProjectSSHKeys = converters.IfArrToStringArr(d.Get("project_ssh_key_ids").([]interface{})) - } - - userKeys := d.Get("user_ssh_key_ids.#").(int) - if userKeys > 0 { - createRequest.UserSSHKeys = converters.IfArrToStringArr(d.Get("user_ssh_key_ids").([]interface{})) - } - tags := d.Get("tags.#").(int) - if tags > 0 { - createRequest.Tags = converters.IfArrToStringArr(d.Get("tags").([]interface{})) - } - - if attr, ok := d.GetOk("storage"); ok { - s, err := structure.NormalizeJsonString(attr.(string)) - if err != nil { - return diag.Errorf("storage param contains invalid JSON: %s", err) - } - var cpr packngo.CPR - err = json.Unmarshal([]byte(s), &cpr) - if err != nil { - return diag.Errorf("error parsing Storage string: %s", err) - } - createRequest.Storage = &cpr + createRequest.DeviceCreateInMetroInput = metroRequest } start := time.Now() - newDevice, _, err := client.Devices.Create(createRequest) + projectID := d.Get("project_id").(string) + newDevice, _, err := client.DevicesApi.CreateDevice(ctx, projectID).CreateDeviceRequest(createRequest).Execute() if err != nil { retErr := equinix_errors.FriendlyError(err) if equinix_errors.IsNotFound(retErr) { - retErr = fmt.Errorf("%s, make sure project \"%s\" exists", retErr, createRequest.ProjectID) + retErr = fmt.Errorf("%s, make sure project \"%s\" exists", retErr, projectID) } return diag.FromErr(retErr) } - d.SetId(newDevice.ID) + d.SetId(newDevice.GetId()) createTimeout := d.Timeout(schema.TimeoutCreate) - 30*time.Second - time.Since(start) if err = waitForActiveDevice(ctx, d, meta, createTimeout); err != nil { @@ -726,21 +682,15 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i } func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal + meta.(*config.Config).AddModuleToMetalGoUserAgent(d) + client := meta.(*config.Config).Metalgo + + ur := metalv1.DeviceUpdateInput{} + // TODO does this need to be a separate device update request? if d.HasChange("locked") { - var action func(string) (*packngo.Response, error) - if d.Get("locked").(bool) { - action = client.Devices.Lock - } else { - action = client.Devices.Unlock - } - if _, err := action(d.Id()); err != nil { - return diag.FromErr(equinix_errors.FriendlyError(err)) - } + ur.Locked = metalv1.PtrBool(d.Get("locked").(bool)) } - ur := packngo.DeviceUpdateRequest{} if d.HasChange("description") { dDesc := d.Get("description").(string) @@ -748,11 +698,15 @@ func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta } if d.HasChange("user_data") { dUserData := d.Get("user_data").(string) - ur.UserData = &dUserData + ur.Userdata = &dUserData } if d.HasChange("custom_data") { - dCustomData := d.Get("custom_data").(string) - ur.CustomData = &dCustomData + var customdata map[string]interface{} + err := json.Unmarshal([]byte(d.Get("custom_data").(string)), &customdata) + if err != nil { + return diag.Errorf("error reading custom_data from state: %v", err) + } + ur.Customdata = customdata } if d.HasChange("hostname") { dHostname := d.Get("hostname").(string) @@ -767,23 +721,23 @@ func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta for _, v := range ts.([]interface{}) { sts = append(sts, v.(string)) } - ur.Tags = &sts + ur.Tags = sts default: return diag.Errorf("garbage in tags: %s", ts) } } if d.HasChange("ipxe_script_url") { dUrl := d.Get("ipxe_script_url").(string) - ur.IPXEScriptURL = &dUrl + ur.IpxeScriptUrl = &dUrl } if d.HasChange("always_pxe") { dPXE := d.Get("always_pxe").(bool) - ur.AlwaysPXE = &dPXE + ur.AlwaysPxe = &dPXE } start := time.Now() - if !reflect.DeepEqual(ur, packngo.DeviceUpdateRequest{}) { - if _, _, err := client.Devices.Update(d.Id(), &ur); err != nil { + if !reflect.DeepEqual(ur, metalv1.DeviceUpdateInput{}) { + if _, _, err := client.DevicesApi.UpdateDevice(ctx, d.Id()).DeviceUpdateInput(ur).Execute(); err != nil { return diag.FromErr(equinix_errors.FriendlyError(err)) } } @@ -795,7 +749,7 @@ func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta return resourceMetalDeviceRead(ctx, d, meta) } -func doReinstall(ctx context.Context, client *packngo.Client, d *schema.ResourceData, meta interface{}, start time.Time) error { +func doReinstall(ctx context.Context, client *metalv1.APIClient, 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") @@ -813,13 +767,14 @@ func doReinstall(ctx context.Context, client *packngo.Client, d *schema.Resource return nil } - reinstallOptions := packngo.DeviceReinstallFields{ - OperatingSystem: d.Get("operating_system").(string), - PreserveData: reinstall_config["preserve_data"].(bool), - DeprovisionFast: reinstall_config["deprovision_fast"].(bool), + reinstallOptions := metalv1.DeviceActionInput{ + Type: metalv1.DEVICEACTIONINPUTTYPE_REINSTALL, + OperatingSystem: metalv1.PtrString(d.Get("operating_system").(string)), + PreserveData: metalv1.PtrBool(reinstall_config["preserve_data"].(bool)), + DeprovisionFast: metalv1.PtrBool(reinstall_config["deprovision_fast"].(bool)), } - if _, err := client.Devices.Reinstall(d.Id(), &reinstallOptions); err != nil { + if _, err := client.DevicesApi.PerformAction(ctx, d.Id()).DeviceActionInput(reinstallOptions).Execute(); err != nil { return equinix_errors.FriendlyError(err) } @@ -833,8 +788,8 @@ func doReinstall(ctx context.Context, client *packngo.Client, d *schema.Resource } func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal + meta.(*config.Config).AddModuleToMetalGoUserAgent(d) + client := meta.(*config.Config).Metalgo fdvIf, fdvOk := d.GetOk("force_detach_volumes") fdv := false @@ -844,8 +799,8 @@ func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta start := time.Now() - resp, err := client.Devices.Delete(d.Id(), fdv) - if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { + resp, err := client.DevicesApi.DeleteDevice(ctx, d.Id()).ForceDelete(fdv).Execute() + if equinix_errors.IgnoreHttpResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { return diag.FromErr(equinix_errors.FriendlyError(err)) } @@ -873,12 +828,12 @@ func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta inter Pending: pending, Target: targets, Refresh: func() (interface{}, string, error) { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal + meta.(*config.Config).AddModuleToMetalGoUserAgent(d) + client := meta.(*config.Config).Metalgo - device, _, err := client.Devices.Get(d.Id(), &packngo.GetOptions{Includes: []string{"project"}}) + device, _, err := client.DevicesApi.FindDeviceById(ctx, d.Id()).Include([]string{"project"}).Execute() if err == nil { - retAttrVal := device.State + retAttrVal := fmt.Sprint(device.GetState()) return retAttrVal, retAttrVal, nil } return "error", "error", err @@ -908,3 +863,171 @@ func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta inter return nil } + +type deviceCreateRequest interface { + SetUserdata(string) + GetUserdata() string + SetCustomdata(map[string]interface{}) + SetAlwaysPxe(bool) + SetIpxeScriptUrl(string) + GetIpxeScriptUrl() string + SetTerminationTime(time.Time) + SetHardwareReservationId(string) + SetBillingCycle(metalv1.DeviceCreateInputBillingCycle) + GetOperatingSystem() string + SetProjectSshKeys([]string) + SetUserSshKeys([]string) + SetTags([]string) + SetStorage(metalv1.Storage) + SetHostname(string) + SetPlan(string) + SetOperatingSystem(string) + SetIpAddresses([]metalv1.IPAddress) +} + +func setupDeviceCreateRequest(d *schema.ResourceData, createRequest deviceCreateRequest) diag.Diagnostics { + var addressTypesSlice []metalv1.IPAddress + _, ok := d.GetOk("ip_address") + if ok { + arr := d.Get("ip_address").([]interface{}) + + addressTypesSlice = getNewIPAddressSlice(arr) + } + + if hostname, ok := d.GetOk("hostname"); ok { + createRequest.SetHostname(hostname.(string)) + } + + createRequest.SetPlan(d.Get("plan").(string)) + createRequest.SetIpAddresses(addressTypesSlice) + createRequest.SetOperatingSystem(d.Get("operating_system").(string)) + + if rawBillingCycle, ok := d.GetOk("billing_cycle"); ok { + billingCycle, err := metalv1.NewDeviceCreateInputBillingCycleFromValue(rawBillingCycle.(string)) + if err != nil { + return diag.Errorf("unknown billing cycle: %v", err) + } + + createRequest.SetBillingCycle(*billingCycle) + } + + if attr, ok := d.GetOk("user_data"); ok { + createRequest.SetUserdata(attr.(string)) + } + + if attr, ok := d.GetOk("custom_data"); ok { + var customdata map[string]interface{} + err := json.Unmarshal([]byte(attr.(string)), &customdata) + if err != nil { + return diag.FromErr(err) + } + createRequest.SetCustomdata(customdata) + } + + if attr, ok := d.GetOk("ipxe_script_url"); ok { + createRequest.SetIpxeScriptUrl(attr.(string)) + } + + if attr, ok := d.GetOk("termination_time"); ok { + tt, err := time.ParseInLocation(time.RFC3339, attr.(string), time.UTC) + if err != nil { + return diag.FromErr(err) + } + createRequest.SetTerminationTime(tt) + } + + if attr, ok := d.GetOk("hardware_reservation_id"); ok { + createRequest.SetHardwareReservationId(attr.(string)) + } else { + wfrd := "wait_for_reservation_deprovision" + if d.Get(wfrd).(bool) { + return diag.Errorf("You can't set %s when not using a hardware reservation", wfrd) + } + } + + if createRequest.GetOperatingSystem() == "custom_ipxe" { + if createRequest.GetIpxeScriptUrl() == "" && createRequest.GetUserdata() == "" { + return diag.Errorf("\"ipxe_script_url\" or \"user_data\"" + + " must be provided when \"custom_ipxe\" OS is selected.") + } + + // ipxe_script_url + user_data is OK, unless user_data is an ipxe script in + // which case it's an error. + if createRequest.GetIpxeScriptUrl() != "" { + if matchIPXEScript.MatchString(createRequest.GetUserdata()) { + return diag.Errorf("\"user_data\" should not be an iPXE " + + "script when \"ipxe_script_url\" is also provided.") + } + } + } + + if createRequest.GetOperatingSystem() != "custom_ipxe" && createRequest.GetIpxeScriptUrl() != "" { + return diag.Errorf("\"ipxe_script_url\" argument provided, but" + + " OS is not \"custom_ipxe\". Please verify and fix device arguments.") + } + + if attr, ok := d.GetOk("always_pxe"); ok { + createRequest.SetAlwaysPxe(attr.(bool)) + } + + projectKeys := d.Get("project_ssh_key_ids.#").(int) + if projectKeys > 0 { + createRequest.SetProjectSshKeys(converters.IfArrToStringArr(d.Get("project_ssh_key_ids").([]interface{}))) + } + + userKeys := d.Get("user_ssh_key_ids.#").(int) + if userKeys > 0 { + createRequest.SetUserSshKeys(converters.IfArrToStringArr(d.Get("user_ssh_key_ids").([]interface{}))) + } + + tags := d.Get("tags.#").(int) + if tags > 0 { + createRequest.SetTags(converters.IfArrToStringArr(d.Get("tags").([]interface{}))) + } + + if attr, ok := d.GetOk("storage"); ok { + s, err := structure.NormalizeJsonString(attr.(string)) + if err != nil { + return diag.Errorf("storage param contains invalid JSON: %s", err) + } + var storage metalv1.Storage + err = json.Unmarshal([]byte(s), &storage) + if err != nil { + return diag.Errorf("error parsing Storage string: %s", err) + } + createRequest.SetStorage(storage) + } + + return nil +} + +func getNewIPAddressSlice(arr []interface{}) []metalv1.IPAddress { + addressTypesSlice := make([]metalv1.IPAddress, len(arr)) + + for i, m := range arr { + addressTypesSlice[i] = ifToIPCreateRequest(m) + } + return addressTypesSlice +} + +func ifToIPCreateRequest(m interface{}) metalv1.IPAddress { + iacr := metalv1.IPAddress{} + ia := m.(map[string]interface{}) + at := ia["type"].(string) + switch at { + case "public_ipv4": + iacr.SetAddressFamily(4) + iacr.SetPublic(true) + case "private_ipv4": + iacr.SetAddressFamily(4) + iacr.SetPublic(false) + case "public_ipv6": + iacr.SetAddressFamily(6) + iacr.SetPublic(true) + } + if cidr := ia["cidr"].(int); cidr > 0 { + iacr.SetCidr(int32(cidr)) + } + iacr.SetIpReservations(converters.IfArrToStringArr(ia["reservation_ids"].([]interface{}))) + return iacr +} diff --git a/equinix/resource_metal_device_acc_test.go b/equinix/resource_metal_device_acc_test.go index df3fe412f..00b4f0ffa 100644 --- a/equinix/resource_metal_device_acc_test.go +++ b/equinix/resource_metal_device_acc_test.go @@ -14,12 +14,12 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/google/uuid" "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" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/packethost/packngo" ) // list of plans and metros and os used as filter criteria to find available hardware to run tests @@ -190,7 +190,7 @@ func testDeviceTerminationTime() string { } func TestAccMetalDevice_facilityList(t *testing.T) { - var device packngo.Device + var device metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test" @@ -249,7 +249,7 @@ func TestAccMetalDevice_sshConfig(t *testing.T) { } func TestAccMetalDevice_basic(t *testing.T) { - var device packngo.Device + var device metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test" @@ -294,7 +294,7 @@ func TestAccMetalDevice_basic(t *testing.T) { } func TestAccMetalDevice_metro(t *testing.T) { - var device packngo.Device + var device metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test" @@ -317,7 +317,7 @@ func TestAccMetalDevice_metro(t *testing.T) { } func TestAccMetalDevice_update(t *testing.T) { - var d1, d2, d3, d4, d5 packngo.Device + var d1, d2, d3, d4, d5 metalv1.Device rs := acctest.RandString(10) rInt := acctest.RandInt() r := "equinix_metal_device.test" @@ -374,7 +374,7 @@ func TestAccMetalDevice_update(t *testing.T) { } func TestAccMetalDevice_IPXEScriptUrl(t *testing.T) { - var device, d2 packngo.Device + var device, d2 metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test_ipxe_script_url" @@ -412,7 +412,7 @@ func TestAccMetalDevice_IPXEScriptUrl(t *testing.T) { } func TestAccMetalDevice_IPXEConflictingFields(t *testing.T) { - var device packngo.Device + var device metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test_ipxe_conflict" @@ -434,7 +434,7 @@ func TestAccMetalDevice_IPXEConflictingFields(t *testing.T) { } func TestAccMetalDevice_IPXEConfigMissing(t *testing.T) { - var device packngo.Device + var device metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test_ipxe_config_missing" @@ -456,7 +456,7 @@ func TestAccMetalDevice_IPXEConfigMissing(t *testing.T) { } func TestAccMetalDevice_allowUserdataChanges(t *testing.T) { - var d1, d2 packngo.Device + var d1, d2 metalv1.Device rs := acctest.RandString(10) rInt := acctest.RandInt() r := "equinix_metal_device.test" @@ -490,7 +490,7 @@ func TestAccMetalDevice_allowUserdataChanges(t *testing.T) { } func TestAccMetalDevice_allowCustomdataChanges(t *testing.T) { - var d1, d2 packngo.Device + var d1, d2 metalv1.Device rs := acctest.RandString(10) rInt := acctest.RandInt() r := "equinix_metal_device.test" @@ -553,20 +553,20 @@ func testAccMetalDeviceCheckDestroyed(s *terraform.State) error { return nil } -func testAccMetalDeviceAttributes(device *packngo.Device) resource.TestCheckFunc { +func testAccMetalDeviceAttributes(device *metalv1.Device) resource.TestCheckFunc { return func(s *terraform.State) error { - if device.Hostname != "tfacc-test-device" { - return fmt.Errorf("Bad name: %s", device.Hostname) + if device.GetHostname() != "tfacc-test-device" { + return fmt.Errorf("Bad name: %s", device.GetHostname()) } - if device.State != "active" { - return fmt.Errorf("Device should be 'active', not '%s'", device.State) + if device.GetState() != metalv1.DEVICESTATE_ACTIVE { + return fmt.Errorf("Device should be 'active', not '%s'", device.GetState()) } return nil } } -func testAccMetalDeviceExists(n string, device *packngo.Device) resource.TestCheckFunc { +func testAccMetalDeviceExists(n string, device *metalv1.Device) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -576,13 +576,13 @@ func testAccMetalDeviceExists(n string, device *packngo.Device) resource.TestChe return fmt.Errorf("No Record ID is set") } - client := testAccProvider.Meta().(*config.Config).Metal + client := testAccProvider.Meta().(*config.Config).Metalgo - foundDevice, _, err := client.Devices.Get(rs.Primary.ID, nil) + foundDevice, _, err := client.DevicesApi.FindDeviceById(context.TODO(), rs.Primary.ID).Execute() if err != nil { return err } - if foundDevice.ID != rs.Primary.ID { + if foundDevice.GetId() != rs.Primary.ID { return fmt.Errorf("Record not found: %v - %v", rs.Primary.ID, foundDevice) } @@ -592,10 +592,10 @@ func testAccMetalDeviceExists(n string, device *packngo.Device) resource.TestChe } } -func testAccMetalSameDevice(t *testing.T, before, after *packngo.Device) resource.TestCheckFunc { +func testAccMetalSameDevice(t *testing.T, before, after *metalv1.Device) resource.TestCheckFunc { return func(s *terraform.State) error { - if before.ID != after.ID { - t.Fatalf("Expected device to be the same, but it was recreated: %s -> %s", before.ID, after.ID) + if before.GetId() != after.GetId() { + t.Fatalf("Expected device to be the same, but it was recreated: %s -> %s", before.GetId(), after.GetId()) } return nil } @@ -1134,21 +1134,21 @@ func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.Im meta := testAccProvider.Meta() rd := new(schema.ResourceData) - meta.(*config.Config).AddModuleToMetalUserAgent(rd) - client := meta.(*config.Config).Metal - devices, _, err := client.Devices.List(rs.Primary.ID, &packngo.ListOptions{Search: deviceHostName}) + meta.(*config.Config).AddModuleToMetalGoUserAgent(rd) + client := meta.(*config.Config).Metalgo + devices, _, err := client.DevicesApi.FindProjectDevices(context.TODO(), rs.Primary.ID).Search(deviceHostName).Execute() if err != nil { return "", fmt.Errorf("error while fetching devices for project [%s], error: %w", rs.Primary.ID, err) } - if len(devices) == 0 { + if len(devices.Devices) == 0 { return "", fmt.Errorf("Not able to find devices in project [%s]", rs.Primary.ID) } - if len(devices) > 1 { + if len(devices.Devices) > 1 { return "", fmt.Errorf("Found more than one device with the hostname in project [%s]", rs.Primary.ID) } - rd.SetId(devices[0].ID) - return devices[0].ID, waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) + rd.SetId(devices.Devices[0].GetId()) + return devices.Devices[0].GetId(), waitForActiveDevice(context.Background(), rd, testAccProvider.Meta(), defaultTimeout) } } @@ -1186,7 +1186,7 @@ func TestAccMetalDeviceCreate_timeout(t *testing.T) { } func TestAccMetalDeviceUpdate_timeout(t *testing.T) { - var d1 packngo.Device + var d1 metalv1.Device rs := acctest.RandString(10) r := "equinix_metal_device.test" diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 59a450ee1..569608e80 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -192,3 +192,22 @@ func IgnoreResponseErrors(ignore ...func(resp *http.Response, err error) bool) f return err } } + +// ignoreHttpResponseErrors ignores http response errors when matched by one of the +// provided checks +func IgnoreHttpResponseErrors(ignore ...func(resp *http.Response, err error) bool) func(resp *http.Response, err error) error { + return func(resp *http.Response, err error) error { + mute := false + for _, ignored := range ignore { + if ignored(resp, err) { + mute = true + break + } + } + + if mute { + return nil + } + return err + } +}