diff --git a/docs/data-sources/fabric_route_aggregation.md b/docs/data-sources/fabric_route_aggregation.md new file mode 100644 index 000000000..042bc66c4 --- /dev/null +++ b/docs/data-sources/fabric_route_aggregation.md @@ -0,0 +1,95 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_route_aggregation (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Route Aggregation by UUID +Additional Documentation: +* API: https://developer.equinix.com/catalog/fabricv4#tag/Route-Aggregations + +## Example Usage + +```terraform +data "equinix_fabric_route_aggregation" "ra_policy" { + uuid = "" +} + +output "id" { + value = data.equinix_fabric_route_aggregation.ra_policy.id +} + +output "type" { + value = data.equinix_fabric_route_aggregation.ra_policy.type +} + +output "state" { + value = data.equinix_fabric_route_aggregation.ra_policy.state +} + +output "connections_count" { + value = data.equinix_fabric_route_aggregation.ra_policy.connections_count +} + +output "rules_count" { + value = data.equinix_fabric_route_aggregation.ra_policy.rules_count +} +``` + + +## Schema + +### Required + +- `route_aggregation_id` (String) The uuid of the route aggregation this data source should retrieve + +### Read-Only + +- `change` (Attributes) Current state of latest Route Aggregation change (see [below for nested schema](#nestedatt--change)) +- `change_log` (Attributes) Details of the last change on the route aggregation resource (see [below for nested schema](#nestedatt--change_log)) +- `connections_count` (Number) Number of Connections attached to route aggregation +- `description` (String) Customer-provided route aggregation description +- `href` (String) Equinix auto generated URI to the route aggregation resource +- `id` (String) The unique identifier of the resource +- `name` (String) Customer provided name of the route aggregation +- `project` (Attributes) Equinix Project attribute object (see [below for nested schema](#nestedatt--project)) +- `rules_count` (Number) Number of Rules attached to route aggregation +- `state` (String) Value representing provisioning status for the route aggregation resource +- `type` (String) Equinix defined Route Aggregation Type; BGP_IPv4_PREFIX_AGGREGATION, BGP_IPv6_PREFIX_AGGREGATION +- `uuid` (String) Equinix-assigned unique id for the route aggregation resource + + +### Nested Schema for `change` + +Read-Only: + +- `href` (String) Equinix auto generated URI to the route aggregation change +- `type` (String) Equinix defined Route Aggregation Change Type +- `uuid` (String) Equinix-assigned unique id for a change + + + +### Nested Schema for `change_log` + +Read-Only: + +- `created_by` (String) User name of creator of the route aggregation resource +- `created_by_email` (String) Email of creator of the route aggregation resource +- `created_by_full_name` (String) Legal name of creator of the route aggregation resource +- `created_date_time` (String) Creation time of the route aggregation resource +- `deleted_by` (String) User name of deleter of the route aggregation resource +- `deleted_by_email` (String) Email of deleter of the route aggregation resource +- `deleted_by_full_name` (String) Legal name of deleter of the route aggregation resource +- `deleted_date_time` (String) Deletion time of the route aggregation resource +- `updated_by` (String) User name of last updater of the route aggregation resource +- `updated_by_email` (String) Email of last updater of the route aggregation resource +- `updated_by_full_name` (String) Legal name of last updater of the route aggregation resource +- `updated_date_time` (String) Last update time of the route aggregation resource + + + +### Nested Schema for `project` + +Read-Only: + +- `project_id` (String) Equinix Subscriber-assigned project ID diff --git a/docs/data-sources/fabric_route_aggregations.md b/docs/data-sources/fabric_route_aggregations.md new file mode 100644 index 000000000..4a4bd5c74 --- /dev/null +++ b/docs/data-sources/fabric_route_aggregations.md @@ -0,0 +1,146 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_route_aggregations (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Route Aggregations with pagination details +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/KnowledgeCenter/Fabric/GettingStarted/Integrating-with-Fabric-V4-APIs/IntegrateWithSink.htm +* API: https://developer.equinix.com/catalog/fabricv4#tag/Streams + +## Example Usage + +```terraform +data "equinix_fabric_route_aggregations" "ra_policy" { + filter = { + property = "/project/projectId" + operator = "=" + values = [""] + } + pagination = { + limit = 2 + offset = 1 + } +} + +output "first_route_aggregation_name" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.name +} + +output "first_route_aggregation_description" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.description +} + +output "first_route_aggregation_connections_count" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.connections_count +} + +output "first_route_aggregation_rules_count" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.rules_count +} +``` + + +## Schema + +### Required + +- `filter` (Attributes) Filters for the Data Source Search Request (see [below for nested schema](#nestedatt--filter)) + +### Optional + +- `pagination` (Attributes) Pagination details for the returned route aggregations list (see [below for nested schema](#nestedatt--pagination)) +- `sort` (Attributes) Filters for the Data Source Search Request (see [below for nested schema](#nestedatt--sort)) + +### Read-Only + +- `data` (Attributes List) Returned list of route aggregation objects (see [below for nested schema](#nestedatt--data)) +- `id` (String) The unique identifier of the resource + + +### Nested Schema for `filter` + +Required: + +- `operator` (String) Operators to use on your filtered field with the values given. One of [ =, !=, >, >=, <, <=, BETWEEN, NOT BETWEEN, LIKE, NOT LIKE, IN, NOT IN, IS NOT NULL, IS NULL] +- `property` (String) possible field names to use on filters. One of [/type /name /project/projectId /uuid /state] +- `values` (List of String) The values that you want to apply the property+operator combination to in order to filter your data search + + + +### Nested Schema for `pagination` + +Optional: + +- `limit` (Number) Maximum number of search results returned per page. Number must be between 1 and 100, and the default is 20 +- `offset` (Number) Index of the first item returned in the response. The default is 0 + +Read-Only: + +- `next` (String) The URL relative to the next item in the response +- `previous` (String) The URL relative to the previous item in the response +- `total` (Number) The total number of route aggregations available to the user making the request + + + +### Nested Schema for `sort` + +Optional: + +- `direction` (String) The sorting direction. Can be one of: [DESC, ASC], Defaults to DESC +- `property` (String) The property name to use in sorting. One of [/type /name /project/projectId /uuid /state] Defaults to /name + + + +### Nested Schema for `data` + +Read-Only: + +- `change` (Attributes) Current state of latest Route Aggregation change (see [below for nested schema](#nestedatt--data--change)) +- `change_log` (Attributes) Details of the last change on the route aggregation resource (see [below for nested schema](#nestedatt--data--change_log)) +- `connections_count` (Number) Number of Connections attached to route aggregation +- `description` (String) Customer-provided route aggregation description +- `href` (String) Equinix auto generated URI to the route aggregation resource +- `name` (String) Customer provided name of the route aggregation +- `project` (Attributes) Equinix Project attribute object (see [below for nested schema](#nestedatt--data--project)) +- `rules_count` (Number) Number of Rules attached to route aggregation +- `state` (String) Value representing provisioning status for the route aggregation resource +- `type` (String) Equinix defined Route Aggregation Type; BGP_IPv4_PREFIX_AGGREGATION, BGP_IPv6_PREFIX_AGGREGATION +- `uuid` (String) Equinix-assigned unique id for the route aggregation resource + + +### Nested Schema for `data.change` + +Read-Only: + +- `href` (String) Equinix auto generated URI to the route aggregation change +- `type` (String) Equinix defined Route Aggregation Change Type +- `uuid` (String) Equinix-assigned unique id for a change + + + +### Nested Schema for `data.change_log` + +Read-Only: + +- `created_by` (String) User name of creator of the route aggregation resource +- `created_by_email` (String) Email of creator of the route aggregation resource +- `created_by_full_name` (String) Legal name of creator of the route aggregation resource +- `created_date_time` (String) Creation time of the route aggregation resource +- `deleted_by` (String) User name of deleter of the route aggregation resource +- `deleted_by_email` (String) Email of deleter of the route aggregation resource +- `deleted_by_full_name` (String) Legal name of deleter of the route aggregation resource +- `deleted_date_time` (String) Deletion time of the route aggregation resource +- `updated_by` (String) User name of last updater of the route aggregation resource +- `updated_by_email` (String) Email of last updater of the route aggregation resource +- `updated_by_full_name` (String) Legal name of last updater of the route aggregation resource +- `updated_date_time` (String) Last update time of the route aggregation resource + + + +### Nested Schema for `data.project` + +Read-Only: + +- `project_id` (String) Equinix Subscriber-assigned project ID diff --git a/docs/resources/fabric_route_aggregation.md b/docs/resources/fabric_route_aggregation.md new file mode 100644 index 000000000..7e033e150 --- /dev/null +++ b/docs/resources/fabric_route_aggregation.md @@ -0,0 +1,95 @@ +--- +subcategory: "Fabric" +--- + +# equinix_fabric_route_aggregation (Resource) + +Fabric V4 API compatible resource allows creation and management of Equinix Fabric Route Aggregation + +Additional Documentation: +* API: https://developer.equinix.com/catalog/fabricv4#tag/Route-Aggregations + +## Example Usage + +```terraform +resource "equinix_fabric_route_aggregation" "new-ra" { + type = "BGP_IPv4_PREFIX_AGGREGATION" + name = "new-ra" + description = "Test aggregation" + project = { + project_id = "776847000642406" + } +} +``` + + +## Schema + +### Required + +- `name` (String) Customer provided name of the route aggregation +- `project` (Attributes) Equinix Project attribute object (see [below for nested schema](#nestedatt--project)) +- `type` (String) Equinix defined Route Aggregation Type; BGP_IPv4_PREFIX_AGGREGATION, BGP_IPv6_PREFIX_AGGREGATION + +### Optional + +- `description` (String) Customer-provided route aggregation description +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `change` (Attributes) Current state of latest Route Aggregation change (see [below for nested schema](#nestedatt--change)) +- `change_log` (Attributes) Details of the last change on the route aggregation resource (see [below for nested schema](#nestedatt--change_log)) +- `connections_count` (Number) Number of Connections attached to route aggregation +- `href` (String) Equinix auto generated URI to the route aggregation resource +- `id` (String) The unique identifier of the resource +- `rules_count` (Number) Number of Rules attached to route aggregation +- `state` (String) Value representing provisioning status for the route aggregation resource +- `uuid` (String) Equinix-assigned unique id for the route aggregation resource + + +### Nested Schema for `project` + +Required: + +- `project_id` (String) Equinix Subscriber-assigned project ID + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `change` + +Required: + +- `href` (String) Equinix auto generated URI to the route aggregation change +- `type` (String) Equinix defined Route Aggregation Change Type +- `uuid` (String) Equinix-assigned unique id for a change + + + +### Nested Schema for `change_log` + +Read-Only: + +- `created_by` (String) User name of creator of the route aggregation resource +- `created_by_email` (String) Email of creator of the route aggregation resource +- `created_by_full_name` (String) Legal name of creator of the route aggregation resource +- `created_date_time` (String) Creation time of the route aggregation resource +- `deleted_by` (String) User name of deleter of the route aggregation resource +- `deleted_by_email` (String) Email of deleter of the route aggregation resource +- `deleted_by_full_name` (String) Legal name of deleter of the route aggregation resource +- `deleted_date_time` (String) Deletion time of the route aggregation resource +- `updated_by` (String) User name of last updater of the route aggregation resource +- `updated_by_email` (String) Email of last updater of the route aggregation resource +- `updated_by_full_name` (String) Legal name of last updater of the route aggregation resource +- `updated_date_time` (String) Last update time of the route aggregation resource diff --git a/examples/data-sources/equinix_fabric_route_aggregation/data-source.tf b/examples/data-sources/equinix_fabric_route_aggregation/data-source.tf new file mode 100644 index 000000000..52dbf78bf --- /dev/null +++ b/examples/data-sources/equinix_fabric_route_aggregation/data-source.tf @@ -0,0 +1,23 @@ +data "equinix_fabric_route_aggregation" "ra_policy" { + uuid = "" +} + +output "id" { + value = data.equinix_fabric_route_aggregation.ra_policy.id +} + +output "type" { + value = data.equinix_fabric_route_aggregation.ra_policy.type +} + +output "state" { + value = data.equinix_fabric_route_aggregation.ra_policy.state +} + +output "connections_count" { + value = data.equinix_fabric_route_aggregation.ra_policy.connections_count +} + +output "rules_count" { + value = data.equinix_fabric_route_aggregation.ra_policy.rules_count +} diff --git a/examples/data-sources/equinix_fabric_route_aggregations/data-source.tf b/examples/data-sources/equinix_fabric_route_aggregations/data-source.tf new file mode 100644 index 000000000..e898d3c68 --- /dev/null +++ b/examples/data-sources/equinix_fabric_route_aggregations/data-source.tf @@ -0,0 +1,27 @@ +data "equinix_fabric_route_aggregations" "ra_policy" { + filter = { + property = "/project/projectId" + operator = "=" + values = [""] + } + pagination = { + limit = 2 + offset = 1 + } +} + +output "first_route_aggregation_name" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.name +} + +output "first_route_aggregation_description" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.description +} + +output "first_route_aggregation_connections_count" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.connections_count +} + +output "first_route_aggregation_rules_count" { + value = data.equinix_fabric_route_aggregations.ra_policy.data.0.rules_count +} diff --git a/examples/resources/equinix_fabric_route_aggregation/resource.tf b/examples/resources/equinix_fabric_route_aggregation/resource.tf new file mode 100644 index 000000000..afab27b8f --- /dev/null +++ b/examples/resources/equinix_fabric_route_aggregation/resource.tf @@ -0,0 +1,8 @@ +resource "equinix_fabric_route_aggregation" "new-ra" { + type = "BGP_IPv4_PREFIX_AGGREGATION" + name = "new-ra" + description = "Test aggregation" + project = { + project_id = "776847000642406" + } +} diff --git a/go.mod b/go.mod index 77da9006e..5e7c7aaf6 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/equinix/terraform-provider-equinix -go 1.23 +go 1.23.0 require ( - github.com/equinix/equinix-sdk-go v0.49.0 + github.com/equinix/equinix-sdk-go v0.49.1 github.com/equinix/ne-go v1.19.0 github.com/equinix/oauth2-go v1.0.0 github.com/equinix/rest-go v1.3.0 diff --git a/go.sum b/go.sum index d2bb35283..67854ed6a 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/equinix/equinix-sdk-go v0.49.0 h1:7VNYQSHL4+m6v/dqH71+pIKNbU5KmTzOLaDovQGuHF8= -github.com/equinix/equinix-sdk-go v0.49.0/go.mod h1:tUf2f7SQyztsdCnI0pu2cVTGQvZvmFi6trEvVjdMyvY= +github.com/equinix/equinix-sdk-go v0.49.1 h1:dMiIZX2O+re3D5zjzTddanvtf51M9XYenaTaSQS4t9A= +github.com/equinix/equinix-sdk-go v0.49.1/go.mod h1:tUf2f7SQyztsdCnI0pu2cVTGQvZvmFi6trEvVjdMyvY= github.com/equinix/ne-go v1.19.0 h1:aueOpe8yh/t3/mjhc/R3ZGAIPYSvixNQ/fU3t2HUQZk= github.com/equinix/ne-go v1.19.0/go.mod h1:eHkkxM4nbTB7DZ9X9zGnwfYnxIJWIsU3aHA+FAoZ1EI= github.com/equinix/oauth2-go v1.0.0 h1:fHtAPGq82PdgtK5vEThs8Vwz6f7D/8SX4tE3NJu+KcU= diff --git a/internal/provider/services/fabric.go b/internal/provider/services/fabric.go index 9df1dc4b6..a2e1a687c 100644 --- a/internal/provider/services/fabric.go +++ b/internal/provider/services/fabric.go @@ -2,7 +2,9 @@ package services import ( "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/metro" + "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/routeaggregation" "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/stream" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -10,6 +12,7 @@ import ( func FabricResources() []func() resource.Resource { return []func() resource.Resource{ stream.NewResource, + routeaggregation.NewResource, } } @@ -17,6 +20,8 @@ func FabricDatasources() []func() datasource.DataSource { return []func() datasource.DataSource{ metro.NewDataSourceMetroCode, metro.NewDataSourceMetros, + routeaggregation.NewDataSourceByRouteAggregationID, + routeaggregation.NewDataSourceAllRouteAggregation, stream.NewDataSourceByStreamID, stream.NewDataSourceAllStreams, } diff --git a/internal/resources/fabric/routeaggregation/datasource_all_aggregations.go b/internal/resources/fabric/routeaggregation/datasource_all_aggregations.go new file mode 100644 index 000000000..76a3adc88 --- /dev/null +++ b/internal/resources/fabric/routeaggregation/datasource_all_aggregations.go @@ -0,0 +1,130 @@ +package routeaggregation + +import ( + "context" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + 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/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func NewDataSourceAllRouteAggregation() datasource.DataSource { + return &DataSourceAllRouteAggregations{ + BaseDataSource: framework.NewBaseDataSource( + framework.BaseDataSourceConfig{ + Name: "equinix_fabric_route_aggregations", + }, + ), + } +} + +type DataSourceAllRouteAggregations struct { + framework.BaseDataSource +} + +func (r *DataSourceAllRouteAggregations) Schema( + ctx context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = dataSourceAllRouteAggregationsSchema(ctx) +} + +func (r *DataSourceAllRouteAggregations) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + client := r.Meta.NewFabricClientForFramework(ctx, request.ProviderMeta) + + var data DatsSourceAllRouteAggregationsModel + + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + var tffilter FilterModel + + diags := data.Filter.As(ctx, &tffilter, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return + } + values := []string{} + if len(tffilter.Values) > 0 { + for _, strVal := range tffilter.Values { + if !strVal.IsNull() && !strVal.IsUnknown() { + values = append(values, strVal.ValueString()) + } + } + } + + propertyValue := fabricv4.RouteFiltersSearchFilterItemProperty(tffilter.Property.ValueString()) + + filterItem := fabricv4.RouteAggregationsSearchFilterItem{ + Property: &propertyValue, + } + + if !tffilter.Operator.IsNull() && !tffilter.Operator.IsUnknown() { + filterItem.Operator = tffilter.Operator.ValueStringPointer() + } + + if len(values) > 0 { + filterItem.Values = values + } + + filter := fabricv4.RouteAggregationsSearchBaseFilter{ + And: []fabricv4.RouteAggregationsSearchFilterItem{filterItem}, + } + + var tfpagination PaginationModel + diags = data.Pagination.As(ctx, &tfpagination, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return + } + offset := tfpagination.Offset.ValueInt32() + limit := tfpagination.Limit.ValueInt32() + if limit == 0 { + limit = 20 + } + + pagination := fabricv4.Pagination{ + Offset: &offset, + Limit: limit, + } + + var tfsort SortModel + diags = data.Sort.As(ctx, &tfsort, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return + } + direction := tfsort.Direction.ValueString() + property := tfsort.Property.ValueString() + + pValue := fabricv4.RouteAggregationSortItemProperty(property) + dValue := fabricv4.SortItemDirection(direction) + + sort := fabricv4.RouteAggregationSortItem{ + Property: &pValue, + Direction: &dValue, + } + + routeAggregationsSearch := fabricv4.RouteAggregationsSearchBase{ + Filter: &filter, + Pagination: &pagination, + Sort: []fabricv4.RouteAggregationSortItem{sort}, + } + + routeAggregations, _, err := client.RouteAggregationsApi.SearchRouteAggregations(ctx).RouteAggregationsSearchBase(routeAggregationsSearch).Execute() + + if err != nil { + response.State.RemoveResource(ctx) + response.Diagnostics.AddError("api error retrieving route aggregations data", equinix_errors.FormatFabricError(err).Error()) + return + } + + response.Diagnostics.Append(data.parse(ctx, routeAggregations)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} diff --git a/internal/resources/fabric/routeaggregation/datasource_by_routeAggregationId.go b/internal/resources/fabric/routeaggregation/datasource_by_routeAggregationId.go new file mode 100644 index 000000000..b80ecce2c --- /dev/null +++ b/internal/resources/fabric/routeaggregation/datasource_by_routeAggregationId.go @@ -0,0 +1,59 @@ +package routeaggregation + +import ( + "context" + + 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" +) + +func NewDataSourceByRouteAggregationID() datasource.DataSource { + return &DataSourceByRouteAggregationID{ + BaseDataSource: framework.NewBaseDataSource( + framework.BaseDataSourceConfig{ + Name: "equinix_fabric_route_aggregation", + }, + ), + } +} + +type DataSourceByRouteAggregationID struct { + framework.BaseDataSource +} + +func (r *DataSourceByRouteAggregationID) Schema( + ctx context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = dataSourceSingleRouteAggregationSchema(ctx) +} + +func (r *DataSourceByRouteAggregationID) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + client := r.Meta.NewFabricClientForFramework(ctx, request.ProviderMeta) + + var data DataSourceByIDModel + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + routeAggregationID := data.RouteAggregationID.ValueString() + + routeAggregation, _, err := client.RouteAggregationsApi.GetRouteAggregationByUuid(ctx, routeAggregationID).Execute() + + if err != nil { + response.State.RemoveResource(ctx) + response.Diagnostics.AddError("api error retrieving route aggregation data", equinix_errors.FormatFabricError(err).Error()) + return + } + + response.Diagnostics.Append(data.parse(ctx, routeAggregation)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) + +} diff --git a/internal/resources/fabric/routeaggregation/datasource_schema.go b/internal/resources/fabric/routeaggregation/datasource_schema.go new file mode 100644 index 000000000..1c274df75 --- /dev/null +++ b/internal/resources/fabric/routeaggregation/datasource_schema.go @@ -0,0 +1,237 @@ +package routeaggregation + +import ( + "context" + "fmt" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/equinix/terraform-provider-equinix/internal/framework" + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func dataSourceAllRouteAggregationsSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Description: `Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Route Aggregations with pagination details +Additional Documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/KnowledgeCenter/Fabric/GettingStarted/Integrating-with-Fabric-V4-APIs/IntegrateWithSink.htm +* API: https://developer.equinix.com/catalog/fabricv4#tag/Streams`, + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttributeDefaultDescription(), + "data": schema.ListNestedAttribute{ + Description: "Returned list of route aggregation objects", + Computed: true, + CustomType: fwtypes.NewListNestedObjectTypeOf[BaseRouteAggregationModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: getRouteAggregationSchema(ctx), + }, + }, + "filter": schema.SingleNestedAttribute{ + Description: "Filters for the Data Source Search Request", + Required: true, + Attributes: map[string]schema.Attribute{ + "property": schema.StringAttribute{ + Description: fmt.Sprintf("possible field names to use on filters. One of %v", fabricv4.AllowedRouteFiltersSearchFilterItemPropertyEnumValues), + Required: true, + }, + "operator": schema.StringAttribute{ + Description: "Operators to use on your filtered field with the values given. One of [ =, !=, >, >=, <, <=, BETWEEN, NOT BETWEEN, LIKE, NOT LIKE, IN, NOT IN, IS NOT NULL, IS NULL]", + Required: true, + }, + "values": schema.ListAttribute{ + Description: "The values that you want to apply the property+operator combination to in order to filter your data search", + ElementType: types.StringType, + Required: true, + }, + }, + }, + "pagination": schema.SingleNestedAttribute{ + Description: "Pagination details for the returned route aggregations list", + Optional: true, + CustomType: fwtypes.NewObjectTypeOf[PaginationModel](ctx), + Attributes: map[string]schema.Attribute{ + "offset": schema.Int32Attribute{ + Description: "Index of the first item returned in the response. The default is 0", + Optional: true, + Computed: true, + }, + "limit": schema.Int32Attribute{ + Description: "Maximum number of search results returned per page. Number must be between 1 and 100, and the default is 20", + Optional: true, + Computed: true, + }, + "total": schema.Int32Attribute{ + Description: "The total number of route aggregations available to the user making the request", + Computed: true, + }, + "next": schema.StringAttribute{ + Description: "The URL relative to the next item in the response", + Computed: true, + }, + "previous": schema.StringAttribute{ + Description: "The URL relative to the previous item in the response", + Computed: true, + }, + }, + }, + "sort": schema.SingleNestedAttribute{ + Description: "Filters for the Data Source Search Request", + Optional: true, + CustomType: fwtypes.NewObjectTypeOf[SortModel](ctx), + Attributes: map[string]schema.Attribute{ + "direction": schema.StringAttribute{ + Description: "The sorting direction. Can be one of: [DESC, ASC], Defaults to DESC", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("DESC", "ASC"), + }, + }, + "property": schema.StringAttribute{ + Description: fmt.Sprintf("The property name to use in sorting. One of %v Defaults to /name", fabricv4.AllowedRouteFiltersSearchFilterItemPropertyEnumValues), + Optional: true, + }, + }, + }, + }, + } +} +func dataSourceSingleRouteAggregationSchema(ctx context.Context) schema.Schema { + baseRouteAggregationSchema := getRouteAggregationSchema(ctx) + baseRouteAggregationSchema["id"] = framework.IDAttributeDefaultDescription() + baseRouteAggregationSchema["route_aggregation_id"] = schema.StringAttribute{ + Description: "The uuid of the route aggregation this data source should retrieve", + Required: true, + } + return schema.Schema{ + Description: `Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Route Aggregation by UUID +Additional Documentation: +* API: https://developer.equinix.com/catalog/fabricv4#tag/Route-Aggregations`, + Attributes: baseRouteAggregationSchema, + } + +} + +func getRouteAggregationSchema(ctx context.Context) map[string]schema.Attribute { + return map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: "Equinix defined Route Aggregation Type; BGP_IPv4_PREFIX_AGGREGATION, BGP_IPv6_PREFIX_AGGREGATION", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Customer provided name of the route aggregation", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Customer-provided route aggregation description", + Computed: true, + }, + "project": schema.SingleNestedAttribute{ + Description: "Equinix Project attribute object", + Computed: true, + CustomType: fwtypes.NewObjectTypeOf[ProjectModel](ctx), + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "Equinix Subscriber-assigned project ID", + Computed: true, + }, + }, + }, + "href": schema.StringAttribute{ + Description: "Equinix auto generated URI to the route aggregation resource", + Computed: true, + }, + "uuid": schema.StringAttribute{ + Description: "Equinix-assigned unique id for the route aggregation resource", + Computed: true, + }, + "state": schema.StringAttribute{ + Description: "Value representing provisioning status for the route aggregation resource", + Computed: true, + }, + "change": schema.SingleNestedAttribute{ + Description: "Current state of latest Route Aggregation change", + Computed: true, + CustomType: fwtypes.NewObjectTypeOf[ChangeModel](ctx), + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Description: "Equinix-assigned unique id for a change", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Equinix defined Route Aggregation Change Type", + Computed: true, + }, + "href": schema.StringAttribute{ + Description: "Equinix auto generated URI to the route aggregation change", + Computed: true, + }, + }, + }, + "connections_count": schema.Int32Attribute{ + Description: "Number of Connections attached to route aggregation", + Computed: true, + }, + "rules_count": schema.Int32Attribute{ + Description: "Number of Rules attached to route aggregation", + Computed: true, + }, + "change_log": schema.SingleNestedAttribute{ + Description: "Details of the last change on the route aggregation resource", + Computed: true, + CustomType: fwtypes.NewObjectTypeOf[ChangeLogModel](ctx), + Attributes: map[string]schema.Attribute{ + "created_by": schema.StringAttribute{ + Description: "User name of creator of the route aggregation resource", + Computed: true, + }, + "created_by_full_name": schema.StringAttribute{ + Description: "Legal name of creator of the route aggregation resource", + Computed: true, + }, + "created_by_email": schema.StringAttribute{ + Description: "Email of creator of the route aggregation resource", + Computed: true, + }, + "created_date_time": schema.StringAttribute{ + Description: "Creation time of the route aggregation resource", + Computed: true, + }, + "updated_by": schema.StringAttribute{ + Description: "User name of last updater of the route aggregation resource", + Computed: true, + }, + "updated_by_full_name": schema.StringAttribute{ + Description: "Legal name of last updater of the route aggregation resource", + Computed: true, + }, + "updated_by_email": schema.StringAttribute{ + Description: "Email of last updater of the route aggregation resource", + Computed: true, + }, + "updated_date_time": schema.StringAttribute{ + Description: "Last update time of the route aggregation resource", + Computed: true, + }, + "deleted_by": schema.StringAttribute{ + Description: "User name of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_by_full_name": schema.StringAttribute{ + Description: "Legal name of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_by_email": schema.StringAttribute{ + Description: "Email of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_date_time": schema.StringAttribute{ + Description: "Deletion time of the route aggregation resource", + Computed: true, + }, + }, + }, + } +} diff --git a/internal/resources/fabric/routeaggregation/datasource_test.go b/internal/resources/fabric/routeaggregation/datasource_test.go new file mode 100644 index 000000000..cc5bdf19b --- /dev/null +++ b/internal/resources/fabric/routeaggregation/datasource_test.go @@ -0,0 +1,96 @@ +package routeaggregation_test + +import ( + "fmt" + "testing" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func testAccFabricRouteAggregationDataSourcesConfig(name, description string) string { + return fmt.Sprintf(` + + resource "equinix_fabric_route_aggregation" "new_ra_1" { + type = "BGP_IPv4_PREFIX_AGGREGATION" + name = "%[1]s" + description = "%[2]s" + project = { + project_id = "4f855852-eb47-4721-8e40-b386a3676abf" + } + } + + resource "equinix_fabric_route_aggregation" "new_ra_2" { + type = "BGP_IPv4_PREFIX_AGGREGATION" + name = "%[1]s" + description = "%[2]s" + project = { + project_id = "4f855852-eb47-4721-8e40-b386a3676abf" + } + } + + data "equinix_fabric_route_aggregation" "data_ra" { + route_aggregation_id = equinix_fabric_route_aggregation.new_ra_2.id + } + + data "equinix_fabric_route_aggregations" "data_ras" { + depends_on = [equinix_fabric_route_aggregation.new_ra_1, equinix_fabric_route_aggregation.new_ra_2] + filter = { + property = "/type" + operator = "=" + values = ["BGP_IPv4_PREFIX_AGGREGATION"] + } + pagination = { + limit = 2 + offset = 1 + } + sort = { + property = "/changeLog/updatedDateTime" + direction = "DESC" + } + } + `, name, description) +} + +func TestAccFabricRouteAggregationDataSources_PFCR(t *testing.T) { + routeAggregationName := "route_agg_PFCR" + routeAggregatioDescription := "route_agg_PFCR" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + CheckDestroy: CheckRouteAggregationDelete, + Steps: []resource.TestStep{ + { + Config: testAccFabricRouteAggregationDataSourcesConfig(routeAggregationName, routeAggregatioDescription), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.equinix_fabric_route_aggregation.data_ra", "name", routeAggregationName), + resource.TestCheckResourceAttr( + "data.equinix_fabric_route_aggregation.data_ra", "type", "BGP_IPv4_PREFIX_AGGREGATION"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_route_aggregation.data_ra", "project.project_id", "4f855852-eb47-4721-8e40-b386a3676abf"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_route_aggregation.data_ra", "description", routeAggregatioDescription), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregation.data_ra", "href"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregation.data_ra", "uuid"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregation.data_ra", "change_log.created_by"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.name"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.type"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.description"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.project.project_id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.href"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.connections_count"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.rules_count"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.uuid"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_route_aggregations.data_ras", "data.0.change_log.created_by"), + resource.TestCheckResourceAttr("data.equinix_fabric_route_aggregations.data_ras", "data.#", "2"), + resource.TestCheckResourceAttr("data.equinix_fabric_route_aggregations.data_ras", "pagination.%", "5"), + resource.TestCheckResourceAttr("data.equinix_fabric_route_aggregations.data_ras", "pagination.limit", "2"), + resource.TestCheckResourceAttr("data.equinix_fabric_route_aggregations.data_ras", "pagination.offset", "1"), + ), + }, + }, + }) + +} diff --git a/internal/resources/fabric/routeaggregation/models.go b/internal/resources/fabric/routeaggregation/models.go new file mode 100644 index 000000000..1b956bb0f --- /dev/null +++ b/internal/resources/fabric/routeaggregation/models.go @@ -0,0 +1,184 @@ +package routeaggregation + +import ( + "context" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type DataSourceByIDModel struct { + RouteAggregationID types.String `tfsdk:"route_aggregation_id"` + ID types.String `tfsdk:"id"` + BaseRouteAggregationModel +} + +type DatsSourceAllRouteAggregationsModel struct { + ID types.String `tfsdk:"id"` + Data fwtypes.ListNestedObjectValueOf[BaseRouteAggregationModel] `tfsdk:"data"` + Filter types.Object `tfsdk:"filter"` + Pagination fwtypes.ObjectValueOf[PaginationModel] `tfsdk:"pagination"` + Sort fwtypes.ObjectValueOf[SortModel] `tfsdk:"sort"` +} + +type FilterModel struct { + Property types.String `tfsdk:"property"` + Operator types.String `tfsdk:"operator"` + Values []types.String `tfsdk:"values"` +} + +type PaginationModel struct { + Offset types.Int32 `tfsdk:"offset"` + Limit types.Int32 `tfsdk:"limit"` + Total types.Int32 `tfsdk:"total"` + Next types.String `tfsdk:"next"` + Previous types.String `tfsdk:"previous"` +} + +type SortModel struct { + Direction types.String `tfsdk:"direction"` + Property types.String `tfsdk:"property"` +} + +type ResourceModel struct { + ID types.String `tfsdk:"id"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + BaseRouteAggregationModel +} + +type BaseRouteAggregationModel struct { + Type types.String `tfsdk:"type"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Href types.String `tfsdk:"href"` + UUID types.String `tfsdk:"uuid"` + State types.String `tfsdk:"state"` + ConnectionsCount types.Int32 `tfsdk:"connections_count"` + RulesCount types.Int32 `tfsdk:"rules_count"` + Project fwtypes.ObjectValueOf[ProjectModel] `tfsdk:"project"` + Change fwtypes.ObjectValueOf[ChangeModel] `tfsdk:"change"` + ChangeLog fwtypes.ObjectValueOf[ChangeLogModel] `tfsdk:"change_log"` +} + +type ProjectModel struct { + ProjectID types.String `tfsdk:"project_id"` +} + +type ChangeModel struct { + UUID types.String `tfsdk:"uuid"` + Type types.String `tfsdk:"type"` + Href types.String `tfsdk:"href"` +} + +type ChangeLogModel struct { + CreatedBy types.String `tfsdk:"created_by"` + CreatedByFullName types.String `tfsdk:"created_by_full_name"` + CreatedByEmail types.String `tfsdk:"created_by_email"` + CreatedDateTime types.String `tfsdk:"created_date_time"` + UpdatedBy types.String `tfsdk:"updated_by"` + UpdatedByFullName types.String `tfsdk:"updated_by_full_name"` + UpdatedByEmail types.String `tfsdk:"updated_by_email"` + UpdatedDateTime types.String `tfsdk:"updated_date_time"` + DeletedBy types.String `tfsdk:"deleted_by"` + DeletedByFullName types.String `tfsdk:"deleted_by_full_name"` + DeletedByEmail types.String `tfsdk:"deleted_by_email"` + DeletedDateTime types.String `tfsdk:"deleted_date_time"` +} + +func (m *DataSourceByIDModel) parse(ctx context.Context, routeAggregation *fabricv4.RouteAggregationsData) diag.Diagnostics { + m.RouteAggregationID = types.StringValue(routeAggregation.GetUuid()) + m.ID = types.StringValue(routeAggregation.GetUuid()) + diags := m.BaseRouteAggregationModel.parse(ctx, routeAggregation) + return diags +} + +func (m *DatsSourceAllRouteAggregationsModel) parse(ctx context.Context, routeAggregationsResponse *fabricv4.RouteAggregationsSearchResponse) diag.Diagnostics { + var diags diag.Diagnostics + + if len(routeAggregationsResponse.GetData()) < 1 { + diags.AddError("no data retrieved by route aggregations data source", "either the account does not have any route aggregations data to pull or the combination of limit and offset needs to be updated") + return diags + } + + data := make([]BaseRouteAggregationModel, len(routeAggregationsResponse.GetData())) + routeAggregations := routeAggregationsResponse.GetData() + for index, routeAggregation := range routeAggregations { + var routeAggregationModel BaseRouteAggregationModel + diags = routeAggregationModel.parse(ctx, &routeAggregation) + if diags.HasError() { + return diags + } + data[index] = routeAggregationModel + } + responsePagination := routeAggregationsResponse.GetPagination() + pagination := PaginationModel{ + Offset: types.Int32Value(responsePagination.GetOffset()), + Limit: types.Int32Value(responsePagination.GetLimit()), + Total: types.Int32Value(responsePagination.GetTotal()), + Next: types.StringValue(responsePagination.GetNext()), + Previous: types.StringValue(responsePagination.GetPrevious()), + } + m.ID = types.StringValue(data[0].UUID.ValueString()) + m.Pagination = fwtypes.NewObjectValueOf[PaginationModel](ctx, &pagination) + + dataPtr := make([]*BaseRouteAggregationModel, len(data)) + for i := range data { + dataPtr[i] = &data[i] + } + m.Data = fwtypes.NewListNestedObjectValueOfSlice[BaseRouteAggregationModel](ctx, dataPtr) + + return diags +} +func (m *ResourceModel) parse(ctx context.Context, routeAggregation *fabricv4.RouteAggregationsData) diag.Diagnostics { + m.ID = types.StringValue(routeAggregation.GetUuid()) + diags := m.BaseRouteAggregationModel.parse(ctx, routeAggregation) + return diags +} + +func (m *BaseRouteAggregationModel) parse(ctx context.Context, routeAggregation *fabricv4.RouteAggregationsData) diag.Diagnostics { + var diag diag.Diagnostics + + m.Type = types.StringValue(string(routeAggregation.GetType())) + m.Name = types.StringValue(routeAggregation.GetName()) + m.Description = types.StringValue(routeAggregation.GetDescription()) + m.Href = types.StringValue(routeAggregation.GetHref()) + m.UUID = types.StringValue(routeAggregation.GetUuid()) + m.State = types.StringValue(string(routeAggregation.GetState())) + m.ConnectionsCount = types.Int32Value(routeAggregation.GetConnectionsCount()) + m.RulesCount = types.Int32Value(routeAggregation.GetRulesCount()) + routeAggregationProject := routeAggregation.GetProject() + projectModel := ProjectModel{ + ProjectID: types.StringValue(routeAggregationProject.GetProjectId()), + } + m.Project = fwtypes.NewObjectValueOf[ProjectModel](ctx, &projectModel) + routeAggregationChange := routeAggregation.GetChange() + changeModel := ChangeModel{ + UUID: types.StringValue(routeAggregationChange.GetUuid()), + Type: types.StringValue(string(routeAggregationChange.GetType())), + Href: types.StringValue(routeAggregationChange.GetHref()), + } + m.Change = fwtypes.NewObjectValueOf[ChangeModel](ctx, &changeModel) + + const TIMEFORMAT = "2008-02-02T14:02:02.000Z" + routeAggregationChangeLog := routeAggregation.GetChangeLog() + changeLogModel := ChangeLogModel{ + CreatedBy: types.StringValue(routeAggregationChangeLog.GetCreatedBy()), + CreatedByFullName: types.StringValue(routeAggregationChangeLog.GetCreatedByFullName()), + CreatedByEmail: types.StringValue(routeAggregationChangeLog.GetCreatedByEmail()), + CreatedDateTime: types.StringValue(routeAggregationChangeLog.GetCreatedDateTime().Format(TIMEFORMAT)), + UpdatedBy: types.StringValue(routeAggregationChangeLog.GetUpdatedBy()), + UpdatedByFullName: types.StringValue(routeAggregationChangeLog.GetUpdatedByFullName()), + UpdatedByEmail: types.StringValue(routeAggregationChangeLog.GetUpdatedByEmail()), + UpdatedDateTime: types.StringValue(routeAggregationChangeLog.GetUpdatedDateTime().Format(TIMEFORMAT)), + DeletedBy: types.StringValue(routeAggregationChangeLog.GetDeletedBy()), + DeletedByFullName: types.StringValue(routeAggregationChangeLog.GetDeletedByFullName()), + DeletedByEmail: types.StringValue(routeAggregationChangeLog.GetDeletedByEmail()), + DeletedDateTime: types.StringValue(routeAggregationChangeLog.GetDeletedDateTime().Format(TIMEFORMAT)), + } + m.ChangeLog = fwtypes.NewObjectValueOf[ChangeLogModel](ctx, &changeLogModel) + + return diag +} diff --git a/internal/resources/fabric/routeaggregation/resource.go b/internal/resources/fabric/routeaggregation/resource.go new file mode 100644 index 000000000..dbcc5e02c --- /dev/null +++ b/internal/resources/fabric/routeaggregation/resource.go @@ -0,0 +1,288 @@ +package routeaggregation + +import ( + "context" + "fmt" + "net/http" + "slices" + "time" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "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/types/basetypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +func NewResource() resource.Resource { + return &Resource{ + BaseResource: framework.NewBaseResource( + framework.BaseResourceConfig{ + Name: "equinix_fabric_route_aggregation", + }, + ), + } +} + +type Resource struct { + framework.BaseResource +} + +func (r Resource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceSchema(ctx) +} + +func (r *Resource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + var plan ResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + createRequest, diags := buildCreateRequest(ctx, plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + routeAggregation, _, err := client.RouteAggregationsApi.CreateRouteAggregation(ctx).RouteAggregationsBase(createRequest).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Failed creating route aggregation", equinix_errors.FormatFabricError(err).Error()) + return + } + + createTimeout, diags := plan.Timeouts.Create(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + createWaiter := getCreateUpdateWaiter(ctx, client, routeAggregation.GetUuid(), createTimeout) + routeAggregationChecked, err := createWaiter.WaitForStateContext(ctx) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed creating Route Aggregation %s", routeAggregation.GetUuid()), err.Error()) + return + } + + resp.Diagnostics.Append(plan.parse(ctx, routeAggregationChecked.(*fabricv4.RouteAggregationsData))...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + +} + +func (r *Resource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state ResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + //Retrieve the API client from the provider metadata + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + // Extract the ID of the resource from the state + id := state.ID.ValueString() + + routeAggregation, _, err := client.RouteAggregationsApi.GetRouteAggregationByUuid(ctx, id).Execute() + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed retrieving Route Aggregation %s", id), equinix_errors.FormatFabricError(err).Error()) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(state.parse(ctx, routeAggregation)...) + if resp.Diagnostics.HasError() { + return + } + + // Update the Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + +} + +func (r *Resource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + //Retrieve values from plan + var state, plan ResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + newName, oldName := plan.Name.ValueString(), state.Name.ValueString() + + if newName == oldName { + resp.Diagnostics.AddWarning("No updatable fields have changed", "Terraform detected a config change, but it is for a field that isn't updatable for the route aggregation resource. Please revert to prior config") + return + } + + updateRequest := []fabricv4.RouteAggregationsPatchRequestItem{{ + Op: "replace", + Path: "/name", + Value: newName, + }} + + _, _, err := client.RouteAggregationsApi.PatchRouteAggregationByUuid(ctx, id).RouteAggregationsPatchRequestItem(updateRequest).Execute() + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed updating Route Aggregation %s", id), equinix_errors.FormatFabricError(err).Error()) + return + } + + updateTimeout, diags := plan.Timeouts.Update(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + updateWaiter := getCreateUpdateWaiter(ctx, client, id, updateTimeout) + routeAggregationChecked, err := updateWaiter.WaitForStateContext(ctx) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed updating Route Aggregation %s", id), err.Error()) + return + } + + //set state to fully populated data + resp.Diagnostics.Append(plan.parse(ctx, routeAggregationChecked.(*fabricv4.RouteAggregationsData))...) + if resp.Diagnostics.HasError() { + return + } + + //Set the updated state back into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + //Retrieve the API client + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + //Retrieve the current state + var state ResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + _, deleteResp, err := client.RouteAggregationsApi.DeleteRouteAggregationByUuid(ctx, id).Execute() + if err != nil { + if deleteResp == nil || !slices.Contains([]int{http.StatusForbidden, http.StatusNotFound}, deleteResp.StatusCode) { + resp.Diagnostics.AddError(fmt.Sprintf("Failed deleting Route Aggregation %s", id), equinix_errors.FormatFabricError(err).Error()) + return + } + } + + deleteTimeout, diags := state.Timeouts.Delete(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + deletewaiter := getDeleteWaiter(ctx, client, id, deleteTimeout) + _, err = deletewaiter.WaitForStateContext(ctx) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed deleting Route Aggregation %s", id), err.Error()) + return + } +} + +func buildCreateRequest(ctx context.Context, plan ResourceModel) (fabricv4.RouteAggregationsBase, diag.Diagnostics) { + var diags diag.Diagnostics + request := fabricv4.RouteAggregationsBase{} + + request.SetType(fabricv4.RouteAggregationsBaseType(plan.Type.ValueString())) + request.SetName(plan.Name.ValueString()) + request.SetDescription(plan.Description.ValueString()) + + var project ProjectModel + if !plan.Project.IsNull() && !plan.Project.IsUnknown() { + diags = plan.Project.As(ctx, &project, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return fabricv4.RouteAggregationsBase{}, diags + } + request.SetProject(fabricv4.Project{ProjectId: project.ProjectID.ValueString()}) + } + return request, diags +} + +func getCreateUpdateWaiter(ctx context.Context, client *fabricv4.APIClient, id string, timeout time.Duration) *retry.StateChangeConf { + return &retry.StateChangeConf{ + Pending: []string{ + string(fabricv4.ROUTEAGGREGATIONSTATE_PROVISIONING), + }, + Target: []string{ + string(fabricv4.ROUTEAGGREGATIONSTATE_PROVISIONED), + }, + Refresh: func() (interface{}, string, error) { + routeAggregation, _, err := client.RouteAggregationsApi.GetRouteAggregationByUuid(ctx, id).Execute() + if err != nil { + return 0, "", err + } + return routeAggregation, string(routeAggregation.GetState()), nil + }, + Timeout: timeout, + Delay: 30 * time.Second, + MinTimeout: 30 * time.Second, + } +} + +func getDeleteWaiter(ctx context.Context, client *fabricv4.APIClient, id string, timeout time.Duration) *retry.StateChangeConf { + // deletedMarker is a terraform-provider-only value that is used by the waiter + // to indicate that the resource appears to be deleted successfully based on + // status code or specific error code + deletedMarker := "tf-marker-for-deleted-route-aggregation" + return &retry.StateChangeConf{ + Pending: []string{ + string(fabricv4.ROUTEAGGREGATIONSTATE_DEPROVISIONING), + }, + Target: []string{ + deletedMarker, + string(fabricv4.ROUTEAGGREGATIONSTATE_DEPROVISIONED), + }, + Refresh: func() (interface{}, string, error) { + routeAggregation, resp, err := client.RouteAggregationsApi.GetRouteAggregationByUuid(ctx, id).Execute() + if err != nil { + if slices.Contains([]int{http.StatusForbidden, http.StatusNotFound}, resp.StatusCode) { + return routeAggregation, deletedMarker, nil + } + return 0, "", err + } + return routeAggregation, string(routeAggregation.GetState()), nil + }, + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } +} diff --git a/internal/resources/fabric/routeaggregation/resource_schema.go b/internal/resources/fabric/routeaggregation/resource_schema.go new file mode 100644 index 000000000..871fcd7f7 --- /dev/null +++ b/internal/resources/fabric/routeaggregation/resource_schema.go @@ -0,0 +1,150 @@ +package routeaggregation + +import ( + "context" + + "github.com/equinix/terraform-provider-equinix/internal/framework" + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +func resourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Description: `Fabric V4 API compatible resource allows creation and management of Equinix Fabric Route Aggregation + +Additional Documentation: +* API: https://developer.equinix.com/catalog/fabricv4#tag/Route-Aggregations`, + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttributeDefaultDescription(), + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }), + "type": schema.StringAttribute{ + Description: "Equinix defined Route Aggregation Type; BGP_IPv4_PREFIX_AGGREGATION, BGP_IPv6_PREFIX_AGGREGATION", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "Customer provided name of the route aggregation", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "Customer-provided route aggregation description", + Optional: true, + }, + "project": schema.SingleNestedAttribute{ + Description: "Equinix Project attribute object", + Required: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + CustomType: fwtypes.NewObjectTypeOf[ProjectModel](ctx), + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "Equinix Subscriber-assigned project ID", + Required: true, + }, + }, + }, + "href": schema.StringAttribute{ + Description: "Equinix auto generated URI to the route aggregation resource", + Computed: true, + }, + "uuid": schema.StringAttribute{ + Description: "Equinix-assigned unique id for the route aggregation resource", + Computed: true, + }, + "state": schema.StringAttribute{ + Description: "Value representing provisioning status for the route aggregation resource", + Computed: true, + }, + "change": schema.SingleNestedAttribute{ + Description: "Current state of latest Route Aggregation change", + Computed: true, + CustomType: fwtypes.NewObjectTypeOf[ChangeModel](ctx), + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Description: "Equinix-assigned unique id for a change", + Required: true, + }, + "type": schema.StringAttribute{ + Description: "Equinix defined Route Aggregation Change Type", + Required: true, + }, + "href": schema.StringAttribute{ + Description: "Equinix auto generated URI to the route aggregation change", + Required: true, + }, + }, + }, + "connections_count": schema.Int32Attribute{ + Description: "Number of Connections attached to route aggregation", + Computed: true, + }, + "rules_count": schema.Int32Attribute{ + Description: "Number of Rules attached to route aggregation", + Computed: true, + }, + "change_log": schema.SingleNestedAttribute{ + Description: "Details of the last change on the route aggregation resource", + Computed: true, + CustomType: fwtypes.NewObjectTypeOf[ChangeLogModel](ctx), + Attributes: map[string]schema.Attribute{ + "created_by": schema.StringAttribute{ + Description: "User name of creator of the route aggregation resource", + Computed: true, + }, + "created_by_full_name": schema.StringAttribute{ + Description: "Legal name of creator of the route aggregation resource", + Computed: true, + }, + "created_by_email": schema.StringAttribute{ + Description: "Email of creator of the route aggregation resource", + Computed: true, + }, + "created_date_time": schema.StringAttribute{ + Description: "Creation time of the route aggregation resource", + Computed: true, + }, + "updated_by": schema.StringAttribute{ + Description: "User name of last updater of the route aggregation resource", + Computed: true, + }, + "updated_by_full_name": schema.StringAttribute{ + Description: "Legal name of last updater of the route aggregation resource", + Computed: true, + }, + "updated_by_email": schema.StringAttribute{ + Description: "Email of last updater of the route aggregation resource", + Computed: true, + }, + "updated_date_time": schema.StringAttribute{ + Description: "Last update time of the route aggregation resource", + Computed: true, + }, + "deleted_by": schema.StringAttribute{ + Description: "User name of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_by_full_name": schema.StringAttribute{ + Description: "Legal name of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_by_email": schema.StringAttribute{ + Description: "Email of deleter of the route aggregation resource", + Computed: true, + }, + "deleted_date_time": schema.StringAttribute{ + Description: "Deletion time of the route aggregation resource", + Computed: true, + }, + }, + }, + }, + } +} diff --git a/internal/resources/fabric/routeaggregation/resource_test.go b/internal/resources/fabric/routeaggregation/resource_test.go new file mode 100644 index 000000000..dc173fbe4 --- /dev/null +++ b/internal/resources/fabric/routeaggregation/resource_test.go @@ -0,0 +1,88 @@ +package routeaggregation_test + +import ( + "context" + "fmt" + "testing" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/equinix/terraform-provider-equinix/internal/config" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func testAccFabricRouteAggregationConfig(name string) string { + return fmt.Sprintf(` + resource "equinix_fabric_route_aggregation" "test" { + type = "BGP_IPv4_PREFIX_AGGREGATION" + name = "%s" + description = "Test Route Aggregation" + project = { + project_id = "4f855852-eb47-4721-8e40-b386a3676abf" + } + } + `, name) +} + +func TestAccFabricRouteAggregation_PFCR(t *testing.T) { + routeAggregationName := "stream_PFCR" + upRouteAggregationName := "stream_up_PFCR" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + CheckDestroy: CheckRouteAggregationDelete, + Steps: []resource.TestStep{ + { + Config: testAccFabricRouteAggregationConfig(routeAggregationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "equinix_fabric_route_aggregation.test", "name", routeAggregationName), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "uuid"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "state"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "href"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "project.project_id"), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "name", routeAggregationName), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "type", "BGP_IPv4_PREFIX_AGGREGATION"), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "description", "Test Route Aggregation"), + ), + }, + { + Config: testAccFabricRouteAggregationConfig(upRouteAggregationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "equinix_fabric_route_aggregation.test", "name", upRouteAggregationName), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "uuid"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "state"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "href"), + resource.TestCheckResourceAttrSet("equinix_fabric_route_aggregation.test", "project.project_id"), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "name", upRouteAggregationName), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "type", "BGP_IPv4_PREFIX_AGGREGATION"), + resource.TestCheckResourceAttr("equinix_fabric_route_aggregation.test", "description", "Test Route Aggregation"), + ), + }, + }, + }) + +} + +func CheckRouteAggregationDelete(s *terraform.State) error { + ctx := context.Background() + client := acceptance.TestAccProvider.Meta().(*config.Config).NewFabricClientForTesting(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "equinix_fabric_route_aggregation" { + continue + } + + if routeAggregation, _, err := client.RouteAggregationsApi.GetRouteAggregationByUuid(ctx, rs.Primary.ID).Execute(); err == nil { + if routeAggregation.GetState() == fabricv4.ROUTEAGGREGATIONSTATE_PROVISIONED { + return fmt.Errorf("fabric route aggregation %s still exists and is %s", + rs.Primary.ID, routeAggregation.GetState()) + } + } + } + return nil +} diff --git a/internal/resources/fabric/routeaggregation/sweeper.go b/internal/resources/fabric/routeaggregation/sweeper.go new file mode 100644 index 000000000..7179bb0b0 --- /dev/null +++ b/internal/resources/fabric/routeaggregation/sweeper.go @@ -0,0 +1,80 @@ +package routeaggregation + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "github.com/equinix/terraform-provider-equinix/internal/sweep" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func AddTestSweeper() { + resource.AddTestSweepers("equinix_fabric_route_aggregation", &resource.Sweeper{ + Name: "equinix_fabric_route_aggregation", + Dependencies: []string{}, + F: testSweepRouteAggregations, + }) +} + +func testSweepRouteAggregations(_ string) error { + var errs []error + log.Printf("[DEBUG] Sweeping Route Aggregations") + ctx := context.Background() + meta, err := sweep.GetConfigForFabric() + if err != nil { + return fmt.Errorf("error getting configuration for sweeping Route Aggregations: %s", err) + } + configLoadErr := meta.Load(ctx) + if configLoadErr != nil { + return fmt.Errorf("error loading configuration for sweeping Route Aggregations: %s", err) + } + fabric := meta.NewFabricClientForTesting(ctx) + + name := fabricv4.ROUTEFILTERSSEARCHFILTERITEMPROPERTY_NAME + equinixState := fabricv4.ROUTEFILTERSSEARCHFILTERITEMPROPERTY_STATE + likeOperator := "like" + equalOperator := "=" + limit := int32(100) + routeAggregationsSearch := fabricv4.RouteAggregationsSearchBase{ + Filter: &fabricv4.RouteAggregationsSearchBaseFilter{ + And: []fabricv4.RouteAggregationsSearchFilterItem{ + { + Property: &name, + Operator: &likeOperator, + Values: []string{"%_PFCR", "%_PNFV"}, + }, + { + Property: &equinixState, + Operator: &equalOperator, + Values: []string{string(fabricv4.ROUTEFILTERSTATE_PROVISIONED)}, + }, + }, + }, + Pagination: &fabricv4.Pagination{ + Limit: limit, + Total: limit, + }, + } + + routeAggregation, _, err := fabric.RouteAggregationsApi.SearchRouteAggregations(ctx).RouteAggregationsSearchBase(routeAggregationsSearch).Execute() + if err != nil { + return fmt.Errorf("error getting streams list for sweeping fabric route aggregations: %s", err) + } + + for _, ra := range routeAggregation.GetData() { + if sweep.IsSweepableFabricTestResource(ra.GetName()) { + log.Printf("[DEBUG] Deleting route aggregation: %s", ra.GetName()) + _, resp, err := fabric.RouteAggregationsApi.DeleteRouteAggregationByUuid(ctx, ra.GetUuid()).Execute() + if equinix_errors.IgnoreHttpResponseErrors(http.StatusForbidden, http.StatusNotFound)(resp, err) != nil { + errs = append(errs, fmt.Errorf("error deleting fabric route aggregation: %s", err)) + } + } + } + + return errors.Join(errs...) +} diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index fa6d09166..7abfe94d8 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -6,6 +6,7 @@ import ( fabric_cloud_router "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/cloud_router" fabric_connection "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/connection" fabric_route_filter "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/route_filter" + fabric_route_aggregation "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/routeaggregation" fabric_stream "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/stream" "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/network" @@ -35,6 +36,7 @@ func addTestSweepers() { fabric_cloud_router.AddTestSweeper() fabric_connection.AddTestSweeper() fabric_route_filter.AddTestSweeper() + fabric_route_aggregation.AddTestSweeper() fabric_stream.AddTestSweeper() network.AddTestSweeper() organization.AddTestSweeper()