From 71f70109d111eb6541fe14e3278e3cafb424d8ae Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 25 Sep 2023 19:18:25 -0400 Subject: [PATCH 1/4] add support for multiple filters --- .../apstra_validator/object_must_have_n_of.go | 142 ++++++++++ .../object_must_have_n_of_test.go | 251 ++++++++++++++++++ apstra/blueprint/node_system_attributes.go | 60 ++++- apstra/blueprint/nodes_system.go | 167 +++++++----- 4 files changed, 541 insertions(+), 79 deletions(-) create mode 100644 apstra/apstra_validator/object_must_have_n_of.go create mode 100644 apstra/apstra_validator/object_must_have_n_of_test.go 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..7c0915f5 --- /dev/null +++ b/apstra/apstra_validator/object_must_have_n_of.go @@ -0,0 +1,142 @@ +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 + } + + 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 + attributeMap := req.ConfigValue.Attributes() + 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 for each known 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..588237f1 --- /dev/null +++ b/apstra/apstra_validator/object_must_have_n_of_test.go @@ -0,0 +1,251 @@ +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": { + 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, + }, + "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..30208d40 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,75 @@ 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()) -} + var queries []apstra.MatchQuery + switch { + case utils.Known(o.Filter): + 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): + 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: + 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()...) - - // []QEEAttribute to match the relationship between system and tag nodes - relationshipAttributes := []apstra.QEEAttribute{{Key: "type", Value: apstra.QEStringVal("tag")}} + idMap := make(map[string]bool) + queryStrings := make([]string, len(queries)) + for i, query := range queries { + query. + 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")}} + 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) + 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)) + 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) + 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) } From bc66489aecdfcba85dacfffa4c7bdf42ac3b1493 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 25 Sep 2023 19:20:17 -0400 Subject: [PATCH 2/4] make docs --- docs/data-sources/datacenter_systems.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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 From 333c10ebfe1e5fd4cdb57b001ce74a529b68b152 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 25 Sep 2023 19:32:49 -0400 Subject: [PATCH 3/4] comments --- apstra/blueprint/nodes_system.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apstra/blueprint/nodes_system.go b/apstra/blueprint/nodes_system.go index 30208d40..f46c20ec 100644 --- a/apstra/blueprint/nodes_system.go +++ b/apstra/blueprint/nodes_system.go @@ -88,9 +88,10 @@ func (o *NodesTypeSystem) ReadFromApi(ctx context.Context, client *apstra.Client } `json:"items"` } + // 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): + 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{})...) @@ -103,7 +104,7 @@ func (o *NodesTypeSystem) ReadFromApi(ctx context.Context, client *apstra.Client if diags.HasError() { return } - case utils.Known(o.Filters): + 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)...) @@ -119,17 +120,20 @@ func (o *NodesTypeSystem) ReadFromApi(ctx context.Context, client *apstra.Client return } } - default: + 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 } } + // 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)) + + // collect IDs and a query string for each filter/query for i, query := range queries { - query. + query. // flesh out the query with info needed to run it SetClient(client). SetBlueprintId(apstra.ObjectId(o.BlueprintId.ValueString())). SetBlueprintType(apstra.BlueprintTypeStaging) @@ -137,19 +141,23 @@ func (o *NodesTypeSystem) ReadFromApi(ctx context.Context, client *apstra.Client return } + // run the query err := query.Do(ctx, &queryResponse) if err != nil { diags.AddError("Error executing Blueprint query", err.Error()) return } + // collect the matching system IDs for j := range queryResponse.Items { idMap[queryResponse.Items[j].System.Id] = true } + // save the query string queryStrings[i] = query.String() } + // pull the IDs out of the map ids := make([]attr.Value, len(idMap)) var i int for id := range idMap { From efe61c6e3056aa5cf2fe3fd613f8ba1ab77def1d Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Tue, 26 Sep 2023 11:11:41 -0400 Subject: [PATCH 4/4] change validator so empty attribute list means all attributes --- .../apstra_validator/object_must_have_n_of.go | 23 ++++++- .../object_must_have_n_of_test.go | 64 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/apstra/apstra_validator/object_must_have_n_of.go b/apstra/apstra_validator/object_must_have_n_of.go index 7c0915f5..f3ea97eb 100644 --- a/apstra/apstra_validator/object_must_have_n_of.go +++ b/apstra/apstra_validator/object_must_have_n_of.go @@ -31,6 +31,22 @@ func (o mustHaveNOfValidator) ValidateObject(_ context.Context, req validator.Ob 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, @@ -46,8 +62,9 @@ func (o mustHaveNOfValidator) ValidateObject(_ context.Context, req validator.Ob return } - foundValueCount := 0 - attributeMap := req.ConfigValue.Attributes() + 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 @@ -78,7 +95,7 @@ func (o mustHaveNOfValidator) ValidateObject(_ context.Context, req validator.Ob return } - // increment the counter for each known value + // increment the counter if the attribute has a value if !foundAttribute.IsNull() { foundValueCount++ } diff --git a/apstra/apstra_validator/object_must_have_n_of_test.go b/apstra/apstra_validator/object_must_have_n_of_test.go index 588237f1..cd8a9508 100644 --- a/apstra/apstra_validator/object_must_have_n_of_test.go +++ b/apstra/apstra_validator/object_must_have_n_of_test.go @@ -96,7 +96,7 @@ func TestObjectMustHaveNOf(t *testing.T) { expAtMostErr: true, expExactlyErr: true, }, - "2:3": { + "2:3:3": { n: 2, checkAttributes: []string{"a1", "a2", "a3"}, attrTypes: map[string]attr.Type{ @@ -187,6 +187,68 @@ func TestObjectMustHaveNOf(t *testing.T) { 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"},