diff --git a/api/mesh/v1alpha1/externalservice_helpers.go b/api/mesh/v1alpha1/externalservice_helpers.go index e2e6e82f518d..486b833d4a85 100644 --- a/api/mesh/v1alpha1/externalservice_helpers.go +++ b/api/mesh/v1alpha1/externalservice_helpers.go @@ -17,6 +17,10 @@ func (es *ExternalService) MatchTags(selector TagSelector) bool { return selector.Matches(es.Tags) } +func (es *ExternalService) MatchTagsFuzzy(selector TagSelector) bool { + return selector.MatchesFuzzy(es.Tags) +} + func (es *ExternalService) GetService() string { if es == nil { return "" diff --git a/pkg/api-server/api_server_suite_test.go b/pkg/api-server/api_server_suite_test.go index cf24530809e3..4f24972e4507 100644 --- a/pkg/api-server/api_server_suite_test.go +++ b/pkg/api-server/api_server_suite_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "io" "net" "net/http" "path/filepath" @@ -13,6 +14,7 @@ import ( "github.com/emicklei/go-restful/v3" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" api_server "github.com/kumahq/kuma/pkg/api-server" @@ -51,6 +53,35 @@ type resourceApiClient struct { path string } +type TestMeta struct { + Type string `json:"type"` + Name string `json:"name"` + Mesh string `json:"mesh"` +} + +type TestListResponse struct { + Total int `json:"total"` + Next string `json:"next"` + Items []TestMeta `json:"items"` +} + +func MatchListResponse(r TestListResponse) types.GomegaMatcher { + return And( + HaveHTTPStatus(http.StatusOK), + WithTransform(func(response *http.Response) (TestListResponse, error) { + res := TestListResponse{} + body, err := io.ReadAll(response.Body) + if err != nil { + return res, nil + } + if err := json.Unmarshal(body, &res); err != nil { + return res, err + } + return res, nil + }, Equal(r)), + ) +} + func (r *resourceApiClient) fullAddress() string { return "http://" + r.address + r.path } diff --git a/pkg/api-server/dataplane_overview_endpoints.go b/pkg/api-server/dataplane_overview_endpoints.go index 1c8819ae0173..69041a085f3e 100644 --- a/pkg/api-server/dataplane_overview_endpoints.go +++ b/pkg/api-server/dataplane_overview_endpoints.go @@ -19,6 +19,7 @@ import ( type dataplaneOverviewEndpoints struct { resManager manager.ResourceManager resourceAccess access.ResourceAccess + filter func(request *restful.Request) (store.ListFilterFunc, error) } func (r *dataplaneOverviewEndpoints) addFindEndpoint(ws *restful.WebService, pathPrefix string) { @@ -106,7 +107,7 @@ func (r *dataplaneOverviewEndpoints) inspectDataplanes(request *restful.Request, return } - filter, err := genFilter(request) + filter, err := r.filter(request) if err != nil { rest_errors.HandleError(request.Request.Context(), response, err, "Could not retrieve dataplane overviews") return diff --git a/pkg/api-server/filtering.go b/pkg/api-server/filters/filtering.go similarity index 61% rename from pkg/api-server/filtering.go rename to pkg/api-server/filters/filtering.go index 988dae5921b9..f92f8ae704e7 100644 --- a/pkg/api-server/filtering.go +++ b/pkg/api-server/filters/filtering.go @@ -1,4 +1,4 @@ -package api_server +package filters import ( "reflect" @@ -13,6 +13,47 @@ import ( "github.com/kumahq/kuma/pkg/core/validators" ) +// Resource return a store filter depending on the resource. We take a descriptor so that we can do advance filtering options +// For example we could make a filter that works on top level targetRef by looking at the descriptor info +func Resource(resDescriptor core_model.ResourceTypeDescriptor) func(request *restful.Request) (store.ListFilterFunc, error) { + switch resDescriptor.Name { + case mesh.DataplaneType: + return func(request *restful.Request) (store.ListFilterFunc, error) { + gatewayFilter, err := gatewayModeFilterFromParameter(request) + if err != nil { + return nil, err + } + + tags := parseTags(request.QueryParameters("tag")) + + return func(rs core_model.Resource) bool { + dataplane := rs.(*mesh.DataplaneResource) + if !gatewayFilter(dataplane.Spec.GetNetworking().GetGateway()) { + return false + } + + if !dataplane.Spec.MatchTagsFuzzy(tags) { + return false + } + + return true + }, nil + } + case mesh.ExternalServiceType: + return func(request *restful.Request) (store.ListFilterFunc, error) { + tags := parseTags(request.QueryParameters("tag")) + + return func(rs core_model.Resource) bool { + return rs.(*mesh.ExternalServiceResource).Spec.MatchTagsFuzzy(tags) + }, nil + } + default: + return func(request *restful.Request) (store.ListFilterFunc, error) { + return nil, nil + } + } +} + type DpFilter func(*mesh_proto.Dataplane_Networking_Gateway) bool func gatewayModeFilterFromParameter(request *restful.Request) (DpFilter, error) { @@ -52,28 +93,6 @@ func gatewayModeFilterFromParameter(request *restful.Request) (DpFilter, error) } } -func genFilter(request *restful.Request) (store.ListFilterFunc, error) { - gatewayFilter, err := gatewayModeFilterFromParameter(request) - if err != nil { - return nil, err - } - - tags := parseTags(request.QueryParameters("tag")) - - return func(rs core_model.Resource) bool { - dataplane := rs.(*mesh.DataplaneResource) - if !gatewayFilter(dataplane.Spec.GetNetworking().GetGateway()) { - return false - } - - if !dataplane.Spec.MatchTagsFuzzy(tags) { - return false - } - - return true - }, nil -} - // Tags should be passed in form of ?tag=service:mobile&tag=version:v1 func parseTags(queryParamValues []string) map[string]string { tags := make(map[string]string) diff --git a/pkg/api-server/resource_endpoints.go b/pkg/api-server/resource_endpoints.go index 6969d3aff3ef..f4a164b13e38 100644 --- a/pkg/api-server/resource_endpoints.go +++ b/pkg/api-server/resource_endpoints.go @@ -40,6 +40,7 @@ type resourceEndpoints struct { descriptor model.ResourceTypeDescriptor resourceAccess access.ResourceAccess k8sMapper k8s.ResourceMapperFunc + filter func(request *restful.Request) (store.ListFilterFunc, error) } func (r *resourceEndpoints) addFindEndpoint(ws *restful.WebService, pathPrefix string) { @@ -115,9 +116,14 @@ func (r *resourceEndpoints) listResources(request *restful.Request, response *re rest_errors.HandleError(request.Request.Context(), response, err, "Could not retrieve resources") return } + filter, err := r.filter(request) + if err != nil { + rest_errors.HandleError(request.Request.Context(), response, err, "Could not retrieve resources") + return + } list := r.descriptor.NewList() - if err := r.resManager.List(request.Request.Context(), list, store.ListByMesh(meshName), store.ListByPage(page.size, page.offset)); err != nil { + if err := r.resManager.List(request.Request.Context(), list, store.ListByMesh(meshName), store.ListByFilterFunc(filter), store.ListByPage(page.size, page.offset)); err != nil { rest_errors.HandleError(request.Request.Context(), response, err, "Could not retrieve resources") } else { restList := rest.From.ResourceList(list) diff --git a/pkg/api-server/resource_endpoints_test.go b/pkg/api-server/resource_endpoints_test.go index a763743de284..d0db35068b02 100644 --- a/pkg/api-server/resource_endpoints_test.go +++ b/pkg/api-server/resource_endpoints_test.go @@ -295,6 +295,108 @@ var _ = Describe("Resource Endpoints", func() { )) }) + It("should list external services with filters", func() { + esWithTags := func(svc string, kv ...string) *core_mesh.ExternalServiceResource { + tags := map[string]string{ + "kuma.io/service": svc, + } + for i := 0; i < len(kv); i += 2 { + tags[kv[i]] = kv[i+1] + } + return &core_mesh.ExternalServiceResource{ + Spec: &mesh_proto.ExternalService{ + Tags: tags, + }, + } + } + // given three resources + for i := 0; i < 3; i++ { + err := resourceStore.Create(context.Background(), esWithTags("my-svc"), store.CreateByKey(fmt.Sprintf("dp-%02d", i), mesh)) + Expect(err).NotTo(HaveOccurred()) + } + err := resourceStore.Create(context.Background(), esWithTags("other-svc"), store.CreateByKey("dp-not-good", mesh)) + Expect(err).NotTo(HaveOccurred()) + + // when ask for dataplanes with "my-svc" filter + client = resourceApiClient{ + address: apiServer.Address(), + path: "/external-services?tag=kuma.io/service:my-svc", + } + response := client.list() + Expect(response).To(MatchListResponse(TestListResponse{ + Total: 3, + Next: "", + Items: []TestMeta{ + { + Mesh: "default", + Name: "dp-00", + Type: "ExternalService", + }, + { + Mesh: "default", + Name: "dp-01", + Type: "ExternalService", + }, + { + Mesh: "default", + Name: "dp-02", + Type: "ExternalService", + }, + }, + })) + }) + + It("should list dp with tag filters", func() { + dpWithService := func(n string) *core_mesh.DataplaneResource { + return &core_mesh.DataplaneResource{ + Spec: &mesh_proto.Dataplane{ + Networking: &mesh_proto.Dataplane_Networking{ + Inbound: []*mesh_proto.Dataplane_Networking_Inbound{ + { + Tags: map[string]string{"kuma.io/service": n}, + }, + }, + }, + }, + } + } + // given three resources + for i := 0; i < 3; i++ { + err := resourceStore.Create(context.Background(), dpWithService("my-svc"), store.CreateByKey(fmt.Sprintf("dp-%02d", i), mesh)) + Expect(err).NotTo(HaveOccurred()) + } + err := resourceStore.Create(context.Background(), dpWithService("other-svc"), store.CreateByKey("dp-not-good", mesh)) + Expect(err).NotTo(HaveOccurred()) + + // when ask for dataplanes with "my-svc" filter + client = resourceApiClient{ + address: apiServer.Address(), + path: "/dataplanes?tag=kuma.io/service:my-svc", + } + response := client.list() + Expect(response).To(MatchListResponse(TestListResponse{ + Total: 3, + Next: "", + Items: []TestMeta{ + { + Mesh: "default", + Name: "dp-00", + Type: "Dataplane", + }, + { + Mesh: "default", + Name: "dp-01", + Type: "Dataplane", + }, + { + Mesh: "default", + Name: "dp-02", + Type: "Dataplane", + }, + }, + })) + }) + It("should list resources using pagination", func() { // given three resources putSampleResourceIntoStore(resourceStore, "tr-1", "mesh-1") diff --git a/pkg/api-server/server.go b/pkg/api-server/server.go index 11dc53cad88d..f6c516a60728 100644 --- a/pkg/api-server/server.go +++ b/pkg/api-server/server.go @@ -25,6 +25,7 @@ import ( "github.com/kumahq/kuma/pkg/api-server/authn" "github.com/kumahq/kuma/pkg/api-server/customization" + "github.com/kumahq/kuma/pkg/api-server/filters" api_server "github.com/kumahq/kuma/pkg/config/api-server" kuma_cp "github.com/kumahq/kuma/pkg/config/app/kuma-cp" config_core "github.com/kumahq/kuma/pkg/config/core" @@ -231,6 +232,7 @@ func addResourcesEndpoints( dpOverviewEndpoints := dataplaneOverviewEndpoints{ resManager: resManager, resourceAccess: resourceAccess, + filter: filters.Resource(mesh.DataplaneResourceTypeDescriptor), } dpOverviewEndpoints.addListEndpoint(ws, "/meshes/{mesh}") dpOverviewEndpoints.addFindEndpoint(ws, "/meshes/{mesh}") @@ -286,6 +288,7 @@ func addResourcesEndpoints( resManager: resManager, descriptor: definition, resourceAccess: resourceAccess, + filter: filters.Resource(definition), } if cfg.Mode == config_core.Zone && cfg.Multizone != nil && cfg.Multizone.Zone != nil { endpoints.zoneName = cfg.Multizone.Zone.Name