Merge branch 'main' into gio/feat/create-correlation-api

This commit is contained in:
Elfo404
2022-07-18 12:13:45 +02:00
62 changed files with 1654 additions and 581 deletions
@@ -13,7 +13,7 @@ weight: 101
Grafana Alerting is enabled by default for new installations or existing installations whether or not legacy alerting is configured.
> **Note**: We recommend that Grafana Enterprise customers with more than a dozen Grafana dashboard alert rules do not upgrade and remain on legacy alerting for now by [opting out]({{< relref "opt-out/" >}}). If you do want to upgrade to Grafana Alerting, contact customer support.
> **Note**: When upgrading, your dashboard alerts are migrated to a new format. This migration can be rolled back easily by [opting out]({{< relref "opt-out/" >}}). If you have any questions regarding this migration, please contact us.
Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration.
+64 -5
View File
@@ -718,11 +718,7 @@ Content-Type: application/json
`POST /api/admin/encryption/rotate-data-keys`
Rotates data encryption keys, so all the active keys are disabled
and no longer used for encryption but kept for decryption operations.
Secrets encrypted with one of the deactivated keys need to be re-encrypted
to actually stop using those keys for both encryption and decryption.
[Rotates]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#rotate-data-keys" >}}) data encryption keys.
**Example Request**:
@@ -738,3 +734,66 @@ Content-Type: application/json
HTTP/1.1 204
Content-Type: application/json
```
## Re-encrypt data encryption keys
`POST /api/admin/encryption/reencrypt-data-keys`
[Re-encrypts]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#re-encrypt-data-keys" >}}) data encryption keys.
**Example Request**:
```http
POST /api/admin/encryption/reencrypt-data-keys HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 204
Content-Type: application/json
```
## Re-encrypt secrets
`POST /api/admin/encryption/reencrypt-secrets`
[Re-encrypts]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#re-encrypt-secrets" >}}) secrets.
**Example Request**:
```http
POST /api/admin/encryption/reencrypt-secrets HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 204
Content-Type: application/json
```
## Roll back secrets
`POST /api/admin/encryption/rollback-secrets`
[Rolls back]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#roll-back-secrets" >}}) secrets.
**Example Request**:
```http
POST /api/admin/encryption/rollback-secrets HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 204
Content-Type: application/json
```
@@ -18,7 +18,7 @@ Grafana encrypts these secrets before they are written to the database, by using
Since Grafana v9.0, it uses [envelope encryption](#envelope-encryption) by default, which adds a layer of indirection to the
encryption process that represents an [**implicit breaking change**](#implicit-breaking-change) for older versions of Grafana.
For further details about how to operate a Grafana instance with envelope encryption, see the [Operational work]({{< relref "/#operational-work" >}}) section below.
For further details about how to operate a Grafana instance with envelope encryption, see the [Operational work](#operational-work) section below.
> **Note:** In Grafana Enterprise, you can also choose to [encrypt secrets in AES-GCM mode]({{< relref "#changing-your-encryption-mode-to-aes-gcm" >}}) instead of AES-CFB.
@@ -31,7 +31,7 @@ Instead of encrypting all secrets with a single key, Grafana uses a set of keys
encrypt them. These data encryption keys are themselves encrypted with a single key encryption key (KEK), configured
through the `secret_key` attribute in your
[Grafana configuration]({{< relref "../../configure-grafana/#secret_key" >}}) or with a
[KMS integration](#kms-integration).
[KMS integration](#encrypting-your-database-with-a-key-from-a-key-management-system-kms).
## Implicit breaking change
@@ -67,7 +67,8 @@ Secrets re-encryption can be performed when a Grafana administrator wants to eit
- Re-encrypt secrets after a [data keys rotation](#rotate-data-keys).
> **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration re-encrypt`
> command. It's safe to run more than once. Recommended to run under maintenance mode.
> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#re-encrypt-secrets" >}}).
> It's safe to run more than once. Recommended to run under maintenance mode.
## Roll back secrets
@@ -75,16 +76,18 @@ Used to roll back secrets encrypted with envelope encryption to legacy encryptio
a Grafana version earlier than Grafana v9.0 after an unsuccessful upgrade.
> **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration rollback`
> command. It's safe to run more than once. Recommended to run under maintenance mode.
> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#roll-back-secrets" >}}).
> It's safe to run more than once. Recommended to run under maintenance mode.
## Re-encrypt data keys
Used to re-encrypt data keys encrypted with a specific key encryption key (KEK). It can be used to either re-encrypt
existing data keys with a new key encryption key version (see [KMS integration](#kms-integration) rotation) or to
existing data keys with a new key encryption key version (see [KMS integration](#encrypting-your-database-with-a-key-from-a-key-management-system-kms) rotation) or to
re-encrypt them with a completely different key encryption key.
> **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration re-encrypt-data-keys`
> command. It's safe to run more than once. Recommended to run under maintenance mode.
> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#re-encrypt-data-encryption-keys" >}}).
> It's safe to run more than once. Recommended to run under maintenance mode.
## Rotate data keys
+35 -1
View File
@@ -9,8 +9,42 @@ import (
func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response {
if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to rotate data key", err)
return response.Error(http.StatusInternalServerError, "Failed to rotate data keys", err)
}
return response.Respond(http.StatusNoContent, "")
}
func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) response.Response {
if err := hs.SecretsService.ReEncryptDataKeys(c.Req.Context()); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to re-encrypt data keys", err)
}
return response.Respond(http.StatusOK, "Data encryption keys re-encrypted successfully")
}
func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Response {
success, err := hs.secretsMigrator.ReEncryptSecrets(c.Req.Context())
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to re-encrypt secrets", err)
}
if !success {
return response.Error(http.StatusPartialContent, "Something unexpected happened, refer to the server logs for more details", err)
}
return response.Respond(http.StatusOK, "Secrets re-encrypted successfully")
}
func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Response {
success, err := hs.secretsMigrator.RollBackSecrets(c.Req.Context())
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to rollback secrets", err)
}
if !success {
return response.Error(http.StatusPartialContent, "Something unexpected happened, refer to the server logs for more details", err)
}
return response.Respond(http.StatusOK, "Secrets rolled back successfully")
}
+3
View File
@@ -575,6 +575,9 @@ func (hs *HTTPServer) registerRoutes() {
}
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys))
adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets))
adminRoute.Post("/encryption/rollback-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminRollbackSecrets))
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
+8 -8
View File
@@ -2,7 +2,7 @@ package definitions
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/playlist"
)
// swagger:route GET /playlists playlists searchPlaylists
@@ -121,7 +121,7 @@ type DeletePlaylistParams struct {
type UpdatePlaylistParams struct {
// in:body
// required:true
Body models.UpdatePlaylistCommand
Body playlist.UpdatePlaylistCommand
// in:path
// required:true
UID string `json:"uid"`
@@ -131,28 +131,28 @@ type UpdatePlaylistParams struct {
type CreatePlaylistParams struct {
// in:body
// required:true
Body models.CreatePlaylistCommand
Body playlist.CreatePlaylistCommand
}
// swagger:response searchPlaylistsResponse
type SearchPlaylistsResponse struct {
// The response message
// in: body
Body models.Playlists `json:"body"`
Body playlist.Playlists `json:"body"`
}
// swagger:response getPlaylistResponse
type GetPlaylistResponse struct {
// The response message
// in: body
Body *models.PlaylistDTO `json:"body"`
Body *playlist.PlaylistDTO `json:"body"`
}
// swagger:response getPlaylistItemsResponse
type GetPlaylistItemsResponse struct {
// The response message
// in: body
Body []models.PlaylistItemDTO `json:"body"`
Body []playlist.PlaylistItemDTO `json:"body"`
}
// swagger:response getPlaylistDashboardsResponse
@@ -166,12 +166,12 @@ type GetPlaylistDashboardsResponse struct {
type UpdatePlaylistResponseResponse struct {
// The response message
// in: body
Body *models.PlaylistDTO `json:"body"`
Body *playlist.PlaylistDTO `json:"body"`
}
// swagger:response createPlaylistResponse
type CreatePlaylistResponse struct {
// The response message
// in: body
Body *models.Playlist `json:"body"`
Body *playlist.Playlist `json:"body"`
}
+4 -1
View File
@@ -58,6 +58,7 @@ import (
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/plugindashboards"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
pref "github.com/grafana/grafana/pkg/services/preference"
@@ -170,6 +171,7 @@ type HTTPServer struct {
dashboardVersionService dashver.Service
PublicDashboardsApi *publicdashboardsApi.Api
starService star.Service
playlistService playlist.Service
CoremodelRegistry *registry.Generic
CoremodelStaticRegistry *registry.Static
kvStore kvstore.KVStore
@@ -208,7 +210,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, entityEventsService store.EntityEventsService,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static,
starService star.Service, playlistService playlist.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static,
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck,
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) {
web.Env = cfg.Env
@@ -292,6 +294,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardPermissionsService: dashboardPermissionsService,
dashboardVersionService: dashboardVersionService,
starService: starService,
playlistService: playlistService,
CoremodelRegistry: coremodelRegistry,
CoremodelStaticRegistry: coremodelStaticRegistry,
kvStore: kvStore,
+36 -31
View File
@@ -6,25 +6,26 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) {
uid := web.Params(c.Req)[":uid"]
query := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
err := hs.SQLStore.GetPlaylist(c.Req.Context(), &query)
query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
p, err := hs.playlistService.Get(c.Req.Context(), &query)
if err != nil {
c.JsonApiErr(404, "Playlist not found", err)
return
}
if query.Result.OrgId == 0 {
if p.OrgId == 0 {
c.JsonApiErr(404, "Playlist not found", err)
return
}
if query.Result.OrgId != c.OrgId {
if p.OrgId != c.OrgId {
c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil)
return
}
@@ -38,53 +39,54 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response {
limit = 1000
}
searchQuery := models.GetPlaylistsQuery{
searchQuery := playlist.GetPlaylistsQuery{
Name: query,
Limit: limit,
OrgId: c.OrgId,
}
err := hs.SQLStore.SearchPlaylists(c.Req.Context(), &searchQuery)
playlists, err := hs.playlistService.Search(c.Req.Context(), &searchQuery)
if err != nil {
return response.Error(500, "Search failed", err)
}
return response.JSON(http.StatusOK, searchQuery.Result)
return response.JSON(http.StatusOK, playlists)
}
func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response {
uid := web.Params(c.Req)[":uid"]
cmd := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
if err := hs.SQLStore.GetPlaylist(c.Req.Context(), &cmd); err != nil {
p, err := hs.playlistService.Get(c.Req.Context(), &cmd)
if err != nil {
return response.Error(500, "Playlist not found", err)
}
playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgId)
dto := &models.PlaylistDTO{
Id: cmd.Result.Id,
UID: cmd.Result.UID,
Name: cmd.Result.Name,
Interval: cmd.Result.Interval,
OrgId: cmd.Result.OrgId,
dto := &playlist.PlaylistDTO{
Id: p.Id,
UID: p.UID,
Name: p.Name,
Interval: p.Interval,
OrgId: p.OrgId,
Items: playlistDTOs,
}
return response.JSON(http.StatusOK, dto)
}
func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItemDTO, error) {
func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItemDTO, error) {
playlistitems, err := hs.LoadPlaylistItems(ctx, uid, orgId)
if err != nil {
return nil, err
}
playlistDTOs := make([]models.PlaylistItemDTO, 0)
playlistDTOs := make([]playlist.PlaylistItemDTO, 0)
for _, item := range playlistitems {
playlistDTOs = append(playlistDTOs, models.PlaylistItemDTO{
playlistDTOs = append(playlistDTOs, playlist.PlaylistItemDTO{
Id: item.Id,
PlaylistId: item.PlaylistId,
Type: item.Type,
@@ -97,13 +99,14 @@ func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgI
return playlistDTOs, nil
}
func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItem, error) {
itemQuery := models.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId}
if err := hs.SQLStore.GetPlaylistItem(ctx, &itemQuery); err != nil {
func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItem, error) {
itemQuery := playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId}
items, err := hs.playlistService.GetItems(ctx, &itemQuery)
if err != nil {
return nil, err
}
return *itemQuery.Result, nil
return items, nil
}
func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response {
@@ -132,8 +135,8 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo
func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response {
uid := web.Params(c.Req)[":uid"]
cmd := models.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId}
if err := hs.SQLStore.DeletePlaylist(c.Req.Context(), &cmd); err != nil {
cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId}
if err := hs.playlistService.Delete(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to delete playlist", err)
}
@@ -141,28 +144,30 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response {
cmd := models.CreatePlaylistCommand{}
cmd := playlist.CreatePlaylistCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgId = c.OrgId
if err := hs.SQLStore.CreatePlaylist(c.Req.Context(), &cmd); err != nil {
p, err := hs.playlistService.Create(c.Req.Context(), &cmd)
if err != nil {
return response.Error(500, "Failed to create playlist", err)
}
return response.JSON(http.StatusOK, cmd.Result)
return response.JSON(http.StatusOK, p)
}
func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response {
cmd := models.UpdatePlaylistCommand{}
cmd := playlist.UpdatePlaylistCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgId = c.OrgId
cmd.UID = web.Params(c.Req)[":uid"]
if err := hs.SQLStore.UpdatePlaylist(c.Req.Context(), &cmd); err != nil {
p, err := hs.playlistService.Update(c.Req.Context(), &cmd)
if err != nil {
return response.Error(500, "Failed to save playlist", err)
}
@@ -171,6 +176,6 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to save playlist", err)
}
cmd.Result.Items = playlistDTOs
return response.JSON(http.StatusOK, cmd.Result)
p.Items = playlistDTOs
return response.JSON(http.StatusOK, p)
}
@@ -12,9 +12,11 @@ func ReEncryptDEKS(_ utils.CommandLine, runner runner.Runner) error {
}
func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
return runner.SecretsMigrator.ReEncryptSecrets(context.Background())
_, err := runner.SecretsMigrator.ReEncryptSecrets(context.Background())
return err
}
func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error {
return runner.SecretsMigrator.RollBackSecrets(context.Background())
_, err := runner.SecretsMigrator.RollBackSecrets(context.Background())
return err
}
+2
View File
@@ -6,6 +6,7 @@ package server
import (
"github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/services/playlist/playlistimpl"
"github.com/grafana/grafana/pkg/services/store/sanitizer"
"github.com/grafana/grafana/pkg/api"
@@ -289,6 +290,7 @@ var wireBasicSet = wire.NewSet(
ossaccesscontrol.ProvideDashboardPermissions,
wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)),
starimpl.ProvideService,
playlistimpl.ProvideService,
dashverimpl.ProvideService,
publicdashboardsService.ProvideService,
wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)),
+7 -7
View File
@@ -93,19 +93,19 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
}
// Register endpoints for proxying to Alertmanager-compatible backends.
api.RegisterAlertmanagerApiEndpoints(NewForkedAM(
api.RegisterAlertmanagerApiEndpoints(NewForkingAM(
api.DatasourceCache,
NewLotexAM(proxy, logger),
&AlertmanagerSrv{crypto: api.MultiOrgAlertmanager.Crypto, log: logger, ac: api.AccessControl, mam: api.MultiOrgAlertmanager},
), m)
// Register endpoints for proxying to Prometheus-compatible backends.
api.RegisterPrometheusApiEndpoints(NewForkedProm(
api.RegisterPrometheusApiEndpoints(NewForkingProm(
api.DatasourceCache,
NewLotexProm(proxy, logger),
&PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore, ac: api.AccessControl},
), m)
// Register endpoints for proxying to Cortex Ruler-compatible backends.
api.RegisterRulerApiEndpoints(NewForkedRuler(
api.RegisterRulerApiEndpoints(NewForkingRuler(
api.DatasourceCache,
NewLotexRuler(proxy, logger),
&RulerSrv{
@@ -120,7 +120,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
ac: api.AccessControl,
},
), m)
api.RegisterTestingApiEndpoints(NewForkedTestingApi(
api.RegisterTestingApiEndpoints(NewTestingApi(
&TestingApiSrv{
AlertingProxy: proxy,
DatasourceCache: api.DatasourceCache,
@@ -128,15 +128,15 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
accessControl: api.AccessControl,
evaluator: eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.SecretsService, api.ExpressionService),
}), m)
api.RegisterConfigurationApiEndpoints(NewForkedConfiguration(
&AdminSrv{
api.RegisterConfigurationApiEndpoints(NewConfiguration(
&ConfigSrv{
store: api.AdminConfigStore,
log: logger,
alertmanagerProvider: api.AlertsRouter,
},
), m)
api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{
api.RegisterProvisioningApiEndpoints(NewProvisioningApi(&ProvisioningSrv{
log: logger,
policies: api.Policies,
contactPointService: api.ContactPointService,
@@ -15,13 +15,13 @@ import (
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
)
type AdminSrv struct {
type ConfigSrv struct {
alertmanagerProvider ExternalAlertmanagerProvider
store store.AdminConfigurationStore
log log.Logger
}
func (srv AdminSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response {
func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response {
urls := srv.alertmanagerProvider.AlertmanagersFor(c.OrgId)
droppedURLs := srv.alertmanagerProvider.DroppedAlertmanagersFor(c.OrgId)
ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))}
@@ -38,7 +38,7 @@ func (srv AdminSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Respons
})
}
func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response {
func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response {
if c.OrgRole != models.ROLE_ADMIN {
return accessForbiddenResp()
}
@@ -61,7 +61,7 @@ func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Respons
return response.JSON(http.StatusOK, resp)
}
func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response {
func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response {
if c.OrgRole != models.ROLE_ADMIN {
return accessForbiddenResp()
}
@@ -97,7 +97,7 @@ func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.
return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"})
}
func (srv AdminSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
func (srv ConfigSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
if c.OrgRole != models.ROLE_ADMIN {
return accessForbiddenResp()
}
+34
View File
@@ -0,0 +1,34 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ConfigurationApiHandler always forwards requests to grafana backend
type ConfigurationApiHandler struct {
grafana *ConfigSrv
}
func NewConfiguration(grafana *ConfigSrv) *ConfigurationApiHandler {
return &ConfigurationApiHandler{
grafana: grafana,
}
}
func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *models.ReqContext) response.Response {
return f.grafana.RouteGetAlertmanagers(c)
}
func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *models.ReqContext) response.Response {
return f.grafana.RouteGetNGalertConfig(c)
}
func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response {
return f.grafana.RoutePostNGalertConfig(c, body)
}
func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
return f.grafana.RouteDeleteNGalertConfig(c)
}
-35
View File
@@ -1,35 +0,0 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ForkedConfigurationApi always forwards requests to grafana backend
type ForkedConfigurationApi struct {
grafana *AdminSrv
}
// NewForkedConfiguration creates a new ForkedConfigurationApi instance
func NewForkedConfiguration(grafana *AdminSrv) *ForkedConfigurationApi {
return &ForkedConfigurationApi{
grafana: grafana,
}
}
func (f *ForkedConfigurationApi) forkRouteGetAlertmanagers(c *models.ReqContext) response.Response {
return f.grafana.RouteGetAlertmanagers(c)
}
func (f *ForkedConfigurationApi) forkRouteGetNGalertConfig(c *models.ReqContext) response.Response {
return f.grafana.RouteGetNGalertConfig(c)
}
func (f *ForkedConfigurationApi) forkRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response {
return f.grafana.RoutePostNGalertConfig(c, body)
}
func (f *ForkedConfigurationApi) forkRouteDeleteNGalertConfig(c *models.ReqContext) response.Response {
return f.grafana.RouteDeleteNGalertConfig(c)
}
@@ -1,108 +0,0 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ForkedProvisioningApi always forwards requests to a Grafana backend.
// We do not currently support provisioning of external systems through Grafana's API.
type ForkedProvisioningApi struct {
svc *ProvisioningSrv
}
// NewForkedProvisioningApi creates a new ForkedProvisioningApi instance.
func NewForkedProvisioningApi(svc *ProvisioningSrv) *ForkedProvisioningApi {
return &ForkedProvisioningApi{
svc: svc,
}
}
func (f *ForkedProvisioningApi) forkRouteGetPolicyTree(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetPolicyTree(ctx)
}
func (f *ForkedProvisioningApi) forkRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response {
return f.svc.RoutePutPolicyTree(ctx, route)
}
func (f *ForkedProvisioningApi) forkRouteResetPolicyTree(ctx *models.ReqContext) response.Response {
return f.svc.RouteResetPolicyTree(ctx)
}
func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetContactPoints(ctx)
}
func (f *ForkedProvisioningApi) forkRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response {
return f.svc.RoutePostContactPoint(ctx, cp)
}
func (f *ForkedProvisioningApi) forkRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response {
return f.svc.RoutePutContactPoint(ctx, cp, UID)
}
func (f *ForkedProvisioningApi) forkRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteDeleteContactPoint(ctx, UID)
}
func (f *ForkedProvisioningApi) forkRouteGetTemplates(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetTemplates(ctx)
}
func (f *ForkedProvisioningApi) forkRouteGetTemplate(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteGetTemplate(ctx, name)
}
func (f *ForkedProvisioningApi) forkRoutePutTemplate(ctx *models.ReqContext, body apimodels.MessageTemplateContent, name string) response.Response {
return f.svc.RoutePutTemplate(ctx, body, name)
}
func (f *ForkedProvisioningApi) forkRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteDeleteTemplate(ctx, name)
}
func (f *ForkedProvisioningApi) forkRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteGetMuteTiming(ctx, name)
}
func (f *ForkedProvisioningApi) forkRouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetMuteTimings(ctx)
}
func (f *ForkedProvisioningApi) forkRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response {
return f.svc.RoutePostMuteTiming(ctx, mt)
}
func (f *ForkedProvisioningApi) forkRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response {
return f.svc.RoutePutMuteTiming(ctx, mt, name)
}
func (f *ForkedProvisioningApi) forkRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteDeleteMuteTiming(ctx, name)
}
func (f *ForkedProvisioningApi) forkRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteRouteGetAlertRule(ctx, UID)
}
func (f *ForkedProvisioningApi) forkRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response {
return f.svc.RoutePostAlertRule(ctx, ar)
}
func (f *ForkedProvisioningApi) forkRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response {
return f.svc.RoutePutAlertRule(ctx, ar, UID)
}
func (f *ForkedProvisioningApi) forkRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteDeleteAlertRule(ctx, UID)
}
func (f *ForkedProvisioningApi) forkRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response {
return f.svc.RouteGetAlertRuleGroup(ctx, folder, group)
}
func (f *ForkedProvisioningApi) forkRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroupMetadata, folder, group string) response.Response {
return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group)
}
@@ -1,31 +0,0 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ForkedTestingApi always forwards requests to grafana backend
type ForkedTestingApi struct {
svc *TestingApiSrv
}
// NewForkedTestingApi creates a new ForkedTestingApi instance
func NewForkedTestingApi(svc *TestingApiSrv) *ForkedTestingApi {
return &ForkedTestingApi{
svc: svc,
}
}
func (f *ForkedTestingApi) forkRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response {
return f.svc.RouteTestRuleConfig(c, body, dsUID)
}
func (f *ForkedTestingApi) forkRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response {
return f.svc.RouteTestGrafanaRuleConfig(c, body)
}
func (f *ForkedTestingApi) forkRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response {
return f.svc.RouteEvalQueries(c, body)
}
@@ -9,22 +9,22 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type ForkedAlertmanagerApi struct {
type AlertmanagerApiHandler struct {
AMSvc *LotexAM
GrafanaSvc *AlertmanagerSrv
DatasourceCache datasources.CacheService
}
// NewForkedAM implements a set of routes that proxy to various Alertmanager-compatible backends.
func NewForkedAM(datasourceCache datasources.CacheService, proxy *LotexAM, grafana *AlertmanagerSrv) *ForkedAlertmanagerApi {
return &ForkedAlertmanagerApi{
// NewForkingAM implements a set of routes that proxy to various Alertmanager-compatible backends.
func NewForkingAM(datasourceCache datasources.CacheService, proxy *LotexAM, grafana *AlertmanagerSrv) *AlertmanagerApiHandler {
return &AlertmanagerApiHandler{
AMSvc: proxy,
GrafanaSvc: grafana,
DatasourceCache: datasourceCache,
}
}
func (f *ForkedAlertmanagerApi) getService(ctx *models.ReqContext) (*LotexAM, error) {
func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, error) {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return nil, err
@@ -38,7 +38,7 @@ func (f *ForkedAlertmanagerApi) getService(ctx *models.ReqContext) (*LotexAM, er
}
}
func (f *ForkedAlertmanagerApi) forkRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return response.Error(400, err.Error(), nil)
@@ -47,7 +47,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMStatus(ctx *models.ReqContext, dsU
return s.RouteGetAMStatus(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -56,7 +56,7 @@ func (f *ForkedAlertmanagerApi) forkRouteCreateSilence(ctx *models.ReqContext, b
return s.RouteCreateSilence(ctx, body)
}
func (f *ForkedAlertmanagerApi) forkRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -65,7 +65,7 @@ func (f *ForkedAlertmanagerApi) forkRouteDeleteAlertingConfig(ctx *models.ReqCon
return s.RouteDeleteAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -74,7 +74,7 @@ func (f *ForkedAlertmanagerApi) forkRouteDeleteSilence(ctx *models.ReqContext, s
return s.RouteDeleteSilence(ctx, silenceID)
}
func (f *ForkedAlertmanagerApi) forkRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -83,7 +83,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAlertingConfig(ctx *models.ReqContex
return s.RouteGetAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -92,7 +92,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMAlertGroups(ctx *models.ReqContext
return s.RouteGetAMAlertGroups(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -101,7 +101,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMAlerts(ctx *models.ReqContext, dsU
return s.RouteGetAMAlerts(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -110,7 +110,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetSilence(ctx *models.ReqContext, sile
return s.RouteGetSilence(ctx, silenceID)
}
func (f *ForkedAlertmanagerApi) forkRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -119,7 +119,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetSilences(ctx *models.ReqContext, dsU
return s.RouteGetSilences(ctx)
}
func (f *ForkedAlertmanagerApi) forkRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -137,7 +137,7 @@ func (f *ForkedAlertmanagerApi) forkRoutePostAlertingConfig(ctx *models.ReqConte
return s.RoutePostAlertingConfig(ctx, body)
}
func (f *ForkedAlertmanagerApi) forkRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -146,7 +146,7 @@ func (f *ForkedAlertmanagerApi) forkRoutePostAMAlerts(ctx *models.ReqContext, bo
return s.RoutePostAMAlerts(ctx, body)
}
func (f *ForkedAlertmanagerApi) forkRoutePostTestReceivers(ctx *models.ReqContext, body apimodels.TestReceiversConfigBodyParams, dsUID string) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostTestReceivers(ctx *models.ReqContext, body apimodels.TestReceiversConfigBodyParams, dsUID string) response.Response {
s, err := f.getService(ctx)
if err != nil {
return ErrResp(400, err, "")
@@ -155,50 +155,50 @@ func (f *ForkedAlertmanagerApi) forkRoutePostTestReceivers(ctx *models.ReqContex
return s.RoutePostTestReceivers(ctx, body)
}
func (f *ForkedAlertmanagerApi) forkRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response {
return f.GrafanaSvc.RouteDeleteSilence(ctx, id)
}
func (f *ForkedAlertmanagerApi) forkRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteDeleteAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response {
func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response {
return f.GrafanaSvc.RouteCreateSilence(ctx, body)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetAMStatus(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetAMAlerts(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetAMAlertGroups(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response {
return f.GrafanaSvc.RouteGetSilence(ctx, id)
}
func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetSilences(ctx)
}
func (f *ForkedAlertmanagerApi) forkRoutePostGrafanaAMAlerts(ctx *models.ReqContext, conf apimodels.PostableAlerts) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAMAlerts(ctx *models.ReqContext, conf apimodels.PostableAlerts) response.Response {
return f.GrafanaSvc.RoutePostAMAlerts(ctx, conf)
}
func (f *ForkedAlertmanagerApi) forkRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response {
return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf)
}
func (f *ForkedAlertmanagerApi) forkRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response {
func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response {
return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf)
}
@@ -9,22 +9,22 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type ForkedPrometheusApi struct {
type PrometheusApiHandler struct {
ProxySvc *LotexProm
GrafanaSvc *PrometheusSrv
DatasourceCache datasources.CacheService
}
// NewForkedProm implements a set of routes that proxy to various Prometheus-compatible backends.
func NewForkedProm(datasourceCache datasources.CacheService, proxy *LotexProm, grafana *PrometheusSrv) *ForkedPrometheusApi {
return &ForkedPrometheusApi{
// NewForkingProm implements a set of routes that proxy to various Prometheus-compatible backends.
func NewForkingProm(datasourceCache datasources.CacheService, proxy *LotexProm, grafana *PrometheusSrv) *PrometheusApiHandler {
return &PrometheusApiHandler{
ProxySvc: proxy,
GrafanaSvc: grafana,
DatasourceCache: datasourceCache,
}
}
func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response {
func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -38,7 +38,7 @@ func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext,
}
}
func (f *ForkedPrometheusApi) forkRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response {
func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -52,10 +52,10 @@ func (f *ForkedPrometheusApi) forkRouteGetRuleStatuses(ctx *models.ReqContext, d
}
}
func (f *ForkedPrometheusApi) forkRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response {
func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetAlertStatuses(ctx)
}
func (f *ForkedPrometheusApi) forkRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response {
func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetRuleStatuses(ctx)
}
@@ -9,23 +9,22 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ForkedRulerApi will validate and proxy requests to the correct backend type depending on the datasource.
type ForkedRulerApi struct {
// RulerApiHandler will validate and proxy requests to the correct backend type depending on the datasource.
type RulerApiHandler struct {
LotexRuler *LotexRuler
GrafanaRuler *RulerSrv
DatasourceCache datasources.CacheService
}
// NewForkedRuler implements a set of routes that proxy to various Cortex Ruler-compatible backends.
func NewForkedRuler(datasourceCache datasources.CacheService, lotex *LotexRuler, grafana *RulerSrv) *ForkedRulerApi {
return &ForkedRulerApi{
func NewForkingRuler(datasourceCache datasources.CacheService, lotex *LotexRuler, grafana *RulerSrv) *RulerApiHandler {
return &RulerApiHandler{
LotexRuler: lotex,
GrafanaRuler: grafana,
DatasourceCache: datasourceCache,
}
}
func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response {
func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -38,7 +37,7 @@ func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqCont
}
}
func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response {
func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -51,7 +50,7 @@ func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext,
}
}
func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response {
func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -64,7 +63,7 @@ func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext
}
}
func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response {
func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -77,7 +76,7 @@ func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext, ds
}
}
func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response {
func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response {
t, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -90,7 +89,7 @@ func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext, dsUID s
}
}
func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response {
func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response {
backendType, err := backendTypeByUID(ctx, f.DatasourceCache)
if err != nil {
return ErrResp(400, err, "")
@@ -109,27 +108,27 @@ func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, co
}
}
func (f *ForkedRulerApi) forkRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response {
func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response {
return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, "")
}
func (f *ForkedRulerApi) forkRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response {
func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response {
return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, groupName)
}
func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response {
func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response {
return f.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx, namespace)
}
func (f *ForkedRulerApi) forkRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response {
func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response {
return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx, namespace, group)
}
func (f *ForkedRulerApi) forkRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
return f.GrafanaRuler.RouteGetRulesConfig(ctx)
}
func (f *ForkedRulerApi) forkRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response {
func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response {
payloadType := conf.Type()
if payloadType != apimodels.GrafanaBackend {
return ErrResp(400, fmt.Errorf("unexpected backend type (%v) vs payload type (%v)", apimodels.GrafanaBackend, payloadType), "")
@@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type AlertmanagerApiForkingService interface {
type AlertmanagerApi interface {
RouteCreateGrafanaSilence(*models.ReqContext) response.Response
RouteCreateSilence(*models.ReqContext) response.Response
RouteDeleteAlertingConfig(*models.ReqContext) response.Response
@@ -45,128 +45,150 @@ type AlertmanagerApiForkingService interface {
RoutePostTestReceivers(*models.ReqContext) response.Response
}
func (f *ForkedAlertmanagerApi) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableSilence{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRouteCreateGrafanaSilence(ctx, conf)
return f.handleRouteCreateGrafanaSilence(ctx, conf)
}
func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
// Parse Request Body
conf := apimodels.PostableSilence{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRouteCreateSilence(ctx, conf, datasourceUIDParam)
return f.handleRouteCreateSilence(ctx, conf, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteDeleteAlertingConfig(ctx, datasourceUIDParam)
return f.handleRouteDeleteAlertingConfig(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.forkRouteDeleteGrafanaAlertingConfig(ctx)
func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.handleRouteDeleteGrafanaAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
silenceIdParam := web.Params(ctx.Req)[":SilenceId"]
return f.forkRouteDeleteGrafanaSilence(ctx, silenceIdParam)
return f.handleRouteDeleteGrafanaSilence(ctx, silenceIdParam)
}
func (f *ForkedAlertmanagerApi) RouteDeleteSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
silenceIdParam := web.Params(ctx.Req)[":SilenceId"]
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam)
return f.handleRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetAMAlertGroups(ctx, datasourceUIDParam)
return f.handleRouteGetAMAlertGroups(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetAMAlerts(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetAMAlerts(ctx, datasourceUIDParam)
return f.handleRouteGetAMAlerts(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetAMStatus(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetAMStatus(ctx, datasourceUIDParam)
return f.handleRouteGetAMStatus(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetAlertingConfig(ctx, datasourceUIDParam)
return f.handleRouteGetAlertingConfig(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaAMAlertGroups(ctx)
func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaAMAlertGroups(ctx)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaAMAlerts(ctx)
func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaAMAlerts(ctx)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaAMStatus(ctx)
func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaAMStatus(ctx)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaAlertingConfig(ctx)
func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaAlertingConfig(ctx)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
silenceIdParam := web.Params(ctx.Req)[":SilenceId"]
return f.forkRouteGetGrafanaSilence(ctx, silenceIdParam)
return f.handleRouteGetGrafanaSilence(ctx, silenceIdParam)
}
func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaSilences(ctx)
func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaSilences(ctx)
}
func (f *ForkedAlertmanagerApi) RouteGetSilence(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
silenceIdParam := web.Params(ctx.Req)[":SilenceId"]
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam)
return f.handleRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RouteGetSilences(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetSilences(ctx, datasourceUIDParam)
return f.handleRouteGetSilences(ctx, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
// Parse Request Body
conf := apimodels.PostableAlerts{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostAMAlerts(ctx, conf, datasourceUIDParam)
return f.handleRoutePostAMAlerts(ctx, conf, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
// Parse Request Body
conf := apimodels.PostableUserConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostAlertingConfig(ctx, conf, datasourceUIDParam)
return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam)
}
func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableAlerts{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostGrafanaAMAlerts(ctx, conf)
return f.handleRoutePostGrafanaAMAlerts(ctx, conf)
}
func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableUserConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostGrafanaAlertingConfig(ctx, conf)
return f.handleRoutePostGrafanaAlertingConfig(ctx, conf)
}
func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.TestReceiversConfigBodyParams{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostTestGrafanaReceivers(ctx, conf)
return f.handleRoutePostTestGrafanaReceivers(ctx, conf)
}
func (f *ForkedAlertmanagerApi) RoutePostTestReceivers(ctx *models.ReqContext) response.Response {
func (f *AlertmanagerApiHandler) RoutePostTestReceivers(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
// Parse Request Body
conf := apimodels.TestReceiversConfigBodyParams{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostTestReceivers(ctx, conf, datasourceUIDParam)
return f.handleRoutePostTestReceivers(ctx, conf, datasourceUIDParam)
}
func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApiForkingService, m *metrics.API) {
func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Post(
toMacaronPath("/api/alertmanager/grafana/api/v2/silences"),
@@ -18,31 +18,32 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type ConfigurationApiForkingService interface {
type ConfigurationApi interface {
RouteDeleteNGalertConfig(*models.ReqContext) response.Response
RouteGetAlertmanagers(*models.ReqContext) response.Response
RouteGetNGalertConfig(*models.ReqContext) response.Response
RoutePostNGalertConfig(*models.ReqContext) response.Response
}
func (f *ForkedConfigurationApi) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response {
return f.forkRouteDeleteNGalertConfig(ctx)
func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response {
return f.handleRouteDeleteNGalertConfig(ctx)
}
func (f *ForkedConfigurationApi) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response {
return f.forkRouteGetAlertmanagers(ctx)
func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response {
return f.handleRouteGetAlertmanagers(ctx)
}
func (f *ForkedConfigurationApi) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response {
return f.forkRouteGetNGalertConfig(ctx)
func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response {
return f.handleRouteGetNGalertConfig(ctx)
}
func (f *ForkedConfigurationApi) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response {
func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableNGalertConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostNGalertConfig(ctx, conf)
return f.handleRoutePostNGalertConfig(ctx, conf)
}
func (api *API) RegisterConfigurationApiEndpoints(srv ConfigurationApiForkingService, m *metrics.API) {
func (api *API) RegisterConfigurationApiEndpoints(srv ConfigurationApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/v1/ngalert/admin_config"),
@@ -17,29 +17,31 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type PrometheusApiForkingService interface {
type PrometheusApi interface {
RouteGetAlertStatuses(*models.ReqContext) response.Response
RouteGetGrafanaAlertStatuses(*models.ReqContext) response.Response
RouteGetGrafanaRuleStatuses(*models.ReqContext) response.Response
RouteGetRuleStatuses(*models.ReqContext) response.Response
}
func (f *ForkedPrometheusApi) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response {
func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetAlertStatuses(ctx, datasourceUIDParam)
return f.handleRouteGetAlertStatuses(ctx, datasourceUIDParam)
}
func (f *ForkedPrometheusApi) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaAlertStatuses(ctx)
func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaAlertStatuses(ctx)
}
func (f *ForkedPrometheusApi) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaRuleStatuses(ctx)
func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaRuleStatuses(ctx)
}
func (f *ForkedPrometheusApi) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response {
func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetRuleStatuses(ctx, datasourceUIDParam)
return f.handleRouteGetRuleStatuses(ctx, datasourceUIDParam)
}
func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApiForkingService, m *metrics.API) {
func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Get(
toMacaronPath("/api/prometheus/{DatasourceUID}/api/v1/alerts"),
@@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type ProvisioningApiForkingService interface {
type ProvisioningApi interface {
RouteDeleteAlertRule(*models.ReqContext) response.Response
RouteDeleteContactpoints(*models.ReqContext) response.Response
RouteDeleteMuteTiming(*models.ReqContext) response.Response
@@ -43,125 +43,147 @@ type ProvisioningApiForkingService interface {
RouteResetPolicyTree(*models.ReqContext) response.Response
}
func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
return f.forkRouteDeleteAlertRule(ctx, uIDParam)
return f.handleRouteDeleteAlertRule(ctx, uIDParam)
}
func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
return f.forkRouteDeleteContactpoints(ctx, uIDParam)
return f.handleRouteDeleteContactpoints(ctx, uIDParam)
}
func (f *ForkedProvisioningApi) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
return f.forkRouteDeleteMuteTiming(ctx, nameParam)
return f.handleRouteDeleteMuteTiming(ctx, nameParam)
}
func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
return f.forkRouteDeleteTemplate(ctx, nameParam)
return f.handleRouteDeleteTemplate(ctx, nameParam)
}
func (f *ForkedProvisioningApi) RouteGetAlertRule(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
return f.forkRouteGetAlertRule(ctx, uIDParam)
return f.handleRouteGetAlertRule(ctx, uIDParam)
}
func (f *ForkedProvisioningApi) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
folderUIDParam := web.Params(ctx.Req)[":FolderUID"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.forkRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam)
return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam)
}
func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response {
return f.forkRouteGetContactpoints(ctx)
func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *models.ReqContext) response.Response {
return f.handleRouteGetContactpoints(ctx)
}
func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
return f.forkRouteGetMuteTiming(ctx, nameParam)
return f.handleRouteGetMuteTiming(ctx, nameParam)
}
func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.forkRouteGetMuteTimings(ctx)
func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.handleRouteGetMuteTimings(ctx)
}
func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response {
return f.forkRouteGetPolicyTree(ctx)
func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *models.ReqContext) response.Response {
return f.handleRouteGetPolicyTree(ctx)
}
func (f *ForkedProvisioningApi) RouteGetTemplate(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
return f.forkRouteGetTemplate(ctx, nameParam)
return f.handleRouteGetTemplate(ctx, nameParam)
}
func (f *ForkedProvisioningApi) RouteGetTemplates(ctx *models.ReqContext) response.Response {
return f.forkRouteGetTemplates(ctx)
func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *models.ReqContext) response.Response {
return f.handleRouteGetTemplates(ctx)
}
func (f *ForkedProvisioningApi) RoutePostAlertRule(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.ProvisionedAlertRule{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostAlertRule(ctx, conf)
return f.handleRoutePostAlertRule(ctx, conf)
}
func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.EmbeddedContactPoint{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostContactpoints(ctx, conf)
return f.handleRoutePostContactpoints(ctx, conf)
}
func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.MuteTimeInterval{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostMuteTiming(ctx, conf)
return f.handleRoutePostMuteTiming(ctx, conf)
}
func (f *ForkedProvisioningApi) RoutePutAlertRule(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
// Parse Request Body
conf := apimodels.ProvisionedAlertRule{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutAlertRule(ctx, conf, uIDParam)
return f.handleRoutePutAlertRule(ctx, conf, uIDParam)
}
func (f *ForkedProvisioningApi) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
folderUIDParam := web.Params(ctx.Req)[":FolderUID"]
groupParam := web.Params(ctx.Req)[":Group"]
// Parse Request Body
conf := apimodels.AlertRuleGroupMetadata{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam)
return f.handleRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam)
}
func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
uIDParam := web.Params(ctx.Req)[":UID"]
// Parse Request Body
conf := apimodels.EmbeddedContactPoint{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutContactpoint(ctx, conf, uIDParam)
return f.handleRoutePutContactpoint(ctx, conf, uIDParam)
}
func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
// Parse Request Body
conf := apimodels.MuteTimeInterval{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutMuteTiming(ctx, conf, nameParam)
return f.handleRoutePutMuteTiming(ctx, conf, nameParam)
}
func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.Route{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutPolicyTree(ctx, conf)
return f.handleRoutePutPolicyTree(ctx, conf)
}
func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response {
func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
// Parse Request Body
conf := apimodels.MessageTemplateContent{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePutTemplate(ctx, conf, nameParam)
return f.handleRoutePutTemplate(ctx, conf, nameParam)
}
func (f *ForkedProvisioningApi) RouteResetPolicyTree(ctx *models.ReqContext) response.Response {
return f.forkRouteResetPolicyTree(ctx)
func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *models.ReqContext) response.Response {
return f.handleRouteResetPolicyTree(ctx)
}
func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) {
func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/v1/provisioning/alert-rules/{UID}"),
@@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type RulerApiForkingService interface {
type RulerApi interface {
RouteDeleteGrafanaRuleGroupConfig(*models.ReqContext) response.Response
RouteDeleteNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response
RouteDeleteNamespaceRulesConfig(*models.ReqContext) response.Response
@@ -33,72 +33,85 @@ type RulerApiForkingService interface {
RoutePostNameRulesConfig(*models.ReqContext) response.Response
}
func (f *ForkedRulerApi) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
namespaceParam := web.Params(ctx.Req)[":Namespace"]
groupnameParam := web.Params(ctx.Req)[":Groupname"]
return f.forkRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam)
return f.handleRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam)
}
func (f *ForkedRulerApi) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
namespaceParam := web.Params(ctx.Req)[":Namespace"]
return f.forkRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam)
return f.handleRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam)
}
func (f *ForkedRulerApi) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
namespaceParam := web.Params(ctx.Req)[":Namespace"]
return f.forkRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam)
return f.handleRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam)
}
func (f *ForkedRulerApi) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
namespaceParam := web.Params(ctx.Req)[":Namespace"]
groupnameParam := web.Params(ctx.Req)[":Groupname"]
return f.forkRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam)
return f.handleRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam)
}
func (f *ForkedRulerApi) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
namespaceParam := web.Params(ctx.Req)[":Namespace"]
groupnameParam := web.Params(ctx.Req)[":Groupname"]
return f.forkRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam)
return f.handleRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam)
}
func (f *ForkedRulerApi) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
return f.forkRouteGetGrafanaRulesConfig(ctx)
func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
return f.handleRouteGetGrafanaRulesConfig(ctx)
}
func (f *ForkedRulerApi) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
namespaceParam := web.Params(ctx.Req)[":Namespace"]
return f.forkRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam)
return f.handleRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam)
}
func (f *ForkedRulerApi) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
namespaceParam := web.Params(ctx.Req)[":Namespace"]
return f.forkRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam)
return f.handleRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam)
}
func (f *ForkedRulerApi) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
namespaceParam := web.Params(ctx.Req)[":Namespace"]
groupnameParam := web.Params(ctx.Req)[":Groupname"]
return f.forkRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam)
return f.handleRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam)
}
func (f *ForkedRulerApi) RouteGetRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
return f.forkRouteGetRulesConfig(ctx, datasourceUIDParam)
return f.handleRouteGetRulesConfig(ctx, datasourceUIDParam)
}
func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
namespaceParam := web.Params(ctx.Req)[":Namespace"]
// Parse Request Body
conf := apimodels.PostableRuleGroupConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam)
return f.handleRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam)
}
func (f *ForkedRulerApi) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response {
func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
namespaceParam := web.Params(ctx.Req)[":Namespace"]
// Parse Request Body
conf := apimodels.PostableRuleGroupConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRoutePostNameRulesConfig(ctx, conf, datasourceUIDParam, namespaceParam)
return f.handleRoutePostNameRulesConfig(ctx, conf, datasourceUIDParam, namespaceParam)
}
func (api *API) RegisterRulerApiEndpoints(srv RulerApiForkingService, m *metrics.API) {
func (api *API) RegisterRulerApiEndpoints(srv RulerApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}"),
@@ -18,36 +18,40 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type TestingApiForkingService interface {
type TestingApi interface {
RouteEvalQueries(*models.ReqContext) response.Response
RouteTestRuleConfig(*models.ReqContext) response.Response
RouteTestRuleGrafanaConfig(*models.ReqContext) response.Response
}
func (f *ForkedTestingApi) RouteEvalQueries(ctx *models.ReqContext) response.Response {
func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.EvalQueriesPayload{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRouteEvalQueries(ctx, conf)
return f.handleRouteEvalQueries(ctx, conf)
}
func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response.Response {
func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
// Parse Request Body
conf := apimodels.TestRulePayload{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRouteTestRuleConfig(ctx, conf, datasourceUIDParam)
return f.handleRouteTestRuleConfig(ctx, conf, datasourceUIDParam)
}
func (f *ForkedTestingApi) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response {
func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.TestRulePayload{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.forkRouteTestRuleGrafanaConfig(ctx, conf)
return f.handleRouteTestRuleGrafanaConfig(ctx, conf)
}
func (api *API) RegisterTestingApiEndpoints(srv TestingApiForkingService, m *metrics.API) {
func (api *API) RegisterTestingApiEndpoints(srv TestingApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Post(
toMacaronPath("/api/v1/eval"),
+105
View File
@@ -0,0 +1,105 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type ProvisioningApiHandler struct {
svc *ProvisioningSrv
}
func NewProvisioningApi(svc *ProvisioningSrv) *ProvisioningApiHandler {
return &ProvisioningApiHandler{
svc: svc,
}
}
func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetPolicyTree(ctx)
}
func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response {
return f.svc.RoutePutPolicyTree(ctx, route)
}
func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetContactPoints(ctx)
}
func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response {
return f.svc.RoutePostContactPoint(ctx, cp)
}
func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response {
return f.svc.RoutePutContactPoint(ctx, cp, UID)
}
func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteDeleteContactPoint(ctx, UID)
}
func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetTemplates(ctx)
}
func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteGetTemplate(ctx, name)
}
func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *models.ReqContext, body apimodels.MessageTemplateContent, name string) response.Response {
return f.svc.RoutePutTemplate(ctx, body, name)
}
func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteDeleteTemplate(ctx, name)
}
func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteGetMuteTiming(ctx, name)
}
func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetMuteTimings(ctx)
}
func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response {
return f.svc.RoutePostMuteTiming(ctx, mt)
}
func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response {
return f.svc.RoutePutMuteTiming(ctx, mt, name)
}
func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response {
return f.svc.RouteDeleteMuteTiming(ctx, name)
}
func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteRouteGetAlertRule(ctx, UID)
}
func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response {
return f.svc.RoutePostAlertRule(ctx, ar)
}
func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response {
return f.svc.RoutePutAlertRule(ctx, ar, UID)
}
func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response {
return f.svc.RouteDeleteAlertRule(ctx, UID)
}
func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *models.ReqContext) response.Response {
return f.svc.RouteResetPolicyTree(ctx)
}
func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response {
return f.svc.RouteGetAlertRuleGroup(ctx, folder, group)
}
func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroupMetadata, folder, group string) response.Response {
return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group)
}
+30
View File
@@ -0,0 +1,30 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// TestingApiHandler always forwards requests to grafana backend
type TestingApiHandler struct {
svc *TestingApiSrv
}
func NewTestingApi(svc *TestingApiSrv) *TestingApiHandler {
return &TestingApiHandler{
svc: svc,
}
}
func (f *TestingApiHandler) handleRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response {
return f.svc.RouteTestRuleConfig(c, body, dsUID)
}
func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response {
return f.svc.RouteTestGrafanaRuleConfig(c, body)
}
func (f *TestingApiHandler) handleRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response {
return f.svc.RouteEvalQueries(c, body)
}
+3 -1
View File
@@ -65,4 +65,6 @@ serve: post.json
serve-stable: api.json
docker run --rm -p 80:8080 -v $$(pwd):/tmp -e SWAGGER_FILE=/tmp/$(<) swaggerapi/swagger-editor
all: post.json validate api.json validate-stable swagger-codegen-api fix copy-files clean
gen: swagger-codegen-api fix copy-files clean
all: post.json api.json gen
+3 -1
View File
@@ -2702,6 +2702,7 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -2734,7 +2735,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object"
},
"Userinfo": {
@@ -3295,6 +3296,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"name": {
"description": "name",
+2 -2
View File
@@ -5289,6 +5289,7 @@
}
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@@ -5487,7 +5488,6 @@
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@@ -5537,7 +5537,6 @@
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
@@ -5688,6 +5687,7 @@
"$ref": "#/definitions/postableSilence"
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"name"
@@ -13,26 +13,25 @@ import (
"github.com/grafana/grafana/pkg/middleware"
)
type {{classname}}ForkingService interface { {{#operation}}
type {{classname}} interface { {{#operation}}
{{nickname}}(*models.ReqContext) response.Response{{/operation}}
}
{{#operations}}{{#operation}}
func (f *Forked{{classname}}) {{nickname}}(ctx *models.ReqContext) response.Response {
{{#pathParams}}
{{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]
{{/pathParams}}
func (f *{{classname}}Handler) {{nickname}}(ctx *models.ReqContext) response.Response { {{#hasPathParams}}
// Parse Path Parameters{{/hasPathParams}}{{#pathParams}}
{{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}}
{{#bodyParams}}
// Parse Request Body
conf := apimodels.{{dataType}}{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
{{/bodyParams}}
return f.fork{{nickname}}(ctx{{#bodyParams}}, conf{{/bodyParams}}{{#pathParams}}, {{paramName}}Param{{/pathParams}})
{{/bodyParams}}return f.handle{{nickname}}(ctx{{#bodyParams}}, conf{{/bodyParams}}{{#pathParams}}, {{paramName}}Param{{/pathParams}})
}
{{/operation}}{{/operations}}
func (api *API) Register{{classname}}Endpoints(srv {{classname}}ForkingService, m *metrics.API) {
func (api *API) Register{{classname}}Endpoints(srv {{classname}}, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister){ {{#operations}}{{#operation}}
group.{{httpMethod}}(
toMacaronPath("{{{path}}}"),
@@ -46,4 +45,4 @@ func (api *API) Register{{classname}}Endpoints(srv {{classname}}ForkingService,
){{/operation}}{{/operations}}
}, middleware.ReqSignedIn)
}{{#operation}}
{{/operation}}{{/operations}}
{{/operation}}{{/operations}}
+95
View File
@@ -0,0 +1,95 @@
package playlist
import (
"errors"
)
// Typed errors
var (
ErrPlaylistNotFound = errors.New("Playlist not found")
ErrPlaylistFailedGenerateUniqueUid = errors.New("failed to generate unique playlist UID")
ErrCommandValidationFailed = errors.New("command missing required fields")
)
// Playlist model
type Playlist struct {
Id int64 `json:"id"`
UID string `json:"uid" xorm:"uid"`
Name string `json:"name"`
Interval string `json:"interval"`
OrgId int64 `json:"-"`
}
type PlaylistDTO struct {
Id int64 `json:"id"`
UID string `json:"uid"`
Name string `json:"name"`
Interval string `json:"interval"`
OrgId int64 `json:"-"`
Items []PlaylistItemDTO `json:"items"`
}
type PlaylistItemDTO struct {
Id int64 `json:"id"`
PlaylistId int64 `json:"playlistid"`
Type string `json:"type"`
Title string `json:"title"`
Value string `json:"value"`
Order int `json:"order"`
}
type PlaylistItem struct {
Id int64
PlaylistId int64
Type string
Value string
Order int
Title string
}
type Playlists []*Playlist
//
// COMMANDS
//
type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"`
UID string `json:"uid"`
Name string `json:"name" binding:"Required"`
Interval string `json:"interval"`
Items []PlaylistItemDTO `json:"items"`
}
type CreatePlaylistCommand struct {
Name string `json:"name" binding:"Required"`
Interval string `json:"interval"`
Items []PlaylistItemDTO `json:"items"`
OrgId int64 `json:"-"`
}
type DeletePlaylistCommand struct {
UID string
OrgId int64
}
//
// QUERIES
//
type GetPlaylistsQuery struct {
Name string
Limit int
OrgId int64
}
type GetPlaylistByUidQuery struct {
UID string
OrgId int64
}
type GetPlaylistItemsByUidQuery struct {
PlaylistUID string
OrgId int64
}
+14
View File
@@ -0,0 +1,14 @@
package playlist
import (
"context"
)
type Service interface {
Create(context.Context, *CreatePlaylistCommand) (*Playlist, error)
Update(context.Context, *UpdatePlaylistCommand) (*PlaylistDTO, error)
Get(context.Context, *GetPlaylistByUidQuery) (*Playlist, error)
GetItems(context.Context, *GetPlaylistItemsByUidQuery) ([]PlaylistItem, error)
Search(context.Context, *GetPlaylistsQuery) (Playlists, error)
Delete(ctx context.Context, cmd *DeletePlaylistCommand) error
}
@@ -0,0 +1,44 @@
package playlistimpl
import (
"context"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
)
type Service struct {
store store
}
func ProvideService(db db.DB) playlist.Service {
return &Service{
store: &sqlStore{
db: db,
},
}
}
func (s *Service) Create(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) {
return s.store.Insert(ctx, cmd)
}
func (s *Service) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) {
return s.store.Update(ctx, cmd)
}
func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) {
return s.store.Get(ctx, q)
}
func (s *Service) GetItems(ctx context.Context, q *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) {
return s.store.GetItems(ctx, q)
}
func (s *Service) Search(ctx context.Context, q *playlist.GetPlaylistsQuery) (playlist.Playlists, error) {
return s.store.List(ctx, q)
}
func (s *Service) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error {
return s.store.Delete(ctx, cmd)
}
+226
View File
@@ -0,0 +1,226 @@
package playlistimpl
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/util"
)
type store interface {
Insert(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error)
Delete(context.Context, *playlist.DeletePlaylistCommand) error
Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error)
GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error)
List(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error)
Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error)
}
type sqlStore struct {
db db.DB
}
func (s *sqlStore) Insert(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) {
p := playlist.Playlist{}
err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
uid, err := generateAndValidateNewPlaylistUid(sess, cmd.OrgId)
if err != nil {
return err
}
p = playlist.Playlist{
Name: cmd.Name,
Interval: cmd.Interval,
OrgId: cmd.OrgId,
UID: uid,
}
_, err = sess.Insert(&p)
if err != nil {
return err
}
playlistItems := make([]playlist.PlaylistItem, 0)
for _, item := range cmd.Items {
playlistItems = append(playlistItems, playlist.PlaylistItem{
PlaylistId: p.Id,
Type: item.Type,
Value: item.Value,
Order: item.Order,
Title: item.Title,
})
}
_, err = sess.Insert(&playlistItems)
return err
})
return &p, err
}
func (s *sqlStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) {
dto := playlist.PlaylistDTO{}
err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
p := playlist.Playlist{
UID: cmd.UID,
OrgId: cmd.OrgId,
Name: cmd.Name,
Interval: cmd.Interval,
}
existingPlaylist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId}
_, err := sess.Get(&existingPlaylist)
if err != nil {
return err
}
p.Id = existingPlaylist.Id
dto = playlist.PlaylistDTO{
Id: p.Id,
UID: p.UID,
OrgId: p.OrgId,
Name: p.Name,
Interval: p.Interval,
}
_, err = sess.Where("id=?", p.Id).Cols("name", "interval").Update(&p)
if err != nil {
return err
}
rawSQL := "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err = sess.Exec(rawSQL, p.Id)
if err != nil {
return err
}
playlistItems := make([]models.PlaylistItem, 0)
for index, item := range cmd.Items {
playlistItems = append(playlistItems, models.PlaylistItem{
PlaylistId: p.Id,
Type: item.Type,
Value: item.Value,
Order: index + 1,
Title: item.Title,
})
}
_, err = sess.Insert(&playlistItems)
return err
})
return &dto, err
}
func (s *sqlStore) Get(ctx context.Context, query *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) {
if query.UID == "" || query.OrgId == 0 {
return nil, playlist.ErrCommandValidationFailed
}
p := playlist.Playlist{}
err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
p = playlist.Playlist{UID: query.UID, OrgId: query.OrgId}
exists, err := sess.Get(&p)
if !exists {
return playlist.ErrPlaylistNotFound
}
return err
})
return &p, err
}
func (s *sqlStore) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error {
if cmd.UID == "" || cmd.OrgId == 0 {
return playlist.ErrCommandValidationFailed
}
return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
playlist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId}
_, err := sess.Get(&playlist)
if err != nil {
return err
}
var rawPlaylistSQL = "DELETE FROM playlist WHERE uid = ? and org_id = ?"
_, err = sess.Exec(rawPlaylistSQL, cmd.UID, cmd.OrgId)
if err != nil {
return err
}
var rawItemSQL = "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err = sess.Exec(rawItemSQL, playlist.Id)
return err
})
}
func (s *sqlStore) List(ctx context.Context, query *playlist.GetPlaylistsQuery) (playlist.Playlists, error) {
playlists := make(playlist.Playlists, 0)
if query.OrgId == 0 {
return playlists, playlist.ErrCommandValidationFailed
}
err := s.db.WithDbSession(ctx, func(dbSess *sqlstore.DBSession) error {
sess := dbSess.Limit(query.Limit)
if query.Name != "" {
sess.Where("name LIKE ?", "%"+query.Name+"%")
}
sess.Where("org_id = ?", query.OrgId)
err := sess.Find(&playlists)
return err
})
return playlists, err
}
func (s *sqlStore) GetItems(ctx context.Context, query *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) {
var playlistItems = make([]playlist.PlaylistItem, 0)
err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if query.PlaylistUID == "" || query.OrgId == 0 {
return models.ErrCommandValidationFailed
}
// getQuery the playlist Id
getQuery := &playlist.GetPlaylistByUidQuery{UID: query.PlaylistUID, OrgId: query.OrgId}
p, err := s.Get(ctx, getQuery)
if err != nil {
return err
}
err = sess.Where("playlist_id=?", p.Id).Find(&playlistItems)
return err
})
return playlistItems, err
}
// generateAndValidateNewPlaylistUid generates a playlistUID and verifies that
// the uid isn't already in use. This is deliberately overly cautious, since users
// can also specify playlist uids during provisioning.
func generateAndValidateNewPlaylistUid(sess *sqlstore.DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
playlist := models.Playlist{OrgId: orgId, UID: uid}
exists, err := sess.Get(&playlist)
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", models.ErrPlaylistFailedGenerateUniqueUid
}
var generateNewUid func() string = util.GenerateShortUID
@@ -0,0 +1,82 @@
package playlistimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/require"
)
func TestIntegrationPlaylistDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ss := sqlstore.InitTestDB(t)
playlistStore := sqlStore{db: ss}
t.Run("Can create playlist", func(t *testing.T) {
items := []playlist.PlaylistItemDTO{
{Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "3", Type: "dashboard_by_id"},
}
cmd := playlist.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
p, err := playlistStore.Insert(context.Background(), &cmd)
require.NoError(t, err)
uid := p.UID
t.Run("Can get playlist items", func(t *testing.T) {
get := &playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: 1}
storedPlaylistItems, err := playlistStore.GetItems(context.Background(), get)
require.NoError(t, err)
require.Equal(t, len(storedPlaylistItems), len(items))
})
t.Run("Can update playlist", func(t *testing.T) {
items := []playlist.PlaylistItemDTO{
{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
}
query := playlist.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, UID: uid, Interval: "10s", Items: items}
_, err = playlistStore.Update(context.Background(), &query)
require.NoError(t, err)
})
t.Run("Can remove playlist", func(t *testing.T) {
deleteQuery := playlist.DeletePlaylistCommand{UID: uid, OrgId: 1}
err = playlistStore.Delete(context.Background(), &deleteQuery)
require.NoError(t, err)
getQuery := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: 1}
p, err := playlistStore.Get(context.Background(), &getQuery)
require.Error(t, err)
require.Equal(t, uid, p.UID, "playlist should've been removed")
require.ErrorIs(t, err, playlist.ErrPlaylistNotFound)
})
})
t.Run("Delete playlist that doesn't exist", func(t *testing.T) {
deleteQuery := playlist.DeletePlaylistCommand{UID: "654312", OrgId: 1}
err := playlistStore.Delete(context.Background(), &deleteQuery)
require.NoError(t, err)
})
t.Run("Delete playlist with invalid command yields error", func(t *testing.T) {
testCases := []struct {
desc string
cmd playlist.DeletePlaylistCommand
}{
{desc: "none", cmd: playlist.DeletePlaylistCommand{}},
{desc: "no OrgId", cmd: playlist.DeletePlaylistCommand{UID: "1"}},
{desc: "no Uid", cmd: playlist.DeletePlaylistCommand{OrgId: 1}},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
err := playlistStore.Delete(context.Background(), &tc.cmd)
require.EqualError(t, err, playlist.ErrCommandValidationFailed.Error())
})
}
})
}
@@ -0,0 +1,43 @@
package playlisttest
import (
"context"
"github.com/grafana/grafana/pkg/services/playlist"
)
type FakePlaylistService struct {
ExpectedPlaylist *playlist.Playlist
ExpectedPlaylistDTO *playlist.PlaylistDTO
ExpectedPlaylistItems []playlist.PlaylistItem
ExpectedPlaylists playlist.Playlists
ExpectedError error
}
func NewPlaylistServiveFake() *FakePlaylistService {
return &FakePlaylistService{}
}
func (f *FakePlaylistService) Create(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) {
return f.ExpectedPlaylist, f.ExpectedError
}
func (f *FakePlaylistService) Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) {
return f.ExpectedPlaylistDTO, f.ExpectedError
}
func (f *FakePlaylistService) Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) {
return f.ExpectedPlaylist, f.ExpectedError
}
func (f *FakePlaylistService) GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) {
return f.ExpectedPlaylistItems, f.ExpectedError
}
func (f *FakePlaylistService) Search(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error) {
return f.ExpectedPlaylists, f.ExpectedError
}
func (f *FakePlaylistService) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error {
return f.ExpectedError
}
+27 -21
View File
@@ -37,14 +37,14 @@ func ProvideSecretsMigrator(
}
}
func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) error {
func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) (bool, error) {
err := m.initProvidersIfNeeded()
if err != nil {
return err
return false, err
}
toReencrypt := []interface {
reencrypt(context.Context, *manager.SecretsService, *sqlstore.SQLStore)
reencrypt(context.Context, *manager.SecretsService, *sqlstore.SQLStore) bool
}{
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted"},
b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}, encoding: base64.StdEncoding},
@@ -56,30 +56,21 @@ func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) error {
alertingSecret{},
}
var anyFailure bool
for _, r := range toReencrypt {
r.reencrypt(ctx, m.secretsSrv, m.sqlStore)
}
return nil
}
func (m *SecretsMigrator) initProvidersIfNeeded() error {
if m.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
logger.Info("Envelope encryption is not enabled but trying to init providers anyway...")
if err := m.secretsSrv.InitProviders(); err != nil {
logger.Error("Envelope encryption providers initialization failed", "error", err)
return err
if success := r.reencrypt(ctx, m.secretsSrv, m.sqlStore); !success {
anyFailure = true
}
}
return nil
return !anyFailure, nil
}
func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) error {
func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) (bool, error) {
err := m.initProvidersIfNeeded()
if err != nil {
return err
return false, err
}
toRollback := []interface {
@@ -110,11 +101,26 @@ func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) error {
if anyFailure {
logger.Warn("Some errors happened, not cleaning up data keys table...")
return nil
return false, nil
}
if _, sqlErr := m.sqlStore.NewSession(ctx).Exec("DELETE FROM data_keys"); sqlErr != nil {
_, sqlErr := m.sqlStore.NewSession(ctx).Exec("DELETE FROM data_keys")
if sqlErr != nil {
logger.Warn("Error while cleaning up data keys table...", "error", sqlErr)
return false, nil
}
return true, nil
}
func (m *SecretsMigrator) initProvidersIfNeeded() error {
if m.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
logger.Info("Envelope encryption is not enabled but trying to init providers anyway...")
if err := m.secretsSrv.InitProviders(); err != nil {
logger.Error("Envelope encryption providers initialization failed", "error", err)
return err
}
}
return nil
+16 -8
View File
@@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) {
func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool {
var rows []struct {
Id int
Secret []byte
@@ -20,7 +20,7 @@ func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secrets
if err := sqlStore.NewSession(ctx).Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
return
return false
}
var anyFailure bool
@@ -62,9 +62,11 @@ func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secrets
} else {
logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
}
return !anyFailure
}
func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) {
func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool {
var rows []struct {
Id int
Secret string
@@ -72,7 +74,7 @@ func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSer
if err := sqlStore.NewSession(ctx).Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
return
return false
}
var anyFailure bool
@@ -128,9 +130,11 @@ func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSer
} else {
logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
}
return !anyFailure
}
func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) {
func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool {
var rows []struct {
Id int
SecureJsonData map[string][]byte
@@ -138,7 +142,7 @@ func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSe
if err := sqlStore.NewSession(ctx).Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil {
logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
return
return false
}
var anyFailure bool
@@ -184,9 +188,11 @@ func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSe
} else {
logger.Info(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted successfully", s.tableName))
}
return !anyFailure
}
func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) {
func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool {
var results []struct {
Id int
AlertmanagerConfiguration string
@@ -195,7 +201,7 @@ func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secre
selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration"
if err := sqlStore.NewSession(ctx).SQL(selectSQL).Find(&results); err != nil {
logger.Warn("Could not find any alert_configuration secret to re-encrypt")
return
return false
}
var anyFailure bool
@@ -261,4 +267,6 @@ func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secre
} else {
logger.Info("Alerting configuration secrets have been re-encrypted successfully")
}
return !anyFailure
}
+10 -2
View File
@@ -72,6 +72,14 @@ type BackgroundProvider interface {
// Migrator is responsible for secrets migrations like re-encrypting or rolling back secrets.
type Migrator interface {
ReEncryptSecrets(ctx context.Context) error
RollBackSecrets(ctx context.Context) error
// ReEncryptSecrets decrypts and re-encrypts the secrets with most recent
// available data key. If a secret-specific decryption / re-encryption fails,
// it does not stop, but returns false as the first return (success or not)
// at the end of the process.
ReEncryptSecrets(ctx context.Context) (bool, error)
// RollBackSecrets decrypts and re-encrypts the secrets using the legacy
// encryption. If a secret-specific decryption / re-encryption fails, it
// does not stop, but returns false as the first return (success or not)
// at the end of the process.
RollBackSecrets(ctx context.Context) (bool, error)
}
+6
View File
@@ -72,11 +72,17 @@ type Store interface {
GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error
WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
// deprecated
CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error
// deprecated
UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error
// deprecated
GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error
// deprecated
DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error
// deprecated
SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error
// deprecated
GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error
GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error
GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error
+121
View File
@@ -0,0 +1,121 @@
package store
import (
"context"
"strings"
"github.com/grafana/grafana/pkg/infra/filestorage"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
const (
ActionFilesRead = "files:read"
ActionFilesWrite = "files:write"
ActionFilesDelete = "files:delete"
)
var (
denyAllPathFilter = filestorage.NewDenyAllPathFilter()
allowAllPathFilter = filestorage.NewAllowAllPathFilter()
)
func isValidAction(action string) bool {
return action == ActionFilesRead || action == ActionFilesWrite || action == ActionFilesDelete
}
type storageAuthService interface {
newGuardian(ctx context.Context, user *models.SignedInUser, prefix string) fileGuardian
}
type fileGuardian interface {
canView(path string) bool
canWrite(path string) bool
canDelete(path string) bool
can(action string, path string) bool
getPathFilter(action string) filestorage.PathFilter
}
type pathFilterFileGuardian struct {
ctx context.Context
user *models.SignedInUser
prefix string
pathFilterByAction map[string]filestorage.PathFilter
log log.Logger
}
func (a *pathFilterFileGuardian) getPathFilter(action string) filestorage.PathFilter {
if !isValidAction(action) {
a.log.Warn("Unsupported action", "action", action)
return denyAllPathFilter
}
if filter, ok := a.pathFilterByAction[action]; ok {
return filter
}
return denyAllPathFilter
}
func (a *pathFilterFileGuardian) canWrite(path string) bool {
return a.can(ActionFilesWrite, path)
}
func (a *pathFilterFileGuardian) canView(path string) bool {
return a.can(ActionFilesRead, path)
}
func (a *pathFilterFileGuardian) canDelete(path string) bool {
return a.can(ActionFilesDelete, path)
}
func (a *pathFilterFileGuardian) can(action string, path string) bool {
if path == a.prefix {
path = filestorage.Delimiter
} else {
path = strings.TrimPrefix(path, a.prefix)
}
allow := false
if !isValidAction(action) {
a.log.Warn("Unsupported action", "action", action, "path", path)
return false
}
pathFilter, ok := a.pathFilterByAction[action]
if !ok {
a.log.Warn("Missing path filter", "action", action, "path", path)
return false
}
allow = pathFilter.IsAllowed(path)
if !allow {
a.log.Warn("denying", "action", action, "path", path)
}
return allow
}
type denyAllFileGuardian struct {
}
func (d denyAllFileGuardian) canView(path string) bool {
return d.can(ActionFilesRead, path)
}
func (d denyAllFileGuardian) canWrite(path string) bool {
return d.can(ActionFilesWrite, path)
}
func (d denyAllFileGuardian) canDelete(path string) bool {
return d.can(ActionFilesDelete, path)
}
func (d denyAllFileGuardian) can(action string, path string) bool {
return false
}
func (d denyAllFileGuardian) getPathFilter(action string) filestorage.PathFilter {
return denyAllPathFilter
}
+4 -1
View File
@@ -37,7 +37,7 @@ func ProvideHTTPService(store StorageService) HTTPStorageService {
func UploadErrorToStatusCode(err error) int {
switch {
case errors.Is(err, ErrUploadFeatureDisabled):
case errors.Is(err, ErrStorageNotFound):
return 404
case errors.Is(err, ErrUnsupportedStorage):
@@ -49,6 +49,9 @@ func UploadErrorToStatusCode(err error) int {
case errors.Is(err, ErrFileAlreadyExists):
return 400
case errors.Is(err, ErrAccessDenied):
return 403
default:
return 500
}
+80 -24
View File
@@ -3,7 +3,6 @@ package store
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
@@ -19,13 +18,16 @@ import (
var grafanaStorageLogger = log.New("grafanaStorageLogger")
var ErrUploadFeatureDisabled = errors.New("upload feature is disabled")
var ErrUnsupportedStorage = errors.New("storage does not support this operation")
var ErrUploadInternalError = errors.New("upload internal error")
var ErrValidationFailed = errors.New("request validation failed")
var ErrFileAlreadyExists = errors.New("file exists")
var ErrStorageNotFound = errors.New("storage not found")
var ErrAccessDenied = errors.New("access denied")
const RootPublicStatic = "public-static"
const RootResources = "resources"
const RootDevenv = "devenv"
const MAX_UPLOAD_SIZE = 1 * 1024 * 1024 // 3MB
@@ -66,9 +68,10 @@ type storageServiceConfig struct {
}
type standardStorageService struct {
sql *sqlstore.SQLStore
tree *nestedTree
cfg storageServiceConfig
sql *sqlstore.SQLStore
tree *nestedTree
cfg storageServiceConfig
authService storageAuthService
}
func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, cfg *setting.Cfg) StorageService {
@@ -90,7 +93,7 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles,
devenv := filepath.Join(cfg.StaticRootPath, "..", "devenv")
if _, err := os.Stat(devenv); !os.IsNotExist(err) {
// path/to/whatever exists
s := newDiskStorage("devenv", "Development Environment", &StorageLocalDiskConfig{
s := newDiskStorage(RootDevenv, "Development Environment", &StorageLocalDiskConfig{
Path: devenv,
Roots: []string{
"/dev-dashboards/",
@@ -104,7 +107,7 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles,
storages := make([]storageRuntime, 0)
if features.IsEnabled(featuremgmt.FlagStorageLocalUpload) {
storages = append(storages,
newSQLStorage("resources",
newSQLStorage(RootResources,
"Resources",
&StorageSQLConfig{orgId: orgId}, sql).
setBuiltin(true).
@@ -114,10 +117,39 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles,
return storages
}
return newStandardStorageService(sql, globalRoots, initializeOrgStorages)
authService := newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter {
if user == nil || !user.IsGrafanaAdmin {
return nil
}
switch storageName {
case RootPublicStatic:
return map[string]filestorage.PathFilter{
ActionFilesRead: allowAllPathFilter,
ActionFilesWrite: denyAllPathFilter,
ActionFilesDelete: denyAllPathFilter,
}
case RootDevenv:
return map[string]filestorage.PathFilter{
ActionFilesRead: allowAllPathFilter,
ActionFilesWrite: denyAllPathFilter,
ActionFilesDelete: denyAllPathFilter,
}
case RootResources:
return map[string]filestorage.PathFilter{
ActionFilesRead: allowAllPathFilter,
ActionFilesWrite: allowAllPathFilter,
ActionFilesDelete: allowAllPathFilter,
}
default:
return nil
}
})
return newStandardStorageService(sql, globalRoots, initializeOrgStorages, authService)
}
func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime) *standardStorageService {
func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime, authService storageAuthService) *standardStorageService {
rootsByOrgId := make(map[int64][]storageRuntime)
rootsByOrgId[ac.GlobalOrgID] = globalRoots
@@ -127,8 +159,9 @@ func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRunt
}
res.init()
return &standardStorageService{
sql: sql,
tree: res,
sql: sql,
tree: res,
authService: authService,
cfg: storageServiceConfig{
allowUnsanitizedSvgUpload: false,
},
@@ -149,12 +182,15 @@ func getOrgId(user *models.SignedInUser) int64 {
}
func (s *standardStorageService) List(ctx context.Context, user *models.SignedInUser, path string) (*StorageListFrame, error) {
// apply access control here
return s.tree.ListFolder(ctx, getOrgId(user), path)
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path))
return s.tree.ListFolder(ctx, getOrgId(user), path, guardian.getPathFilter(ActionFilesRead))
}
func (s *standardStorageService) Read(ctx context.Context, user *models.SignedInUser, path string) (*filestorage.File, error) {
// TODO: permission check!
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path))
if !guardian.canView(path) {
return nil, ErrAccessDenied
}
return s.tree.GetFile(ctx, getOrgId(user), path)
}
@@ -171,12 +207,17 @@ type UploadRequest struct {
}
func (s *standardStorageService) Upload(ctx context.Context, user *models.SignedInUser, req *UploadRequest) error {
upload, storagePath := s.tree.getRoot(getOrgId(user), req.Path)
if upload == nil {
return ErrUploadFeatureDisabled
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(req.Path))
if !guardian.canWrite(req.Path) {
return ErrAccessDenied
}
if upload.Meta().ReadOnly {
root, storagePath := s.tree.getRoot(getOrgId(user), req.Path)
if root == nil {
return ErrStorageNotFound
}
if root.Meta().ReadOnly {
return ErrUnsupportedStorage
}
@@ -195,7 +236,7 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed
grafanaStorageLogger.Info("uploading a file", "filetype", req.MimeType, "path", req.Path)
if !req.OverwriteExistingFile {
file, err := upload.Store().Get(ctx, storagePath)
file, err := root.Store().Get(ctx, storagePath)
if err != nil {
grafanaStorageLogger.Error("failed while checking file existence", "err", err, "path", req.Path)
return ErrUploadInternalError
@@ -206,7 +247,7 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed
}
}
if err := upload.Store().Upsert(ctx, upsertCommand); err != nil {
if err := root.Store().Upsert(ctx, upsertCommand); err != nil {
grafanaStorageLogger.Error("failed while uploading the file", "err", err, "path", req.Path)
return ErrUploadInternalError
}
@@ -215,9 +256,14 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed
}
func (s *standardStorageService) DeleteFolder(ctx context.Context, user *models.SignedInUser, cmd *DeleteFolderCmd) error {
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(cmd.Path))
if !guardian.canDelete(cmd.Path) {
return ErrAccessDenied
}
root, storagePath := s.tree.getRoot(getOrgId(user), cmd.Path)
if root == nil {
return fmt.Errorf("resources storage is not enabled")
return ErrStorageNotFound
}
if root.Meta().ReadOnly {
@@ -227,13 +273,18 @@ func (s *standardStorageService) DeleteFolder(ctx context.Context, user *models.
if storagePath == "" {
storagePath = filestorage.Delimiter
}
return root.Store().DeleteFolder(ctx, storagePath, &filestorage.DeleteFolderOptions{Force: true})
return root.Store().DeleteFolder(ctx, storagePath, &filestorage.DeleteFolderOptions{Force: true, AccessFilter: guardian.getPathFilter(ActionFilesDelete)})
}
func (s *standardStorageService) CreateFolder(ctx context.Context, user *models.SignedInUser, cmd *CreateFolderCmd) error {
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(cmd.Path))
if !guardian.canWrite(cmd.Path) {
return ErrAccessDenied
}
root, storagePath := s.tree.getRoot(getOrgId(user), cmd.Path)
if root == nil {
return fmt.Errorf("resources storage is not enabled")
return ErrStorageNotFound
}
if root.Meta().ReadOnly {
@@ -248,9 +299,14 @@ func (s *standardStorageService) CreateFolder(ctx context.Context, user *models.
}
func (s *standardStorageService) Delete(ctx context.Context, user *models.SignedInUser, path string) error {
guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path))
if !guardian.canDelete(path) {
return ErrAccessDenied
}
root, storagePath := s.tree.getRoot(getOrgId(user), path)
if root == nil {
return fmt.Errorf("resources storage is not enabled")
return ErrStorageNotFound
}
if root.Meta().ReadOnly {
+65 -25
View File
@@ -16,29 +16,41 @@ import (
)
var (
dummyUser = &models.SignedInUser{OrgId: 1}
dummyUser = &models.SignedInUser{OrgId: 1}
allowAllAuthService = newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter {
return map[string]filestorage.PathFilter{
ActionFilesDelete: allowAllPathFilter,
ActionFilesWrite: allowAllPathFilter,
ActionFilesRead: allowAllPathFilter,
}
})
denyAllAuthService = newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter {
return map[string]filestorage.PathFilter{
ActionFilesDelete: denyAllPathFilter,
ActionFilesWrite: denyAllPathFilter,
ActionFilesRead: denyAllPathFilter,
}
})
publicRoot, _ = filepath.Abs("../../../public")
publicStaticFilesStorage = newDiskStorage("public", "Public static files", &StorageLocalDiskConfig{
Path: publicRoot,
Roots: []string{
"/testdata/",
"/img/icons/",
"/img/bg/",
"/gazetteer/",
"/maps/",
"/upload/",
},
}).setReadOnly(true).setBuiltin(true)
)
func TestListFiles(t *testing.T) {
publicRoot, err := filepath.Abs("../../../public")
require.NoError(t, err)
roots := []storageRuntime{
newDiskStorage("public", "Public static files", &StorageLocalDiskConfig{
Path: publicRoot,
Roots: []string{
"/testdata/",
"/img/icons/",
"/img/bg/",
"/gazetteer/",
"/maps/",
"/upload/",
},
}).setReadOnly(true).setBuiltin(true),
}
roots := []storageRuntime{publicStaticFilesStorage}
store := newStandardStorageService(sqlstore.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
return make([]storageRuntime, 0)
})
}, allowAllAuthService)
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
require.NoError(t, err)
@@ -53,22 +65,38 @@ func TestListFiles(t *testing.T) {
experimental.CheckGoldenJSONFrame(t, "testdata", "public_testdata_js_libraries.golden", testDsFrame, true)
}
func setupUploadStore(t *testing.T) (StorageService, *filestorage.MockFileStorage, string) {
func TestListFilesWithoutPermissions(t *testing.T) {
roots := []storageRuntime{publicStaticFilesStorage}
store := newStandardStorageService(sqlstore.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
return make([]storageRuntime, 0)
}, denyAllAuthService)
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
require.NoError(t, err)
rowLen, err := frame.RowLen()
require.NoError(t, err)
require.Equal(t, 0, rowLen)
}
func setupUploadStore(t *testing.T, authService storageAuthService) (StorageService, *filestorage.MockFileStorage, string) {
t.Helper()
storageName := "resources"
mockStorage := &filestorage.MockFileStorage{}
sqlStorage := newSQLStorage(storageName, "Testing upload", &StorageSQLConfig{orgId: 1}, sqlstore.InitTestDB(t))
sqlStorage.store = mockStorage
if authService == nil {
authService = allowAllAuthService
}
store := newStandardStorageService(sqlstore.InitTestDB(t), []storageRuntime{sqlStorage}, func(orgId int64) []storageRuntime {
return make([]storageRuntime, 0)
})
}, authService)
return store, mockStorage, storageName
}
func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t)
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(nil, nil)
mockStorage.On("Upsert", mock.Anything, mock.Anything).Return(nil)
@@ -82,8 +110,20 @@ func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) {
require.NoError(t, err)
}
func TestShouldFailUploadWithoutAccess(t *testing.T) {
service, _, storageName := setupUploadStore(t, denyAllAuthService)
err := service.Upload(context.Background(), dummyUser, &UploadRequest{
EntityType: EntityTypeImage,
Contents: make([]byte, 0),
Path: storageName + "/myFile.jpg",
MimeType: "image/jpg",
})
require.ErrorIs(t, err, ErrAccessDenied)
}
func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t)
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(&filestorage.File{Contents: make([]byte, 0)}, nil)
@@ -97,7 +137,7 @@ func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) {
}
func TestShouldDelegateFileDeletion(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t)
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("Delete", mock.Anything, "/myFile.jpg").Return(nil)
@@ -106,7 +146,7 @@ func TestShouldDelegateFileDeletion(t *testing.T) {
}
func TestShouldDelegateFolderCreation(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t)
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("CreateFolder", mock.Anything, "/nestedFolder/mostNestedFolder").Return(nil)
@@ -115,9 +155,9 @@ func TestShouldDelegateFolderCreation(t *testing.T) {
}
func TestShouldDelegateFolderDeletion(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t)
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("DeleteFolder", mock.Anything, "/", &filestorage.DeleteFolderOptions{Force: true}).Return(nil)
mockStorage.On("DeleteFolder", mock.Anything, "/", mock.Anything).Return(nil)
err := service.DeleteFolder(context.Background(), dummyUser, &DeleteFolderCmd{
Path: storageName,
+41
View File
@@ -0,0 +1,41 @@
package store
import (
"context"
"github.com/grafana/grafana/pkg/infra/filestorage"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
type createPathFilterByAction func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter
func newStaticStorageAuthService(createPathFilterByAction createPathFilterByAction) storageAuthService {
return &staticStorageAuth{
denyAllFileGuardian: &denyAllFileGuardian{},
createPathFilterByAction: createPathFilterByAction,
log: log.New("staticStorageAuthService"),
}
}
type staticStorageAuth struct {
log log.Logger
denyAllFileGuardian fileGuardian
createPathFilterByAction createPathFilterByAction
}
func (a *staticStorageAuth) newGuardian(ctx context.Context, user *models.SignedInUser, storageName string) fileGuardian {
pathFilter := a.createPathFilterByAction(ctx, user, storageName)
if pathFilter == nil {
return a.denyAllFileGuardian
}
return &pathFilterFileGuardian{
ctx: ctx,
user: user,
log: a.log,
prefix: storageName,
pathFilterByAction: pathFilter,
}
}
+2 -1
View File
@@ -85,7 +85,7 @@ func (t *nestedTree) GetFile(ctx context.Context, orgId int64, path string) (*fi
return root.Store().Get(ctx, path)
}
func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string) (*StorageListFrame, error) {
func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error) {
if path == "" || path == "/" {
t.assureOrgIsInitialized(orgId)
@@ -150,6 +150,7 @@ func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string) (
Recursive: false,
WithFolders: true,
WithFiles: true,
Filter: accessFilter,
})
if err != nil {
+1 -1
View File
@@ -30,7 +30,7 @@ type WriteValueResponse struct {
type storageTree interface {
GetFile(ctx context.Context, orgId int64, path string) (*filestorage.File, error)
ListFolder(ctx context.Context, orgId int64, path string) (*StorageListFrame, error)
ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error)
}
//-------------------------------------------
+5
View File
@@ -28,3 +28,8 @@ func getPathAndScope(c *models.ReqContext) (string, string) {
}
return splitFirstSegment(path)
}
func getFirstSegment(path string) string {
firstSegment, _ := splitFirstSegment(path)
return firstSegment
}
-6
View File
@@ -10397,9 +10397,6 @@
"CreatePlaylistCommand": {
"type": "object",
"properties": {
"Result": {
"$ref": "#/definitions/Playlist"
},
"interval": {
"type": "string"
},
@@ -15896,9 +15893,6 @@
"UpdatePlaylistCommand": {
"type": "object",
"properties": {
"Result": {
"$ref": "#/definitions/PlaylistDTO"
},
"interval": {
"type": "string"
},
-6
View File
@@ -9505,9 +9505,6 @@
"CreatePlaylistCommand": {
"type": "object",
"properties": {
"Result": {
"$ref": "#/definitions/Playlist"
},
"interval": {
"type": "string"
},
@@ -12783,9 +12780,6 @@
"UpdatePlaylistCommand": {
"type": "object",
"properties": {
"Result": {
"$ref": "#/definitions/PlaylistDTO"
},
"interval": {
"type": "string"
},
@@ -1,6 +1,5 @@
import { css, cx } from '@emotion/css';
import React, { PropsWithChildren, useState } from 'react';
import { useToggle } from 'react-use';
import React, { PropsWithChildren } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
@@ -17,8 +16,6 @@ export interface Props extends PropsWithChildren<{}> {}
export function AppChrome({ children }: Props) {
const styles = useStyles2(getStyles);
const [searchBarHidden, toggleSearchBar] = useToggle(false); // repace with local storage
const [megaMenuOpen, setMegaMenuOpen] = useState(false);
const state = appChromeService.useState();
if (state.chromeless || !config.featureToggles.topnav) {
@@ -28,18 +25,20 @@ export function AppChrome({ children }: Props) {
return (
<main className="main-view">
<div className={styles.topNav}>
{!searchBarHidden && <TopSearchBar />}
{!state.searchBarHidden && <TopSearchBar />}
<NavToolbar
searchBarHidden={searchBarHidden}
searchBarHidden={state.searchBarHidden}
sectionNav={state.sectionNav}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={toggleSearchBar}
onToggleMegaMenu={() => setMegaMenuOpen(!megaMenuOpen)}
onToggleSearchBar={appChromeService.toggleSearchBar}
onToggleMegaMenu={appChromeService.toggleMegaMenu}
/>
</div>
<div className={cx(styles.content, searchBarHidden && styles.contentNoSearchBar)}>{children}</div>
{megaMenuOpen && <MegaMenu searchBarHidden={searchBarHidden} onClose={() => setMegaMenuOpen(false)} />}
<div className={cx(styles.content, state.searchBarHidden && styles.contentNoSearchBar)}>{children}</div>
{state.megaMenuOpen && (
<MegaMenu searchBarHidden={state.searchBarHidden} onClose={appChromeService.toggleMegaMenu} />
)}
</main>
);
}
@@ -2,6 +2,7 @@ import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs';
import { NavModelItem } from '@grafana/data';
import store from 'app/core/store';
import { isShallowEqual } from 'app/core/utils/isShallowEqual';
import { RouteDescriptor } from '../../navigation/types';
@@ -11,14 +12,19 @@ export interface AppChromeState {
sectionNav: NavModelItem;
pageNav?: NavModelItem;
actions?: React.ReactNode;
searchBarHidden?: boolean;
megaMenuOpen?: boolean;
}
const defaultSection: NavModelItem = { text: 'Grafana' };
export class AppChromeService {
searchBarStorageKey = 'SearchBar_Hidden';
readonly state = new BehaviorSubject<AppChromeState>({
chromeless: true, // start out hidden to not flash it on pages without chrome
sectionNav: defaultSection,
searchBarHidden: store.getBool(this.searchBarStorageKey, false),
});
routeMounted(route: RouteDescriptor) {
@@ -42,6 +48,16 @@ export class AppChromeService {
}
}
toggleMegaMenu = () => {
this.update({ megaMenuOpen: !this.state.getValue().megaMenuOpen });
};
toggleSearchBar = () => {
const searchBarHidden = !this.state.getValue().searchBarHidden;
store.set(this.searchBarStorageKey, searchBarHidden);
this.update({ searchBarHidden });
};
useState() {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useObservable(this.state, this.state.getValue());
@@ -143,10 +143,11 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone, updateWe
liveNow={dashboard.liveNow}
/>
{/* @todo: Update "Graph tooltip" description to remove prompt about reloading when resolving #46581 */}
<CollapsableSection label="Panel options" isOpen={true}>
<Field
label="Graph tooltip"
description="Controls tooltip and hover highlight behavior across different panels"
description="Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect"
>
<RadioButtonGroup onChange={onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={dashboard.graphTooltip} />
</Field>
@@ -24,7 +24,7 @@ import {
instanceSettings as expressionInstanceSettings,
} from 'app/features/expressions/ExpressionDatasource';
import { DataSourceVariableModel } from '../variables/types';
import { isDataSource } from '../variables/guard';
import { importDataSourcePlugin } from './plugin_loader';
@@ -245,11 +245,12 @@ export class DatasourceSrv implements DataSourceService {
});
if (filters.variables) {
for (const variable of this.templateSrv.getVariables().filter((variable) => variable.type === 'datasource')) {
const dsVar = variable as DataSourceVariableModel;
const first = dsVar.current.value === 'default' ? this.defaultName : dsVar.current.value;
const dsName = first as unknown as string;
const dsSettings = this.settingsMapByName[dsName];
for (const variable of this.templateSrv.getVariables()) {
if (!isDataSource(variable) || variable.multi || variable.includeAll) {
continue;
}
const dsName = variable.current.value === 'default' ? this.defaultName : variable.current.value;
const dsSettings = !Array.isArray(dsName) && this.settingsMapByName[dsName];
if (dsSettings) {
const key = `$\{${variable.name}\}`;
+5
View File
@@ -25,6 +25,7 @@ import {
VariableQueryEditorProps,
VariableWithMultiSupport,
VariableWithOptions,
DataSourceVariableModel,
} from './types';
export const isQuery = (model: VariableModel): model is QueryVariableModel => {
@@ -39,6 +40,10 @@ export const isConstant = (model: VariableModel): model is ConstantVariableModel
return model.type === 'constant';
};
export const isDataSource = (model: VariableModel): model is DataSourceVariableModel => {
return model.type === 'datasource';
};
export const isMulti = (model: VariableModel): model is VariableWithMultiSupport => {
const withMulti = model as VariableWithMultiSupport;
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean';
@@ -30,7 +30,17 @@ jest.mock('../language_provider', () => {
});
});
const mockQuery = {
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => ({
replace: jest.fn(),
containsTemplate: (val: string): boolean => {
return val.includes('$');
},
}),
}));
let mockQuery = {
refId: 'A',
queryType: 'nativeSearch',
key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0',
@@ -117,7 +127,38 @@ describe('NativeSearch', () => {
expect(option).toBeDefined();
await user.type(select, 'a');
option = await screen.findByText('No options found');
option = await screen.findByText('Hit enter to add');
expect(option).toBeDefined();
});
it('should add variable to select menu options', async () => {
mockQuery = {
...mockQuery,
refId: '121314',
serviceName: '$service',
spanName: '$span',
};
render(
<NativeSearch datasource={{} as TempoDatasource} query={mockQuery} onChange={() => {}} onRunQuery={() => {}} />
);
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
expect(asyncServiceSelect).toBeInTheDocument();
await user.click(asyncServiceSelect);
jest.advanceTimersByTime(3000);
await user.type(asyncServiceSelect, '$');
var serviceOption = await screen.findByText('$service');
expect(serviceOption).toBeDefined();
const asyncSpanSelect = screen.getByRole('combobox', { name: 'select-span-name' });
expect(asyncSpanSelect).toBeInTheDocument();
await user.click(asyncSpanSelect);
jest.advanceTimersByTime(3000);
await user.type(asyncSpanSelect, '$');
var operationOption = await screen.findByText('$span');
expect(operationOption).toBeDefined();
});
});
@@ -3,7 +3,7 @@ import Prism from 'prismjs';
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { Node } from 'slate';
import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data';
import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data';
import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime';
import {
InlineFieldRow,
@@ -90,7 +90,13 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
const fetchOptions = async () => {
try {
const [services, spans] = await Promise.all([loadOptions('serviceName'), loadOptions('spanName')]);
if (query.serviceName && getTemplateSrv().containsTemplate(query.serviceName)) {
services.push(toOption(query.serviceName));
}
setServiceOptions(services);
if (query.spanName && getTemplateSrv().containsTemplate(query.spanName)) {
spans.push(toOption(query.spanName));
}
setSpanOptions(spans);
} catch (error) {
// Display message if Tempo is connected but search 404's
@@ -102,7 +108,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
}
};
fetchOptions();
}, [languageProvider, loadOptions]);
}, [languageProvider, loadOptions, query.serviceName, query.spanName]);
useEffect(() => {
const fetchTags = async () => {
@@ -161,6 +167,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
isClearable
onKeyDown={onKeyDown}
aria-label={'select-service-name'}
allowCustomValue={true}
/>
</InlineField>
</InlineFieldRow>
@@ -184,6 +191,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
isClearable
onKeyDown={onKeyDown}
aria-label={'select-span-name'}
allowCustomValue={true}
/>
</InlineField>
</InlineFieldRow>
@@ -77,9 +77,11 @@ describe('Tempo data source', () => {
const queries = ds.interpolateVariablesInQueries([getQuery()], {
interpolationVar: { text: text, value: text },
});
expect(templateSrv.replace).toBeCalledTimes(5);
expect(templateSrv.replace).toBeCalledTimes(7);
expect(queries[0].linkedQuery?.expr).toBe(text);
expect(queries[0].query).toBe(text);
expect(queries[0].serviceName).toBe(text);
expect(queries[0].spanName).toBe(text);
expect(queries[0].search).toBe(text);
expect(queries[0].minDuration).toBe(text);
expect(queries[0].maxDuration).toBe(text);
@@ -94,9 +96,11 @@ describe('Tempo data source', () => {
const resp = ds.applyTemplateVariables(getQuery(), {
interpolationVar: { text: text, value: text },
});
expect(templateSrv.replace).toBeCalledTimes(5);
expect(templateSrv.replace).toBeCalledTimes(7);
expect(resp.linkedQuery?.expr).toBe(text);
expect(resp.query).toBe(text);
expect(resp.serviceName).toBe(text);
expect(resp.spanName).toBe(text);
expect(resp.search).toBe(text);
expect(resp.minDuration).toBe(text);
expect(resp.maxDuration).toBe(text);
@@ -296,6 +296,8 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
return {
...expandedQuery,
query: this.templateSrv.replace(query.query ?? '', scopedVars),
serviceName: this.templateSrv.replace(query.serviceName ?? '', scopedVars),
spanName: this.templateSrv.replace(query.spanName ?? '', scopedVars),
search: this.templateSrv.replace(query.search ?? '', scopedVars),
minDuration: this.templateSrv.replace(query.minDuration ?? '', scopedVars),
maxDuration: this.templateSrv.replace(query.maxDuration ?? '', scopedVars),
@@ -2,6 +2,7 @@ import { lastValueFrom, of } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { DataSourceInstanceSettings, FieldType } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { ZipkinDatasource } from './datasource';
@@ -15,16 +16,24 @@ jest.mock('@grafana/runtime', () => ({
describe('ZipkinDatasource', () => {
describe('query', () => {
const templateSrv: TemplateSrv = {
replace: jest.fn(),
getVariables: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
it('runs query', async () => {
setupBackendSrv(zipkinResponse);
const ds = new ZipkinDatasource(defaultSettings);
const ds = new ZipkinDatasource(defaultSettings, templateSrv);
await expect(ds.query({ targets: [{ query: '12345' }] } as any)).toEmitValuesWith((val) => {
expect(val[0].data[0].fields).toMatchObject(traceFrameFields);
});
});
it('runs query with traceId that includes special characters', async () => {
setupBackendSrv(zipkinResponse);
const ds = new ZipkinDatasource(defaultSettings);
const ds = new ZipkinDatasource(defaultSettings, templateSrv);
await expect(ds.query({ targets: [{ query: 'a/b' }] } as any)).toEmitValuesWith((val) => {
expect(val[0].data[0].fields).toMatchObject(traceFrameFields);
});
@@ -9,8 +9,9 @@ import {
DataSourceJsonData,
FieldType,
MutableDataFrame,
ScopedVars,
} from '@grafana/data';
import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime';
import { BackendSrvRequest, FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components';
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
@@ -29,7 +30,10 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery, ZipkinJsonData>
uploadedJson: string | ArrayBuffer | null = null;
nodeGraph?: NodeGraphOptions;
spanBar?: SpanBarOptions;
constructor(private instanceSettings: DataSourceInstanceSettings<ZipkinJsonData>) {
constructor(
private instanceSettings: DataSourceInstanceSettings<ZipkinJsonData>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
}
@@ -50,7 +54,8 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery, ZipkinJsonData>
}
if (target.query) {
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe(
const query = this.applyVariables(target, options.scopedVars);
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe(
map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled))
);
}
@@ -71,6 +76,29 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery, ZipkinJsonData>
return query.query;
}
interpolateVariablesInQueries(queries: ZipkinQuery[], scopedVars: ScopedVars): ZipkinQuery[] {
if (!queries || queries.length === 0) {
return [];
}
return queries.map((query) => {
return {
...query,
datasource: this.getRef(),
...this.applyVariables(query, scopedVars),
};
});
}
applyVariables(query: ZipkinQuery, scopedVars: ScopedVars) {
const expandedQuery = { ...query };
return {
...expandedQuery,
query: this.templateSrv.replace(query.query ?? '', scopedVars),
};
}
private request<T = any>(
apiUrl: string,
data?: any,