diff --git a/apstra/apstra_validator/object_must_have_n_of.go b/apstra/apstra_validator/object_must_have_n_of.go
new file mode 100644
index 00000000..f3ea97eb
--- /dev/null
+++ b/apstra/apstra_validator/object_must_have_n_of.go
@@ -0,0 +1,159 @@
+package apstravalidator
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "strings"
+)
+
+var _ validator.Object = mustHaveNOfValidator{}
+
+type mustHaveNOfValidator struct {
+ n int
+ attributes []string
+ atLeast bool
+ atMost bool
+}
+
+func (o mustHaveNOfValidator) Description(ctx context.Context) string {
+ return fmt.Sprintf("ensure that the object has at least %d of the following attributes configured: ['%s']",
+ o.n, strings.Join(o.attributes, "', '"))
+}
+
+func (o mustHaveNOfValidator) MarkdownDescription(ctx context.Context) string {
+ return o.Description(ctx)
+}
+
+func (o mustHaveNOfValidator) ValidateObject(_ context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return // can't validate null or unknown objects
+ }
+
+ // extract the attributes from the configured object
+ attributeMap := req.ConfigValue.Attributes()
+
+ // when no attributes are enumerated during validator creation, that's a signal
+ // to consider all attributes. Rewrite the attribute slice with all attributes.
+ if len(o.attributes) == 0 {
+ o.attributes = make([]string, len(attributeMap))
+ i := 0
+ for attributeName := range attributeMap {
+ o.attributes[i] = attributeName
+ i++
+ }
+
+ }
+
+ // n can never be larger than the number of known attributes
+ if o.n > len(o.attributes) {
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "Invalid validator for element value",
+ "While performing schema-based validation, an unexpected error occurred. "+
+ "A schema validator which validates specific attributes has been configured for this object, "+
+ "but the validator has been configured to check more objects than it knows about.\n\n"+
+ fmt.Sprintf("Count of attributes to check: %d\n Attributes known to the validator: ['%s']",
+ o.n, strings.Join(o.attributes, "', '"),
+ ),
+ )
+
+ return
+ }
+
+ foundValueCount := 0 // we'll compare this to 'n' later
+
+ // loop over the attributes the caller asked us to count
+ for _, requiredAttribute := range o.attributes {
+ var foundAttribute attr.Value
+ var ok bool
+
+ // make sure the specified attributes exist
+ if foundAttribute, ok = attributeMap[requiredAttribute]; !ok {
+ attributeSlice := make([]string, len(attributeMap))
+ var i int
+ for s := range attributeMap {
+ attributeSlice[i] = s
+ }
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "Invalid validator for element value",
+ "While performing schema-based validation, an unexpected error occurred. "+
+ "A schema validator which validates specific attributes has been configured for this object, "+
+ "but the available attributes don't include all of the attributes requested for validation.\n\n"+
+ fmt.Sprintf("Available attributes: ['%s']\n Attributes to be validated: ['%s']",
+ strings.Join(attributeSlice, "', '"),
+ strings.Join(o.attributes, "', '"),
+ ),
+ )
+ return
+ }
+
+ // Can't validate with unknown values
+ if foundAttribute.IsUnknown() {
+ return
+ }
+
+ // increment the counter if the attribute has a value
+ if !foundAttribute.IsNull() {
+ foundValueCount++
+ }
+ }
+
+ if o.atLeast && foundValueCount < o.n {
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "Insufficient attribute configuration",
+ fmt.Sprintf("At least %d values from: ['%s'] must be configured.",
+ o.n,
+ strings.Join(o.attributes, "', '")),
+ )
+ return
+ }
+
+ if o.atMost && foundValueCount > o.n {
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "Too many attributes configured",
+ fmt.Sprintf("At most %d values from: ['%s'] must be configured.",
+ o.n,
+ strings.Join(o.attributes, "', '")),
+ )
+ return
+ }
+
+ if !o.atLeast && !o.atMost && foundValueCount != o.n {
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "Wrong number of attributes configured",
+ fmt.Sprintf("exactly %d values from: ['%s'] must be configured.",
+ o.n,
+ strings.Join(o.attributes, "', '")),
+ )
+ return
+ }
+}
+
+func AtLeastNAttributes(n int, attributes ...string) validator.Object {
+ return &mustHaveNOfValidator{
+ n: n,
+ attributes: attributes,
+ atLeast: true,
+ }
+}
+
+func AtMostNAttributes(n int, attributes ...string) validator.Object {
+ return &mustHaveNOfValidator{
+ n: n,
+ attributes: attributes,
+ atMost: true,
+ }
+}
+
+func ExactlyNAttributes(n int, attributes ...string) validator.Object {
+ return &mustHaveNOfValidator{
+ n: n,
+ attributes: attributes,
+ }
+}
diff --git a/apstra/apstra_validator/object_must_have_n_of_test.go b/apstra/apstra_validator/object_must_have_n_of_test.go
new file mode 100644
index 00000000..cd8a9508
--- /dev/null
+++ b/apstra/apstra_validator/object_must_have_n_of_test.go
@@ -0,0 +1,313 @@
+package apstravalidator
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "testing"
+)
+
+func TestObjectMustHaveNOf(t *testing.T) {
+ ctx := context.Background()
+
+ type testCase struct {
+ n int
+ checkAttributes []string
+ attrTypes map[string]attr.Type
+ attributes map[string]attr.Value
+ expAtLeastErr bool
+ expAtMostErr bool
+ expExactlyErr bool
+ }
+
+ testCases := map[string]testCase{
+ "zero": {
+ n: 0,
+ checkAttributes: nil,
+ attrTypes: nil,
+ attributes: nil,
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "1:1:1": {
+ n: 1,
+ checkAttributes: []string{"a1"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "1:1:2": {
+ n: 1,
+ checkAttributes: []string{"a1"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "1:3:3": {
+ n: 1,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "1:3:4": {
+ n: 1,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ "a4": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ "a4": types.StringValue("bang"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "2:3:3": {
+ n: 2,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "2:3:4": {
+ n: 2,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ "a4": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ "a4": types.StringValue("bang"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "3:3:3": {
+ n: 3,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "3:3:4": {
+ n: 3,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ "a4": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ "a4": types.StringValue("bang"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "4:3:4": {
+ n: 4,
+ checkAttributes: []string{"a1", "a2", "a3"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ "a4": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ "a4": types.StringValue("bang"),
+ },
+ expAtLeastErr: true,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "1:0:1": {
+ n: 1,
+ checkAttributes: []string{},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "1:0:2": {
+ n: 1,
+ checkAttributes: []string{},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "2:0:3": {
+ n: 2,
+ checkAttributes: []string{},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ "3:0:3": {
+ n: 3,
+ checkAttributes: []string{},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ "a3": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ "a3": types.StringValue("baz"),
+ },
+ expAtLeastErr: false,
+ expAtMostErr: false,
+ expExactlyErr: false,
+ },
+ "bad_data": {
+ n: 1,
+ checkAttributes: []string{"b1", "b2"},
+ attrTypes: map[string]attr.Type{
+ "a1": types.StringType,
+ "a2": types.StringType,
+ },
+ attributes: map[string]attr.Value{
+ "a1": types.StringValue("foo"),
+ "a2": types.StringValue("bar"),
+ },
+ expAtLeastErr: true,
+ expAtMostErr: true,
+ expExactlyErr: true,
+ },
+ }
+
+ for tName, tCase := range testCases {
+ tName, tCase := tName, tCase
+ t.Run(tName, func(t *testing.T) {
+ t.Parallel()
+ var resp validator.ObjectResponse
+ req := validator.ObjectRequest{
+ Path: path.Root("test"),
+ ConfigValue: types.ObjectValueMust(tCase.attrTypes, tCase.attributes),
+ }
+
+ resp = validator.ObjectResponse{}
+ atLeastValidator := AtLeastNAttributes(tCase.n, tCase.checkAttributes...)
+ atLeastValidator.ValidateObject(ctx, req, &resp)
+ if resp.Diagnostics.HasError() && !tCase.expAtLeastErr {
+ t.Fatal("got an error in the 'at least' case where none was expected")
+ }
+ if !resp.Diagnostics.HasError() && tCase.expAtLeastErr {
+ t.Fatal("got no error in the 'at least' case where one was expected")
+ }
+
+ resp = validator.ObjectResponse{}
+ atMostValidator := AtMostNAttributes(tCase.n, tCase.checkAttributes...)
+ atMostValidator.ValidateObject(ctx, req, &resp)
+ if resp.Diagnostics.HasError() && !tCase.expAtMostErr {
+ t.Fatal("got an error in the 'at most' case where none was expected")
+ }
+ if !resp.Diagnostics.HasError() && tCase.expAtMostErr {
+ t.Fatal("got no error in the 'at most' case where one was expected")
+ }
+
+ resp = validator.ObjectResponse{}
+ exactlyValidator := ExactlyNAttributes(tCase.n, tCase.checkAttributes...)
+ exactlyValidator.ValidateObject(ctx, req, &resp)
+ if resp.Diagnostics.HasError() && !tCase.expExactlyErr {
+ t.Fatal("got an error in the 'exactly' case where none was expected")
+ }
+ if !resp.Diagnostics.HasError() && tCase.expExactlyErr {
+ t.Fatal("got no error in the 'exactly' case where one was expected")
+ }
+
+ //atMostValidator := AtMostNAttributes(tCase.n, tCase.checkAttributes...)
+ //exactlyValidator := ExactlyNAttributes(tCase.n, tCase.checkAttributes...)
+ })
+ }
+}
diff --git a/apstra/blueprint/node_system_attributes.go b/apstra/blueprint/node_system_attributes.go
index 7949285e..a3f8ea67 100644
--- a/apstra/blueprint/node_system_attributes.go
+++ b/apstra/blueprint/node_system_attributes.go
@@ -1,13 +1,14 @@
package blueprint
import (
+ "context"
"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
- "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -107,15 +108,6 @@ func (o NodeTypeSystemAttributes) DataSourceAttributesAsFilter() map[string]data
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
- setvalidator.AtLeastOneOf(
- path.MatchRoot("filter").AtName("hostname"),
- path.MatchRoot("filter").AtName("id"),
- path.MatchRoot("filter").AtName("label"),
- path.MatchRoot("filter").AtName("role"),
- path.MatchRoot("filter").AtName("system_id"),
- path.MatchRoot("filter").AtName("system_type"),
- path.MatchRoot("filter").AtName("tag_ids"),
- ),
},
},
}
@@ -150,3 +142,51 @@ func (o NodeTypeSystemAttributes) QEEAttributes() []apstra.QEEAttribute {
return result
}
+
+func (o NodeTypeSystemAttributes) query(ctx context.Context, diags *diag.Diagnostics) *apstra.MatchQuery {
+ var tagIds []string
+ if utils.Known(o.TagIds) {
+ diags.Append(o.TagIds.ElementsAs(ctx, &tagIds, false)...)
+ if diags.HasError() {
+ return nil
+ }
+ }
+
+ systemNodeBaseAttributes := []apstra.QEEAttribute{
+ {Key: "type", Value: apstra.QEStringVal("system")},
+ {Key: "name", Value: apstra.QEStringVal("n_system")},
+ }
+
+ // []QEEAttribute to match the system hostname, label, role, etc...
+ systemNodeAttributes := append(systemNodeBaseAttributes, o.QEEAttributes()...)
+
+ // []QEEAttribute to match the relationship between system and tag nodes
+ relationshipAttributes := []apstra.QEEAttribute{{Key: "type", Value: apstra.QEStringVal("tag")}}
+
+ // []QEEAttribute to match the tag node (further qualified in the loop below)
+ tagNodeBaseAttributes := []apstra.QEEAttribute{{Key: "type", Value: apstra.QEStringVal("tag")}}
+
+ // This is the query we actually want to execute. It's a `match()`
+ // query-of-queries which selects the system node using
+ // `systemNodeAttributes` and also selects paths from the system node to
+ // each specified tag.
+ query := new(apstra.MatchQuery)
+
+ // first query: the system node with filter.
+ query.Match(new(apstra.PathQuery).Node(systemNodeAttributes))
+
+ // now add each tag-path query.
+ for i := range tagIds {
+ tagLabelAttribute := apstra.QEEAttribute{
+ Key: "label",
+ Value: apstra.QEStringVal(tagIds[i]),
+ }
+ tagQuery := new(apstra.PathQuery).
+ Node(systemNodeBaseAttributes).
+ In(relationshipAttributes).
+ Node(append(tagNodeBaseAttributes, tagLabelAttribute))
+ query.Match(tagQuery)
+ }
+
+ return query
+}
diff --git a/apstra/blueprint/nodes_system.go b/apstra/blueprint/nodes_system.go
index 931bef9d..f46c20ec 100644
--- a/apstra/blueprint/nodes_system.go
+++ b/apstra/blueprint/nodes_system.go
@@ -3,21 +3,26 @@ package blueprint
import (
"context"
"github.com/Juniper/apstra-go-sdk/apstra"
+ apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/apstra_validator"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
type NodesTypeSystem struct {
- BlueprintId types.String `tfsdk:"blueprint_id"`
- Filter types.Object `tfsdk:"filter"`
- Ids types.Set `tfsdk:"ids"`
- QueryString types.String `tfsdk:"query_string"`
+ BlueprintId types.String `tfsdk:"blueprint_id"`
+ Filter types.Object `tfsdk:"filter"`
+ Filters types.List `tfsdk:"filters"`
+ Ids types.Set `tfsdk:"ids"`
+ QueryStrings types.List `tfsdk:"query_strings"`
}
func (o NodesTypeSystem) DataSourceAttributes() map[string]dataSourceSchema.Attribute {
@@ -31,10 +36,40 @@ func (o NodesTypeSystem) DataSourceAttributes() map[string]dataSourceSchema.Attr
MarkdownDescription: "Filter used to select only desired node IDs. All specified attributes must match.",
Optional: true,
Attributes: NodeTypeSystemAttributes{}.DataSourceAttributesAsFilter(),
+ DeprecationMessage: "The `filter` attribute is deprecated and will be removed in a future " +
+ "release. Please migrate your configuration to use `filters` instead.",
+ Validators: []validator.Object{
+ objectvalidator.ExactlyOneOf(
+ path.MatchRelative(),
+ path.MatchRoot("filters"),
+ ),
+ apstravalidator.AtLeastNAttributes(
+ 1,
+ "hostname", "id", "label", "role", "system_id", "system_type", "tag_ids",
+ ),
+ },
},
- "query_string": dataSourceSchema.StringAttribute{
- MarkdownDescription: "Graph DB query string based on the supplied filter; possibly useful for troubleshooting.",
+ "filters": dataSourceSchema.ListNestedAttribute{
+ MarkdownDescription: "Set of filters used to select only desired node IDs. For a System " +
+ "node to match a filter, all specified attributes must match (each the attributes within " +
+ "a filter are AND-ed together). The returned System 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: dataSourceSchema.NestedAttributeObject{
+ Attributes: NodeTypeSystemAttributes{}.DataSourceAttributesAsFilter(),
+ Validators: []validator.Object{
+ apstravalidator.AtLeastNAttributes(
+ 1,
+ "hostname", "id", "label", "role", "system_id", "system_type", "tag_ids",
+ ),
+ },
+ },
+ },
+ "query_strings": dataSourceSchema.ListAttribute{
+ MarkdownDescription: "Graph DB query strings based on the supplied filters; possibly useful for troubleshooting.",
Computed: true,
+ ElementType: types.StringType,
},
"ids": dataSourceSchema.SetAttribute{
MarkdownDescription: "IDs of matching `system` Graph DB nodes.",
@@ -53,81 +88,83 @@ func (o *NodesTypeSystem) ReadFromApi(ctx context.Context, client *apstra.Client
} `json:"items"`
}
- query := o.query(ctx, diags).
- SetClient(client).
- SetBlueprintId(apstra.ObjectId(o.BlueprintId.ValueString())).
- SetBlueprintType(apstra.BlueprintTypeStaging)
- if diags.HasError() { // catch errors fro
- return
- }
-
- err := query.Do(ctx, &queryResponse)
- if err != nil {
- diags.AddError("Error executing Blueprint query", err.Error())
- return
- }
-
- ids := make([]attr.Value, len(queryResponse.Items))
- for i := range queryResponse.Items {
- ids[i] = types.StringValue(queryResponse.Items[i].System.Id)
- }
-
- o.Ids = types.SetValueMust(types.StringType, ids)
- o.QueryString = types.StringValue(query.String())
-}
+ // we're always going to perform at least one query, but we keep 'em as a slice
+ var queries []apstra.MatchQuery
+ switch {
+ case utils.Known(o.Filter): // one query, because the user specified 'filter' (deprecated)
+ var filter NodeTypeSystemAttributes
+ if utils.Known(o.Filter) {
+ diags.Append(o.Filter.As(ctx, &filter, basetypes.ObjectAsOptions{})...)
+ if diags.HasError() {
+ return
+ }
+ }
-func (o *NodesTypeSystem) query(ctx context.Context, diags *diag.Diagnostics) *apstra.MatchQuery {
- var filter NodeTypeSystemAttributes
- if utils.Known(o.Filter) {
- diags.Append(o.Filter.As(ctx, &filter, basetypes.ObjectAsOptions{})...)
+ queries = []apstra.MatchQuery{*filter.query(ctx, diags)}
if diags.HasError() {
- return nil
+ return
+ }
+ case utils.Known(o.Filters): // many queries, because the user specified 'filters'
+ var filters []NodeTypeSystemAttributes
+ if utils.Known(o.Filters) {
+ diags.Append(o.Filters.ElementsAs(ctx, &filters, false)...)
+ if diags.HasError() {
+ return
+ }
}
- }
- var tagIds []string
- if utils.Known(filter.TagIds) {
- diags.Append(filter.TagIds.ElementsAs(ctx, &tagIds, false)...)
+ queries = make([]apstra.MatchQuery, len(filters))
+ for i, filter := range filters {
+ queries[i] = *filter.query(ctx, diags)
+ if diags.HasError() {
+ return
+ }
+ }
+ default: // one query because the user specified no filters - and we create a catchall
+ queries = []apstra.MatchQuery{*NodeTypeSystemAttributes{}.query(ctx, diags)}
if diags.HasError() {
- return nil
+ return
}
}
- systemNodeBaseAttributes := []apstra.QEEAttribute{
- {Key: "type", Value: apstra.QEStringVal("system")},
- {Key: "name", Value: apstra.QEStringVal("n_system")},
- }
-
- // []QEEAttribute to match the system hostname, label, role, etc... as specified by `filter`
- systemNodeAttributes := append(systemNodeBaseAttributes, filter.QEEAttributes()...)
+ // create a map of node IDs (for unique-ness) and a slice of query strings
+ idMap := make(map[string]bool)
+ queryStrings := make([]string, len(queries))
- // []QEEAttribute to match the relationship between system and tag nodes
- relationshipAttributes := []apstra.QEEAttribute{{Key: "type", Value: apstra.QEStringVal("tag")}}
+ // collect IDs and a query string for each filter/query
+ for i, query := range queries {
+ query. // flesh out the query with info needed to run it
+ SetClient(client).
+ SetBlueprintId(apstra.ObjectId(o.BlueprintId.ValueString())).
+ SetBlueprintType(apstra.BlueprintTypeStaging)
+ if diags.HasError() {
+ return
+ }
- // []QEEAttribute to match the tag node (further qualified in the loop below)
- tagNodeBaseAttributes := []apstra.QEEAttribute{{Key: "type", Value: apstra.QEStringVal("tag")}}
+ // run the query
+ err := query.Do(ctx, &queryResponse)
+ if err != nil {
+ diags.AddError("Error executing Blueprint query", err.Error())
+ return
+ }
- // This is the query we actually want to execute. It's a `match()`
- // query-of-queries which selects the system node using
- // `systemNodeAttributes` and also selects paths from the system node to
- // each specified tag.
- query := new(apstra.MatchQuery)
+ // collect the matching system IDs
+ for j := range queryResponse.Items {
+ idMap[queryResponse.Items[j].System.Id] = true
+ }
- // first query: the system node with filter.
- query.Match(new(apstra.PathQuery).Node(systemNodeAttributes))
+ // save the query string
+ queryStrings[i] = query.String()
+ }
- // now add each tag-path query.
- for i := range tagIds {
- tagLabelAttribute := apstra.QEEAttribute{
- Key: "label",
- Value: apstra.QEStringVal(tagIds[i]),
- }
- tagQuery := new(apstra.PathQuery).
- Node(systemNodeBaseAttributes).
- In(relationshipAttributes).
- Node(append(tagNodeBaseAttributes, tagLabelAttribute))
- query.Match(tagQuery)
+ // pull the IDs out of the map
+ ids := make([]attr.Value, len(idMap))
+ var i int
+ for id := range idMap {
+ ids[i] = types.StringValue(id)
+ i++
}
- return query
+ o.Ids = types.SetValueMust(types.StringType, ids)
+ o.QueryStrings = utils.ListValueOrNull(ctx, types.StringType, queryStrings, diags)
}
diff --git a/docs/data-sources/datacenter_systems.md b/docs/data-sources/datacenter_systems.md
index 12502fda..f92f0f54 100644
--- a/docs/data-sources/datacenter_systems.md
+++ b/docs/data-sources/datacenter_systems.md
@@ -40,12 +40,13 @@ output "qfx_spines" {
### Optional
-- `filter` (Attributes) Filter used to select only desired node IDs. All specified attributes must match. (see [below for nested schema](#nestedatt--filter))
+- `filter` (Attributes, Deprecated) Filter used to select only desired node IDs. All specified attributes must match. (see [below for nested schema](#nestedatt--filter))
+- `filters` (Attributes List) Set of filters used to select only desired node IDs. For a System node to match a filter, all specified attributes must match (each the attributes within a filter are AND-ed together). The returned System 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
- `ids` (Set of String) IDs of matching `system` Graph DB nodes.
-- `query_string` (String) Graph DB query string based on the supplied filter; possibly useful for troubleshooting.
+- `query_strings` (List of String) Graph DB query strings based on the supplied filters; possibly useful for troubleshooting.
### Nested Schema for `filter`
@@ -59,3 +60,17 @@ Optional:
- `system_id` (String) Apstra ID of the physical system (not to be confused with its fabric role)
- `system_type` (String) Apstra Graph DB node `system_type`
- `tag_ids` (Set of String) Set of Tag IDs (labels) - only nodes with all tags will match this filter
+
+
+
+### Nested Schema for `filters`
+
+Optional:
+
+- `hostname` (String) Apstra Graph DB node `hostname`
+- `id` (String) Apstra Graph DB node ID
+- `label` (String) Apstra Graph DB node `label`
+- `role` (String) Apstra Graph DB node `role`
+- `system_id` (String) Apstra ID of the physical system (not to be confused with its fabric role)
+- `system_type` (String) Apstra Graph DB node `system_type`
+- `tag_ids` (Set of String) Set of Tag IDs (labels) - only nodes with all tags will match this filter