From dc36de0b437c74ee6c50c232a521582990076c21 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Wed, 31 Jul 2024 19:13:39 -0500 Subject: [PATCH 1/4] feat: add support for VRF BGP dynamic neighbors --- .../metal_vrf_bgp_dynamic_neighbor.md | 65 +++++++ .../resource.tf | 35 ++++ internal/provider/provider.go | 8 +- .../metal/vrf_bgp_dynamic_neighbor/model.go | 36 ++++ .../vrf_bgp_dynamic_neighbor/resource.go | 171 ++++++++++++++++++ .../resource_schema.go | 62 +++++++ .../vrf_bgp_dynamic_neighbor/resource_test.go | 100 ++++++++++ 7 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 docs/resources/metal_vrf_bgp_dynamic_neighbor.md create mode 100644 examples/resources/equinix_metal_vrf_bgp_dynamic_neighbor/resource.tf create mode 100644 internal/resources/metal/vrf_bgp_dynamic_neighbor/model.go create mode 100644 internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go create mode 100644 internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_schema.go create mode 100644 internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_test.go diff --git a/docs/resources/metal_vrf_bgp_dynamic_neighbor.md b/docs/resources/metal_vrf_bgp_dynamic_neighbor.md new file mode 100644 index 000000000..5184b9af1 --- /dev/null +++ b/docs/resources/metal_vrf_bgp_dynamic_neighbor.md @@ -0,0 +1,65 @@ +--- +subcategory: "Metal" +--- + +# equinix_metal_vrf_bgp_dynamic_neighbor (Resource) + +This resource manages BGP dynamic neighbor ranges for an Equinix Metal VRF, but with markdown + +## Example Usage + +```terraform +resource "equinix_metal_vlan" "example" { + description = "proj-vrf-bgp-neighbor-example VLAN in SV" + metro = "sv" + project_id = local.project_id +} + +resource "equinix_metal_vrf" "example" { + description = "proj-vrf-bgp-neighbor-example VRF in SV" + name = "tfacc-vrf-example" + metro = "sv" + local_asn = "65000" + ip_ranges = ["2001:d78::/59"] + project_id = local.project_id +} + +resource "equinix_metal_reserved_ip_block" "example" { + project_id = local.project_id + type = "vrf" + vrf_id = equinix_metal_vrf.example.id + network = "2001:d78::" + metro = "sv" + cidr = 64 +} + +resource "equinix_metal_gateway" "example" { + project_id = local.project_id + vlan_id = equinix_metal_vlan.example.id + ip_reservation_id = equinix_metal_reserved_ip_block.example.id +} + +resource "equinix_metal_vrf_bgp_dynamic_neighbor" "example" { + gateway_id = equinix_metal_gateway.example.id + range = "2001:d78:0:0:4000::/66" + asn = "56789" +} +``` + + +## Schema + +### Required + +- `asn` (Number) The ASN of the dynamic BGP neighbor +- `gateway_id` (String) The ID of the Equinix Metal VRF gateway for this dynamic BGP neighbor range +- `range` (String) Network range of the dynamic BGP neighbor in CIDR format + +### Optional + +- `tags` (List of String) Tags attached to the dynamic BGP neighbor + +### Read-Only + +- `id` (String) The unique identifier for this the dynamic BGP neighbor +- `state` (String) The state of the dynamic BGP neighbor diff --git a/examples/resources/equinix_metal_vrf_bgp_dynamic_neighbor/resource.tf b/examples/resources/equinix_metal_vrf_bgp_dynamic_neighbor/resource.tf new file mode 100644 index 000000000..13f3e8626 --- /dev/null +++ b/examples/resources/equinix_metal_vrf_bgp_dynamic_neighbor/resource.tf @@ -0,0 +1,35 @@ +resource "equinix_metal_vlan" "example" { + description = "proj-vrf-bgp-neighbor-example VLAN in SV" + metro = "sv" + project_id = local.project_id +} + +resource "equinix_metal_vrf" "example" { + description = "proj-vrf-bgp-neighbor-example VRF in SV" + name = "tfacc-vrf-example" + metro = "sv" + local_asn = "65000" + ip_ranges = ["2001:d78::/59"] + project_id = local.project_id +} + +resource "equinix_metal_reserved_ip_block" "example" { + project_id = local.project_id + type = "vrf" + vrf_id = equinix_metal_vrf.example.id + network = "2001:d78::" + metro = "sv" + cidr = 64 +} + +resource "equinix_metal_gateway" "example" { + project_id = local.project_id + vlan_id = equinix_metal_vlan.example.id + ip_reservation_id = equinix_metal_reserved_ip_block.example.id +} + +resource "equinix_metal_vrf_bgp_dynamic_neighbor" "example" { + gateway_id = equinix_metal_gateway.example.id + range = "2001:d78:0:0:4000::/66" + asn = "56789" +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4e0d18653..b959dca6e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -12,7 +12,8 @@ import ( 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" + metalvlan "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" + metalvrfbgpdynamicneighbor "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf_bgp_dynamic_neighbor" equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -121,7 +122,8 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res metalconnection.NewResource, metalorganization.NewResource, metalorganizationmember.NewResource, - vlan.NewResource, + metalvlan.NewResource, + metalvrfbgpdynamicneighbor.NewResource, } } @@ -132,6 +134,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource metalprojectsshkey.NewDataSource, metalconnection.NewDataSource, metalorganization.NewDataSource, - vlan.NewDataSource, + metalvlan.NewDataSource, } } diff --git a/internal/resources/metal/vrf_bgp_dynamic_neighbor/model.go b/internal/resources/metal/vrf_bgp_dynamic_neighbor/model.go new file mode 100644 index 000000000..5d3589c03 --- /dev/null +++ b/internal/resources/metal/vrf_bgp_dynamic_neighbor/model.go @@ -0,0 +1,36 @@ +package vrfbgpdynamicneighbor + +import ( + "context" + + "github.com/equinix/equinix-sdk-go/services/metalv1" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type Model struct { + ID types.String `tfsdk:"id"` + GatewayID types.String `tfsdk:"gateway_id"` + Range types.String `tfsdk:"range"` + ASN types.Int64 `tfsdk:"asn"` + State types.String `tfsdk:"state"` + Tags types.List `tfsdk:"tags"` // List of strings +} + +func (m *Model) parse(ctx context.Context, neighbor *metalv1.BgpDynamicNeighbor) (d diag.Diagnostics) { + m.ID = types.StringValue(neighbor.GetId()) + + m.GatewayID = types.StringValue(neighbor.MetalGateway.GetId()) + m.Range = types.StringValue(neighbor.GetBgpNeighborRange()) + m.ASN = types.Int64Value(neighbor.GetBgpNeighborAsn()) + m.State = types.StringValue(string(neighbor.GetState())) + + tags, diags := types.ListValueFrom(ctx, types.StringType, neighbor.GetTags()) + if diags.HasError() { + return diags + } + + m.Tags = tags + + return nil +} diff --git a/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go new file mode 100644 index 000000000..99503734c --- /dev/null +++ b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go @@ -0,0 +1,171 @@ +package vrfbgpdynamicneighbor + +import ( + "context" + + "github.com/equinix/equinix-sdk-go/services/metalv1" + "github.com/equinix/terraform-provider-equinix/internal/framework" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +var ( + bgpNeighborIncludes = []string{"metal_gateway"} + // `created_by` is specified as a UserLimited. To avoid an error + // due to missing UserLimited.id field, have to either exclude + // or include `created_by`. Since we're including `metal_gateway` + // we also have to either exclude or include `ip_reservation` to + // avoid a deserialization error due to the required, enumerated + // `type` property on VRF IP reservations + bgpNeighborExcludes = []string{"created_by", "ip_reservation"} +) + +type Resource struct { + framework.BaseResource + framework.WithTimeouts +} + +func NewResource() resource.Resource { + r := Resource{ + BaseResource: framework.NewBaseResource( + framework.BaseResourceConfig{ + Name: "equinix_metal_vrf_bgp_dynamic_neighbor", + }, + ), + } + + return &r +} + +func (r *Resource) Schema( + ctx context.Context, + req resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + s := resourceSchema(ctx) + if s.Blocks == nil { + s.Blocks = make(map[string]schema.Block) + } + + resp.Schema = s +} + +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + client := r.Meta.NewMetalClientForFramework(ctx, request.ProviderMeta) + + var plan Model + response.Diagnostics.Append(request.Config.Get(ctx, &plan)...) + if response.Diagnostics.HasError() { + return + } + + createRequest := metalv1.BgpDynamicNeighborCreateInput{ + BgpNeighborRange: plan.Range.ValueString(), + BgpNeighborAsn: plan.ASN.ValueInt64(), + } + + response.Diagnostics.Append(getPlanTags(ctx, plan, &createRequest.Tags)...) + if response.Diagnostics.HasError() { + return + } + + neighbor, _, err := client.VRFsApi.CreateBgpDynamicNeighbor(ctx, plan.GatewayID.ValueString()). + BgpDynamicNeighborCreateInput(createRequest). + Exclude(bgpNeighborExcludes). + Include(bgpNeighborIncludes). + Execute() + + if err != nil { + response.Diagnostics.AddError( + "Error creating VRF BGP dynamic neighbor range", + "Could not create VRF BGP dynamic neighbor range: "+err.Error(), + ) + } + + // Parse API response into the Terraform state + response.Diagnostics.Append(plan.parse(ctx, neighbor)...) + if response.Diagnostics.HasError() { + return + } + + // Set state to fully populated data + response.Diagnostics.Append(response.State.Set(ctx, &plan)...) +} + +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + client := r.Meta.NewMetalClientForFramework(ctx, request.ProviderMeta) + + var data Model + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + neighbor, _, err := client.VRFsApi.BgpDynamicNeighborsIdGet(ctx, data.ID.ValueString()). + Exclude(bgpNeighborExcludes). + Include(bgpNeighborIncludes). + Execute() + + if err != nil { + response.Diagnostics.AddError( + "Error reading VRF BGP dynamic neighbor range", + "Could not read VRF BGP dynamic neighbor with ID "+data.ID.ValueString()+": "+err.Error(), + ) + } + + response.Diagnostics.Append(data.parse(ctx, neighbor)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // TODO: ideally it should be possible to update tags, but the API doesn't have an + // update endpoint for BGP dynamic neighbors, so for now update is a no-op and + // tag changes force resource recreation + var data Model + if diag := req.Plan.Get(ctx, &data); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + if diag := resp.State.Set(ctx, &data); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } +} + +func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + client := r.Meta.NewMetalClientForFramework(ctx, request.ProviderMeta) + + var data Model + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + // TODO: should we do something with the neighbor object returned here? + // For example: do we need to poll the API until neighbor.GetState() has + // as particular value? + _, _, err := client.VRFsApi.DeleteBgpDynamicNeighborById(ctx, data.ID.ValueString()). + Exclude(bgpNeighborExcludes). + Execute() + + if err != nil { + response.Diagnostics.AddError( + "Error deleting VRF BGP dynamic neighbor range", + "Could not delete VRF BGP dynamic neighbor with ID "+data.ID.ValueString()+": "+err.Error(), + ) + } +} + +func getPlanTags(ctx context.Context, plan Model, tags *[]string) diag.Diagnostics { + if len(plan.Tags.Elements()) != 0 { + return plan.Tags.ElementsAs(ctx, tags, false) + } + return diag.Diagnostics{} +} diff --git a/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_schema.go b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_schema.go new file mode 100644 index 000000000..83bd94e1b --- /dev/null +++ b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_schema.go @@ -0,0 +1,62 @@ +package vrfbgpdynamicneighbor + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func resourceSchema(_ context.Context) schema.Schema { + return schema.Schema{ + Description: "This resource manages BGP dynamic neighbor ranges for an Equinix Metal VRF", + MarkdownDescription: "This resource manages BGP dynamic neighbor ranges for an Equinix Metal VRF, but with markdown", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier for this the dynamic BGP neighbor", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "gateway_id": schema.StringAttribute{ + Description: "The ID of the Equinix Metal VRF gateway for this dynamic BGP neighbor range", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "range": schema.StringAttribute{ + Description: "Network range of the dynamic BGP neighbor in CIDR format", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "asn": schema.Int64Attribute{ + Description: "The ASN of the dynamic BGP neighbor", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "state": schema.StringAttribute{ + Description: "The state of the dynamic BGP neighbor", + Computed: true, + }, + "tags": schema.ListAttribute{ + Description: "Tags attached to the dynamic BGP neighbor", + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, + }, + } +} diff --git a/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_test.go b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_test.go new file mode 100644 index 000000000..528a86608 --- /dev/null +++ b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource_test.go @@ -0,0 +1,100 @@ +package vrfbgpdynamicneighbor_test + +import ( + "context" + "fmt" + "testing" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" + "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccMetalVrfBgpDynamicNeighbor_basic(t *testing.T) { + rs := acctest.RandString(10) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccMetalVrfBgpDynamicNeighborConfig(rs), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair( + "equinix_metal_vrf_bgp_dynamic_neighbor.test", "gateway_id", + "equinix_metal_gateway.test", "id"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf_bgp_dynamic_neighbor.test", "range", + "2001:d78:0:0:4000::/66"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf_bgp_dynamic_neighbor.test", "asn", + "56789"), + ), + }, + }, + }) +} + +func testAccMetalVrfBgpDynamicNeighborConfig(projSuffix string) string { + return fmt.Sprintf(` +resource "equinix_metal_project" "test" { + name = "tfacc-vrf-bgp-neighbor-test-%s" +} + +resource "equinix_metal_vlan" "test" { + description = "tfacc-vlan VLAN in SV" + metro = "sv" + project_id = equinix_metal_project.test.id +} + +resource "equinix_metal_vrf" "test" { + description = "tfacc-vrf VRF in SV" + name = "tfacc-vrf-%s" + metro = "sv" + local_asn = "65000" + ip_ranges = ["2001:d78::/59"] + bgp_dynamic_neighbors_enabled = true + + project_id = equinix_metal_project.test.id +} + +resource "equinix_metal_reserved_ip_block" "test" { + project_id = equinix_metal_project.test.id + type = "vrf" + vrf_id = equinix_metal_vrf.test.id + network = "2001:d78::" + metro = "sv" + cidr = 64 +} + +resource "equinix_metal_gateway" "test" { + project_id = equinix_metal_project.test.id + vlan_id = equinix_metal_vlan.test.id + ip_reservation_id = equinix_metal_reserved_ip_block.test.id +} + +resource "equinix_metal_vrf_bgp_dynamic_neighbor" "test" { + gateway_id = equinix_metal_gateway.test.id + range = "2001:d78:0:0:4000::/66" + asn = "56789" +} +`, projSuffix, projSuffix) +} + +func testAccCheckDestroyed(s *terraform.State) error { + client := acceptance.TestAccProvider.Meta().(*config.Config).NewMetalClientForTesting() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "equinix_metal_vrf_bgp_dynamic_gateway" { + continue + } + if _, _, err := client.VRFsApi.BgpDynamicNeighborsIdGet(context.Background(), rs.Primary.ID).Execute(); err == nil { + return fmt.Errorf("Metal VRF BGP dynamic neighbor still exists") + } + } + + return nil +} From e5563a4937158831c68234eec2c915abf748a942 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Fri, 2 Aug 2024 10:35:22 -0500 Subject: [PATCH 2/4] feat: add support for BGP dynamic neighbor flags on VRFs --- docs/resources/metal_vrf.md | 27 ++++--- internal/resources/metal/vrf/datasource.go | 28 +++++++ internal/resources/metal/vrf/resource.go | 57 ++++++++++--- internal/resources/metal/vrf/resource_test.go | 79 +++++++++++++++++-- templates/resources/metal_vrf.md.tmpl | 15 +--- 5 files changed, 165 insertions(+), 41 deletions(-) diff --git a/docs/resources/metal_vrf.md b/docs/resources/metal_vrf.md index 92848e959..a28b97890 100644 --- a/docs/resources/metal_vrf.md +++ b/docs/resources/metal_vrf.md @@ -75,20 +75,27 @@ resource "equinix_metal_virtual_circuit" "example" { } ``` -## Argument Reference + +## Schema -The following arguments are supported: +### Required -* `name` - (Required) User-supplied name of the VRF, unique to the project -* `metro` - (Required) Metro ID or Code where the VRF will be deployed. -* `project_id` - (Required) Project ID where the VRF will be deployed. -* `description` - (Optional) Description of the VRF. -* `local_asn` - (Optional) The 4-byte ASN set on the VRF. -* `ip_ranges` - (Optional) All IPv4 and IPv6 Ranges that will be available to BGP Peers. IPv4 addresses must be /8 or smaller with a minimum size of /29. IPv6 must be /56 or smaller with a minimum size of /64. Ranges must not overlap other ranges within the VRF. +- `metro` (String) Metro ID or Code where the VRF will be deployed +- `name` (String) User-supplied name of the VRF, unique to the project +- `project_id` (String) Project ID where the VRF will be deployed -## Attributes Reference +### Optional -No additional attributes are exported. +- `bgp_dynamic_neighbors_bfd_enabled` (Boolean) Toggle BFD on dynamic bgp neighbors sessions +- `bgp_dynamic_neighbors_enabled` (Boolean) Toggle to enable the dynamic bgp neighbors feature on the VRF +- `bgp_dynamic_neighbors_export_route_map` (Boolean) Toggle to export the VRF route-map to the dynamic bgp neighbors +- `description` (String) Description of the VRF +- `ip_ranges` (Set of String) All IPv4 and IPv6 Ranges that will be available to BGP Peers. IPv4 addresses must be /8 or smaller with a minimum size of /29. IPv6 must be /56 or smaller with a minimum size of /64. Ranges must not overlap other ranges within the VRF. +- `local_asn` (Number) The 4-byte ASN set on the VRF + +### Read-Only + +- `id` (String) The ID of this resource. ## Import diff --git a/internal/resources/metal/vrf/datasource.go b/internal/resources/metal/vrf/datasource.go index 5662511b3..9ee326abf 100644 --- a/internal/resources/metal/vrf/datasource.go +++ b/internal/resources/metal/vrf/datasource.go @@ -48,6 +48,34 @@ func DataSource() *schema.Resource { Computed: true, Description: "Project ID", }, + "bgp_dynamic_neighbors": { + Type: schema.TypeList, + Description: "BGP dynamic neighbor settings for this VRF", + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle to enable the dynamic bgp neighbors feature on the VRF", + }, + "export_route_map": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle to export the VRF route-map to the dynamic bgp neighbors", + }, + "bfd_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle BFD on dynamic bgp neighbors sessions", + }, + }, + }, + }, }, } } diff --git a/internal/resources/metal/vrf/resource.go b/internal/resources/metal/vrf/resource.go index e8bf6688d..d8e447322 100644 --- a/internal/resources/metal/vrf/resource.go +++ b/internal/resources/metal/vrf/resource.go @@ -38,13 +38,13 @@ func Resource() *schema.Resource { "metro": { Type: schema.TypeString, Required: true, - Description: "Metro Code", + Description: "Metro ID or Code where the VRF will be deployed", }, "local_asn": { Type: schema.TypeInt, Optional: true, Computed: true, - Description: "The 4-byte ASN set on the VRF.", + Description: "The 4-byte ASN set on the VRF", }, "ip_ranges": { Type: schema.TypeSet, @@ -55,7 +55,25 @@ func Resource() *schema.Resource { "project_id": { Type: schema.TypeString, Required: true, - Description: "Project ID", + Description: "Project ID where the VRF will be deployed", + }, + "bgp_dynamic_neighbors_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle to enable the dynamic bgp neighbors feature on the VRF", + }, + "bgp_dynamic_neighbors_export_route_map": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle to export the VRF route-map to the dynamic bgp neighbors", + }, + "bgp_dynamic_neighbors_bfd_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Toggle BFD on dynamic bgp neighbors sessions", }, // TODO: created_by, created_at, updated_at, href }, @@ -75,6 +93,15 @@ func resourceMetalVRFCreate(ctx context.Context, d *schema.ResourceData, meta in if value, ok := d.GetOk("local_asn"); ok { createRequest.LocalAsn = metalv1.PtrInt64(int64(value.(int))) } + if value, ok := d.GetOk("bgp_dynamic_neighbors_enabled"); ok { + createRequest.SetBgpDynamicNeighborsEnabled(value.(bool)) + } + if value, ok := d.GetOk("bgp_dynamic_neighbors_export_route_map"); ok { + createRequest.SetBgpDynamicNeighborsExportRouteMap(value.(bool)) + } + if value, ok := d.GetOk("bgp_dynamic_neighbors_bfd_enabled"); ok { + createRequest.SetBgpDynamicNeighborsBfdEnabled(value.(bool)) + } projectId := d.Get("project_id").(string) vrf, _, err := client.VRFsApi. @@ -107,6 +134,15 @@ func resourceMetalVRFUpdate(ctx context.Context, d *schema.ResourceData, meta in ipRanges := converters.SetToStringList(d.Get("ip_ranges").(*schema.Set)) updateRequest.SetIpRanges(ipRanges) } + if d.HasChange("bgp_dynamic_neighbors_enabled") { + updateRequest.SetBgpDynamicNeighborsEnabled(d.Get("bgp_dynamic_neighbors_enabled").(bool)) + } + if d.HasChange("bgp_dynamic_neighbors_export_route_map") { + updateRequest.SetBgpDynamicNeighborsExportRouteMap(d.Get("bgp_dynamic_neighbors_export_route_map").(bool)) + } + if d.HasChange("bgp_dynamic_neighbors_bfd_enabled") { + updateRequest.SetBgpDynamicNeighborsBfdEnabled(d.Get("bgp_dynamic_neighbors_bfd_enabled").(bool)) + } _, _, err := client.VRFsApi. UpdateVrf(ctx, d.Id()). @@ -136,12 +172,15 @@ func resourceMetalVRFRead(ctx context.Context, d *schema.ResourceData, meta inte return diag.FromErr(err) } m := map[string]interface{}{ - "name": vrf.GetName(), - "description": vrf.GetDescription(), - "metro": vrf.Metro.GetCode(), - "local_asn": vrf.GetLocalAsn(), - "ip_ranges": vrf.GetIpRanges(), - "project_id": vrf.Project.GetId(), + "name": vrf.GetName(), + "description": vrf.GetDescription(), + "metro": vrf.Metro.GetCode(), + "local_asn": vrf.GetLocalAsn(), + "ip_ranges": vrf.GetIpRanges(), + "project_id": vrf.Project.GetId(), + "bgp_dynamic_neighbors_enabled": vrf.GetBgpDynamicNeighborsEnabled(), + "bgp_dynamic_neighbors_export_route_map": vrf.GetBgpDynamicNeighborsExportRouteMap(), + "bgp_dynamic_neighbors_bfd_enabled": vrf.GetBgpDynamicNeighborsBfdEnabled(), } return diag.FromErr(equinix_schema.SetMap(d, m)) diff --git a/internal/resources/metal/vrf/resource_test.go b/internal/resources/metal/vrf/resource_test.go index 4dde01138..4359a1107 100644 --- a/internal/resources/metal/vrf/resource_test.go +++ b/internal/resources/metal/vrf/resource_test.go @@ -17,6 +17,9 @@ import ( const ( metalDedicatedConnIDEnvVar = "TF_ACC_METAL_DEDICATED_CONNECTION_ID" + // This used to be repeated in each config function + // Extracting it for now but other test suites pick a dynamic metro + testMetro = "da" ) func TestAccMetalVRF_basic(t *testing.T) { @@ -48,6 +51,58 @@ func TestAccMetalVRF_basic(t *testing.T) { }) } +func TestAccMetalVRF_bgpDynamicNeighbors(t *testing.T) { + var vrf metalv1.Vrf + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccMetalVRFCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccMetalVRFConfig_bgpDynamicNeighbors(rInt, true, true, true), + Check: resource.ComposeTestCheckFunc( + testAccMetalVRFExists("equinix_metal_vrf.test", &vrf), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_enabled", "true"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_export_route_map", "true"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_bfd_enabled", "true"), + ), + }, + { + ResourceName: "equinix_metal_vrf.test", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccMetalVRFConfig_bgpDynamicNeighbors(rInt, false, false, false), + Check: resource.ComposeTestCheckFunc( + testAccMetalVRFExists("equinix_metal_vrf.test", &vrf), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_enabled", "false"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_export_route_map", "false"), + resource.TestCheckResourceAttr( + "equinix_metal_vrf.test", "bgp_dynamic_neighbors_bfd_enabled", "false"), + ), + }, + { + ResourceName: "equinix_metal_vrf.test", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccMetalVRFConfig_basic(rInt), + PlanOnly: true, + }, + }, + }) +} + func TestAccMetalVRF_withIPRanges(t *testing.T) { var vrf metalv1.Vrf rInt := acctest.RandInt() @@ -289,8 +344,6 @@ func testAccMetalVRFExists(n string, vrf *metalv1.Vrf) resource.TestCheckFunc { } func testAccMetalVRFConfig_basic(r int) string { - testMetro := "da" - return fmt.Sprintf(` resource "equinix_metal_project" "test" { name = "tfacc-vrfs-%d" @@ -303,9 +356,23 @@ resource "equinix_metal_vrf" "test" { }`, r, r, testMetro) } -func testAccMetalVRFConfig_withIPRanges(r int) string { - testMetro := "da" +func testAccMetalVRFConfig_bgpDynamicNeighbors(r int, enabled, export_route_map, bfd_enabled bool) string { + return fmt.Sprintf(` +resource "equinix_metal_project" "test" { + name = "tfacc-vrfs-%d" +} + +resource "equinix_metal_vrf" "test" { + name = "tfacc-vrf-%d" + metro = "%s" + project_id = "${equinix_metal_project.test.id}" + bgp_dynamic_neighbors_enabled = %v + bgp_dynamic_neighbors_export_route_map = %v + bgp_dynamic_neighbors_bfd_enabled = %v +}`, r, r, testMetro, enabled, export_route_map, bfd_enabled) +} +func testAccMetalVRFConfig_withIPRanges(r int) string { return fmt.Sprintf(` resource "equinix_metal_project" "test" { name = "tfacc-vrfs-%d" @@ -322,8 +389,6 @@ resource "equinix_metal_vrf" "test" { } func testAccMetalVRFConfig_withIPReservations(r int) string { - testMetro := "da" - return testAccMetalVRFConfig_withIPRanges(r) + fmt.Sprintf(` resource "equinix_metal_reserved_ip_block" "test" { @@ -339,8 +404,6 @@ resource "equinix_metal_reserved_ip_block" "test" { } func testAccMetalVRFConfig_withGateway(r int) string { - testMetro := "da" - return testAccMetalVRFConfig_withIPReservations(r) + fmt.Sprintf(` resource "equinix_metal_vlan" "test" { diff --git a/templates/resources/metal_vrf.md.tmpl b/templates/resources/metal_vrf.md.tmpl index 4d7e4732a..b496ed370 100644 --- a/templates/resources/metal_vrf.md.tmpl +++ b/templates/resources/metal_vrf.md.tmpl @@ -26,20 +26,7 @@ Attach a Virtual Circuit from a Dedicated Metal Connection to the Metal Gateway. {{tffile "examples/resources/metal_vrf/example_3.tf"}} -## Argument Reference - -The following arguments are supported: - -* `name` - (Required) User-supplied name of the VRF, unique to the project -* `metro` - (Required) Metro ID or Code where the VRF will be deployed. -* `project_id` - (Required) Project ID where the VRF will be deployed. -* `description` - (Optional) Description of the VRF. -* `local_asn` - (Optional) The 4-byte ASN set on the VRF. -* `ip_ranges` - (Optional) All IPv4 and IPv6 Ranges that will be available to BGP Peers. IPv4 addresses must be /8 or smaller with a minimum size of /29. IPv6 must be /56 or smaller with a minimum size of /64. Ranges must not overlap other ranges within the VRF. - -## Attributes Reference - -No additional attributes are exported. +{{ .SchemaMarkdown | trimspace }} ## Import From c736e3dd3271efbb3102315036a49e7b1ca139eb Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Fri, 2 Aug 2024 14:26:25 -0500 Subject: [PATCH 3/4] fix: recreate VRF if metro or project is changed --- internal/resources/metal/vrf/resource.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/resources/metal/vrf/resource.go b/internal/resources/metal/vrf/resource.go index d8e447322..ed09ea367 100644 --- a/internal/resources/metal/vrf/resource.go +++ b/internal/resources/metal/vrf/resource.go @@ -38,6 +38,7 @@ func Resource() *schema.Resource { "metro": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "Metro ID or Code where the VRF will be deployed", }, "local_asn": { @@ -55,6 +56,7 @@ func Resource() *schema.Resource { "project_id": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "Project ID where the VRF will be deployed", }, "bgp_dynamic_neighbors_enabled": { From 85e3d3868dfdbc05496dd7980f20c5a73d586eb7 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Wed, 21 Aug 2024 09:32:20 -0500 Subject: [PATCH 4/4] change API call to match latest SDK release --- internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go index 99503734c..c86e1c93c 100644 --- a/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go +++ b/internal/resources/metal/vrf_bgp_dynamic_neighbor/resource.go @@ -71,7 +71,7 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } - neighbor, _, err := client.VRFsApi.CreateBgpDynamicNeighbor(ctx, plan.GatewayID.ValueString()). + neighbor, _, err := client.MetalGatewaysApi.CreateBgpDynamicNeighbor(ctx, plan.GatewayID.ValueString()). BgpDynamicNeighborCreateInput(createRequest). Exclude(bgpNeighborExcludes). Include(bgpNeighborIncludes).