From f2f699488108369775fa901cdb0b0397128d45a2 Mon Sep 17 00:00:00 2001 From: thogarty <139183873+thogarty@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:27:50 -0700 Subject: [PATCH] feat: fabric resource connection_route_filter (#795) * Add equinix_fabric_connection_route_filter resource to attach route filter policies to fabric cloud router connections * Add data source for retrieving connection_route_filters by connection and route filter uuids * Add data source to get all route filters for a given connection uuid * Add docs with make docs * Add acceptance tests for resource and data sources Local tests passing: image --- .../fabric_connection_route_filter.md | 57 +++++ .../fabric_connection_route_filters.md | 75 +++++++ .../fabric_connection_route_filter.md | 72 +++++++ equinix/provider.go | 56 ++--- .../data-source.tf | 24 +++ .../data-source.tf | 23 ++ .../resource.tf | 25 +++ .../connection_route_filter/datasources.go | 56 +++++ .../datasources_schema.go | 110 ++++++++++ .../datasources_test.go | 3 + .../fabric/connection_route_filter/models.go | 67 ++++++ .../connection_route_filter/resource.go | 203 ++++++++++++++++++ .../resource_schema.go | 49 +++++ .../connection_route_filter/resource_test.go | 196 +++++++++++++++++ 14 files changed, 990 insertions(+), 26 deletions(-) create mode 100644 docs/data-sources/fabric_connection_route_filter.md create mode 100644 docs/data-sources/fabric_connection_route_filters.md create mode 100644 docs/resources/fabric_connection_route_filter.md create mode 100644 examples/data-sources/equinix_fabric_connection_route_filter/data-source.tf create mode 100644 examples/data-sources/equinix_fabric_connection_route_filters/data-source.tf create mode 100644 examples/resources/equinix_fabric_connection_route_filter/resource.tf create mode 100644 internal/resources/fabric/connection_route_filter/datasources.go create mode 100644 internal/resources/fabric/connection_route_filter/datasources_schema.go create mode 100644 internal/resources/fabric/connection_route_filter/datasources_test.go create mode 100644 internal/resources/fabric/connection_route_filter/models.go create mode 100644 internal/resources/fabric/connection_route_filter/resource.go create mode 100644 internal/resources/fabric/connection_route_filter/resource_schema.go create mode 100644 internal/resources/fabric/connection_route_filter/resource_test.go diff --git a/docs/data-sources/fabric_connection_route_filter.md b/docs/data-sources/fabric_connection_route_filter.md new file mode 100644 index 000000000..2511ed554 --- /dev/null +++ b/docs/data-sources/fabric_connection_route_filter.md @@ -0,0 +1,57 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_connection_route_filter (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch route filter policy attachment to a fabric connection + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filter-rules + +## Example Usage + +```terraform +data "equinix_fabric_connection_route_filter" "attached_policy" { + connection_id = "" + route_filter_id = "" +} + +output "connection_route_filter_id" { + value = data.equinix_fabric_connection_route_filter.attached_policy.id +} + +output "connection_route_filter_connection_id" { + value = data.equinix_fabric_connection_route_filter.attached_policy.connection_id +} + +output "connection_route_filter_direction" { + value = data.equinix_fabric_connection_route_filter.attached_policy.direction +} + +output "connection_route_filter_type" { + value = data.equinix_fabric_connection_route_filter.attached_policy.type +} + +output "connection_route_filter_attachment_status" { + value = data.equinix_fabric_connection_route_filter.attached_policy.attachment_status +} +``` + + +## Schema + +### Required + +- `connection_id` (String) Equinix Assigned UUID of the Equinix Connection to attach the Route Filter Policy to +- `route_filter_id` (String) Equinix Assigned UUID of the Route Filter Policy to attach to the Equinix Connection + +### Read-Only + +- `attachment_status` (String) Status of the Route Filter Policy attachment lifecycle +- `direction` (String) Direction of the filtering of the attached Route Filter Policy +- `href` (String) URI to the attached Route Filter Policy on the Connection +- `id` (String) The ID of this resource. +- `type` (String) Route Filter Type. One of [ "BGP_IPv4_PREFIX_FILTER", "BGP_IPv6_PREFIX_FILTER" ] +- `uuid` (String) Equinix Assigned ID for Route Filter Policy diff --git a/docs/data-sources/fabric_connection_route_filters.md b/docs/data-sources/fabric_connection_route_filters.md new file mode 100644 index 000000000..e6da98fce --- /dev/null +++ b/docs/data-sources/fabric_connection_route_filters.md @@ -0,0 +1,75 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_connection_route_filters (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch all route filter policies attached to a fabric connection + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filter-rules + +## Example Usage + +```terraform +data "equinix_connection_route_filters" "attached_policies" { + connection_id = "" +} + +output "connection_first_route_filter_uuid" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.uuid +} + +output "connection_first_route_filter_connection_id" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.connection_id +} + +output "connection_first_route_filter_direction" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.direction +} + +output "connection_first_route_filter_type" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.type +} + +output "connection_first_route_filter_attachment_status" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.attachment_status +} +``` + + +## Schema + +### Required + +- `connection_id` (String) Equinix Assigned UUID of the Equinix Connection to attach the Route Filter Policy to + +### Read-Only + +- `data` (List of Object) The list of Rules attached to the given Route Filter Policy UUID (see [below for nested schema](#nestedatt--data)) +- `id` (String) The ID of this resource. +- `pagination` (Set of Object) Pagination details for the Data Source Search Request (see [below for nested schema](#nestedatt--pagination)) + + +### Nested Schema for `data` + +Read-Only: + +- `attachment_status` (String) +- `direction` (String) +- `href` (String) +- `type` (String) +- `uuid` (String) + + + +### Nested Schema for `pagination` + +Read-Only: + +- `limit` (Number) +- `next` (String) +- `offset` (Number) +- `previous` (String) +- `total` (Number) diff --git a/docs/resources/fabric_connection_route_filter.md b/docs/resources/fabric_connection_route_filter.md new file mode 100644 index 000000000..302735413 --- /dev/null +++ b/docs/resources/fabric_connection_route_filter.md @@ -0,0 +1,72 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_connection_route_filter (Resource) + +Fabric V4 API compatible resource allows attachment of Route Filter Polices to Fabric Connections + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filters + +## Example Usage + +```terraform +resource "equinix_fabric_connection_route_filter" "policy_attachment" { + connection_id = "" + route_filter_id = "" + direction = "INBOUND" +} + +output "connection_route_filter_id" { + value = equinix_fabric_connection_route_filter.policy_attachment.id +} + +output "connection_route_filter_connection_id" { + value = equinix_fabric_connection_route_filter.policy_attachment.connection_id +} + +output "connection_route_filter_direction" { + value = equinix_fabric_connection_route_filter.policy_attachment.direction +} + +output "connection_route_filter_type" { + value = equinix_fabric_connection_route_filter.policy_attachment.type +} + +output "connection_route_filter_attachment_status" { + value = equinix_fabric_connection_route_filter.policy_attachment.attachment_status +} +``` + + +## Schema + +### Required + +- `connection_id` (String) Equinix Assigned UUID of the Equinix Connection to attach the Route Filter Policy to +- `direction` (String) Direction of the filtering of the attached Route Filter Policy +- `route_filter_id` (String) Equinix Assigned UUID of the Route Filter Policy to attach to the Equinix Connection + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `attachment_status` (String) Status of the Route Filter Policy attachment lifecycle +- `href` (String) URI to the attached Route Filter Policy on the Connection +- `id` (String) The ID of this resource. +- `type` (String) Route Filter Type. One of [ "BGP_IPv4_PREFIX_FILTER", "BGP_IPv6_PREFIX_FILTER" ] +- `uuid` (String) Equinix Assigned ID for Route Filter Policy + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `read` (String) +- `update` (String) diff --git a/equinix/provider.go b/equinix/provider.go index 2ff55f43d..bf38b2592 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -8,6 +8,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" fabric_connection "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/connection" + fabric_connection_route_filter "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/connection_route_filter" fabric_market_place_subscription "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/marketplace" fabric_network "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/network" fabric_route_filter "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/route_filter" @@ -87,6 +88,8 @@ func Provider() *schema.Provider { "equinix_fabric_routing_protocol": dataSourceRoutingProtocol(), "equinix_fabric_connection": fabric_connection.DataSource(), "equinix_fabric_connections": fabric_connection.DataSourceSearch(), + "equinix_fabric_connection_route_filter": fabric_connection_route_filter.DataSource(), + "equinix_fabric_connection_route_filters": fabric_connection_route_filter.DataSourceGetAllRules(), "equinix_fabric_cloud_router": dataSourceFabricCloudRouter(), "equinix_fabric_cloud_routers": dataSourceFabricGetCloudRouters(), "equinix_fabric_market_place_subscription": fabric_market_place_subscription.DataSourceFabricMarketplaceSubscription(), @@ -125,33 +128,34 @@ func Provider() *schema.Provider { "equinix_metal_vrf": vrf.DataSource(), }, ResourcesMap: map[string]*schema.Resource{ - "equinix_fabric_network": fabric_network.Resource(), - "equinix_fabric_cloud_router": resourceFabricCloudRouter(), - "equinix_fabric_connection": fabric_connection.Resource(), - "equinix_fabric_route_filter": fabric_route_filter.Resource(), - "equinix_fabric_route_filter_rule": fabric_route_filter_rule.Resource(), - "equinix_fabric_routing_protocol": resourceFabricRoutingProtocol(), - "equinix_fabric_service_profile": resourceFabricServiceProfile(), + "equinix_fabric_network": fabric_network.Resource(), + "equinix_fabric_cloud_router": resourceFabricCloudRouter(), + "equinix_fabric_connection": fabric_connection.Resource(), + "equinix_fabric_connection_route_filter": fabric_connection_route_filter.Resource(), + "equinix_fabric_route_filter": fabric_route_filter.Resource(), + "equinix_fabric_route_filter_rule": fabric_route_filter_rule.Resource(), + "equinix_fabric_routing_protocol": resourceFabricRoutingProtocol(), + "equinix_fabric_service_profile": resourceFabricServiceProfile(), "equinix_fabric_service_token": fabric_service_token.Resource(), - "equinix_network_device": resourceNetworkDevice(), - "equinix_network_ssh_user": resourceNetworkSSHUser(), - "equinix_network_bgp": resourceNetworkBGP(), - "equinix_network_ssh_key": resourceNetworkSSHKey(), - "equinix_network_acl_template": resourceNetworkACLTemplate(), - "equinix_network_device_link": resourceNetworkDeviceLink(), - "equinix_network_file": resourceNetworkFile(), - "equinix_metal_user_api_key": resourceMetalUserAPIKey(), - "equinix_metal_project_api_key": resourceMetalProjectAPIKey(), - "equinix_metal_device": metal_device.Resource(), - "equinix_metal_device_network_type": resourceMetalDeviceNetworkType(), - "equinix_metal_port": metal_port.Resource(), - "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), - "equinix_metal_ip_attachment": resourceMetalIPAttachment(), - "equinix_metal_spot_market_request": resourceMetalSpotMarketRequest(), - "equinix_metal_virtual_circuit": virtual_circuit.Resource(), - "equinix_metal_vrf": vrf.Resource(), - "equinix_metal_bgp_session": resourceMetalBGPSession(), - "equinix_metal_port_vlan_attachment": resourceMetalPortVlanAttachment(), + "equinix_network_device": resourceNetworkDevice(), + "equinix_network_ssh_user": resourceNetworkSSHUser(), + "equinix_network_bgp": resourceNetworkBGP(), + "equinix_network_ssh_key": resourceNetworkSSHKey(), + "equinix_network_acl_template": resourceNetworkACLTemplate(), + "equinix_network_device_link": resourceNetworkDeviceLink(), + "equinix_network_file": resourceNetworkFile(), + "equinix_metal_user_api_key": resourceMetalUserAPIKey(), + "equinix_metal_project_api_key": resourceMetalProjectAPIKey(), + "equinix_metal_device": metal_device.Resource(), + "equinix_metal_device_network_type": resourceMetalDeviceNetworkType(), + "equinix_metal_port": metal_port.Resource(), + "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), + "equinix_metal_ip_attachment": resourceMetalIPAttachment(), + "equinix_metal_spot_market_request": resourceMetalSpotMarketRequest(), + "equinix_metal_virtual_circuit": virtual_circuit.Resource(), + "equinix_metal_vrf": vrf.Resource(), + "equinix_metal_bgp_session": resourceMetalBGPSession(), + "equinix_metal_port_vlan_attachment": resourceMetalPortVlanAttachment(), }, ProviderMetaSchema: map[string]*schema.Schema{ "module_name": { diff --git a/examples/data-sources/equinix_fabric_connection_route_filter/data-source.tf b/examples/data-sources/equinix_fabric_connection_route_filter/data-source.tf new file mode 100644 index 000000000..728d0d1e7 --- /dev/null +++ b/examples/data-sources/equinix_fabric_connection_route_filter/data-source.tf @@ -0,0 +1,24 @@ +data "equinix_fabric_connection_route_filter" "attached_policy" { + connection_id = "" + route_filter_id = "" +} + +output "connection_route_filter_id" { + value = data.equinix_fabric_connection_route_filter.attached_policy.id +} + +output "connection_route_filter_connection_id" { + value = data.equinix_fabric_connection_route_filter.attached_policy.connection_id +} + +output "connection_route_filter_direction" { + value = data.equinix_fabric_connection_route_filter.attached_policy.direction +} + +output "connection_route_filter_type" { + value = data.equinix_fabric_connection_route_filter.attached_policy.type +} + +output "connection_route_filter_attachment_status" { + value = data.equinix_fabric_connection_route_filter.attached_policy.attachment_status +} diff --git a/examples/data-sources/equinix_fabric_connection_route_filters/data-source.tf b/examples/data-sources/equinix_fabric_connection_route_filters/data-source.tf new file mode 100644 index 000000000..c2e22a893 --- /dev/null +++ b/examples/data-sources/equinix_fabric_connection_route_filters/data-source.tf @@ -0,0 +1,23 @@ +data "equinix_connection_route_filters" "attached_policies" { + connection_id = "" +} + +output "connection_first_route_filter_uuid" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.uuid +} + +output "connection_first_route_filter_connection_id" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.connection_id +} + +output "connection_first_route_filter_direction" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.direction +} + +output "connection_first_route_filter_type" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.type +} + +output "connection_first_route_filter_attachment_status" { + value = data.equinix_fabric_connection_route_filter.attached_policies.0.attachment_status +} diff --git a/examples/resources/equinix_fabric_connection_route_filter/resource.tf b/examples/resources/equinix_fabric_connection_route_filter/resource.tf new file mode 100644 index 000000000..6ea69b7d8 --- /dev/null +++ b/examples/resources/equinix_fabric_connection_route_filter/resource.tf @@ -0,0 +1,25 @@ +resource "equinix_fabric_connection_route_filter" "policy_attachment" { + connection_id = "" + route_filter_id = "" + direction = "INBOUND" +} + +output "connection_route_filter_id" { + value = equinix_fabric_connection_route_filter.policy_attachment.id +} + +output "connection_route_filter_connection_id" { + value = equinix_fabric_connection_route_filter.policy_attachment.connection_id +} + +output "connection_route_filter_direction" { + value = equinix_fabric_connection_route_filter.policy_attachment.direction +} + +output "connection_route_filter_type" { + value = equinix_fabric_connection_route_filter.policy_attachment.type +} + +output "connection_route_filter_attachment_status" { + value = equinix_fabric_connection_route_filter.policy_attachment.attachment_status +} diff --git a/internal/resources/fabric/connection_route_filter/datasources.go b/internal/resources/fabric/connection_route_filter/datasources.go new file mode 100644 index 000000000..e8eaeb309 --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/datasources.go @@ -0,0 +1,56 @@ +package connection_route_filter + +import ( + "context" + "fmt" + + "github.com/equinix/terraform-provider-equinix/internal/config" + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func DataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceRead, + Schema: dataSourceByUUIDSchema(), + Description: `Fabric V4 API compatible data resource that allow user to fetch route filter policy attachment to a fabric connection + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filter-rules`, + } +} + +func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + uuid := d.Get("route_filter_id").(string) + d.SetId(uuid) + return resourceRead(ctx, d, meta) +} + +func DataSourceGetAllRules() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGetAllFilters, + Schema: dataSourceAllFiltersSchema(), + Description: `Fabric V4 API compatible data resource that allow user to fetch all route filter policies attached to a fabric connection + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filter-rules`, + } +} + +func dataSourceGetAllFilters(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionId := d.Get("connection_id").(string) + connectionRouteFilters, _, err := client.RouteFiltersApi.GetConnectionRouteFilters(ctx, connectionId).Execute() + if err != nil { + return diag.FromErr(equinix_errors.FormatFabricError(err)) + } + if len(connectionRouteFilters.Data) < 1 { + return diag.FromErr(fmt.Errorf("no records are found for the connection (%s) - %d , please change the search criteria", connectionId, len(connectionRouteFilters.Data))) + } + d.SetId(connectionId) + return setConnectionRouteFilterData(d, connectionRouteFilters) +} diff --git a/internal/resources/fabric/connection_route_filter/datasources_schema.go b/internal/resources/fabric/connection_route_filter/datasources_schema.go new file mode 100644 index 000000000..42da7fc92 --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/datasources_schema.go @@ -0,0 +1,110 @@ +package connection_route_filter + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceByUUIDSchema() map[string]*schema.Schema { + dsSchema := baseSchema() + dsSchema["connection_id"] = connectionIdSchema() + dsSchema["route_filter_id"] = routeFilterIdSchema() + return dsSchema +} + +func dataSourceAllFiltersSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "connection_id": connectionIdSchema(), + "pagination": { + Type: schema.TypeSet, + Computed: true, + Description: "Pagination details for the Data Source Search Request", + Elem: paginationSchema(), + }, + "data": { + Type: schema.TypeList, + Computed: true, + Description: "The list of Rules attached to the given Route Filter Policy UUID", + Elem: &schema.Resource{ + Schema: baseSchema(), + }, + }, + } +} + +func baseSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "direction": { + Type: schema.TypeString, + Computed: true, + Description: "Direction of the filtering of the attached Route Filter Policy", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "Route Filter Type. One of [ \"BGP_IPv4_PREFIX_FILTER\", \"BGP_IPv6_PREFIX_FILTER\" ] ", + }, + "href": { + Type: schema.TypeString, + Computed: true, + Description: "URI to the attached Route Filter Policy on the Connection", + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + Description: "Equinix Assigned ID for Route Filter Policy", + }, + "attachment_status": { + Type: schema.TypeString, + Computed: true, + Description: "Status of the Route Filter Policy attachment lifecycle", + }, + } +} + +func routeFilterIdSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Equinix Assigned UUID of the Route Filter Policy to attach to the Equinix Connection", + } +} + +func connectionIdSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Equinix Assigned UUID of the Equinix Connection to attach the Route Filter Policy to", + } +} + +func paginationSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "offset": { + Type: schema.TypeInt, + Computed: true, + Description: "The page offset for the pagination request. Index of the first element. Default is 0.", + }, + "limit": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of elements to be requested per page. Number must be between 1 and 100. Default is 20", + }, + "total": { + Type: schema.TypeInt, + Computed: true, + Description: "Total number of elements returned.", + }, + "next": { + Type: schema.TypeString, + Computed: true, + Description: "URL relative to the last item in the response.", + }, + "previous": { + Type: schema.TypeString, + Computed: true, + Description: "URL relative to the first item in the response.", + }, + }, + } +} diff --git a/internal/resources/fabric/connection_route_filter/datasources_test.go b/internal/resources/fabric/connection_route_filter/datasources_test.go new file mode 100644 index 000000000..2636823ca --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/datasources_test.go @@ -0,0 +1,3 @@ +package connection_route_filter_test + +// Tested in resource_test.go because of the heavy resource setup constraints diff --git a/internal/resources/fabric/connection_route_filter/models.go b/internal/resources/fabric/connection_route_filter/models.go new file mode 100644 index 000000000..f59edd980 --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/models.go @@ -0,0 +1,67 @@ +package connection_route_filter + +import ( + "github.com/equinix/equinix-sdk-go/services/fabricv4" + equinix_schema "github.com/equinix/terraform-provider-equinix/internal/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func setConnectionRouteFilterMap(d *schema.ResourceData, connectionRouteFilter *fabricv4.ConnectionRouteFilterData) diag.Diagnostics { + diags := diag.Diagnostics{} + routeFilterMap := connectionRouteFilterResponseMap(connectionRouteFilter) + err := equinix_schema.SetMap(d, routeFilterMap) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func setConnectionRouteFilterData(d *schema.ResourceData, connectionRouteFilters *fabricv4.GetAllConnectionRouteFiltersResponse) diag.Diagnostics { + diags := diag.Diagnostics{} + mappedRouteFilters := make([]map[string]interface{}, len(connectionRouteFilters.Data)) + pagination := connectionRouteFilters.GetPagination() + if connectionRouteFilters.Data != nil { + for index, routeFilter := range connectionRouteFilters.Data { + mappedRouteFilters[index] = connectionRouteFilterResponseMap(&routeFilter) + } + } else { + mappedRouteFilters = nil + } + err := equinix_schema.SetMap(d, map[string]interface{}{ + "data": mappedRouteFilters, + "pagination": paginationGoToTerraform(&pagination), + }) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func connectionRouteFilterResponseMap(data *fabricv4.ConnectionRouteFilterData) map[string]interface{} { + connectionRouteFilterMap := make(map[string]interface{}) + connectionRouteFilterMap["href"] = data.GetHref() + connectionRouteFilterMap["type"] = string(data.GetType()) + connectionRouteFilterMap["uuid"] = data.GetUuid() + connectionRouteFilterMap["attachment_status"] = string(data.GetAttachmentStatus()) + connectionRouteFilterMap["direction"] = string(data.GetDirection()) + + return connectionRouteFilterMap +} + +func paginationGoToTerraform(pagination *fabricv4.Pagination) *schema.Set { + if pagination == nil { + return nil + } + mappedPagination := make(map[string]interface{}) + mappedPagination["offset"] = int(pagination.GetOffset()) + mappedPagination["limit"] = int(pagination.GetLimit()) + mappedPagination["total"] = int(pagination.GetTotal()) + mappedPagination["next"] = pagination.GetNext() + mappedPagination["previous"] = pagination.GetPrevious() + + return schema.NewSet( + schema.HashResource(paginationSchema()), + []interface{}{mappedPagination}, + ) +} diff --git a/internal/resources/fabric/connection_route_filter/resource.go b/internal/resources/fabric/connection_route_filter/resource.go new file mode 100644 index 000000000..b65e36c35 --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/resource.go @@ -0,0 +1,203 @@ +package connection_route_filter + +import ( + "context" + "log" + "strings" + "time" + + "github.com/equinix/terraform-provider-equinix/internal/config" + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func Resource() *schema.Resource { + return &schema.Resource{ + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + }, + ReadContext: resourceRead, + CreateContext: resourceCreate, + UpdateContext: resourceUpdate, + DeleteContext: resourceDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: resourceSchema(), + Description: `Fabric V4 API compatible resource allows attachment of Route Filter Polices to Fabric Connections + +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Interconnection/FCR/FCR-route-filters.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#route-filters`, + } +} + +func resourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionId := d.Get("connection_id").(string) + connectionRouteFilter, _, err := client.RouteFiltersApi.GetConnectionRouteFilterByUuid(ctx, d.Id(), connectionId).Execute() + if err != nil { + log.Printf("[WARN] Route Filter Policy %s not found on Connection %s, error %s", d.Id(), connectionId, err) + if !strings.Contains(err.Error(), "500") { + d.SetId("") + } + return diag.FromErr(equinix_errors.FormatFabricError(err)) + } + d.SetId(connectionRouteFilter.GetUuid()) + return setConnectionRouteFilterMap(d, connectionRouteFilter) +} + +func resourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionId := d.Get("connection_id").(string) + routeFilterId := d.Get("route_filter_id").(string) + direction := d.Get("direction").(string) + + start := time.Now() + routeFilter, _, err := client.RouteFiltersApi. + AttachConnectionRouteFilter(ctx, routeFilterId, connectionId). + ConnectionRouteFiltersBase( + fabricv4.ConnectionRouteFiltersBase{ + Direction: fabricv4.ConnectionRouteFiltersBaseDirection(direction), + }, + ).Execute() + if err != nil { + return diag.FromErr(equinix_errors.FormatFabricError(err)) + } + + if err = d.Set("connection_id", connectionId); err != nil { + return diag.Errorf("error setting connection_id to state %s", err) + } + d.SetId(routeFilter.GetUuid()) + + createTimeout := d.Timeout(schema.TimeoutCreate) - 30*time.Second - time.Since(start) + if err = waitForStability(connectionId, d.Id(), meta, d, ctx, createTimeout); err != nil { + return diag.Errorf("error waiting for route filter (%s) to be attached to connection (%s): %s", d.Id(), connectionId, err) + } + + return resourceRead(ctx, d, meta) +} + +func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionId := d.Get("connection_id").(string) + routeFilterId := d.Get("route_filter_id").(string) + oldDirection, newDirection := d.GetChange("direction") + if oldDirection.(string) == newDirection.(string) { + return diag.Diagnostics{} + } + + start := time.Now() + connectionRouteFilter, _, err := client.RouteFiltersApi. + AttachConnectionRouteFilter(ctx, routeFilterId, connectionId). + ConnectionRouteFiltersBase( + fabricv4.ConnectionRouteFiltersBase{ + Direction: fabricv4.ConnectionRouteFiltersBaseDirection(newDirection.(string)), + }, + ).Execute() + if err != nil { + return diag.FromErr(equinix_errors.FormatFabricError(err)) + } + + updateTimeout := d.Timeout(schema.TimeoutUpdate) - 30*time.Second - time.Since(start) + if err = waitForStability(routeFilterId, d.Id(), meta, d, ctx, updateTimeout); err != nil { + return diag.Errorf("error waiting for route filter policy (%s) on connection (%s) to be updated: %s", routeFilterId, connectionId, err) + } + + return setConnectionRouteFilterMap(d, connectionRouteFilter) +} + +func resourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionId := d.Get("connection_id").(string) + + start := time.Now() + _, _, err := client.RouteFiltersApi.DetachConnectionRouteFilter(ctx, d.Id(), connectionId).Execute() + if err != nil { + if genericError, ok := err.(*fabricv4.GenericOpenAPIError); ok { + if fabricErrs, ok := genericError.Model().([]fabricv4.Error); ok { + // EQ-3142509 = Connection already deleted + if equinix_errors.HasErrorCode(fabricErrs, "EQ-3142509") { + return diags + } + } + } + return diag.FromErr(equinix_errors.FormatFabricError(err)) + } + + deleteTimeout := d.Timeout(schema.TimeoutDelete) - 30*time.Second - time.Since(start) + if err = WaitForDeletion(connectionId, d.Id(), meta, d, ctx, deleteTimeout); err != nil { + return diag.Errorf("error waiting for route filter (%s) to be detached from connection (%s): %s", d.Id(), connectionId, err) + } + return diags +} + +func waitForStability(connectionId, routeFilterId string, meta interface{}, d *schema.ResourceData, ctx context.Context, timeout time.Duration) error { + log.Printf("Waiting for route filter policy (%x) attachment to connection (%s) to be stable", connectionId, routeFilterId) + stateConf := &retry.StateChangeConf{ + Pending: []string{ + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_ATTACHING), + }, + Target: []string{ + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_ATTACHED), + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_PENDING_BGP_CONFIGURATION), + }, + Refresh: func() (interface{}, string, error) { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionRouteFilter, _, err := client.RouteFiltersApi.GetConnectionRouteFilterByUuid(ctx, routeFilterId, connectionId).Execute() + if err != nil { + return "", "", equinix_errors.FormatFabricError(err) + } + return connectionRouteFilter, string(connectionRouteFilter.GetAttachmentStatus()), nil + }, + Timeout: timeout, + Delay: 30 * time.Second, + MinTimeout: 30 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} + +func WaitForDeletion(connectionId, routeFilterId string, meta interface{}, d *schema.ResourceData, ctx context.Context, timeout time.Duration) error { + log.Printf("Waiting for route filter policy (%s) to be detached from connection (%s)", routeFilterId, connectionId) + stateConf := &retry.StateChangeConf{ + Pending: []string{ + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_ATTACHED), + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_DETACHING), + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_PENDING_BGP_CONFIGURATION), + }, + Target: []string{ + string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_DETACHED), + }, + Refresh: func() (interface{}, string, error) { + client := meta.(*config.Config).NewFabricClientForSDK(d) + connectionRouteFilter, body, err := client.RouteFiltersApi.GetConnectionRouteFilterByUuid(ctx, routeFilterId, connectionId).Execute() + if err != nil { + if body.StatusCode >= 400 && body.StatusCode <= 499 { + // Already deleted resource + return connectionRouteFilter, string(fabricv4.ROUTEFILTERSTATE_DEPROVISIONED), nil + } + return "", "", equinix_errors.FormatFabricError(err) + } + return connectionRouteFilter, string(connectionRouteFilter.GetAttachmentStatus()), nil + }, + Timeout: timeout, + Delay: 30 * time.Second, + MinTimeout: 30 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} diff --git a/internal/resources/fabric/connection_route_filter/resource_schema.go b/internal/resources/fabric/connection_route_filter/resource_schema.go new file mode 100644 index 000000000..528628f7b --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/resource_schema.go @@ -0,0 +1,49 @@ +package connection_route_filter + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "connection_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Equinix Assigned UUID of the Equinix Connection to attach the Route Filter Policy to", + }, + "route_filter_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Equinix Assigned UUID of the Route Filter Policy to attach to the Equinix Connection", + }, + "direction": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"INBOUND", "OUTBOUND"}, false), + Description: "Direction of the filtering of the attached Route Filter Policy", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "Route Filter Type. One of [ \"BGP_IPv4_PREFIX_FILTER\", \"BGP_IPv6_PREFIX_FILTER\" ] ", + }, + "href": { + Type: schema.TypeString, + Computed: true, + Description: "URI to the attached Route Filter Policy on the Connection", + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + Description: "Equinix Assigned ID for Route Filter Policy", + }, + "attachment_status": { + Type: schema.TypeString, + Computed: true, + Description: "Status of the Route Filter Policy attachment lifecycle", + }, + } +} diff --git a/internal/resources/fabric/connection_route_filter/resource_test.go b/internal/resources/fabric/connection_route_filter/resource_test.go new file mode 100644 index 000000000..59463ce4d --- /dev/null +++ b/internal/resources/fabric/connection_route_filter/resource_test.go @@ -0,0 +1,196 @@ +package connection_route_filter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" + "github.com/equinix/terraform-provider-equinix/internal/fabric/testing_helpers" + "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/connection_route_filter" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccFabricConnectionRouteFilter_PFCR(t *testing.T) { + ports := testing_helpers.GetFabricEnvPorts(t) + var portUuid string + if len(ports) > 0 { + portUuid = ports["pfcr"]["dot1q"][0].GetUuid() + } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: CheckConnectionRouteFilterDelete, + Steps: []resource.TestStep{ + { + Config: testAccFabricConnectionRouteFilterConfig(portUuid), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("equinix_fabric_connection_route_filter.test", "id"), + resource.TestCheckResourceAttrSet("equinix_fabric_connection_route_filter.test", "connection_id"), + resource.TestCheckResourceAttrSet("equinix_fabric_connection_route_filter.test", "route_filter_id"), + + resource.TestCheckResourceAttr( + "equinix_fabric_connection_route_filter.test", "direction", "INBOUND"), + resource.TestCheckResourceAttr( + "equinix_fabric_connection_route_filter.test", "type", "BGP_IPv4_PREFIX_FILTER"), + resource.TestCheckResourceAttr( + "equinix_fabric_connection_route_filter.test", "attachment_status", string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_PENDING_BGP_CONFIGURATION)), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filter.test", "id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filter.test", "connection_id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filter.test", "route_filter_id"), + + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filter.test", "direction", "INBOUND"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filter.test", "type", "BGP_IPv4_PREFIX_FILTER"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filter.test", "attachment_status", string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_PENDING_BGP_CONFIGURATION)), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filters.test", "id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filters.test", "connection_id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_connection_route_filters.test", "data.0.uuid"), + + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filters.test", "data.0.direction", "INBOUND"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filters.test", "data.0.type", "BGP_IPv4_PREFIX_FILTER"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_connection_route_filters.test", "data.0.attachment_status", string(fabricv4.CONNECTIONROUTEFILTERDATAATTACHMENTSTATUS_PENDING_BGP_CONFIGURATION)), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) + +} + +func testAccFabricConnectionRouteFilterConfig(portUuid string) string { + return fmt.Sprintf(` + resource "equinix_fabric_cloud_router" "test" { + type = "XF_ROUTER" + name = "RF_CR_PFCR" + location { + metro_code = "DC" + } + package { + code = "STANDARD" + } + order { + purchase_order_number = "1-234567" + } + notifications { + type = "ALL" + emails = [ + "test@equinix.com", + "test1@equinix.com" + ] + } + project { + project_id = "291639000636552" + } + account { + account_number = 201257 + } + } + + resource "equinix_fabric_connection" "test" { + type = "IP_VC" + name = "RF_CR_Connection_PFCR" + notifications { + type = "ALL" + emails = ["test@equinix.com","test1@equinix.com"] + } + order { + purchase_order_number = "123485" + } + bandwidth = 50 + redundancy { + priority= "PRIMARY" + } + a_side { + access_point { + type = "CLOUD_ROUTER" + router { + uuid = equinix_fabric_cloud_router.test.id + } + } + } + project { + project_id = "291639000636552" + } + z_side { + access_point { + type = "COLO" + port{ + uuid = "%s" + } + link_protocol { + type= "DOT1Q" + vlan_tag= 2571 + } + location { + metro_code = "DC" + } + } + } + } + + resource "equinix_fabric_route_filter" "test" { + name = "rf_test_PFCR" + project { + project_id = "291639000636552" + } + type = "BGP_IPv4_PREFIX_FILTER" + description = "Route Filter Policy for X Purpose" + } + + resource "equinix_fabric_route_filter_rule" "test" { + route_filter_id = equinix_fabric_route_filter.test.id + name = "RF_Rule_PFCR" + prefix = "192.168.0.0/24" + prefix_match = "exact" + description = "Route Filter Rule for X Purpose" + } + + resource "equinix_fabric_connection_route_filter" "test" { + depends_on = [ equinix_fabric_route_filter_rule.test ] + connection_id = equinix_fabric_connection.test.id + route_filter_id = equinix_fabric_route_filter.test.id + direction = "INBOUND" + } + + data "equinix_fabric_connection_route_filter" "test" { + depends_on = [ equinix_fabric_connection_route_filter.test ] + connection_id = equinix_fabric_connection.test.id + route_filter_id = equinix_fabric_route_filter.test.id + } + + data "equinix_fabric_connection_route_filters" "test" { + depends_on = [ equinix_fabric_connection_route_filter.test ] + connection_id = equinix_fabric_connection.test.id + } + + `, portUuid) +} + +func CheckConnectionRouteFilterDelete(s *terraform.State) error { + ctx := context.Background() + for _, rs := range s.RootModule().Resources { + if rs.Type != "equinix_fabric_connection_route_filter" { + continue + } + + connectionId := rs.Primary.Attributes["connection_id"] + + err := connection_route_filter.WaitForDeletion(connectionId, rs.Primary.ID, acceptance.TestAccProvider.Meta(), &schema.ResourceData{}, ctx, 10*time.Minute) + if err != nil { + return fmt.Errorf("API call failed while waiting for resource deletion") + } + } + return nil +}