Skip to content

Commit

Permalink
feat(api-server): add filtering on list external-services and datapla…
Browse files Browse the repository at this point in the history
…nes (#7810)

* feat(api-server): add filtering on list external-services and dataplanes

This also opens the path to adding more filters to the api depending on
the resource descriptor

Fix #5908

Signed-off-by: Charly Molter <[email protected]>

* fix nil dereference

Signed-off-by: Charly Molter <[email protected]>

---------

Signed-off-by: Charly Molter <[email protected]>
  • Loading branch information
lahabana authored Sep 25, 2023
1 parent 39da2c4 commit 95ab6f4
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 25 deletions.
4 changes: 4 additions & 0 deletions api/mesh/v1alpha1/externalservice_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
31 changes: 31 additions & 0 deletions pkg/api-server/api_server_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"path/filepath"
Expand All @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/api-server/dataplane_overview_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
65 changes: 42 additions & 23 deletions pkg/api-server/filtering.go → pkg/api-server/filters/filtering.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package api_server
package filters

import (
"reflect"
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion pkg/api-server/resource_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
102 changes: 102 additions & 0 deletions pkg/api-server/resource_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions pkg/api-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 95ab6f4

Please sign in to comment.