diff --git a/go.mod b/go.mod index a2109077..595cbf29 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.29.0 + github.com/thoas/go-funk v0.9.3 go.uber.org/mock v0.3.0 k8s.io/api v0.28.4 k8s.io/apimachinery v0.28.4 @@ -39,7 +40,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/thoas/go-funk v0.9.3 // indirect golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/term v0.14.0 // indirect diff --git a/internal/graphql/graphql.go b/internal/graphql/graphql.go new file mode 100644 index 00000000..14b28691 --- /dev/null +++ b/internal/graphql/graphql.go @@ -0,0 +1,110 @@ +/* +Copyright (c) 2023 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in +compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions and limitations under the +License. +*/ + +package graphql + +import ( + "fmt" + + "github.com/openshift-kni/oran-o2ims/internal/model" + "github.com/openshift-kni/oran-o2ims/internal/search" +) + +type FilterOperator search.Operator + +// String generates a GraphQL string representation of the operator. It panics if used on an unknown +// operator. +func (o FilterOperator) String() (result string, err error) { + switch search.Operator(o) { + case search.Eq: + result = "=" + case search.Neq: + result = "!=" + case search.Gt: + result = ">" + case search.Gte: + result = ">=" + case search.Lt: + result = "<" + case search.Lte: + result = "<=" + default: + err = fmt.Errorf("unknown operator %d", o) + } + return +} + +type PropertyCluster string + +// MapProperty maps a specified O2 property name to the search API property name +func (p PropertyCluster) MapProperty() string { + switch p { + case "name": + return "name" + case "resourcePoolID": + return "cluster" + default: + // unknown property + return "" + } +} + +type PropertyNode string + +// MapProperty maps a specified O2 property name to the search API property name +func (p PropertyNode) MapProperty() string { + switch p { + case "description": + return "name" + case "resourcePoolID": + return "cluster" + case "globalAssetID": + return "_uid" + case "resourceID": + return "_systemUUID" + default: + // unknown property + return "" + } +} + +type FilterTerm search.Term + +// Map a filter term to a GraphQL SearchFilter +func (t FilterTerm) MapFilter(mapPropertyFunc func(string) string) (searchFilter *model.SearchFilter, err error) { + // Get filter operator + var operator string + operator, err = FilterOperator(t.Operator).String() + if err != nil { + return + } + + // Generate values + values := []*string{} + for _, v := range t.Values { + value := fmt.Sprintf("%s%s", operator, v.(string)) + values = append(values, &value) + } + + // Convert to GraphQL property + searchProperty := mapPropertyFunc(t.Path[0]) + + // Build search filter + searchFilter = &model.SearchFilter{ + Property: searchProperty, + Values: values, + } + + return +} diff --git a/internal/graphql/graphql_test.go b/internal/graphql/graphql_test.go new file mode 100644 index 00000000..aebdce33 --- /dev/null +++ b/internal/graphql/graphql_test.go @@ -0,0 +1,73 @@ +/* +Copyright (c) 2023 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in +compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions and limitations under the +License. +*/ + +package graphql + +import ( + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/ginkgo/v2/dsl/table" + . "github.com/onsi/gomega" + "github.com/openshift-kni/oran-o2ims/internal/model" + "github.com/openshift-kni/oran-o2ims/internal/search" + "k8s.io/utils/ptr" +) + +var _ = Describe("GraphQL filters", func() { + DescribeTable( + "Map a filter term to a SearchFilter", + func(term search.Term, expected *model.SearchFilter, mapPropertyFunc func(string) string) { + actual, err := FilterTerm(term).MapFilter(mapPropertyFunc) + Expect(err).ToNot(HaveOccurred()) + Expect(actual).To(Equal(expected)) + }, + Entry( + "Filter term for Cluster", + search.Term{ + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + func(s string) string { + return PropertyCluster(s).MapProperty() + }, + ), + Entry( + "Filter term for Node", + search.Term{ + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + func(s string) string { + return PropertyNode(s).MapProperty() + }, + ), + ) +}) diff --git a/internal/graphql/suite_test.go b/internal/graphql/suite_test.go new file mode 100644 index 00000000..0ed5954d --- /dev/null +++ b/internal/graphql/suite_test.go @@ -0,0 +1,40 @@ +/* +Copyright (c) 2023 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in +compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions and limitations under the +License. +*/ + +package graphql + +import ( + "log/slog" + "testing" + + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/gomega" +) + +func TestSearch(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Search") +} + +var logger *slog.Logger + +var _ = BeforeSuite(func() { + // Create a logger that writes to the Ginkgo writer, so that the log messages will be + // attached to the output of the right test: + options := &slog.HandlerOptions{ + Level: slog.LevelDebug, + } + handler := slog.NewJSONHandler(GinkgoWriter, options) + logger = slog.New(handler) +}) diff --git a/internal/service/resource_fetcher.go b/internal/service/resource_fetcher.go index 0f78113b..3b3a2303 100644 --- a/internal/service/resource_fetcher.go +++ b/internal/service/resource_fetcher.go @@ -217,7 +217,6 @@ func (r *ResourceFetcher) getSearchResponse(ctx context.Context) (result io.Read if err != nil { return } - r.logger.Error(fmt.Sprintf("%v", graphqlVars)) // Build GraphQL request body var requestBody bytes.Buffer diff --git a/internal/service/resource_handler.go b/internal/service/resource_handler.go index 42b29ef8..09ac5fe2 100644 --- a/internal/service/resource_handler.go +++ b/internal/service/resource_handler.go @@ -24,6 +24,7 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/openshift-kni/oran-o2ims/internal/data" + "github.com/openshift-kni/oran-o2ims/internal/graphql" "github.com/openshift-kni/oran-o2ims/internal/model" "github.com/openshift-kni/oran-o2ims/internal/search" ) @@ -190,7 +191,7 @@ func (b *ResourceHandlerBuilder) Build() ( func (h *ResourceHandler) List(ctx context.Context, request *ListRequest) (response *ListResponse, err error) { // Transform the items into what we need: - resources, err := h.fetchItems(ctx, request.ParentID) + resources, err := h.fetchItems(ctx, request.ParentID, request.Selector) if err != nil { return } @@ -230,7 +231,7 @@ func (h *ResourceHandler) Get(ctx context.Context, } // Fetch the object: - resource, err := h.fetchItem(ctx, request.ID, request.ParentID, h.resourceFetcher) + resource, err := h.fetchItem(ctx, request.ID, request.ParentID) if err != nil { return } @@ -243,7 +244,7 @@ func (h *ResourceHandler) Get(ctx context.Context, } func (h *ResourceHandler) fetchItems( - ctx context.Context, parentID string) (result data.Stream, err error) { + ctx context.Context, parentID string, selector *search.Selector) (result data.Stream, err error) { h.resourceFetcher, err = NewResourceFetcher(). SetLogger(h.logger). SetTransportWrapper(h.transportWrapper). @@ -251,7 +252,7 @@ func (h *ResourceHandler) fetchItems( SetBackendURL(h.backendURL). SetBackendToken(h.backendToken). SetGraphqlQuery(h.graphqlQuery). - SetGraphqlVars(h.getCollectionGraphqlVars(ctx, parentID)). + SetGraphqlVars(h.getCollectionGraphqlVars(ctx, parentID, selector)). Build() if err != nil { return @@ -269,14 +270,14 @@ func (h *ResourceHandler) fetchItems( } func (h *ResourceHandler) fetchItem(ctx context.Context, - id, parentID string, resourceFetcher *ResourceFetcher) (resource data.Object, err error) { + id, parentID string) (resource data.Object, err error) { // Fetch items - items, err := resourceFetcher.FetchItems(ctx) + items, err := h.resourceFetcher.FetchItems(ctx) if err != nil { return } - // Transform Items to Resources + // Transform Items to O2 Resources resources := data.Map(items, h.mapItem) // Get first result @@ -286,7 +287,7 @@ func (h *ResourceHandler) fetchItem(ctx context.Context, } func (h *ResourceHandler) getObjectGraphqlVars(ctx context.Context, id, parentId string) (graphqlVars *model.SearchInput) { - graphqlVars = h.getCollectionGraphqlVars(ctx, parentId) + graphqlVars = h.getCollectionGraphqlVars(ctx, parentId, nil) // Filter results by resource ID graphqlVars.Filters = append(graphqlVars.Filters, &model.SearchFilter{ @@ -296,7 +297,7 @@ func (h *ResourceHandler) getObjectGraphqlVars(ctx context.Context, id, parentId return } -func (h *ResourceHandler) getCollectionGraphqlVars(ctx context.Context, id string) (graphqlVars *model.SearchInput) { +func (h *ResourceHandler) getCollectionGraphqlVars(ctx context.Context, id string, selector *search.Selector) (graphqlVars *model.SearchInput) { graphqlVars = &model.SearchInput{} graphqlVars.Keywords = h.graphqlVars.Keywords graphqlVars.Filters = h.graphqlVars.Filters @@ -315,11 +316,35 @@ func (h *ResourceHandler) getCollectionGraphqlVars(ctx context.Context, id strin Values: []*string{&nonEmpty}, }) + // Add filters from the request params + if selector != nil { + for _, term := range selector.Terms { + searchFilter, err := graphql.FilterTerm(*term).MapFilter(func(s string) string { + return graphql.PropertyNode(s).MapProperty() + }) + if err != nil { + h.logger.Error( + "Failed to map GraphQL filter term (fallback to selector filtering).", + slog.String("filter", term.String()), + slog.String("error", err.Error()), + ) + continue + } + h.logger.Debug( + "Mapped filter term to GraphQL SearchFilter", + slog.String("term", term.String()), + slog.String("mapped property", searchFilter.Property), + slog.String("mapped value", *searchFilter.Values[0]), + ) + graphqlVars.Filters = append(graphqlVars.Filters, searchFilter) + } + } + return } -// Map an item to and O2 Resource object. -func (h *ResourceHandler) mapItem(ctx context.Context, +// Map an item to an O2 Resource object. +func (r *ResourceHandler) mapItem(ctx context.Context, from data.Object) (to data.Object, err error) { kind, err := data.GetString(from, "kind") if err != nil { @@ -328,21 +353,23 @@ func (h *ResourceHandler) mapItem(ctx context.Context, switch kind { case KindNode: - return h.mapNodeItem(ctx, from) + return r.mapNodeItem(ctx, from) } return } // Map a Node to an O2 Resource object. -func (h *ResourceHandler) mapNodeItem(ctx context.Context, +func (r *ResourceHandler) mapNodeItem(ctx context.Context, from data.Object) (to data.Object, err error) { - description, err := data.GetString(from, "name") + description, err := data.GetString(from, + graphql.PropertyNode("description").MapProperty()) if err != nil { return } - resourcePoolID, err := data.GetString(from, "cluster") + resourcePoolID, err := data.GetString(from, + graphql.PropertyNode("resourcePoolID").MapProperty()) if err != nil { return } @@ -353,17 +380,19 @@ func (h *ResourceHandler) mapNodeItem(ctx context.Context, } labelsMap := data.GetLabelsMap(labels) - globalAssetID, err := data.GetString(from, "_uid") + globalAssetID, err := data.GetString(from, + graphql.PropertyNode("globalAssetID").MapProperty()) if err != nil { return } - resourceID, err := data.GetString(from, "_systemUUID") + resourceID, err := data.GetString(from, + graphql.PropertyNode("resourceID").MapProperty()) if err != nil { return } - resourceTypeID, err := h.resourceFetcher.GetResourceTypeID(from) + resourceTypeID, err := r.resourceFetcher.GetResourceTypeID(from) if err != nil { return } diff --git a/internal/service/resource_handler_test.go b/internal/service/resource_handler_test.go index fed91260..d202afd7 100644 --- a/internal/service/resource_handler_test.go +++ b/internal/service/resource_handler_test.go @@ -25,6 +25,7 @@ import ( "github.com/openshift-kni/oran-o2ims/internal/data" "github.com/openshift-kni/oran-o2ims/internal/model" + "github.com/openshift-kni/oran-o2ims/internal/search" . "github.com/openshift-kni/oran-o2ims/internal/testing" "github.com/openshift-kni/oran-o2ims/internal/text" ) @@ -225,6 +226,126 @@ var _ = Describe("Resource handler", func() { Expect(items[1]).To(MatchJQ(`.description`, "my-node-1")) Expect(items[1]).To(MatchJQ(`.resourcePoolID`, "1")) }) + + It("Accepts a filter", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{{ + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + Expect(handler.resourceFetcher.graphqlVars.Filters).To(HaveLen(4)) + Expect(handler.resourceFetcher.graphqlVars.Filters).To(ContainElement( + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + )) + }) + + It("Accepts multiple filters", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{ + { + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + { + Operator: search.Neq, + Path: []string{ + "description", + }, + Values: []any{ + "node0", + }, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + Expect(handler.resourceFetcher.graphqlVars.Filters).To(HaveLen(5)) + Expect(handler.resourceFetcher.graphqlVars.Filters).To(ContainElements( + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + &model.SearchFilter{ + Property: "name", + Values: []*string{ptr.To("!=node0")}, + }, + )) + }) + + It("Ignore invalid filters", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{ + { + Operator: search.Cont, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + { + Operator: search.In, + Path: []string{ + "description", + }, + Values: []any{ + "node0", + }, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + // (3 filters are added by default) + Expect(handler.resourceFetcher.graphqlVars.Filters).To(HaveLen(3)) + }) }) Describe("Get", func() { diff --git a/internal/service/resource_pool_fetcher.go b/internal/service/resource_pool_fetcher.go index cb3108d5..d746ed7e 100644 --- a/internal/service/resource_pool_fetcher.go +++ b/internal/service/resource_pool_fetcher.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/openshift-kni/oran-o2ims/internal/data" + "github.com/openshift-kni/oran-o2ims/internal/graphql" "github.com/openshift-kni/oran-o2ims/internal/k8s" "github.com/openshift-kni/oran-o2ims/internal/model" "github.com/thoas/go-funk" @@ -242,12 +243,14 @@ func (r *ResourcePoolFetcher) getSearchResponse(ctx context.Context) (result io. // Map Cluster to an O2 ResourcePool object. func (r *ResourcePoolFetcher) mapClusterItem(ctx context.Context, from data.Object) (to data.Object, err error) { - resourcePoolID, err := data.GetString(from, "cluster") + resourcePoolID, err := data.GetString(from, + graphql.PropertyCluster("resourcePoolID").MapProperty()) if err != nil { return } - name, err := data.GetString(from, "name") + name, err := data.GetString(from, + graphql.PropertyCluster("name").MapProperty()) if err != nil { return } diff --git a/internal/service/resource_pool_handler.go b/internal/service/resource_pool_handler.go index dc240b5f..c4425726 100644 --- a/internal/service/resource_pool_handler.go +++ b/internal/service/resource_pool_handler.go @@ -24,6 +24,7 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/openshift-kni/oran-o2ims/internal/data" + "github.com/openshift-kni/oran-o2ims/internal/graphql" "github.com/openshift-kni/oran-o2ims/internal/model" "github.com/openshift-kni/oran-o2ims/internal/search" ) @@ -44,16 +45,17 @@ type ResourcePoolHandlerBuilder struct { // ResourcePoolHandler knows how to respond to requests to list resource pools. Don't create // instances of this type directly, use the NewResourcePoolHandler function instead. type ResourcePoolHandler struct { - logger *slog.Logger - transportWrapper func(http.RoundTripper) http.RoundTripper - cloudID string - backendURL string - backendToken string - backendClient *http.Client - jsonAPI jsoniter.API - selectorEvaluator *search.SelectorEvaluator - graphqlQuery string - graphqlVars *model.SearchInput + logger *slog.Logger + transportWrapper func(http.RoundTripper) http.RoundTripper + cloudID string + backendURL string + backendToken string + backendClient *http.Client + jsonAPI jsoniter.API + selectorEvaluator *search.SelectorEvaluator + graphqlQuery string + graphqlVars *model.SearchInput + resourcePoolFetcher *ResourcePoolFetcher } // NewResourcePoolHandler creates a builder that can then be used to configure and create a @@ -190,7 +192,7 @@ func (h *ResourcePoolHandler) List(ctx context.Context, request *ListRequest) (response *ListResponse, err error) { // Transform the items into what we need: - resourcePools, err := h.fetchItems(ctx) + resourcePools, err := h.fetchItems(ctx, request.Selector) if err != nil { return } @@ -217,7 +219,7 @@ func (h *ResourcePoolHandler) List(ctx context.Context, func (h *ResourcePoolHandler) Get(ctx context.Context, request *GetRequest) (response *GetResponse, err error) { - resourcePoolFetcher, err := NewResourcePoolFetcher(). + h.resourcePoolFetcher, err = NewResourcePoolFetcher(). SetLogger(h.logger). SetTransportWrapper(h.transportWrapper). SetCloudID(h.cloudID). @@ -231,7 +233,7 @@ func (h *ResourcePoolHandler) Get(ctx context.Context, } // Fetch the object: - resourcePool, err := h.fetchItem(ctx, request.ID, resourcePoolFetcher) + resourcePool, err := h.fetchItem(ctx, request.ID) if err != nil { return } @@ -245,26 +247,26 @@ func (h *ResourcePoolHandler) Get(ctx context.Context, } func (h *ResourcePoolHandler) fetchItems( - ctx context.Context) (result data.Stream, err error) { - resourcePoolFetcher, err := NewResourcePoolFetcher(). + ctx context.Context, selector *search.Selector) (result data.Stream, err error) { + h.resourcePoolFetcher, err = NewResourcePoolFetcher(). SetLogger(h.logger). SetTransportWrapper(h.transportWrapper). SetCloudID(h.cloudID). SetBackendURL(h.backendURL). SetBackendToken(h.backendToken). SetGraphqlQuery(h.graphqlQuery). - SetGraphqlVars(h.graphqlVars). + SetGraphqlVars(h.getCollectionGraphqlVars(ctx, selector)). Build() if err != nil { return } - return resourcePoolFetcher.FetchItems(ctx) + return h.resourcePoolFetcher.FetchItems(ctx) } func (h *ResourcePoolHandler) fetchItem(ctx context.Context, - id string, resourcePoolFetcher *ResourcePoolFetcher) (resourcePool data.Object, err error) { + id string) (resourcePool data.Object, err error) { // Fetch resource pools - resourcePools, err := resourcePoolFetcher.FetchItems(ctx) + resourcePools, err := h.resourcePoolFetcher.FetchItems(ctx) if err != nil { return } @@ -275,6 +277,38 @@ func (h *ResourcePoolHandler) fetchItem(ctx context.Context, return } +func (h *ResourcePoolHandler) getCollectionGraphqlVars(ctx context.Context, selector *search.Selector) (graphqlVars *model.SearchInput) { + graphqlVars = &model.SearchInput{} + graphqlVars.Keywords = h.graphqlVars.Keywords + graphqlVars.Filters = h.graphqlVars.Filters + + // Add filters from the request params + if selector != nil { + for _, term := range selector.Terms { + searchFilter, err := graphql.FilterTerm(*term).MapFilter(func(s string) string { + return graphql.PropertyCluster(s).MapProperty() + }) + if err != nil { + h.logger.Error( + "Failed to map GraphQL filter term (fallback to selector filtering).", + slog.String("term", term.String()), + slog.String("error", err.Error()), + ) + continue + } + h.logger.Debug( + "Mapped filter term to GraphQL SearchFilter", + slog.String("term", term.String()), + slog.String("mapped property", searchFilter.Property), + slog.String("mapped value", *searchFilter.Values[0]), + ) + graphqlVars.Filters = append(graphqlVars.Filters, searchFilter) + } + } + + return +} + func (h *ResourcePoolHandler) getObjectGraphqlVars(ctx context.Context, id string) (graphqlVars *model.SearchInput) { graphqlVars = &model.SearchInput{} graphqlVars.Keywords = h.graphqlVars.Keywords diff --git a/internal/service/resource_pool_handler_test.go b/internal/service/resource_pool_handler_test.go index 68de3951..19a23dca 100644 --- a/internal/service/resource_pool_handler_test.go +++ b/internal/service/resource_pool_handler_test.go @@ -25,6 +25,7 @@ import ( "github.com/openshift-kni/oran-o2ims/internal/data" "github.com/openshift-kni/oran-o2ims/internal/model" + "github.com/openshift-kni/oran-o2ims/internal/search" . "github.com/openshift-kni/oran-o2ims/internal/testing" "github.com/openshift-kni/oran-o2ims/internal/text" ) @@ -217,6 +218,126 @@ var _ = Describe("Resource pool handler", func() { Expect(items[1]).To(MatchJQ(`.oCloudID`, "123")) Expect(items[1]).To(MatchJQ(`.resourcePoolID`, "1")) }) + + It("Accepts a filter", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{{ + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + Expect(handler.resourcePoolFetcher.graphqlVars.Filters).To(HaveLen(2)) + Expect(handler.resourcePoolFetcher.graphqlVars.Filters).To(ContainElement( + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + )) + }) + + It("Accepts multiple filters", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{ + { + Operator: search.Eq, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + { + Operator: search.Neq, + Path: []string{ + "name", + }, + Values: []any{ + "cluster0", + }, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + Expect(handler.resourcePoolFetcher.graphqlVars.Filters).To(HaveLen(3)) + Expect(handler.resourcePoolFetcher.graphqlVars.Filters).To(ContainElements( + &model.SearchFilter{ + Property: "cluster", + Values: []*string{ptr.To("=spoke0")}, + }, + &model.SearchFilter{ + Property: "name", + Values: []*string{ptr.To("!=cluster0")}, + }, + )) + }) + + It("Ignore invalid filters", func() { + // Prepare the backend: + backend.AppendHandlers( + RespondWithItems(), + ) + + // Send the request: + response, err := handler.List(ctx, &ListRequest{ + Selector: &search.Selector{ + Terms: []*search.Term{ + { + Operator: search.Cont, + Path: []string{ + "resourcePoolID", + }, + Values: []any{ + "spoke0", + }, + }, + { + Operator: search.In, + Path: []string{ + "name", + }, + Values: []any{ + "cluster0", + }, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + + // Verify GraphQL filters: + // (1 filter is added by default) + Expect(handler.resourcePoolFetcher.graphqlVars.Filters).To(HaveLen(1)) + }) }) Describe("Get", func() { diff --git a/internal/service/resource_type_handler.go b/internal/service/resource_type_handler.go index bd1ed53b..ec200262 100644 --- a/internal/service/resource_type_handler.go +++ b/internal/service/resource_type_handler.go @@ -340,7 +340,7 @@ func (h *ResourceTypeHandler) getGraphqlVars(ctx context.Context) (graphqlVars * return } -// Map an item to and O2 Resource object. +// Map an item to an O2 Resource object. func (h *ResourceTypeHandler) mapItem(ctx context.Context, from data.Object) (to data.Object, err error) { kind, err := data.GetString(from, "kind")