Skip to content

Commit

Permalink
refactor: migrate resource and datasource equinix_metal_project to fr…
Browse files Browse the repository at this point in the history
…amework (#604)

Part of #612

---------

Signed-off-by: ocobleseqx <oscar.cobles@eu.equinix.com>
Signed-off-by: Óscar Cobles <68897552+ocobles@users.noreply.github.com>
Co-authored-by: Charles Treatman <ctreatman@equinix.com>
  • Loading branch information
ocobles and ctreatma authored Mar 20, 2024
1 parent e86dd8a commit 42737f8
Show file tree
Hide file tree
Showing 16 changed files with 1,169 additions and 459 deletions.
4 changes: 0 additions & 4 deletions equinix/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import (
"time"

"github.com/equinix/terraform-provider-equinix/internal/config"
metal_project "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project"
"github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf"

"github.com/equinix/ecx-go/v2"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -106,7 +104,6 @@ func Provider() *schema.Provider {
"equinix_metal_device_bgp_neighbors": dataSourceMetalDeviceBGPNeighbors(),
"equinix_metal_plans": dataSourceMetalPlans(),
"equinix_metal_port": dataSourceMetalPort(),
"equinix_metal_project": metal_project.DataSource(),
"equinix_metal_reserved_ip_block": dataSourceMetalReservedIPBlock(),
"equinix_metal_spot_market_request": dataSourceMetalSpotMarketRequest(),
"equinix_metal_virtual_circuit": dataSourceMetalVirtualCircuit(),
Expand All @@ -133,7 +130,6 @@ func Provider() *schema.Provider {
"equinix_metal_device": resourceMetalDevice(),
"equinix_metal_device_network_type": resourceMetalDeviceNetworkType(),
"equinix_metal_port": resourceMetalPort(),
"equinix_metal_project": metal_project.Resource(),
"equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(),
"equinix_metal_ip_attachment": resourceMetalIPAttachment(),
"equinix_metal_spot_market_request": resourceMetalSpotMarketRequest(),
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/terraform-plugin-docs v0.18.0
github.com/hashicorp/terraform-plugin-framework v1.6.1
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1
Expand Down Expand Up @@ -69,7 +70,6 @@ require (
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.6.3 // indirect
github.com/hashicorp/hcl/v2 v2.19.1 // indirect
Expand Down
46 changes: 46 additions & 0 deletions internal/planmodifiers/immutable_int64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package planmodifiers

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func ImmutableInt64() planmodifier.Int64 {
return &immutableInt64PlanModifier{}
}

type immutableInt64PlanModifier struct{}

func (d *immutableInt64PlanModifier) PlanModifyInt64(ctx context.Context, request planmodifier.Int64Request, response *planmodifier.Int64Response) {
if request.StateValue.IsNull() && request.PlanValue.IsUnknown() {
return
}

oldValue := request.StateValue.ValueInt64()
newValue := request.PlanValue.ValueInt64()

if oldValue != 0 && newValue != oldValue {
response.Diagnostics.AddAttributeError(
request.Path,
"Change not allowed",
fmt.Sprintf(
"Cannot modify the value of the `%s` field. Resource recreation would be required.",
request.Path.String(),
),
)
return
}

response.PlanValue = types.Int64Value(newValue)
}

func (d *immutableInt64PlanModifier) Description(ctx context.Context) string {
return "Prevents modification of a int64 value if the old value is not null."
}

func (d *immutableInt64PlanModifier) MarkdownDescription(ctx context.Context) string {
return d.Description(ctx)
}
61 changes: 61 additions & 0 deletions internal/planmodifiers/immutable_int64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package planmodifiers

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func TestImmutableStringSet(t *testing.T) {
testCases := []struct {
Old, New, Expected int64
ExpectError bool
}{
{
Old: 0,
New: 1234,
Expected: 1234,
ExpectError: false,
},
{
Old: 1234,
New: 4321,
Expected: 0,
ExpectError: true,
},
}

testPlanModifier := ImmutableInt64()

for i, testCase := range testCases {
stateValue := types.Int64Value(testCase.Old)
planValue := types.Int64Value(testCase.New)
expectedValue := types.Int64Null()
if testCase.Expected != 0 {
expectedValue = types.Int64Value(testCase.Expected)
}

req := planmodifier.Int64Request{
StateValue: stateValue,
PlanValue: planValue,
Path: path.Root("test"),
}

var resp planmodifier.Int64Response

testPlanModifier.PlanModifyInt64(context.Background(), req, &resp)

if resp.Diagnostics.HasError() {
if testCase.ExpectError == false {
t.Fatalf("%d: got error modifying plan: %v", i, resp.Diagnostics.Errors())
}
}

if !resp.PlanValue.Equal(expectedValue) {
t.Fatalf("%d: output plan value does not equal expected. Want %d plan value, got %d", i, expectedValue, resp.PlanValue.ValueInt64())
}
}
}
43 changes: 43 additions & 0 deletions internal/planmodifiers/immutable_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package planmodifiers

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

func ImmutableList() planmodifier.List {
return &immutableListPlanModifier{}
}

type immutableListPlanModifier struct{}

func (d *immutableListPlanModifier) PlanModifyList(ctx context.Context, request planmodifier.ListRequest, response *planmodifier.ListResponse) {

if request.StateValue.IsNull() && request.PlanValue.IsNull() {
return
}

if request.PlanValue.IsNull() && len(request.StateValue.Elements()) > 0 {
response.Diagnostics.AddAttributeError(
request.Path,
"Change not allowed",
fmt.Sprintf(
"Elements of the `%s` list field can not be removed. Resource recreation would be required.",
request.Path.String(),
),
)
return
}

response.PlanValue = request.PlanValue
}

func (d *immutableListPlanModifier) Description(ctx context.Context) string {
return "Allows adding elements to a list if it was initially empty and permits modifications, but disallows removals, requiring resource recreation."
}

func (d *immutableListPlanModifier) MarkdownDescription(ctx context.Context) string {
return d.Description(ctx)
}
65 changes: 65 additions & 0 deletions internal/planmodifiers/immutable_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package planmodifiers

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func TestImmutableListSet(t *testing.T) {
testCases := []struct {
Old, New, Expected []string
ExpectError bool
}{
{
Old: []string{},
New: []string{"test"},
Expected: []string{"test"},
ExpectError: false,
},
{
Old: []string{"test"},
New: []string{},
Expected: []string{},
ExpectError: true,
},
{
Old: []string{"foo"},
New: []string{"bar"},
Expected: []string{"bar"},
ExpectError: true,
},
}

testPlanModifier := ImmutableList()

for i, testCase := range testCases {
stateValue, _ := types.ListValueFrom(context.Background(), types.StringType, testCase.Old)
planValue, _ := types.ListValueFrom(context.Background(), types.StringType, testCase.New)
expectedValue, _ := types.ListValueFrom(context.Background(), types.StringType, testCase.Expected)

req := planmodifier.ListRequest{
StateValue: stateValue,
PlanValue: planValue,
Path: path.Root("test"),
}

var resp planmodifier.ListResponse

testPlanModifier.PlanModifyList(context.Background(), req, &resp)

if resp.Diagnostics.HasError() {
if testCase.ExpectError == false {
t.Fatalf("%d: got error modifying plan: %v", i, resp.Diagnostics.Errors())
}
}

if !resp.PlanValue.Equal(expectedValue) {
value, _ := resp.PlanValue.ToListValue(context.Background())
t.Fatalf("%d: output plan value does not equal expected. Want %v plan value, got %v", i, expectedValue, value)
}
}
}
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway"
metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization"
metalorganizationmember "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization_member"
metalproject "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project"
metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key"
metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key"
"github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan"
Expand Down Expand Up @@ -114,6 +115,7 @@ func (p *FrameworkProvider) MetaSchema(
func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
metalgateway.NewResource,
metalproject.NewResource,
metalprojectsshkey.NewResource,
metalsshkey.NewResource,
metalconnection.NewResource,
Expand All @@ -126,6 +128,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res
func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
metalgateway.NewDataSource,
metalproject.NewDataSource,
metalprojectsshkey.NewDataSource,
metalconnection.NewDataSource,
metalorganization.NewDataSource,
Expand Down
86 changes: 86 additions & 0 deletions internal/resources/metal/project/bgp_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package project

import (
"context"

"github.com/equinix/equinix-sdk-go/services/metalv1"
equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types"

"github.com/hashicorp/terraform-plugin-framework/diag"
)

func fetchBGPConfig(ctx context.Context, client *metalv1.APIClient, projectID string) (*metalv1.BgpConfig, diag.Diagnostics) {
var diags diag.Diagnostics

bgpConfig, _, err := client.BGPApi.FindBgpConfigByProject(ctx, projectID).Execute()
if err != nil {
friendlyErr := equinix_errors.FriendlyError(err)
diags.AddError(
"Error reading BGP configuration",
"Could not read BGP configuration for project with ID "+projectID+": "+friendlyErr.Error(),
)
return nil, diags
}

return bgpConfig, diags
}

func expandBGPConfig(ctx context.Context, bgpConfig fwtypes.ListNestedObjectValueOf[BGPConfigModel]) (*metalv1.BgpConfigRequestInput, error) {
bgpConfigModel, _ := bgpConfig.ToSlice(ctx)
bgpDeploymentType, err := metalv1.NewBgpConfigRequestInputDeploymentTypeFromValue(bgpConfigModel[0].DeploymentType.ValueString())
if err != nil {
return nil, err
}
bgpCreateRequest := metalv1.BgpConfigRequestInput{
DeploymentType: *bgpDeploymentType,
Asn: int32(bgpConfigModel[0].ASN.ValueInt64()),
}
if !bgpConfigModel[0].MD5.IsNull() {
bgpCreateRequest.Md5 = bgpConfigModel[0].MD5.ValueStringPointer()
}

return &bgpCreateRequest, nil
}

func handleBGPConfigChanges(ctx context.Context, client *metalv1.APIClient, plan, state *ResourceModel, projectID string) (*metalv1.BgpConfig, diag.Diagnostics) {
var diags diag.Diagnostics
var bgpConfig *metalv1.BgpConfig

if plan.BGPConfig.IsNull() && state.BGPConfig.IsNull() {
return bgpConfig, nil
}

bgpAdded := !plan.BGPConfig.IsNull() && state.BGPConfig.IsNull()
bgpChanged := !plan.BGPConfig.IsNull() && !state.BGPConfig.IsNull() && !plan.BGPConfig.Equal(state.BGPConfig)

if bgpAdded || bgpChanged {
// Create BGP Config
bgpCreateRequest, err := expandBGPConfig(ctx, plan.BGPConfig)
if err != nil {
diags.AddError(
"Error creating project",
"Could not validate BGP Config: "+err.Error(),
)
return nil, diags
}
createResp, err := client.BGPApi.RequestBgpConfig(ctx, projectID).BgpConfigRequestInput(*bgpCreateRequest).Execute()
if err != nil {
err = equinix_errors.FriendlyErrorForMetalGo(err, createResp)
diags.AddError(
"Error creating BGP configuration",
"Could not create BGP configuration for project: "+err.Error(),
)
return nil, diags
}
// Fetch the newly created BGP Config
bgpConfig, diags = fetchBGPConfig(ctx, client, projectID)
diags.Append(diags...)
} else { // assuming already exists
// Fetch the existing BGP Config
bgpConfig, diags = fetchBGPConfig(ctx, client, projectID)
diags.Append(diags...)
}

return bgpConfig, diags
}
Loading

0 comments on commit 42737f8

Please sign in to comment.