Merge branch 'main' into gio/feat/create-correlation-api
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
+9
-6
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-5
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+29
-29
@@ -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)
|
||||
}
|
||||
+8
-8
@@ -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)
|
||||
}
|
||||
+16
-17
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
//-------------------------------------------
|
||||
|
||||
@@ -28,3 +28,8 @@ func getPathAndScope(c *models.ReqContext) (string, string) {
|
||||
}
|
||||
return splitFirstSegment(path)
|
||||
}
|
||||
|
||||
func getFirstSegment(path string) string {
|
||||
firstSegment, _ := splitFirstSegment(path)
|
||||
return firstSegment
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}\}`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user