Skip to content

Commit

Permalink
refactor: finish moving device resource off of packngo
Browse files Browse the repository at this point in the history
  • Loading branch information
ctreatma committed Dec 22, 2023
1 parent e69b062 commit f0d1583
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 321 deletions.
72 changes: 6 additions & 66 deletions equinix/helpers_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"path"
"sort"
"strings"
"sync"
"time"

Expand All @@ -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"
)

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down
192 changes: 100 additions & 92 deletions equinix/helpers_device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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()
})
}
}
Loading

0 comments on commit f0d1583

Please sign in to comment.