From 5890d74a66b783250374947100f6c62be4460fea Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Wed, 26 Jun 2024 12:02:54 -0500 Subject: [PATCH] feat: convert metal_gateway to equinix-sdk-go (#706) This converts the metal_gateway resource and data source from packngo to equinix-sdk-go to enable the later introduction of IPv6 support. --- .../resources/metal/gateway/datasource.go | 11 +-- .../metal/gateway/datasource_test.go | 5 +- internal/resources/metal/gateway/models.go | 97 +++++++++++------- internal/resources/metal/gateway/resource.go | 98 +++++++++++++------ .../metal/gateway/resource_schema.go | 2 +- 5 files changed, 135 insertions(+), 78 deletions(-) diff --git a/internal/resources/metal/gateway/datasource.go b/internal/resources/metal/gateway/datasource.go index 26c869f69..408d682ae 100644 --- a/internal/resources/metal/gateway/datasource.go +++ b/internal/resources/metal/gateway/datasource.go @@ -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" ) @@ -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 @@ -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(), diff --git a/internal/resources/metal/gateway/datasource_test.go b/internal/resources/metal/gateway/datasource_test.go index aa46db0cd..8633afd36 100644 --- a/internal/resources/metal/gateway/datasource_test.go +++ b/internal/resources/metal/gateway/datasource_test.go @@ -1,7 +1,6 @@ package gateway_test import ( - "fmt" "testing" "github.com/equinix/terraform-provider-equinix/internal/acceptance" @@ -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" } @@ -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 diff --git a/internal/resources/metal/gateway/models.go b/internal/resources/metal/gateway/models.go index e659789fc..e2b915248 100644 --- a/internal/resources/metal/gateway/models.go +++ b/internal/resources/metal/gateway/models.go @@ -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 { @@ -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 } @@ -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() diff --git a/internal/resources/metal/gateway/resource.go b/internal/resources/metal/gateway/resource.go index 1f28b6fbb..345febef7 100644 --- a/internal/resources/metal/gateway/resource.go +++ b/internal/resources/metal/gateway/resource.go @@ -5,6 +5,7 @@ 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" @@ -12,7 +13,10 @@ import ( "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 { @@ -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 } @@ -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( @@ -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 @@ -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) } @@ -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, diff --git a/internal/resources/metal/gateway/resource_schema.go b/internal/resources/metal/gateway/resource_schema.go index afab1ab61..5f863f97a 100644 --- a/internal/resources/metal/gateway/resource_schema.go +++ b/internal/resources/metal/gateway/resource_schema.go @@ -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{