Skip to content

Commit

Permalink
feat(kuma-cp) add GlobalInsights api endpoint (#3018)
Browse files Browse the repository at this point in the history
* feat(kuma-cp) add GlobalInsights api endpoint

`/global-insights` is an endpoint which will return aggregated
statistics for global resources, like `Zone`, `ZoneIngress` or
`Mesh`. It's purpose is similar to the `MeshInsights`, but for
global resources, and will be used in our GUI to simplify
displaying statistics of these objects.

I've spent to much time thinking about the approach for adding
this endpoint and finally decided to go with the easiest one,
which is just asking for `Zone`s, `ZoneIngress`es and `Mesh`es
every time endpoint is hitted as the `GlobalInsights` is different
than other resources like `MeshInsights` as there always will be
one. The assumption is also, that there won't be a lot of
the resources we'll be asking for every endpoint hit.

Other solutions considered were:
1. Adding `GlobalInsights` as the real resource, like
   `MeshInsights`, but then we would have to create separate k8s'
   CRD, for just the resource which would exist only one at a time.
   Also, as it's the global resource, reconciliation of it would
   be more complex.
2. Storing the internal data in Config resource (which maps to
   json string in k8s' ConfigMap), but I really think it wouldn't
   be right, as I think it would be not obvious and harder to
   maintain in the future. The complexity of reconciliation would
   be the same as in the option above.

I think this solution is very easy, and if anyhow it will become
performance bottleneck, we can just improve it/refactor it, without
breaking backward compatibility.

Signed-off-by: Bart Smykla <bartek@smykla.com>
  • Loading branch information
bartsmykla authored Oct 27, 2021
1 parent a7c04ee commit 636e31c
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 0 deletions.
77 changes: 77 additions & 0 deletions pkg/api-server/global_insights_endpoints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api_server

import (
"time"

"github.com/emicklei/go-restful"

"github.com/kumahq/kuma/pkg/core"
"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
"github.com/kumahq/kuma/pkg/core/resources/apis/system"
"github.com/kumahq/kuma/pkg/core/resources/manager"
"github.com/kumahq/kuma/pkg/core/resources/rbac"
rest_errors "github.com/kumahq/kuma/pkg/core/rest/errors"
)

type globalInsightsEndpoints struct {
resManager manager.ResourceManager
resourceAccess rbac.ResourceAccess
}

type globalInsightsStat struct {
Total uint32 `json:"total"`
}

type globalInsightsResponse struct {
Type string `json:"type"`
CreationTime time.Time `json:"creationTime"`
Meshes globalInsightsStat `json:"meshes"`
Zones globalInsightsStat `json:"zones"`
ZoneIngresses globalInsightsStat `json:"zoneIngresses"`
}

func newGlobalInsightsResponse(meshes, zones, zoneIngresses globalInsightsStat) *globalInsightsResponse {
return &globalInsightsResponse{
Type: "GlobalInsights",
CreationTime: core.Now(),
Meshes: meshes,
Zones: zones,
ZoneIngresses: zoneIngresses,
}
}

func (r *globalInsightsEndpoints) addEndpoint(ws *restful.WebService) {
ws.Route(ws.GET("/global-insights").To(r.inspectGlobalResources).
Doc("Inspect all global resources").
Returns(200, "OK", nil))
}

func (r *globalInsightsEndpoints) inspectGlobalResources(request *restful.Request, response *restful.Response) {
meshes := &mesh.MeshResourceList{}
if err := r.resManager.List(request.Request.Context(), meshes); err != nil {
rest_errors.HandleError(response, err, "Could not retrieve global insights")
return
}

zones := &system.ZoneResourceList{}
if err := r.resManager.List(request.Request.Context(), zones); err != nil {
rest_errors.HandleError(response, err, "Could not retrieve global insights")
return
}

zoneIngresses := &mesh.ZoneIngressResourceList{}
if err := r.resManager.List(request.Request.Context(), zoneIngresses); err != nil {
rest_errors.HandleError(response, err, "Could not retrieve global insights")
return
}

insights := newGlobalInsightsResponse(
globalInsightsStat{Total: uint32(len(meshes.Items))},
globalInsightsStat{Total: uint32(len(zones.Items))},
globalInsightsStat{Total: uint32(len(zoneIngresses.Items))},
)

if err := response.WriteAsJson(insights); err != nil {
rest_errors.HandleError(response, err, "Could not retrieve global insights")
}
}
128 changes: 128 additions & 0 deletions pkg/api-server/global_insights_endpoints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package api_server_test

import (
"context"
"io/ioutil"
"net/http"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

api_server "github.com/kumahq/kuma/pkg/api-server"
config "github.com/kumahq/kuma/pkg/config/api-server"
"github.com/kumahq/kuma/pkg/core"
core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
"github.com/kumahq/kuma/pkg/core/resources/apis/system"
core_model "github.com/kumahq/kuma/pkg/core/resources/model"
"github.com/kumahq/kuma/pkg/core/resources/store"
"github.com/kumahq/kuma/pkg/metrics"
"github.com/kumahq/kuma/pkg/plugins/resources/memory"
)

var _ = Describe("Global Insights Endpoints", func() {
var apiServer *api_server.ApiServer
var resourceStore store.ResourceStore
var stop chan struct{}

BeforeEach(func() {
core.Now = func() time.Time {
now, _ := time.Parse(time.RFC3339, "2018-07-17T16:05:36.995+00:00")
return now
}

resourceStore = memory.NewStore()

metrics, err := metrics.NewMetrics("Standalone")
Expect(err).ToNot(HaveOccurred())

apiServer = createTestApiServer(resourceStore, config.DefaultApiServerConfig(), true, metrics)

client := resourceApiClient{
address: apiServer.Address(),
path: "/global-insights",
}

stop = make(chan struct{})

go func() {
defer GinkgoRecover()
Expect(apiServer.Start(stop)).To(Succeed())
}()

waitForServer(&client)
}, 5)

AfterEach(func() {
close(stop)
core.Now = time.Now
})

BeforeEach(func() {
Expect(resourceStore.Create(
context.Background(),
system.NewZoneResource(),
store.CreateByKey("zone-1", core_model.NoMesh),
)).To(Succeed())

Expect(resourceStore.Create(
context.Background(),
system.NewZoneResource(),
store.CreateByKey("zone-2", core_model.NoMesh),
)).To(Succeed())

Expect(resourceStore.Create(
context.Background(),
core_mesh.NewZoneIngressResource(),
store.CreateByKey("zone-ingress-1", core_model.NoMesh),
)).To(Succeed())

Expect(resourceStore.Create(
context.Background(),
core_mesh.NewMeshResource(),
store.CreateByKey("mesh-1", core_model.NoMesh),
)).To(Succeed())

Expect(resourceStore.Create(
context.Background(),
core_mesh.NewMeshResource(),
store.CreateByKey("mesh-2", core_model.NoMesh),
)).To(Succeed())

Expect(resourceStore.Create(
context.Background(),
core_mesh.NewMeshResource(),
store.CreateByKey("mesh-3", core_model.NoMesh),
)).To(Succeed())
})

globalInsightsJSON := `
{
"type": "GlobalInsights",
"creationTime": "2018-07-17T16:05:36.995Z",
"meshes": {
"total": 3
},
"zones": {
"total": 2
},
"zoneIngresses": {
"total": 1
}
}
`

Describe("On GET", func() {
It("should return an existing resource", func() {
// when
response, err := http.Get("http://" + apiServer.Address() + "/global-insights")
Expect(err).ToNot(HaveOccurred())

// then
Expect(response.StatusCode).To(Equal(200))
body, err := ioutil.ReadAll(response.Body)
Expect(err).ToNot(HaveOccurred())
Expect(body).To(MatchJSON(globalInsightsJSON))
})
})
})
6 changes: 6 additions & 0 deletions pkg/api-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ func addResourcesEndpoints(ws *restful.WebService, defs []model.ResourceTypeDesc
zoneIngressOverviewEndpoints.addFindEndpoint(ws)
zoneIngressOverviewEndpoints.addListEndpoint(ws)

globalInsightsEndpoints := globalInsightsEndpoints{
resManager: resManager,
resourceAccess: resourceAccess,
}
globalInsightsEndpoints.addEndpoint(ws)

for _, definition := range defs {
defType := definition.Name
if cfg.ApiServer.ReadOnly || (defType == mesh.DataplaneType && cfg.Mode == config_core.Global) || (defType != mesh.DataplaneType && cfg.Mode == config_core.Zone) {
Expand Down

0 comments on commit 636e31c

Please sign in to comment.