mirror of https://github.com/grafana/grafana
Correlations: Add CreateCorrelation HTTP API (#51630)
* Correlations: add migration
* Correlations: Add CreateCorrelation API
* Correlations: Make correlations work with provisioning
* Handle version changes
* Fix lining error
* lint fixes
* rebuild betterer results
* add a UID to each correlation
* Fix lint errors
* add docs
* better wording in API docs
* remove leftover comment
* handle ds updates
* Fix error message typo
* add bad data test
* make correlations a separate table
* skip readonly check when provisioning correlations
* delete stale correlations when datasources are deleted
* restore provisioned readonly ds
* publish deletion event with full data
* generate swagger and HTTP API docs
* apply source datasource permission to create correlation API
* Fix tests & lint errors
* ignore empty deletion events
* fix last lint errors
* fix more lint error
* Only publish deletion event if datasource was actually deleted
* delete DS provisioning deletes correlations, added & fixed tests
* Fix unmarshalling tests
* Fix linting errors
* Fix deltion event tests
* fix small linting error
* fix lint errors
* update betterer
* fix test
* make path singular
* Revert "make path singular"
This reverts commit 420c3d315e.
* add integration tests
* remove unneeded id from correlations table
* update spec
* update leftover references to CorrelationDTO
* fix tests
* cleanup tests
* fix lint error
pull/52735/head
parent
dbc2171401
commit
5ce4baf6f5
@ -0,0 +1,70 @@ |
|||||||
|
--- |
||||||
|
aliases: |
||||||
|
- /docs/grafana/latest/developers/http_api/correlations/ |
||||||
|
- /docs/grafana/latest/http_api/correlations/ |
||||||
|
description: Grafana Correlations HTTP API |
||||||
|
keywords: |
||||||
|
- grafana |
||||||
|
- http |
||||||
|
- documentation |
||||||
|
- api |
||||||
|
- correlations |
||||||
|
- Glue |
||||||
|
title: 'Correlations HTTP API ' |
||||||
|
--- |
||||||
|
|
||||||
|
# Correlations API |
||||||
|
|
||||||
|
This API can be used to define correlations between data sources. |
||||||
|
|
||||||
|
## Create correlations |
||||||
|
|
||||||
|
`POST /api/datasources/uid/:sourceUid/correlations` |
||||||
|
|
||||||
|
Creates a correlation between two data sources - the source data source indicated by the path UID, and the target data source which is specified in the body. |
||||||
|
|
||||||
|
**Example request:** |
||||||
|
|
||||||
|
```http |
||||||
|
POST /api/datasources/uid/uyBf2637k/correlations HTTP/1.1 |
||||||
|
Accept: application/json |
||||||
|
Content-Type: application/json |
||||||
|
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk |
||||||
|
{ |
||||||
|
"targetUid": "PDDA8E780A17E7EF1", |
||||||
|
"label": "My Label", |
||||||
|
"description": "Logs to Traces", |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
JSON body schema: |
||||||
|
|
||||||
|
- **targetUid** – Target data source uid. |
||||||
|
- **label** – A label for the correlation. |
||||||
|
- **description** – A description for the correlation. |
||||||
|
|
||||||
|
**Example response:** |
||||||
|
|
||||||
|
```http |
||||||
|
HTTP/1.1 200 |
||||||
|
Content-Type: application/json |
||||||
|
{ |
||||||
|
"message": "Correlation created", |
||||||
|
"result": { |
||||||
|
"description": "Logs to Traces", |
||||||
|
"label": "My Label", |
||||||
|
"sourceUid": "uyBf2637k", |
||||||
|
"targetUid": "PDDA8E780A17E7EF1", |
||||||
|
"uid": "50xhMlg9k" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Status codes: |
||||||
|
|
||||||
|
- **200** – OK |
||||||
|
- **400** - Errors (invalid JSON, missing or invalid fields) |
||||||
|
- **401** – Unauthorized |
||||||
|
- **403** – Forbidden, source data source is read-only |
||||||
|
- **404** – Not found, either source or target data source could not be found |
||||||
|
- **500** – Internal error |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
package definitions |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/grafana/grafana/pkg/services/correlations" |
||||||
|
) |
||||||
|
|
||||||
|
// swagger:route POST /datasources/uid/{uid}/correlations correlations createCorrelation
|
||||||
|
//
|
||||||
|
// Add correlation.
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: createCorrelationResponse
|
||||||
|
// 400: badRequestError
|
||||||
|
// 401: unauthorisedError
|
||||||
|
// 403: forbiddenError
|
||||||
|
// 404: notFoundError
|
||||||
|
// 500: internalServerError
|
||||||
|
|
||||||
|
// swagger:parameters createCorrelation
|
||||||
|
type CreateCorrelationParams struct { |
||||||
|
// in:body
|
||||||
|
// required:true
|
||||||
|
Body correlations.CreateCorrelationCommand `json:"body"` |
||||||
|
// in:path
|
||||||
|
// required:true
|
||||||
|
SourceUID string `json:"uid"` |
||||||
|
} |
||||||
|
|
||||||
|
//swagger:response createCorrelationResponse
|
||||||
|
type CreateCorrelationResponse struct { |
||||||
|
// in: body
|
||||||
|
Body correlations.CreateCorrelationResponse `json:"body"` |
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
package correlations |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/response" |
||||||
|
"github.com/grafana/grafana/pkg/api/routing" |
||||||
|
"github.com/grafana/grafana/pkg/middleware" |
||||||
|
"github.com/grafana/grafana/pkg/models" |
||||||
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol" |
||||||
|
"github.com/grafana/grafana/pkg/services/datasources" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/web" |
||||||
|
) |
||||||
|
|
||||||
|
func (s *CorrelationsService) registerAPIEndpoints() { |
||||||
|
uidScope := datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid")) |
||||||
|
authorize := ac.Middleware(s.AccessControl) |
||||||
|
|
||||||
|
s.RouteRegister.Group("/api/datasources/uid/:uid/correlations", func(entities routing.RouteRegister) { |
||||||
|
entities.Post("/", middleware.ReqSignedIn, authorize(ac.ReqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(s.createHandler)) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// createHandler handles POST /datasources/uid/:uid/correlations
|
||||||
|
func (s *CorrelationsService) createHandler(c *models.ReqContext) response.Response { |
||||||
|
cmd := CreateCorrelationCommand{} |
||||||
|
if err := web.Bind(c.Req, &cmd); err != nil { |
||||||
|
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||||
|
} |
||||||
|
cmd.SourceUID = web.Params(c.Req)[":uid"] |
||||||
|
cmd.OrgId = c.OrgId |
||||||
|
|
||||||
|
correlation, err := s.CreateCorrelation(c.Req.Context(), cmd) |
||||||
|
if err != nil { |
||||||
|
if errors.Is(err, ErrSourceDataSourceDoesNotExists) || errors.Is(err, ErrTargetDataSourceDoesNotExists) { |
||||||
|
return response.Error(http.StatusNotFound, "Data source not found", err) |
||||||
|
} |
||||||
|
|
||||||
|
if errors.Is(err, ErrSourceDataSourceReadOnly) { |
||||||
|
return response.Error(http.StatusForbidden, "Data source is read only", err) |
||||||
|
} |
||||||
|
|
||||||
|
return response.Error(http.StatusInternalServerError, "Failed to add correlation", err) |
||||||
|
} |
||||||
|
|
||||||
|
return response.JSON(http.StatusOK, CreateCorrelationResponse{Result: correlation, Message: "Correlation created"}) |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
package correlations |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/routing" |
||||||
|
"github.com/grafana/grafana/pkg/bus" |
||||||
|
"github.com/grafana/grafana/pkg/events" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||||
|
"github.com/grafana/grafana/pkg/services/datasources" |
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||||
|
) |
||||||
|
|
||||||
|
func ProvideService(sqlStore *sqlstore.SQLStore, routeRegister routing.RouteRegister, ds datasources.DataSourceService, ac accesscontrol.AccessControl, bus bus.Bus) *CorrelationsService { |
||||||
|
s := &CorrelationsService{ |
||||||
|
SQLStore: sqlStore, |
||||||
|
RouteRegister: routeRegister, |
||||||
|
log: log.New("correlations"), |
||||||
|
DataSourceService: ds, |
||||||
|
AccessControl: ac, |
||||||
|
} |
||||||
|
|
||||||
|
s.registerAPIEndpoints() |
||||||
|
|
||||||
|
bus.AddEventListener(s.handleDatasourceDeletion) |
||||||
|
|
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
type Service interface { |
||||||
|
CreateCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) |
||||||
|
DeleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error |
||||||
|
DeleteCorrelationsByTargetUID(ctx context.Context, cmd DeleteCorrelationsByTargetUIDCommand) error |
||||||
|
} |
||||||
|
|
||||||
|
type CorrelationsService struct { |
||||||
|
SQLStore *sqlstore.SQLStore |
||||||
|
RouteRegister routing.RouteRegister |
||||||
|
log log.Logger |
||||||
|
DataSourceService datasources.DataSourceService |
||||||
|
AccessControl accesscontrol.AccessControl |
||||||
|
} |
||||||
|
|
||||||
|
func (s CorrelationsService) CreateCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) { |
||||||
|
return s.createCorrelation(ctx, cmd) |
||||||
|
} |
||||||
|
|
||||||
|
func (s CorrelationsService) DeleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error { |
||||||
|
return s.deleteCorrelationsBySourceUID(ctx, cmd) |
||||||
|
} |
||||||
|
|
||||||
|
func (s CorrelationsService) DeleteCorrelationsByTargetUID(ctx context.Context, cmd DeleteCorrelationsByTargetUIDCommand) error { |
||||||
|
return s.deleteCorrelationsByTargetUID(ctx, cmd) |
||||||
|
} |
||||||
|
|
||||||
|
func (s CorrelationsService) handleDatasourceDeletion(ctx context.Context, event *events.DataSourceDeleted) error { |
||||||
|
return s.SQLStore.InTransaction(ctx, func(ctx context.Context) error { |
||||||
|
if err := s.deleteCorrelationsBySourceUID(ctx, DeleteCorrelationsBySourceUIDCommand{ |
||||||
|
SourceUID: event.UID, |
||||||
|
}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := s.deleteCorrelationsByTargetUID(ctx, DeleteCorrelationsByTargetUIDCommand{ |
||||||
|
TargetUID: event.UID, |
||||||
|
}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
package correlations |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources" |
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||||
|
"github.com/grafana/grafana/pkg/util" |
||||||
|
) |
||||||
|
|
||||||
|
// createCorrelation adds a correlation
|
||||||
|
func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) { |
||||||
|
correlation := Correlation{ |
||||||
|
UID: util.GenerateShortUID(), |
||||||
|
SourceUID: cmd.SourceUID, |
||||||
|
TargetUID: cmd.TargetUID, |
||||||
|
Label: cmd.Label, |
||||||
|
Description: cmd.Description, |
||||||
|
} |
||||||
|
|
||||||
|
err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *sqlstore.DBSession) error { |
||||||
|
var err error |
||||||
|
|
||||||
|
query := &datasources.GetDataSourceQuery{ |
||||||
|
OrgId: cmd.OrgId, |
||||||
|
Uid: cmd.SourceUID, |
||||||
|
} |
||||||
|
if err = s.DataSourceService.GetDataSource(ctx, query); err != nil { |
||||||
|
return ErrSourceDataSourceDoesNotExists |
||||||
|
} |
||||||
|
|
||||||
|
if !cmd.SkipReadOnlyCheck && query.Result.ReadOnly { |
||||||
|
return ErrSourceDataSourceReadOnly |
||||||
|
} |
||||||
|
|
||||||
|
if err = s.DataSourceService.GetDataSource(ctx, &datasources.GetDataSourceQuery{ |
||||||
|
OrgId: cmd.OrgId, |
||||||
|
Uid: cmd.TargetUID, |
||||||
|
}); err != nil { |
||||||
|
return ErrTargetDataSourceDoesNotExists |
||||||
|
} |
||||||
|
|
||||||
|
_, err = session.Insert(correlation) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
}) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return Correlation{}, err |
||||||
|
} |
||||||
|
|
||||||
|
return correlation, 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}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func (s CorrelationsService) deleteCorrelationsByTargetUID(ctx context.Context, cmd DeleteCorrelationsByTargetUIDCommand) error { |
||||||
|
return s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error { |
||||||
|
_, err := session.Delete(&Correlation{TargetUID: cmd.TargetUID}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
package correlations |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrSourceDataSourceReadOnly = errors.New("source data source is read only") |
||||||
|
ErrSourceDataSourceDoesNotExists = errors.New("source data source does not exist") |
||||||
|
ErrTargetDataSourceDoesNotExists = errors.New("target data source does not exist") |
||||||
|
ErrCorrelationFailedGenerateUniqueUid = errors.New("failed to generate unique correlation UID") |
||||||
|
ErrCorrelationIdentifierNotSet = errors.New("source identifier and org id are needed to be able to edit correlations") |
||||||
|
) |
||||||
|
|
||||||
|
// Correlation is the model for correlations definitions
|
||||||
|
type Correlation struct { |
||||||
|
// Unique identifier of the correlation
|
||||||
|
// example: 50xhMlg9k
|
||||||
|
UID string `json:"uid" xorm:"pk 'uid'"` |
||||||
|
// UID of the data source the correlation originates from
|
||||||
|
// example:d0oxYRg4z
|
||||||
|
SourceUID string `json:"sourceUid" xorm:"pk 'source_uid'"` |
||||||
|
// UID of the data source the correlation points to
|
||||||
|
// example:PE1C5CBDA0504A6A3
|
||||||
|
TargetUID string `json:"targetUid" xorm:"target_uid"` |
||||||
|
// Label identifying the correlation
|
||||||
|
// example: My Label
|
||||||
|
Label string `json:"label" xorm:"label"` |
||||||
|
// Description of the correlation
|
||||||
|
// example: Logs to Traces
|
||||||
|
Description string `json:"description" xorm:"description"` |
||||||
|
} |
||||||
|
|
||||||
|
// CreateCorrelationResponse is the response struct for CreateCorrelationCommand
|
||||||
|
// swagger:model
|
||||||
|
type CreateCorrelationResponse struct { |
||||||
|
Result Correlation `json:"result"` |
||||||
|
// example: Correlation created
|
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
|
|
||||||
|
// CreateCorrelationCommand is the command for creating a correlation
|
||||||
|
// swagger:model
|
||||||
|
type CreateCorrelationCommand struct { |
||||||
|
// UID of the data source for which correlation is created.
|
||||||
|
SourceUID string `json:"-"` |
||||||
|
OrgId int64 `json:"-"` |
||||||
|
SkipReadOnlyCheck bool `json:"-"` |
||||||
|
// Target data source UID to which the correlation is created
|
||||||
|
// example:PE1C5CBDA0504A6A3
|
||||||
|
TargetUID string `json:"targetUid" binding:"Required"` |
||||||
|
// Optional label identifying the correlation
|
||||||
|
// example: My label
|
||||||
|
Label string `json:"label"` |
||||||
|
// Optional description of the correlation
|
||||||
|
// example: Logs to Traces
|
||||||
|
Description string `json:"description"` |
||||||
|
} |
||||||
|
|
||||||
|
type DeleteCorrelationsBySourceUIDCommand struct { |
||||||
|
SourceUID string |
||||||
|
} |
||||||
|
|
||||||
|
type DeleteCorrelationsByTargetUIDCommand struct { |
||||||
|
TargetUID string |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
apiVersion: 1 |
||||||
|
|
||||||
|
datasources: |
||||||
|
- name: Graphite |
||||||
|
type: graphite |
||||||
|
uid: graphite |
||||||
|
access: proxy |
||||||
|
url: http://localhost:8080 |
||||||
|
correlations: |
||||||
|
- targetUid: graphite |
||||||
|
label: a label |
||||||
|
description: a description |
||||||
|
- targetUid: graphite |
||||||
|
label: a second label |
||||||
|
description: a second description |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
package migrations |
||||||
|
|
||||||
|
import ( |
||||||
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||||
|
) |
||||||
|
|
||||||
|
func addCorrelationsMigrations(mg *Migrator) { |
||||||
|
correlationsV1 := Table{ |
||||||
|
Name: "correlation", |
||||||
|
Columns: []*Column{ |
||||||
|
{Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: false, IsPrimaryKey: true}, |
||||||
|
{Name: "source_uid", Type: DB_NVarchar, Length: 40, Nullable: false, IsPrimaryKey: true}, |
||||||
|
// Nullable because in the future we want to have correlations to external resources
|
||||||
|
{Name: "target_uid", Type: DB_NVarchar, Length: 40, Nullable: true}, |
||||||
|
{Name: "label", Type: DB_Text, Nullable: false}, |
||||||
|
{Name: "description", Type: DB_Text, Nullable: false}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
mg.AddMigration("create correlation table v1", NewAddTableMigration(correlationsV1)) |
||||||
|
} |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
package correlations |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/server" |
||||||
|
"github.com/grafana/grafana/pkg/services/datasources" |
||||||
|
"github.com/grafana/grafana/pkg/services/user" |
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
type TestContext struct { |
||||||
|
env server.TestEnv |
||||||
|
t *testing.T |
||||||
|
} |
||||||
|
|
||||||
|
func NewTestEnv(t *testing.T) TestContext { |
||||||
|
t.Helper() |
||||||
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ |
||||||
|
DisableAnonymous: true, |
||||||
|
}) |
||||||
|
_, env := testinfra.StartGrafanaEnv(t, dir, path) |
||||||
|
|
||||||
|
return TestContext{ |
||||||
|
env: *env, |
||||||
|
t: t, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type User struct { |
||||||
|
username string |
||||||
|
password string |
||||||
|
} |
||||||
|
|
||||||
|
type PostParams struct { |
||||||
|
url string |
||||||
|
body string |
||||||
|
user User |
||||||
|
} |
||||||
|
|
||||||
|
func (c TestContext) Post(params PostParams) *http.Response { |
||||||
|
c.t.Helper() |
||||||
|
buf := bytes.NewReader([]byte(params.body)) |
||||||
|
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr()) |
||||||
|
if params.user.username != "" && params.user.password != "" { |
||||||
|
baseUrl = fmt.Sprintf("http://%s:%s@%s", params.user.username, params.user.password, c.env.Server.HTTPServer.Listener.Addr()) |
||||||
|
} |
||||||
|
|
||||||
|
// nolint:gosec
|
||||||
|
resp, err := http.Post( |
||||||
|
fmt.Sprintf( |
||||||
|
"%s%s", |
||||||
|
baseUrl, |
||||||
|
params.url, |
||||||
|
), |
||||||
|
"application/json", |
||||||
|
buf, |
||||||
|
) |
||||||
|
require.NoError(c.t, err) |
||||||
|
|
||||||
|
return resp |
||||||
|
} |
||||||
|
|
||||||
|
func (c TestContext) createUser(cmd user.CreateUserCommand) { |
||||||
|
c.t.Helper() |
||||||
|
|
||||||
|
c.env.SQLStore.Cfg.AutoAssignOrg = true |
||||||
|
c.env.SQLStore.Cfg.AutoAssignOrgId = 1 |
||||||
|
|
||||||
|
_, err := c.env.SQLStore.CreateUser(context.Background(), cmd) |
||||||
|
require.NoError(c.t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func (c TestContext) createDs(cmd *datasources.AddDataSourceCommand) { |
||||||
|
c.t.Helper() |
||||||
|
|
||||||
|
err := c.env.SQLStore.AddDataSource(context.Background(), cmd) |
||||||
|
require.NoError(c.t, err) |
||||||
|
} |
||||||
@ -0,0 +1,248 @@ |
|||||||
|
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" |
||||||
|
) |
||||||
|
|
||||||
|
type errorResponseBody struct { |
||||||
|
Message string `json:"message"` |
||||||
|
Error string `json:"error"` |
||||||
|
} |
||||||
|
|
||||||
|
func TestIntegrationCreateCorrelation(t *testing.T) { |
||||||
|
if testing.Short() { |
||||||
|
t.Skip("skipping integration test") |
||||||
|
} |
||||||
|
ctx := NewTestEnv(t) |
||||||
|
|
||||||
|
adminUser := User{ |
||||||
|
username: "admin", |
||||||
|
password: "admin", |
||||||
|
} |
||||||
|
editorUser := User{ |
||||||
|
username: "editor", |
||||||
|
password: "editor", |
||||||
|
} |
||||||
|
|
||||||
|
ctx.createUser(user.CreateUserCommand{ |
||||||
|
DefaultOrgRole: string(models.ROLE_EDITOR), |
||||||
|
Password: editorUser.password, |
||||||
|
Login: editorUser.username, |
||||||
|
}) |
||||||
|
ctx.createUser(user.CreateUserCommand{ |
||||||
|
DefaultOrgRole: string(models.ROLE_ADMIN), |
||||||
|
Password: adminUser.password, |
||||||
|
Login: adminUser.username, |
||||||
|
}) |
||||||
|
|
||||||
|
createDsCommand := &datasources.AddDataSourceCommand{ |
||||||
|
Name: "read-only", |
||||||
|
Type: "loki", |
||||||
|
ReadOnly: true, |
||||||
|
OrgId: 1, |
||||||
|
} |
||||||
|
ctx.createDs(createDsCommand) |
||||||
|
readOnlyDS := createDsCommand.Result.Uid |
||||||
|
|
||||||
|
createDsCommand = &datasources.AddDataSourceCommand{ |
||||||
|
Name: "writable", |
||||||
|
Type: "loki", |
||||||
|
OrgId: 1, |
||||||
|
} |
||||||
|
ctx.createDs(createDsCommand) |
||||||
|
writableDs := createDsCommand.Result.Uid |
||||||
|
|
||||||
|
t.Run("Unauthenticated users shouldn't be able to create correlations", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "some-ds-uid"), |
||||||
|
body: ``, |
||||||
|
}) |
||||||
|
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("non org admin shouldn't be able to create correlations", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "some-ds-uid"), |
||||||
|
body: ``, |
||||||
|
user: editorUser, |
||||||
|
}) |
||||||
|
require.Equal(t, http.StatusForbidden, 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.Contains(t, response.Message, "Permissions needed: datasources:write") |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("missing source data source in body should result in a 400", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "nonexistent-ds-uid"), |
||||||
|
body: `{}`, |
||||||
|
user: adminUser, |
||||||
|
}) |
||||||
|
require.Equal(t, http.StatusBadRequest, 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, "bad request data", response.Message) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("inexistent source data source should result in a 404", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", "nonexistent-ds-uid"), |
||||||
|
body: fmt.Sprintf(`{ |
||||||
|
"targetUid": "%s" |
||||||
|
}`, writableDs), |
||||||
|
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, "Data source not found", response.Message) |
||||||
|
require.Equal(t, correlations.ErrSourceDataSourceDoesNotExists.Error(), response.Error) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("inexistent target data source should result in a 404", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", writableDs), |
||||||
|
body: `{ |
||||||
|
"targetUid": "nonexistent-uid-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, "Data source not found", response.Message) |
||||||
|
require.Equal(t, correlations.ErrTargetDataSourceDoesNotExists.Error(), response.Error) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("creating a correlation originating from a read-only data source should result in a 403", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", readOnlyDS), |
||||||
|
body: fmt.Sprintf(`{ |
||||||
|
"targetUid": "%s" |
||||||
|
}`, readOnlyDS), |
||||||
|
user: adminUser, |
||||||
|
}) |
||||||
|
require.Equal(t, http.StatusForbidden, 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, "Data source is read only", response.Message) |
||||||
|
require.Equal(t, correlations.ErrSourceDataSourceReadOnly.Error(), response.Error) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("creating a correlation pointing to a read-only data source should work", func(t *testing.T) { |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", writableDs), |
||||||
|
body: fmt.Sprintf(`{ |
||||||
|
"targetUid": "%s" |
||||||
|
}`, readOnlyDS), |
||||||
|
user: adminUser, |
||||||
|
}) |
||||||
|
require.Equal(t, http.StatusOK, res.StatusCode) |
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(res.Body) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
var response correlations.CreateCorrelationResponse |
||||||
|
err = json.Unmarshal(responseBody, &response) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
require.Equal(t, "Correlation created", response.Message) |
||||||
|
require.Equal(t, writableDs, response.Result.SourceUID) |
||||||
|
require.Equal(t, readOnlyDS, response.Result.TargetUID) |
||||||
|
require.Equal(t, "", response.Result.Description) |
||||||
|
require.Equal(t, "", response.Result.Label) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Should correctly create a correlation", func(t *testing.T) { |
||||||
|
description := "a description" |
||||||
|
label := "a label" |
||||||
|
res := ctx.Post(PostParams{ |
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", writableDs), |
||||||
|
body: fmt.Sprintf(`{ |
||||||
|
"targetUid": "%s", |
||||||
|
"description": "%s", |
||||||
|
"label": "%s" |
||||||
|
}`, writableDs, description, label), |
||||||
|
user: adminUser, |
||||||
|
}) |
||||||
|
require.Equal(t, http.StatusOK, res.StatusCode) |
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(res.Body) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
var response correlations.CreateCorrelationResponse |
||||||
|
err = json.Unmarshal(responseBody, &response) |
||||||
|
require.NoError(t, err) |
||||||
|
|
||||||
|
require.Equal(t, "Correlation created", response.Message) |
||||||
|
require.Equal(t, writableDs, response.Result.SourceUID) |
||||||
|
require.Equal(t, writableDs, response.Result.TargetUID) |
||||||
|
require.Equal(t, description, response.Result.Description) |
||||||
|
require.Equal(t, label, response.Result.Label) |
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close()) |
||||||
|
}) |
||||||
|
} |
||||||
Loading…
Reference in new issue