diff --git a/apstra/apstra_validator/at_most_n_of.go b/apstra/apstra_validator/at_most_n_of.go index c607a730..7274bd4d 100644 --- a/apstra/apstra_validator/at_most_n_of.go +++ b/apstra/apstra_validator/at_most_n_of.go @@ -96,7 +96,7 @@ func (o AtMostNOfValidator) Validate(ctx context.Context, req AtMostNOfValidator resp.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.Path, - fmt.Sprintf("At most %d attributes out of %s must be specified, but %d matches were found", + fmt.Sprintf("At most %d attributes out of %s may be specified, but %d non-null attributes were found", req.N, expressions, len(notNullPaths)), )) } diff --git a/apstra/blueprint/datacenter_virtual_network.go b/apstra/blueprint/datacenter_virtual_network.go index cbe3f534..a02f8251 100644 --- a/apstra/blueprint/datacenter_virtual_network.go +++ b/apstra/blueprint/datacenter_virtual_network.go @@ -162,22 +162,6 @@ func (o DatacenterVirtualNetwork) DataSourceFilterAttributes() map[string]dataSo "name": dataSourceSchema.StringAttribute{ MarkdownDescription: "Virtual Network Name", Optional: true, - Validators: []validator.String{stringvalidator.AtLeastOneOf( - path.MatchRelative(), - path.MatchRoot("filter").AtName("type"), - path.MatchRoot("filter").AtName("routing_zone_id"), - path.MatchRoot("filter").AtName("vni"), - path.MatchRoot("filter").AtName("reserve_vlan"), - path.MatchRoot("filter").AtName("dhcp_service_enabled"), - path.MatchRoot("filter").AtName("ipv4_connectivity_enabled"), - path.MatchRoot("filter").AtName("ipv6_connectivity_enabled"), - path.MatchRoot("filter").AtName("ipv4_subnet"), - path.MatchRoot("filter").AtName("ipv6_subnet"), - path.MatchRoot("filter").AtName("ipv4_virtual_gateway_enabled"), - path.MatchRoot("filter").AtName("ipv6_virtual_gateway_enabled"), - path.MatchRoot("filter").AtName("ipv4_virtual_gateway"), - path.MatchRoot("filter").AtName("ipv6_virtual_gateway"), - )}, }, "type": dataSourceSchema.StringAttribute{ MarkdownDescription: "Virtual Network Type", diff --git a/apstra/data_source_datacenter_virtual_networks.go b/apstra/data_source_datacenter_virtual_networks.go index 5e16dbcf..781a4676 100644 --- a/apstra/data_source_datacenter_virtual_networks.go +++ b/apstra/data_source_datacenter_virtual_networks.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "github.com/Juniper/apstra-go-sdk/apstra" + apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/apstra_validator" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -52,13 +54,49 @@ func (o *dataSourceDatacenterVirtualNetworks) Schema(_ context.Context, _ dataso "one filter attribute must be included when this attribute is used.", Optional: true, Attributes: blueprint.DatacenterVirtualNetwork{}.DataSourceFilterAttributes(), + Validators: []validator.Object{ + apstravalidator.AtMostNOf(1, + path.MatchRelative(), + path.MatchRoot("filters"), + ), + apstravalidator.AtLeastNAttributes( + 1, + "name", "type", "routing_zone_id", "vni", "reserve_vlan", "dhcp_service_enabled", + "ipv4_connectivity_enabled", "ipv6_connectivity_enabled", "ipv4_subnet", "ipv6_subnet", + "ipv4_virtual_gateway_enabled", "ipv6_virtual_gateway_enabled", "ipv4_virtual_gateway", + "ipv6_virtual_gateway", + ), + }, + DeprecationMessage: "The `filter` attribute is deprecated and will be removed in a future " + + "release. Please migrate your configuration to use `filters` instead.", }, - "graph_query": schema.StringAttribute{ + "filters": schema.ListNestedAttribute{ + MarkdownDescription: "List of filters used to select only desired node IDs. For a node " + + "to match a filter, all specified attributes must match (each attribute within a " + + "filter is AND-ed together). The returned node IDs represent the nodes matched by " + + "all of the filters together (filters are OR-ed together).", + Optional: true, + Validators: []validator.List{listvalidator.SizeAtLeast(1)}, + NestedObject: schema.NestedAttributeObject{ + Attributes: blueprint.DatacenterVirtualNetwork{}.DataSourceFilterAttributes(), + Validators: []validator.Object{ + apstravalidator.AtLeastNAttributes( + 1, + "name", "type", "routing_zone_id", "vni", "reserve_vlan", "dhcp_service_enabled", + "ipv4_connectivity_enabled", "ipv6_connectivity_enabled", "ipv4_subnet", "ipv6_subnet", + "ipv4_virtual_gateway_enabled", "ipv6_virtual_gateway_enabled", "ipv4_virtual_gateway", + "ipv6_virtual_gateway", + ), + }, + }, + }, + "graph_queries": schema.ListAttribute{ MarkdownDescription: "The graph datastore query based on `filter` used to " + "perform the lookup. Note that the `ipv6_subnet` and `ipv6_gateway` " + "attributes are never part of the graph query because IPv6 zero " + "compression rules make string matches unreliable.", - Computed: true, + ElementType: types.StringType, + Computed: true, }, }, } @@ -69,7 +107,8 @@ func (o *dataSourceDatacenterVirtualNetworks) Read(ctx context.Context, req data BlueprintId types.String `tfsdk:"blueprint_id"` IDs types.Set `tfsdk:"ids"` Filter types.Object `tfsdk:"filter"` - Query types.String `tfsdk:"graph_query"` + Filters types.List `tfsdk:"filters"` + Queries types.List `tfsdk:"graph_queries"` } var config virtualNetworks @@ -78,29 +117,43 @@ func (o *dataSourceDatacenterVirtualNetworks) Read(ctx context.Context, req data return } - var ids []attr.Value - var query *apstra.MatchQuery // todo change to interface after SDK update bpId := apstra.ObjectId(config.BlueprintId.ValueString()) - if config.Filter.IsNull() { + if config.Filter.IsNull() && config.Filters.IsNull() { // just pull the VN IDs via API when no filter is specified - ids = o.getAllVnIds(ctx, bpId, &resp.Diagnostics) + ids := o.getAllVnIds(ctx, bpId, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // set the state config.IDs = types.SetValueMust(types.StringType, ids) - } else { - // use a graph query (and some IPv6 value matching) - filter := blueprint.DatacenterVirtualNetwork{} + config.Queries = types.ListNull(types.StringType) + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) + return + } + + var filters []blueprint.DatacenterVirtualNetwork + if !config.Filter.IsNull() { + var filter blueprint.DatacenterVirtualNetwork resp.Diagnostics.Append(config.Filter.As(ctx, &filter, basetypes.ObjectAsOptions{})...) if resp.Diagnostics.HasError() { return } - ids, query = o.getFilteredVnIds(ctx, bpId, filter, &resp.Diagnostics) - config.IDs = types.SetValueMust(types.StringType, ids) - config.Query = types.StringValue(query.String()) + filters = append(filters, filter) } - if resp.Diagnostics.HasError() { - return + + if !config.Filters.IsNull() { + resp.Diagnostics.Append(config.Filters.ElementsAs(ctx, &filters, false)...) + if resp.Diagnostics.HasError() { + return + } } + ids, queries := o.getVnIdsWithFilters(ctx, bpId, filters, &resp.Diagnostics) + config.IDs = types.SetValueMust(types.StringType, ids) + config.Queries = types.ListValueMust(types.StringType, queries) + // set the state resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } @@ -131,8 +184,32 @@ func (o *dataSourceDatacenterVirtualNetworks) getAllVnIds(ctx context.Context, b return result } -// todo change returned query to interface after SDK update -func (o *dataSourceDatacenterVirtualNetworks) getFilteredVnIds(ctx context.Context, bpId apstra.ObjectId, filter blueprint.DatacenterVirtualNetwork, diags *diag.Diagnostics) ([]attr.Value, *apstra.MatchQuery) { +func (o *dataSourceDatacenterVirtualNetworks) getVnIdsWithFilters(ctx context.Context, bpId apstra.ObjectId, filters []blueprint.DatacenterVirtualNetwork, diags *diag.Diagnostics) ([]attr.Value, []attr.Value) { + queries := make([]attr.Value, len(filters)) + resultMap := make(map[string]bool) + for i, filter := range filters { + ids, query := o.getVnIdsWithFilter(ctx, bpId, filter, diags) + if diags.HasError() { + return nil, nil + } + + queries[i] = types.StringValue(query.String()) + for _, id := range ids { + resultMap[id] = true + } + } + + ids := make([]attr.Value, len(resultMap)) + var i int + for id := range resultMap { + ids[i] = types.StringValue(id) + i++ + } + + return ids, queries +} + +func (o *dataSourceDatacenterVirtualNetworks) getVnIdsWithFilter(ctx context.Context, bpId apstra.ObjectId, filter blueprint.DatacenterVirtualNetwork, diags *diag.Diagnostics) ([]string, apstra.QEQuery) { query := filter.Query("n_virtual_network") queryResponse := new(struct { Items []struct { @@ -145,13 +222,10 @@ func (o *dataSourceDatacenterVirtualNetworks) getFilteredVnIds(ctx context.Conte }) // todo remove this type assertion when QEQuery is extended with new methods used below - query2 := query.(*apstra.MatchQuery) - query2. - SetClient(o.client). - SetBlueprintId(bpId). - SetBlueprintType(apstra.BlueprintTypeStaging) - - err := query2.Do(ctx, queryResponse) + query.(*apstra.MatchQuery).SetClient(o.client) + query.(*apstra.MatchQuery).SetBlueprintId(bpId) + query.(*apstra.MatchQuery).SetBlueprintType(apstra.BlueprintTypeStaging) + err := query.Do(ctx, queryResponse) if err != nil { diags.AddError("error querying graph datastore", err.Error()) return nil, nil @@ -218,10 +292,10 @@ func (o *dataSourceDatacenterVirtualNetworks) getFilteredVnIds(ctx context.Conte } } - result := make([]attr.Value, len(queryResponse.Items)) + result := make([]string, len(queryResponse.Items)) for i, item := range queryResponse.Items { - result[i] = types.StringValue(item.VirtualNetwork.Id) + result[i] = item.VirtualNetwork.Id } - return result, query2 + return result, query } diff --git a/docs/data-sources/datacenter_virtual_networks.md b/docs/data-sources/datacenter_virtual_networks.md index 36438fe7..f8ee7f50 100644 --- a/docs/data-sources/datacenter_virtual_networks.md +++ b/docs/data-sources/datacenter_virtual_networks.md @@ -32,11 +32,13 @@ data "apstra_datacenter_virtual_networks" "all" { # ) data "apstra_datacenter_virtual_networks" "prod_unreserved_with_dhcp" { blueprint_id = "b726704d-f80e-4733-9103-abd6ccd8752c" - filter = { - reserve_vlan = false - dhcp_service_enabled = true - routing_zone_id = apstra_datacenter_routing_zone.prod.id - } + filters = [ + { + reserve_vlan = false + dhcp_service_enabled = true + routing_zone_id = "Zplm0niOFCCCfjaXkXo" + } + ] } ``` @@ -49,11 +51,12 @@ data "apstra_datacenter_virtual_networks" "prod_unreserved_with_dhcp" { ### Optional -- `filter` (Attributes) Virtual Network attributes used as filter. At least one filter attribute must be included when this attribute is used. (see [below for nested schema](#nestedatt--filter)) +- `filter` (Attributes, Deprecated) Virtual Network attributes used as filter. At least one filter attribute must be included when this attribute is used. (see [below for nested schema](#nestedatt--filter)) +- `filters` (Attributes List) List of filters used to select only desired node IDs. For a node to match a filter, all specified attributes must match (each attribute within a filter is AND-ed together). The returned node IDs represent the nodes matched by all of the filters together (filters are OR-ed together). (see [below for nested schema](#nestedatt--filters)) ### Read-Only -- `graph_query` (String) The graph datastore query based on `filter` used to perform the lookup. Note that the `ipv6_subnet` and `ipv6_gateway` attributes are never part of the graph query because IPv6 zero compression rules make string matches unreliable. +- `graph_queries` (List of String) The graph datastore query based on `filter` used to perform the lookup. Note that the `ipv6_subnet` and `ipv6_gateway` attributes are never part of the graph query because IPv6 zero compression rules make string matches unreliable. - `ids` (Set of String) Set of Virtual Network IDs @@ -85,3 +88,35 @@ Read-Only: ### Nested Schema for `filter.bindings` + + + + +### Nested Schema for `filters` + +Optional: + +- `dhcp_service_enabled` (Boolean) Enables a DHCP relay agent. +- `ipv4_connectivity_enabled` (Boolean) Enables IPv4 within the Virtual Network. +- `ipv4_subnet` (String) IPv4 subnet associated with the Virtual Network. +- `ipv4_virtual_gateway` (String) Specifies the IPv4 virtual gateway address within the Virtual Network. +- `ipv4_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv4 gateway within the Virtual Network is enabled. +- `ipv6_connectivity_enabled` (Boolean) Enables IPv6 within the Virtual Network. +- `ipv6_subnet` (String) IPv6 subnet associated with the Virtual Network. Note that this attribute will not appear in the `graph_query` output because IPv6 zero compression rules are problematic for mechanisms which rely on string matching. +- `ipv6_virtual_gateway` (String) Specifies the IPv6 virtual gateway address within the Virtual Network. Note that this attribute will not appear in the `graph_query` output because IPv6 zero compression rules are problematic for mechanisms which rely on string matching. +- `ipv6_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv6 gateway within the Virtual Network is enabled. +- `name` (String) Virtual Network Name +- `reserve_vlan` (Boolean) For use only with `vxlan` type Virtual networks when all `bindings` use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to which the Virtual Network has not yet been deployed. +- `routing_zone_id` (String) Routing Zone ID (required when `type == vxlan` +- `type` (String) Virtual Network Type +- `vni` (Number) EVPN Virtual Network ID to be associated with this Virtual Network. + +Read-Only: + +- `bindings` (Attributes Map) Not applicable in filter context. Ignore. (see [below for nested schema](#nestedatt--filters--bindings)) +- `blueprint_id` (String) Not applicable in filter context. Ignore. +- `had_prior_vni_config` (Boolean) Not applicable in filter context. Ignore. +- `id` (String) Not applicable in filter context. Ignore. + + +### Nested Schema for `filters.bindings` diff --git a/examples/data-sources/apstra_datacenter_virtual_networks/example.tf b/examples/data-sources/apstra_datacenter_virtual_networks/example.tf index 316c440a..3a54b099 100644 --- a/examples/data-sources/apstra_datacenter_virtual_networks/example.tf +++ b/examples/data-sources/apstra_datacenter_virtual_networks/example.tf @@ -17,9 +17,11 @@ data "apstra_datacenter_virtual_networks" "all" { # ) data "apstra_datacenter_virtual_networks" "prod_unreserved_with_dhcp" { blueprint_id = "b726704d-f80e-4733-9103-abd6ccd8752c" - filter = { - reserve_vlan = false - dhcp_service_enabled = true - routing_zone_id = apstra_datacenter_routing_zone.prod.id - } + filters = [ + { + reserve_vlan = false + dhcp_service_enabled = true + routing_zone_id = "Zplm0niOFCCCfjaXkXo" + } + ] }