From 636e31c65746a912eed79ad3820afd76b6616b09 Mon Sep 17 00:00:00 2001 From: Bart Smykla Date: Wed, 27 Oct 2021 09:42:23 +0200 Subject: [PATCH] feat(kuma-cp) add GlobalInsights api endpoint (#3018) * 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 --- pkg/api-server/global_insights_endpoints.go | 77 +++++++++++ .../global_insights_endpoints_test.go | 128 ++++++++++++++++++ pkg/api-server/server.go | 6 + 3 files changed, 211 insertions(+) create mode 100644 pkg/api-server/global_insights_endpoints.go create mode 100644 pkg/api-server/global_insights_endpoints_test.go diff --git a/pkg/api-server/global_insights_endpoints.go b/pkg/api-server/global_insights_endpoints.go new file mode 100644 index 000000000000..a4aa16ed2bf3 --- /dev/null +++ b/pkg/api-server/global_insights_endpoints.go @@ -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") + } +} diff --git a/pkg/api-server/global_insights_endpoints_test.go b/pkg/api-server/global_insights_endpoints_test.go new file mode 100644 index 000000000000..50eaaffbb114 --- /dev/null +++ b/pkg/api-server/global_insights_endpoints_test.go @@ -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)) + }) + }) +}) diff --git a/pkg/api-server/server.go b/pkg/api-server/server.go index 1e78e866a512..0c533182d7cc 100644 --- a/pkg/api-server/server.go +++ b/pkg/api-server/server.go @@ -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) {