From c1b30c56c912385c899d6375b4406ca8f109767c Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Thu, 11 Aug 2022 16:58:11 +0100 Subject: [PATCH] Correlations: Add GetCorrelation(s) HTTP APIs (#52517) * Correlations: Get Single correlations * Correlations: Get all correlations for given source ds * Correlations: Get all correlations * add tests * add DB indices * fix lint errors * remove skip from tests * use DatasourceService in test --- .../developers/http_api/correlations.md | 123 +++++++ pkg/services/correlations/api.go | 133 +++++++- pkg/services/correlations/correlations.go | 12 + pkg/services/correlations/database.go | 84 +++++ pkg/services/correlations/models.go | 20 ++ .../sqlstore/migrations/correlations_mig.go | 7 + pkg/tests/api/correlations/common_test.go | 16 +- .../correlations/correlations_read_test.go | 315 ++++++++++++++++++ public/api-merged.json | 105 ++++++ public/api-spec.json | 105 ++++++ 10 files changed, 917 insertions(+), 3 deletions(-) create mode 100644 pkg/tests/api/correlations/correlations_read_test.go diff --git a/docs/sources/developers/http_api/correlations.md b/docs/sources/developers/http_api/correlations.md index 7da9ba84a4f..9d536b35916 100644 --- a/docs/sources/developers/http_api/correlations.md +++ b/docs/sources/developers/http_api/correlations.md @@ -150,3 +150,126 @@ Status codes: - **403** – Forbidden, source data source is read-only - **404** – Not found, either source or target data source could not be found - **500** – Internal error + +## Get single correlation + +`GET /api/datasources/uid/:sourceUID/correlations/:correlationUID` + +Gets a single correlation. + +**Example request:** + +```http +GET /api/datasources/uid/uyBf2637k/correlations/J6gn7d31L HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response:** + +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "description": "Logs to Traces", + "label": "My Label", + "sourceUID": "uyBf2637k", + "targetUID": "PDDA8E780A17E7EF1", + "uid": "J6gn7d31L" +} +``` + +Status codes: + +- **200** – OK +- **401** – Unauthorized +- **404** – Not found, either source data source or correlation were not found +- **500** – Internal error + +## Get all correlations originating from a given data source + +`GET /api/datasources/uid/:sourceUID/correlations` + +Get all correlations originating from the data source identified by the given `sourceUID` in the path. + +**Example request:** + +```http +GET /api/datasources/uid/uyBf2637k/correlations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response:** + +```http +HTTP/1.1 200 +Content-Type: application/json +[ + { + "description": "Logs to Traces", + "label": "My Label", + "sourceUID": "uyBf2637k", + "targetUID": "PDDA8E780A17E7EF1", + "uid": "J6gn7d31L" + }, + { + "description": "Logs to Metrics", + "label": "Another Label", + "sourceUID": "uyBf2637k", + "targetUID": "P15396BDD62B2BE29", + "uid": "uWCpURgVk" + } +] +``` + +Status codes: + +- **200** – OK +- **401** – Unauthorized +- **404** – Not found, either source data source is not found or no correlation exists originating from the given data source +- **500** – Internal error + +## Get all correlations + +`GET /api/datasources/correlations` + +Get all correlations. + +**Example request:** + +```http +GET /api/datasources/correlations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response:** + +```http +HTTP/1.1 200 +Content-Type: application/json +[ + { + "description": "Prometheus to Loki", + "label": "My Label", + "sourceUID": "uyBf2637k", + "targetUID": "PDDA8E780A17E7EF1", + "uid": "J6gn7d31L" + }, + { + "description": "Loki to Tempo", + "label": "Another Label", + "sourceUID": "PDDA8E780A17E7EF1", + "targetUID": "P15396BDD62B2BE29", + "uid": "uWCpURgVk" + } +] +``` + +Status codes: + +- **200** – OK +- **401** – Unauthorized +- **404** – Not found, no correlation is found +- **500** – Internal error diff --git a/pkg/services/correlations/api.go b/pkg/services/correlations/api.go index 73f4de58587..3d2bfc13563 100644 --- a/pkg/services/correlations/api.go +++ b/pkg/services/correlations/api.go @@ -18,10 +18,17 @@ func (s *CorrelationsService) registerAPIEndpoints() { uidScope := datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid")) authorize := ac.Middleware(s.AccessControl) + s.RouteRegister.Get("/api/datasources/correlations", middleware.ReqSignedIn, authorize(ac.ReqViewer, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(s.getCorrelationsHandler)) + s.RouteRegister.Group("/api/datasources/uid/:uid/correlations", func(entities routing.RouteRegister) { + entities.Get("/", middleware.ReqSignedIn, authorize(ac.ReqViewer, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(s.getCorrelationsBySourceUIDHandler)) entities.Post("/", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.createHandler)) - entities.Delete("/:correlationUID", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.deleteHandler)) - entities.Patch("/:correlationUID", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.updateHandler)) + + entities.Group("/:correlationUID", func(entities routing.RouteRegister) { + entities.Get("/", middleware.ReqSignedIn, authorize(ac.ReqViewer, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(s.getCorrelationHandler)) + entities.Delete("/", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.deleteHandler)) + entities.Patch("/", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.updateHandler)) + }) }) } @@ -191,3 +198,125 @@ type UpdateCorrelationResponse struct { // in: body Body UpdateCorrelationResponseBody `json:"body"` } + +// swagger:route GET /datasources/uid/{sourceUID}/correlations/{correlationUID} correlations getCorrelation +// +// Gets a correlation. +// +// Responses: +// 200: getCorrelationResponse +// 401: unauthorisedError +// 404: notFoundError +// 500: internalServerError +func (s *CorrelationsService) getCorrelationHandler(c *models.ReqContext) response.Response { + query := GetCorrelationQuery{ + UID: web.Params(c.Req)[":correlationUID"], + SourceUID: web.Params(c.Req)[":uid"], + OrgId: c.OrgId, + } + + correlation, err := s.getCorrelation(c.Req.Context(), query) + if err != nil { + if errors.Is(err, ErrCorrelationNotFound) { + return response.Error(http.StatusNotFound, "Correlation not found", err) + } + if errors.Is(err, ErrSourceDataSourceDoesNotExists) { + return response.Error(http.StatusNotFound, "Source data source not found", err) + } + + return response.Error(http.StatusInternalServerError, "Failed to update correlation", err) + } + + return response.JSON(http.StatusOK, correlation) +} + +// swagger:parameters getCorrelation +type GetCorrelationParams struct { + // in:path + // required:true + DatasourceUID string `json:"sourceUID"` + // in:path + // required:true + CorrelationUID string `json:"correlationUID"` +} + +//swagger:response getCorrelationResponse +type GetCorrelationResponse struct { + // in: body + Body Correlation `json:"body"` +} + +// swagger:route GET /datasources/uid/{sourceUID}/correlations correlations getCorrelationsBySourceUID +// +// Gets all correlations originating from the given data source. +// +// Responses: +// 200: getCorrelationsBySourceUIDResponse +// 401: unauthorisedError +// 404: notFoundError +// 500: internalServerError +func (s *CorrelationsService) getCorrelationsBySourceUIDHandler(c *models.ReqContext) response.Response { + query := GetCorrelationsBySourceUIDQuery{ + SourceUID: web.Params(c.Req)[":uid"], + OrgId: c.OrgId, + } + + correlations, err := s.getCorrelationsBySourceUID(c.Req.Context(), query) + if err != nil { + if errors.Is(err, ErrCorrelationNotFound) { + return response.Error(http.StatusNotFound, "No correlation found", err) + } + if errors.Is(err, ErrSourceDataSourceDoesNotExists) { + return response.Error(http.StatusNotFound, "Source data source not found", err) + } + + return response.Error(http.StatusInternalServerError, "Failed to update correlation", err) + } + + return response.JSON(http.StatusOK, correlations) +} + +// swagger:parameters getCorrelationsBySourceUID +type GetCorrelationsBySourceUIDParams struct { + // in:path + // required:true + DatasourceUID string `json:"sourceUID"` +} + +//swagger:response getCorrelationsBySourceUIDResponse +type GetCorrelationsBySourceUIDResponse struct { + // in: body + Body []Correlation `json:"body"` +} + +// swagger:route GET /datasources/correlations correlations getCorrelations +// +// Gets all correlations. +// +// Responses: +// 200: getCorrelationsResponse +// 401: unauthorisedError +// 404: notFoundError +// 500: internalServerError +func (s *CorrelationsService) getCorrelationsHandler(c *models.ReqContext) response.Response { + query := GetCorrelationsQuery{ + OrgId: c.OrgId, + } + + correlations, err := s.getCorrelations(c.Req.Context(), query) + if err != nil { + if errors.Is(err, ErrCorrelationNotFound) { + return response.Error(http.StatusNotFound, "No correlation found", err) + } + + return response.Error(http.StatusInternalServerError, "Failed to update correlation", err) + } + + return response.JSON(http.StatusOK, correlations) +} + +//swagger:response getCorrelationsResponse +type GetCorrelationsResponse struct { + // in: body + Body []Correlation `json:"body"` +} diff --git a/pkg/services/correlations/correlations.go b/pkg/services/correlations/correlations.go index 92088a09a7c..0326e56ec39 100644 --- a/pkg/services/correlations/correlations.go +++ b/pkg/services/correlations/correlations.go @@ -56,6 +56,18 @@ func (s CorrelationsService) UpdateCorrelation(ctx context.Context, cmd UpdateCo return s.updateCorrelation(ctx, cmd) } +func (s CorrelationsService) GetCorrelation(ctx context.Context, cmd GetCorrelationQuery) (Correlation, error) { + return s.getCorrelation(ctx, cmd) +} + +func (s CorrelationsService) GetCorrelationsBySourceUID(ctx context.Context, cmd GetCorrelationsBySourceUIDQuery) ([]Correlation, error) { + return s.getCorrelationsBySourceUID(ctx, cmd) +} + +func (s CorrelationsService) GetCorrelations(ctx context.Context, cmd GetCorrelationsQuery) ([]Correlation, error) { + return s.getCorrelations(ctx, cmd) +} + func (s CorrelationsService) DeleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error { return s.deleteCorrelationsBySourceUID(ctx, cmd) } diff --git a/pkg/services/correlations/database.go b/pkg/services/correlations/database.go index c77f59f8897..679a3224527 100644 --- a/pkg/services/correlations/database.go +++ b/pkg/services/correlations/database.go @@ -132,6 +132,90 @@ func (s CorrelationsService) updateCorrelation(ctx context.Context, cmd UpdateCo return correlation, nil } +func (s CorrelationsService) getCorrelation(ctx context.Context, cmd GetCorrelationQuery) (Correlation, error) { + correlation := Correlation{ + UID: cmd.UID, + SourceUID: cmd.SourceUID, + } + + err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *sqlstore.DBSession) error { + query := &datasources.GetDataSourceQuery{ + OrgId: cmd.OrgId, + Uid: cmd.SourceUID, + } + if err := s.DataSourceService.GetDataSource(ctx, query); err != nil { + return ErrSourceDataSourceDoesNotExists + } + + found, err := session.Where("uid = ? AND source_uid = ?", correlation.UID, correlation.SourceUID).Get(&correlation) + if !found { + return ErrCorrelationNotFound + } + if err != nil { + return err + } + + return err + }) + + if err != nil { + return Correlation{}, err + } + + return correlation, nil +} + +func (s CorrelationsService) getCorrelationsBySourceUID(ctx context.Context, cmd GetCorrelationsBySourceUIDQuery) ([]Correlation, error) { + correlationsCondiBean := Correlation{ + SourceUID: cmd.SourceUID, + } + correlations := make([]Correlation, 0) + + err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *sqlstore.DBSession) error { + query := &datasources.GetDataSourceQuery{ + OrgId: cmd.OrgId, + Uid: cmd.SourceUID, + } + if err := s.DataSourceService.GetDataSource(ctx, query); err != nil { + return ErrSourceDataSourceDoesNotExists + } + + err := session.Find(&correlations, correlationsCondiBean) + if err != nil { + return err + } + + return err + }) + + if err != nil { + return []Correlation{}, err + } + + if len(correlations) == 0 { + return []Correlation{}, ErrCorrelationNotFound + } + + return correlations, nil +} + +func (s CorrelationsService) getCorrelations(ctx context.Context, cmd GetCorrelationsQuery) ([]Correlation, error) { + correlations := make([]Correlation, 0) + + err := s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error { + return session.Select("correlation.*").Join("", "data_source", "correlation.source_uid = data_source.uid").Where("data_source.org_id = ?", cmd.OrgId).Find(&correlations) + }) + if err != nil { + return []Correlation{}, err + } + + if len(correlations) == 0 { + return []Correlation{}, ErrCorrelationNotFound + } + + return correlations, nil +} + func (s CorrelationsService) deleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error { return s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error { _, err := session.Delete(&Correlation{SourceUID: cmd.SourceUID}) diff --git a/pkg/services/correlations/models.go b/pkg/services/correlations/models.go index da7d2f9d6cf..969dbb8d79d 100644 --- a/pkg/services/correlations/models.go +++ b/pkg/services/correlations/models.go @@ -94,6 +94,26 @@ type UpdateCorrelationCommand struct { Description *string `json:"description"` } +// GetCorrelationQuery is the query to retrieve a single correlation +type GetCorrelationQuery struct { + // UID of the correlation + UID string `json:"-"` + // UID of the source data source + SourceUID string `json:"-"` + OrgId int64 `json:"-"` +} + +// GetCorrelationsBySourceUIDQuery is the query to retrieve all correlations originating by the given Data Source +type GetCorrelationsBySourceUIDQuery struct { + SourceUID string `json:"-"` + OrgId int64 `json:"-"` +} + +// GetCorrelationsQuery is the query to retrieve all correlations +type GetCorrelationsQuery struct { + OrgId int64 `json:"-"` +} + type DeleteCorrelationsBySourceUIDCommand struct { SourceUID string } diff --git a/pkg/services/sqlstore/migrations/correlations_mig.go b/pkg/services/sqlstore/migrations/correlations_mig.go index ccc2d49cf3e..98a3297ddd4 100644 --- a/pkg/services/sqlstore/migrations/correlations_mig.go +++ b/pkg/services/sqlstore/migrations/correlations_mig.go @@ -15,7 +15,14 @@ func addCorrelationsMigrations(mg *Migrator) { {Name: "label", Type: DB_Text, Nullable: false}, {Name: "description", Type: DB_Text, Nullable: false}, }, + Indices: []*Index{ + {Cols: []string{"uid"}}, + {Cols: []string{"source_uid"}}, + }, } mg.AddMigration("create correlation table v1", NewAddTableMigration(correlationsV1)) + + mg.AddMigration("add index correlations.uid", NewAddIndexMigration(correlationsV1, correlationsV1.Indices[0])) + mg.AddMigration("add index correlations.source_uid", NewAddIndexMigration(correlationsV1, correlationsV1.Indices[1])) } diff --git a/pkg/tests/api/correlations/common_test.go b/pkg/tests/api/correlations/common_test.go index e9c7ab61605..1f8ee1c4736 100644 --- a/pkg/tests/api/correlations/common_test.go +++ b/pkg/tests/api/correlations/common_test.go @@ -43,6 +43,20 @@ type User struct { password string } +type GetParams struct { + url string + user User +} + +func (c TestContext) Get(params GetParams) *http.Response { + c.t.Helper() + + resp, err := http.Get(c.getURL(params.url, params.user)) + require.NoError(c.t, err) + + return resp +} + type PostParams struct { url string body string @@ -127,7 +141,7 @@ func (c TestContext) createUser(cmd user.CreateUserCommand) { func (c TestContext) createDs(cmd *datasources.AddDataSourceCommand) { c.t.Helper() - err := c.env.SQLStore.AddDataSource(context.Background(), cmd) + err := c.env.Server.HTTPServer.DataSourcesService.AddDataSource(context.Background(), cmd) require.NoError(c.t, err) } diff --git a/pkg/tests/api/correlations/correlations_read_test.go b/pkg/tests/api/correlations/correlations_read_test.go new file mode 100644 index 00000000000..0f37880ec06 --- /dev/null +++ b/pkg/tests/api/correlations/correlations_read_test.go @@ -0,0 +1,315 @@ +package correlations + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/correlations" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/require" +) + +func TestIntegrationReadCorrelation(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := NewTestEnv(t) + + adminUser := User{ + username: "admin", + password: "admin", + } + viewerUser := User{ + username: "viewer", + password: "viewer", + } + + ctx.createUser(user.CreateUserCommand{ + DefaultOrgRole: string(models.ROLE_VIEWER), + Password: viewerUser.password, + Login: viewerUser.username, + }) + ctx.createUser(user.CreateUserCommand{ + DefaultOrgRole: string(models.ROLE_ADMIN), + Password: adminUser.password, + Login: adminUser.username, + }) + + t.Run("Get all correlations", func(t *testing.T) { + // Running this here before creating a correlation in order to test this path. + t.Run("If no correlation exists it should return 404", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: "/api/datasources/correlations", + user: adminUser, + }) + require.Equal(t, http.StatusNotFound, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "No correlation found", response.Message) + + require.NoError(t, res.Body.Close()) + }) + }) + + createDsCommand := &datasources.AddDataSourceCommand{ + Name: "with-correlations", + Type: "loki", + OrgId: 1, + } + ctx.createDs(createDsCommand) + dsWithCorrelations := createDsCommand.Result + correlation := ctx.createCorrelation(correlations.CreateCorrelationCommand{ + SourceUID: dsWithCorrelations.Uid, + TargetUID: dsWithCorrelations.Uid, + OrgId: dsWithCorrelations.OrgId, + }) + + createDsCommand = &datasources.AddDataSourceCommand{ + Name: "without-correlations", + Type: "loki", + OrgId: 1, + } + ctx.createDs(createDsCommand) + dsWithoutCorrelations := createDsCommand.Result + + t.Run("Get all correlations", func(t *testing.T) { + t.Run("Unauthenticated users shouldn't be able to read correlations", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: "/api/datasources/correlations", + }) + require.Equal(t, http.StatusUnauthorized, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Unauthorized", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Authenticated users shouldn't get unauthorized or forbidden errors", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: "/api/datasources/correlations", + user: viewerUser, + }) + require.NotEqual(t, http.StatusUnauthorized, res.StatusCode) + require.NotEqual(t, http.StatusForbidden, res.StatusCode) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Should correctly return correlations", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: "/api/datasources/correlations", + user: adminUser, + }) + require.Equal(t, http.StatusOK, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response []correlations.Correlation + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Len(t, response, 1) + require.EqualValues(t, correlation, response[0]) + + require.NoError(t, res.Body.Close()) + }) + }) + + t.Run("Get all correlations for a given data source", func(t *testing.T) { + t.Run("Unauthenticated users shouldn't be able to read correlations", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "some-uid"), + }) + require.Equal(t, http.StatusUnauthorized, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Unauthorized", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Authenticated users shouldn't get unauthorized or forbidden errors", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "some-uid"), + user: viewerUser, + }) + require.NotEqual(t, http.StatusUnauthorized, res.StatusCode) + require.NotEqual(t, http.StatusForbidden, res.StatusCode) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("if datasource does not exist it should return 404", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "some-uid"), + user: adminUser, + }) + require.Equal(t, http.StatusNotFound, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Source data source not found", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("If no correlation exists it should return 404", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations", dsWithoutCorrelations.Uid), + user: adminUser, + }) + require.Equal(t, http.StatusNotFound, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "No correlation found", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Should correctly return correlations", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations", dsWithCorrelations.Uid), + user: adminUser, + }) + require.Equal(t, http.StatusOK, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response []correlations.Correlation + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Len(t, response, 1) + require.EqualValues(t, correlation, response[0]) + + require.NoError(t, res.Body.Close()) + }) + }) + + t.Run("Get a single correlation", func(t *testing.T) { + t.Run("Unauthenticated users shouldn't be able to read correlations", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "some-ds-uid", "some-correlation-uid"), + }) + require.Equal(t, http.StatusUnauthorized, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Unauthorized", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Authenticated users shouldn't get unauthorized or forbidden errors", func(t *testing.T) { + // FIXME: don't skip this test + t.Skip("this test should pass but somehow testing with accesscontrol works different than live grafana") + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "some-ds-uid", "some-correlation-uid"), + user: viewerUser, + }) + require.NotEqual(t, http.StatusUnauthorized, res.StatusCode) + require.NotEqual(t, http.StatusForbidden, res.StatusCode) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("if datasource does not exist it should return 404", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "some-ds-uid", "some-correlation-uid"), + user: adminUser, + }) + require.Equal(t, http.StatusNotFound, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Source data source not found", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("If no correlation exists it should return 404", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", dsWithoutCorrelations.Uid, "some-correlation-uid"), + user: adminUser, + }) + require.Equal(t, http.StatusNotFound, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response errorResponseBody + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.Equal(t, "Correlation not found", response.Message) + + require.NoError(t, res.Body.Close()) + }) + + t.Run("Should correctly return correlation", func(t *testing.T) { + res := ctx.Get(GetParams{ + url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", dsWithCorrelations.Uid, correlation.UID), + user: adminUser, + }) + require.Equal(t, http.StatusOK, res.StatusCode) + + responseBody, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + + var response correlations.Correlation + err = json.Unmarshal(responseBody, &response) + require.NoError(t, err) + + require.EqualValues(t, correlation, response) + + require.NoError(t, res.Body.Close()) + }) + }) +} diff --git a/public/api-merged.json b/public/api-merged.json index 998232a2f05..896510c8947 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4020,6 +4020,27 @@ } } }, + "/datasources/correlations": { + "get": { + "tags": ["correlations"], + "summary": "Gets all correlations.", + "operationId": "getCorrelations", + "responses": { + "200": { + "$ref": "#/responses/getCorrelationsResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/datasources/id/{name}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:name:*` and `datasources:name:test_datasource` (single data source).", @@ -4402,6 +4423,33 @@ } }, "/datasources/uid/{sourceUID}/correlations": { + "get": { + "tags": ["correlations"], + "summary": "Gets all correlations originating from the given data source.", + "operationId": "getCorrelationsBySourceUID", + "parameters": [ + { + "type": "string", + "name": "sourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/getCorrelationsBySourceUIDResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, "post": { "tags": [ "correlations" @@ -4447,6 +4495,39 @@ } }, "/datasources/uid/{sourceUID}/correlations/{correlationUID}": { + "get": { + "tags": ["correlations"], + "summary": "Gets a correlation.", + "operationId": "getCorrelation", + "parameters": [ + { + "type": "string", + "name": "sourceUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "correlationUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/getCorrelationResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, "patch": { "tags": [ "correlations" @@ -19174,6 +19255,30 @@ } } }, + "getCorrelationResponse": { + "description": "(empty)", + "schema": { + "$ref": "#/definitions/Correlation" + } + }, + "getCorrelationsBySourceUIDResponse": { + "description": "(empty)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Correlation" + } + } + }, + "getCorrelationsResponse": { + "description": "(empty)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Correlation" + } + } + }, "getCurrentOrgResponse": { "description": "(empty)", "schema": { diff --git a/public/api-spec.json b/public/api-spec.json index 62049a252cd..48935208d7f 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -3373,6 +3373,27 @@ } } }, + "/datasources/correlations": { + "get": { + "tags": ["correlations"], + "summary": "Gets all correlations.", + "operationId": "getCorrelations", + "responses": { + "200": { + "$ref": "#/responses/getCorrelationsResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/datasources/id/{name}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:name:*` and `datasources:name:test_datasource` (single data source).", @@ -3755,6 +3776,33 @@ } }, "/datasources/uid/{sourceUID}/correlations": { + "get": { + "tags": ["correlations"], + "summary": "Gets all correlations originating from the given data source.", + "operationId": "getCorrelationsBySourceUID", + "parameters": [ + { + "type": "string", + "name": "sourceUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/getCorrelationsBySourceUIDResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, "post": { "tags": [ "correlations" @@ -3800,6 +3848,39 @@ } }, "/datasources/uid/{sourceUID}/correlations/{correlationUID}": { + "get": { + "tags": ["correlations"], + "summary": "Gets a correlation.", + "operationId": "getCorrelation", + "parameters": [ + { + "type": "string", + "name": "sourceUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "correlationUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/getCorrelationResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, "patch": { "tags": [ "correlations" @@ -15173,6 +15254,30 @@ } } }, + "getCorrelationResponse": { + "description": "", + "schema": { + "$ref": "#/definitions/Correlation" + } + }, + "getCorrelationsBySourceUIDResponse": { + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Correlation" + } + } + }, + "getCorrelationsResponse": { + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Correlation" + } + } + }, "getCurrentOrgResponse": { "description": "", "schema": {