Skip to content

Commit

Permalink
feat: convert metal_gateway to equinix-sdk-go (#706)
Browse files Browse the repository at this point in the history
This converts the metal_gateway resource and data source from packngo to
equinix-sdk-go to enable the later introduction of IPv6 support.
  • Loading branch information
ctreatma committed Jun 26, 2024
1 parent 3ab210f commit 5890d74
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 78 deletions.
11 changes: 3 additions & 8 deletions internal/resources/metal/gateway/datasource.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package gateway

import (
equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
"github.com/equinix/terraform-provider-equinix/internal/framework"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/packethost/packngo"

"context"
)
Expand All @@ -29,9 +27,7 @@ func (r *DataSource) Read(
req datasource.ReadRequest,
resp *datasource.ReadResponse,
) {
// Retrieve the API client from the provider metadata
r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
client := r.Meta.Metal
client := r.Meta.NewMetalClientForFramework(ctx, req.ProviderMeta)

// Retrieve values from plan
var data DataSourceModel
Expand All @@ -44,10 +40,9 @@ func (r *DataSource) Read(
id := data.GatewayID.ValueString()

// API call to get the Metal Gateway
includes := &packngo.GetOptions{Includes: []string{"project", "ip_reservation", "virtual_network", "vrf"}}
gw, _, err := client.MetalGateways.Get(id, includes)
includes := []string{"project", "ip_reservation", "virtual_network", "vrf"}
gw, _, err := client.MetalGatewaysApi.FindMetalGatewayById(ctx, id).Include(includes).Execute()
if err != nil {
err = equinix_errors.FriendlyError(err)
resp.Diagnostics.AddError(
"Error reading Metal Gateway",
"Could not read Metal Gateway with ID "+id+": "+err.Error(),
Expand Down
5 changes: 2 additions & 3 deletions internal/resources/metal/gateway/datasource_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package gateway_test

import (
"fmt"
"testing"

"github.com/equinix/terraform-provider-equinix/internal/acceptance"
Expand Down Expand Up @@ -30,7 +29,7 @@ func TestAccDataSourceMetalGateway_privateIPv4(t *testing.T) {
}

func testAccDataSourceMetalGatewayConfig_privateIPv4() string {
return fmt.Sprintf(`
return `
resource "equinix_metal_project" "test" {
name = "tfacc-gateway-test"
}
Expand All @@ -50,7 +49,7 @@ resource "equinix_metal_gateway" "test" {
data "equinix_metal_gateway" "test" {
gateway_id = equinix_metal_gateway.test.id
}
`)
`
}

// Test to verify that switching from SDKv2 to the Framework has not affected provider's behavior
Expand Down
97 changes: 62 additions & 35 deletions internal/resources/metal/gateway/models.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package gateway

import (
"github.com/equinix/equinix-sdk-go/services/metalv1"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/packethost/packngo"
)

type ResourceModel struct {
Expand All @@ -19,26 +19,37 @@ type ResourceModel struct {
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

func (m *ResourceModel) parse(gw *packngo.MetalGateway) diag.Diagnostics {
func (m *ResourceModel) parse(gw *metalv1.FindMetalGatewayById200Response) diag.Diagnostics {
// Convert Metal Gateway data to the Terraform state
m.ID = types.StringValue(gw.ID)
m.ProjectID = types.StringValue(gw.Project.ID)
m.VlanID = types.StringValue(gw.VirtualNetwork.ID)

if gw.VRF != nil {
m.VrfID = types.StringValue(gw.VRF.ID)
} else {
if gw.MetalGateway != nil {
m.ID = types.StringValue(gw.MetalGateway.GetId())
m.ProjectID = types.StringValue(gw.MetalGateway.Project.GetId())
m.VlanID = types.StringValue(gw.MetalGateway.VirtualNetwork.GetId())
m.VrfID = types.StringNull()
}

if gw.IPReservation != nil {
m.IPReservationID = types.StringValue(gw.IPReservation.ID)
if gw.MetalGateway.IpReservation != nil {
m.IPReservationID = types.StringValue(gw.MetalGateway.IpReservation.GetId())
} else {
m.IPReservationID = types.StringNull()
}

m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.MetalGateway.IpReservation)
m.State = types.StringValue(string(gw.MetalGateway.GetState()))
} else {
m.IPReservationID = types.StringNull()
}
m.ID = types.StringValue(gw.VrfMetalGateway.GetId())
m.ProjectID = types.StringValue(gw.VrfMetalGateway.Project.GetId())
m.VlanID = types.StringValue(gw.VrfMetalGateway.VirtualNetwork.GetId())
m.VrfID = types.StringValue(gw.VrfMetalGateway.Vrf.GetId())

if gw.VrfMetalGateway.IpReservation != nil {
m.IPReservationID = types.StringValue(gw.VrfMetalGateway.IpReservation.GetId())
} else {
m.IPReservationID = types.StringNull()
}

m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.IPReservation)
m.State = types.StringValue(string(gw.State))
m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.VrfMetalGateway.IpReservation)
m.State = types.StringValue(string(gw.VrfMetalGateway.GetState()))
}
return nil
}

Expand All @@ -53,34 +64,50 @@ type DataSourceModel struct {
State types.String `tfsdk:"state"`
}

func (m *DataSourceModel) parse(gw *packngo.MetalGateway) diag.Diagnostics {
func (m *DataSourceModel) parse(gw *metalv1.FindMetalGatewayById200Response) diag.Diagnostics {
if gw.MetalGateway != nil {
// Convert Metal Gateway data to the Terraform state
m.ID = types.StringValue(gw.MetalGateway.GetId())
m.ProjectID = types.StringValue(gw.MetalGateway.Project.GetId())
m.VlanID = types.StringValue(gw.MetalGateway.VirtualNetwork.GetId())
m.VrfID = types.StringNull()

// Convert Metal Gateway data to the Terraform state
m.ID = types.StringValue(gw.ID)
m.ProjectID = types.StringValue(gw.Project.ID)
m.VlanID = types.StringValue(gw.VirtualNetwork.ID)
if gw.MetalGateway.IpReservation != nil {
m.IPReservationID = types.StringValue(gw.MetalGateway.IpReservation.GetId())
} else {
m.IPReservationID = types.StringNull()
}

if gw.VRF != nil {
m.VrfID = types.StringValue(gw.VRF.ID)
m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.MetalGateway.IpReservation)
m.State = types.StringValue(string(gw.MetalGateway.GetState()))
} else {
m.VrfID = types.StringNull()
}
// Convert Metal Gateway data to the Terraform state
m.ID = types.StringValue(gw.VrfMetalGateway.GetId())
m.ProjectID = types.StringValue(gw.VrfMetalGateway.Project.GetId())
m.VlanID = types.StringValue(gw.VrfMetalGateway.VirtualNetwork.GetId())
m.VrfID = types.StringValue(gw.VrfMetalGateway.Vrf.GetId())

if gw.IPReservation != nil {
m.IPReservationID = types.StringValue(gw.IPReservation.ID)
} else {
m.IPReservationID = types.StringNull()
}
if gw.VrfMetalGateway.IpReservation != nil {
m.IPReservationID = types.StringValue(gw.VrfMetalGateway.IpReservation.GetId())
} else {
m.IPReservationID = types.StringNull()
}

m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.IPReservation)
m.State = types.StringValue(string(gw.State))
m.PrivateIPv4SubnetSize = calculateSubnetSize(gw.VrfMetalGateway.IpReservation)
m.State = types.StringValue(string(gw.VrfMetalGateway.GetState()))
}
return nil
}

func calculateSubnetSize(ip *packngo.IPAddressReservation) basetypes.Int64Value {
type ipReservationCommon interface {
GetCidr() int32
GetPublic() bool
}

func calculateSubnetSize(ip ipReservationCommon) basetypes.Int64Value {
privateIPv4SubnetSize := uint64(0)
if !ip.Public {
privateIPv4SubnetSize = 1 << (32 - ip.CIDR)
if !ip.GetPublic() {
privateIPv4SubnetSize = 1 << (32 - ip.GetCidr())
return types.Int64Value(int64(privateIPv4SubnetSize))
}
return types.Int64Null()
Expand Down
98 changes: 67 additions & 31 deletions internal/resources/metal/gateway/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import (
"fmt"
"time"

"github.com/equinix/equinix-sdk-go/services/metalv1"
equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
"github.com/equinix/terraform-provider-equinix/internal/framework"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/packethost/packngo"
)

var (
includes = []string{"project", "ip_reservation", "virtual_network", "vrf"}
)

func NewResource() resource.Resource {
Expand Down Expand Up @@ -56,31 +60,52 @@ func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp
return
}

// Retrieve the API client from the provider metadata
r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
client := r.Meta.Metal
client := r.Meta.NewMetalClientForFramework(ctx, req.ProviderMeta)

// Build the create request based on the plan
createRequest := packngo.MetalGatewayCreateRequest{
VirtualNetworkID: plan.VlanID.ValueString(),
IPReservationID: plan.IPReservationID.ValueString(),
PrivateIPv4SubnetSize: int(plan.PrivateIPv4SubnetSize.ValueInt64()),
// NOTE: the API spec provides 2 separate schemas for creating a
// VRF Metal Gateway or a non-VRF Metal Gateway. Since we can't
// tell from resource configuration which is being requested, we
// just use the non-VRF Metal Gateway request object.
createRequest := metalv1.CreateMetalGatewayRequest{
MetalGatewayCreateInput: &metalv1.MetalGatewayCreateInput{
VirtualNetworkId: plan.VlanID.ValueString(),
},
}

if reservationId := plan.IPReservationID.ValueString(); reservationId != "" {
createRequest.MetalGatewayCreateInput.IpReservationId = &reservationId
} else {
// PrivateIpv4SubnetSize is specified as an int32 by the API, but
// there is currently only an Int64 attribute defined in the plugin
// framework. For now we cast to int32; when Int32 attributes are
// supported we can redefine the schema attribute to match the API
// Reference: https://github.com/hashicorp/terraform-plugin-framework/pull/1010
privateSubnetSize := int32(plan.PrivateIPv4SubnetSize.ValueInt64())
createRequest.MetalGatewayCreateInput.PrivateIpv4SubnetSize = &privateSubnetSize
}

// Call the API to create the resource
gw, _, err := client.MetalGateways.Create(plan.ProjectID.ValueString(), &createRequest)
gw, _, err := client.MetalGatewaysApi.CreateMetalGateway(ctx, plan.ProjectID.ValueString()).CreateMetalGatewayRequest(createRequest).Include(includes).Execute()
if err != nil {
resp.Diagnostics.AddError("Error creating MetalGateway", err.Error())
return
}

// API call to get the Metal Gateway
diags, err = getGatewayAndParse(client, &plan, gw.ID)
gwId := ""
if gw.MetalGateway != nil {
gwId = gw.MetalGateway.GetId()
} else {
gwId = gw.VrfMetalGateway.GetId()
}

diags, err = getGatewayAndParse(ctx, client, &plan, gwId)
resp.Diagnostics.Append(diags...)
if err != nil {
resp.Diagnostics.AddError(
"Error reading Metal Gateway",
"Could not read Metal Gateway with ID "+gw.ID+": "+err.Error(),
"Could not read Metal Gateway with ID "+gwId+": "+err.Error(),
)
return
}
Expand All @@ -97,15 +122,13 @@ func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *res
return
}

// Retrieve the API client from the provider metadata
r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
client := r.Meta.Metal
client := r.Meta.NewMetalClientForFramework(ctx, req.ProviderMeta)

// Extract the ID of the resource from the state
id := state.ID.ValueString()

// API call to get the Metal Gateway
diags, err := getGatewayAndParse(client, &state, id)
diags, err := getGatewayAndParse(ctx, client, &state, id)
resp.Diagnostics.Append(diags...)
if err != nil {
resp.Diagnostics.AddError(
Expand All @@ -129,8 +152,7 @@ func (r *Resource) Update(

func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Retrieve the API client
r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
client := r.Meta.Metal
client := r.Meta.NewMetalClientForFramework(ctx, req.ProviderMeta)

// Retrieve the current state
var state ResourceModel
Expand All @@ -144,34 +166,41 @@ func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp
id := state.ID.ValueString()

// API call to delete the Metal Gateway
deleteResp, err := client.MetalGateways.Delete(id)
// NOTE: we have to send `include` params on the delete request
// because the delete request returns the gateway JSON and it will
// only match one of MetalGateway or VrfMetalGateway if the included
// fields are present in the response
_, deleteResp, err := client.MetalGatewaysApi.DeleteMetalGateway(ctx, id).Include(includes).Execute()

if err != nil {
if deleteResp != nil {
err = equinix_errors.FriendlyErrorForMetalGo(err, deleteResp)
}
}
if err == nil {
deleteResp = nil
// Wait for the deletion to be completed
deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts)
deleteWaiter := getGatewayStateWaiter(
ctx,
client,
id,
deleteTimeout,
[]string{string(packngo.MetalGatewayDeleting)},
[]string{string(metalv1.METALGATEWAYSTATE_DELETING)},
[]string{},
)
_, err = deleteWaiter.WaitForStateContext(ctx)
}

if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(deleteResp, err) != nil {
if equinix_errors.IgnoreHttpResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(nil, err) != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Failed to delete Metal Gateway %s", id),
equinix_errors.FriendlyError(err).Error(),
fmt.Sprintf("Failed to delete Metal Gateway %s", id), err.Error(),
)
}
}

func getGatewayAndParse(client *packngo.Client, state *ResourceModel, id string) (diags diag.Diagnostics, err error) {
func getGatewayAndParse(ctx context.Context, client *metalv1.APIClient, state *ResourceModel, id string) (diags diag.Diagnostics, err error) {
// API call to get the Metal Gateway
includes := &packngo.GetOptions{Includes: []string{"project", "ip_reservation", "virtual_network", "vrf"}}
gw, _, err := client.MetalGateways.Get(id, includes)
gw, _, err := client.MetalGatewaysApi.FindMetalGatewayById(ctx, id).Include(includes).Execute()
if err != nil {
return diags, equinix_errors.FriendlyError(err)
}
Expand All @@ -185,18 +214,25 @@ func getGatewayAndParse(client *packngo.Client, state *ResourceModel, id string)
return diags, nil
}

func getGatewayStateWaiter(client *packngo.Client, id string, timeout time.Duration, pending, target []string) *retry.StateChangeConf {
func getGatewayStateWaiter(ctx context.Context, client *metalv1.APIClient, id string, timeout time.Duration, pending, target []string) *retry.StateChangeConf {
return &retry.StateChangeConf{
Pending: pending,
Target: target,
Refresh: func() (interface{}, string, error) {
getOpts := &packngo.GetOptions{Includes: []string{"project", "ip_reservation", "virtual_network", "vrf"}}

gw, _, err := client.MetalGateways.Get(id, getOpts) // TODO: we are not using the returned gw. Remove the includes?
gw, resp, err := client.MetalGatewaysApi.FindMetalGatewayById(ctx, id).Include(includes).Execute()
if err != nil {
if resp != nil {
err = equinix_errors.FriendlyErrorForMetalGo(err, resp)
}
return 0, "", err
}
return gw, string(gw.State), nil
state := ""
if gw.MetalGateway != nil {
state = string(gw.MetalGateway.GetState())
} else {
state = string(gw.VrfMetalGateway.GetState())
}
return gw, state, nil
},
Timeout: timeout,
Delay: 10 * time.Second,
Expand Down
2 changes: 1 addition & 1 deletion internal/resources/metal/gateway/resource_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

var subnetSizes = []int64{8, 16, 32, 64, 128}

func resourceSchema(ctx context.Context) schema.Schema {
func resourceSchema(_ context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Expand Down

0 comments on commit 5890d74

Please sign in to comment.