From 8c6a5f043a71e18269f8f7a5e8c8b6507d432d4e Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Thu, 3 Feb 2022 09:59:02 +0100 Subject: [PATCH 01/34] [docs] Clarify legacy alerting deprecation (#44759) * Clarify legacy alerting deprecation * Lint Markdown --- docs/sources/alerting/_index.md | 9 +++++---- docs/sources/alerting/old-alerting/_index.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index ac3a4398e98..d94d09cda63 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -15,10 +15,11 @@ Grafana 8.0 introduced new and improved alerting that centralizes alerting infor Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature. -| Release | Cloud | Enterprise | OSS | -| ----------- | ------------- | ---------- | -------------------------------- | -| Grafana 8.2 | On by default | Opt-in | Opt-in | -| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Release | Cloud | Enterprise | OSS | +| ------------------------ | ------------- | ------------- | -------------------------------- | +| Grafana 8.2 | On by default | Opt-in | Opt-in | +| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Grafana 9.0 (unreleased) | On by default | On by default | On by default | > **Note:** New installs include existing installs which do not have any alerts configured. diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/alerting/old-alerting/_index.md index 32695e28573..8d0cd673290 100644 --- a/docs/sources/alerting/old-alerting/_index.md +++ b/docs/sources/alerting/old-alerting/_index.md @@ -7,7 +7,7 @@ weight = 114 Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in.md" >}}) feature. -> **Note**: Legacy dashboard alerts is deprecated and will be removed in a future release. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. +> **Note**: Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. Legacy dashboard alerts have two main components: From f38f10416a424843acdff65bd6d31ba17a5d9243 Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Thu, 3 Feb 2022 09:59:26 +0100 Subject: [PATCH 02/34] Revert fixed roles and service accounts (#44778) * Revert fixed roles and service accounts * Leave the fixed role for service accounts --- pkg/api/api.go | 9 ++- pkg/services/serviceaccounts/manager/roles.go | 60 ------------------- .../serviceaccounts/manager/service.go | 6 +- 3 files changed, 8 insertions(+), 67 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index e1aff6d4591..884ef9f712e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,7 +13,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" "github.com/grafana/grafana/pkg/services/featuremgmt" - sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" ) var plog = log.New("api") @@ -256,10 +255,10 @@ func (hs *HTTPServer) registerRoutes() { // auth api keys apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) { - keysRoute.Get("/", authorize(reqOrgAdmin, sa.ActionApikeyListEv), routing.Wrap(GetAPIKeys)) - keysRoute.Post("/", authorize(reqOrgAdmin, sa.ActionApikeyAddEv), quota("api_key"), routing.Wrap(hs.AddAPIKey)) - keysRoute.Post("/additional", authorize(reqOrgAdmin, sa.ActionApikeyAddAdditionalEv), quota("api_key"), routing.Wrap(hs.AdditionalAPIKey)) - keysRoute.Delete("/:id", authorize(reqOrgAdmin, sa.ActionApikeyRemoveEv), routing.Wrap(DeleteAPIKey)) + keysRoute.Get("/", routing.Wrap(GetAPIKeys)) + keysRoute.Post("/", quota("api_key"), routing.Wrap(hs.AddAPIKey)) + keysRoute.Post("/additional", quota("api_key"), routing.Wrap(hs.AdditionalAPIKey)) + keysRoute.Delete("/:id", routing.Wrap(DeleteAPIKey)) }, reqOrgAdmin) // Preferences diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go index e8038241322..a51f6c63bd3 100644 --- a/pkg/services/serviceaccounts/manager/roles.go +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -5,22 +5,6 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" ) -var ( - ActionApikeyList = "apikey:list" - ActionApikeyAdd = "apikey:add" - ActionApikeyRemove = "apikey:remove" - ActionApikeyAddAdditional = "apikey:addadditional" - - apikeyWriter = "fixed:apikey:writer" - apikeyReader = "fixed:apikey:reader" - - //API key actions - ActionApikeyListEv = accesscontrol.EvalPermission(ActionApikeyList) - ActionApikeyAddEv = accesscontrol.EvalPermission(ActionApikeyAdd) - ActionApikeyRemoveEv = accesscontrol.EvalPermission(ActionApikeyRemove) //Improvement:Check here or in database layer that user has permissiono modify the service account attached to this api key - ActionApikeyAddAdditionalEv = accesscontrol.EvalPermission(ActionApikeyAddAdditional) -) - func RegisterRoles(ac accesscontrol.AccessControl) error { role := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ @@ -50,49 +34,5 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { return err } - apikeyAdminReadRole := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Version: 1, - Name: apikeyReader, - DisplayName: "Apikeys reader", - Description: "Gives access to list apikeys.", - Group: "Service accounts", - Permissions: []accesscontrol.Permission{ - { - Action: ActionApikeyList, - Scope: accesscontrol.ScopeUsersAll, - }, - }, - }, - Grants: []string{"Admin"}, - } - if err := ac.DeclareFixedRoles(apikeyAdminReadRole); err != nil { - return err - } - - apikeyAdminEditRole := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Version: 1, - Name: apikeyWriter, - DisplayName: "Apikeys writer", - Description: "Gives access to add and delete api keys.", - Group: "Service accounts", - Permissions: accesscontrol.ConcatPermissions(apikeyAdminReadRole.Role.Permissions, []accesscontrol.Permission{ - { - Action: ActionApikeyAdd, - Scope: accesscontrol.ScopeUsersAll, - }, - { - Action: ActionApikeyRemove, - Scope: accesscontrol.ScopeUsersAll, - }, - }), - }, - Grants: []string{"Admin"}, - } - if err := ac.DeclareFixedRoles(apikeyAdminEditRole); err != nil { - return err - } - return nil } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 747a3ee3c2e..447093b3266 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -36,8 +36,10 @@ func ProvideServiceAccountsService( log: log.New("serviceaccounts"), } - if err := RegisterRoles(ac); err != nil { - s.log.Error("Failed to register roles", "error", err) + if features.IsEnabled(featuremgmt.FlagServiceAccounts) { + if err := RegisterRoles(ac); err != nil { + s.log.Error("Failed to register roles", "error", err) + } } serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store) From 3314178a0afce7860466cde66737a227c820f87e Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Thu, 3 Feb 2022 10:13:19 +0100 Subject: [PATCH 03/34] grafana/ui: Fix RelativeTimeRange supported formats (#44535) * remove link to docs site * extract tooltip to component * text and formatting * use div instead of p --- .../RelativeTimeRangePicker.tsx | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index 939ac33c704..02f7945ceb5 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -18,6 +18,7 @@ import { import { Field } from '../../Forms/Field'; import { getInputStyles, Input } from '../../Input/Input'; import { Icon } from '../../Icon/Icon'; +import { Tooltip } from '../../Tooltip/Tooltip'; /** * @internal @@ -115,14 +116,13 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
- Specify time range -
- Specify a relative time range, for more information see{' '} - - docs - - . -
+ + } placement="bottom" theme="info"> +
+ Specify time range +
+
+
{ + const styles = useStyles2(toolTipStyles); + return ( + <> +
+ Supported formats: now-[digit]s/m/h/d/w +
+
Example: to select a time range from 10 minutes ago to now
+ From: now-10m To: now +
+ For more information see{' '} + + docs + + . +
+ + ); +}; + +const toolTipStyles = (theme: GrafanaTheme2) => ({ + supported: css` + margin-bottom: ${theme.spacing(1)}; + `, + tooltip: css` + margin: 0; + `, + link: css` + margin-top: ${theme.spacing(1)}; + `, +}); + const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme2) => { const inputStyles = getInputStyles({ theme, invalid: false }); const bodyMinimumHeight = 250; From f36ed878e982648bf499def12befaec9609bdc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 3 Feb 2022 10:25:37 +0100 Subject: [PATCH 04/34] Loki: add helper function to handle instant/range queries (#44785) * loki: add helper function to handle range/instant queries * improved comment Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- .../datasource/loki/query_utils.test.ts | 39 ++++++++++++++++++- .../plugins/datasource/loki/query_utils.ts | 24 ++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/query_utils.test.ts b/public/app/plugins/datasource/loki/query_utils.test.ts index d69692462f6..4e087955488 100644 --- a/public/app/plugins/datasource/loki/query_utils.test.ts +++ b/public/app/plugins/datasource/loki/query_utils.test.ts @@ -1,4 +1,5 @@ -import { getHighlighterExpressionsFromQuery } from './query_utils'; +import { LokiQuery, LokiQueryType } from './types'; +import { getHighlighterExpressionsFromQuery, getNormalizedLokiQuery } from './query_utils'; describe('getHighlighterExpressionsFromQuery', () => { it('returns no expressions for empty query', () => { @@ -61,3 +62,39 @@ describe('getHighlighterExpressionsFromQuery', () => { expect(getHighlighterExpressionsFromQuery('{foo="bar"} |~ `\\w+`')).toEqual(['\\w+']); }); }); + +describe('getNormalizedLokiQuery', () => { + function expectNormalized(inputProps: Object, outputQueryType: LokiQueryType) { + const input: LokiQuery = { refId: 'A', expr: 'test1', ...inputProps }; + const output = getNormalizedLokiQuery(input); + expect(output).toStrictEqual({ refId: 'A', expr: 'test1', queryType: outputQueryType }); + } + + it('handles no props case', () => { + expectNormalized({}, LokiQueryType.Range); + }); + + it('handles old-style instant case', () => { + expectNormalized({ instant: true, range: false }, LokiQueryType.Instant); + }); + + it('handles old-style range case', () => { + expectNormalized({ instant: false, range: true }, LokiQueryType.Range); + }); + + it('handles new+old style instant', () => { + expectNormalized({ instant: true, range: false, queryType: LokiQueryType.Range }, LokiQueryType.Range); + }); + + it('handles new+old style range', () => { + expectNormalized({ instant: false, range: true, queryType: LokiQueryType.Instant }, LokiQueryType.Instant); + }); + + it('handles new<>old conflict (new wins), range', () => { + expectNormalized({ instant: false, range: true, queryType: LokiQueryType.Range }, LokiQueryType.Range); + }); + + it('handles new<>old conflict (new wins), instant', () => { + expectNormalized({ instant: true, range: false, queryType: LokiQueryType.Instant }, LokiQueryType.Instant); + }); +}); diff --git a/public/app/plugins/datasource/loki/query_utils.ts b/public/app/plugins/datasource/loki/query_utils.ts index 7c2d2244c93..d9c9393fe5e 100644 --- a/public/app/plugins/datasource/loki/query_utils.ts +++ b/public/app/plugins/datasource/loki/query_utils.ts @@ -1,5 +1,6 @@ import { escapeRegExp } from 'lodash'; import { PIPE_PARSERS } from './syntax'; +import { LokiQuery, LokiQueryType } from './types'; export function formatQuery(selector: string | undefined): string { return `${selector || ''}`.trim(); @@ -71,3 +72,26 @@ export function queryHasPipeParser(expr: string): boolean { export function addParsedLabelToQuery(expr: string, key: string, value: string | number, operator: string) { return expr + ` | ${key}${operator}"${value.toString()}"`; } + +// we are migrating from `.instant` and `.range` to `.queryType` +// this function returns a new query object that: +// - has `.queryType` +// - does not have `.instant` +// - does not have `.range` +export function getNormalizedLokiQuery(query: LokiQuery): LokiQuery { + // if queryType exists, it is respected + if (query.queryType !== undefined) { + const { instant, range, ...rest } = query; + return rest; + } + + // if no queryType, and instant===true, it's instant + if (query.instant === true) { + const { instant, range, ...rest } = query; + return { ...rest, queryType: LokiQueryType.Instant }; + } + + // otherwise it is range + const { instant, range, ...rest } = query; + return { ...rest, queryType: LokiQueryType.Range }; +} From f582e6c86a02eb1ae140969047436d3e6cb2db32 Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Thu, 3 Feb 2022 04:33:46 -0500 Subject: [PATCH 05/34] Chore: Remove bus from password (#44482) * Chore: Remove bus from password * Refactor: Remove bus from password.go and adjust tests * remove sqlstore dependency from notifications * Chore: Remove bus from password * Refactor: Remove bus from password.go and adjust tests * remove sqlstore dependency (again) * remove fmt printf * fix dependencies in http server * fix renamed method in tests Co-authored-by: Serge Zaitsev --- pkg/api/api.go | 4 ++-- pkg/api/http_server.go | 6 ++++- pkg/api/password.go | 20 +++++++++++------ pkg/services/notifications/codes.go | 1 - pkg/services/notifications/notifications.go | 22 ++++++++++++------- .../notifications/notifications_test.go | 20 ++++++++--------- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 884ef9f712e..497251aafa1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -126,8 +126,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/user/password/send-reset-email", reqNotSignedIn, hs.Index) r.Get("/user/password/reset", hs.Index) - r.Post("/api/user/password/send-reset-email", routing.Wrap(SendResetPasswordEmail)) - r.Post("/api/user/password/reset", routing.Wrap(ResetPassword)) + r.Post("/api/user/password/send-reset-email", routing.Wrap(hs.SendResetPasswordEmail)) + r.Post("/api/user/password/reset", routing.Wrap(hs.ResetPassword)) // dashboard snapshots r.Get("/dashboard/snapshot/*", reqNoAuth, hs.Index) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 3c84c2bf73f..c9e52e6dbe5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -46,6 +46,7 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" @@ -129,6 +130,7 @@ type HTTPServer struct { serviceAccountsService serviceaccounts.Service authInfoService authinfoservice.Service TeamPermissionsService *resourcepermissions.Service + NotificationService *notifications.NotificationService } type ServerOptions struct { @@ -155,7 +157,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service, dataSourcesService *datasources.Service, secretsService secrets.Service, queryDataService *query.Service, ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service, - authInfoService authinfoservice.Service, resourcePermissionServices *resourceservices.ResourceServices) (*HTTPServer, error) { + authInfoService authinfoservice.Service, resourcePermissionServices *resourceservices.ResourceServices, + notificationService *notifications.NotificationService) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -215,6 +218,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi serviceAccountsService: serviceaccountsService, authInfoService: authInfoService, TeamPermissionsService: resourcePermissionServices.GetTeamService(), + NotificationService: notificationService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/password.go b/pkg/api/password.go index 04c09a7470b..487c1e1c33d 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -1,19 +1,19 @@ package api import ( + "context" "errors" "net/http" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func SendResetPasswordEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Response { form := dtos.SendResetPasswordEmailForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -27,27 +27,33 @@ func SendResetPasswordEmail(c *models.ReqContext) response.Response { userQuery := models.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} - if err := bus.Dispatch(c.Req.Context(), &userQuery); err != nil { + if err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery); err != nil { c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail) return response.Error(200, "Email sent", err) } emailCmd := models.SendResetPasswordEmailCommand{User: userQuery.Result} - if err := bus.Dispatch(c.Req.Context(), &emailCmd); err != nil { + if err := hs.NotificationService.SendResetPasswordEmail(c.Req.Context(), &emailCmd); err != nil { return response.Error(500, "Failed to send email", err) } return response.Success("Email sent") } -func ResetPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { form := dtos.ResetUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } query := models.ValidateResetPasswordCodeQuery{Code: form.Code} - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + getUserByLogin := func(ctx context.Context, login string) (*models.User, error) { + userQuery := models.GetUserByLoginQuery{LoginOrEmail: login} + err := hs.SQLStore.GetUserByLogin(ctx, &userQuery) + return userQuery.Result, err + } + + if err := hs.NotificationService.ValidateResetPasswordCode(c.Req.Context(), &query, getUserByLogin); err != nil { if errors.Is(err, models.ErrInvalidEmailCode) { return response.Error(400, "Invalid or expired reset password code", nil) } @@ -66,7 +72,7 @@ func ResetPassword(c *models.ReqContext) response.Response { return response.Error(500, "Failed to encode password", err) } - if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil { + if err := hs.SQLStore.ChangeUserPassword(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to change user password", err) } diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go index 4f95316d322..32cd5dd7cd1 100644 --- a/pkg/services/notifications/codes.go +++ b/pkg/services/notifications/codes.go @@ -70,7 +70,6 @@ func validateUserEmailCode(cfg *setting.Cfg, user *models.User, code string) (bo if err != nil { return false, err } - fmt.Printf("code : %s\ncode2: %s", retCode, code) if retCode == code && minutes > 0 { // check time is expired or not before, _ := time.ParseInLocation("200601021504", start, time.Local) diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index eae202d6a04..3ed26249234 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -29,6 +29,10 @@ type Service interface { EmailSender } +type Store interface { + GetUserByLogin(context.Context, *models.GetUserByLoginQuery) error +} + var mailTemplates *template.Template var tmplResetPassword = "reset_password" var tmplSignUpStarted = "signup_started" @@ -44,8 +48,8 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer) (*Notification mailer: mailer, } - ns.Bus.AddHandler(ns.sendResetPasswordEmail) - ns.Bus.AddHandler(ns.validateResetPasswordCode) + ns.Bus.AddHandler(ns.SendResetPasswordEmail) + ns.Bus.AddHandler(ns.ValidateResetPasswordCode) ns.Bus.AddHandler(ns.SendEmailCommandHandler) ns.Bus.AddHandler(ns.SendEmailCommandHandlerSync) @@ -163,7 +167,7 @@ func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd return nil } -func (ns *NotificationService) sendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { +func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { code, err := createUserEmailCode(ns.Cfg, cmd.User, nil) if err != nil { return err @@ -178,18 +182,20 @@ func (ns *NotificationService) sendResetPasswordEmail(ctx context.Context, cmd * }) } -func (ns *NotificationService) validateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery) error { +type GetUserByLoginFunc = func(c context.Context, login string) (*models.User, error) + +func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { login := getLoginForEmailCode(query.Code) if login == "" { return models.ErrInvalidEmailCode } - userQuery := models.GetUserByLoginQuery{LoginOrEmail: login} - if err := bus.Dispatch(ctx, &userQuery); err != nil { + user, err := userByLogin(ctx, login) + if err != nil { return err } - validEmailCode, err := validateUserEmailCode(ns.Cfg, userQuery.Result, query.Code) + validEmailCode, err := validateUserEmailCode(ns.Cfg, user, query.Code) if err != nil { return err } @@ -197,7 +203,7 @@ func (ns *NotificationService) validateResetPasswordCode(ctx context.Context, qu return models.ErrInvalidEmailCode } - query.Result = userQuery.Result + query.Result = user return nil } diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 3e40661dfab..ac5e1551ae0 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -17,7 +17,7 @@ func TestProvideService(t *testing.T) { t.Run("When invalid from_address in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.FromAddress = "@notanemail@" - _, _, err := createSutWithConfig(bus, cfg) + _, _, err := createSutWithConfig(t, bus, cfg) require.Error(t, err) }) @@ -25,7 +25,7 @@ func TestProvideService(t *testing.T) { t.Run("When template_patterns fails to parse", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.TemplatesPatterns = append(cfg.Smtp.TemplatesPatterns, "/usr/not-a-dir/**") - _, _, err := createSutWithConfig(bus, cfg) + _, _, err := createSutWithConfig(t, bus, cfg) require.Error(t, err) }) @@ -119,7 +119,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -139,7 +139,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -178,7 +178,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When sending reset email password", func(t *testing.T) { sut, _ := createSut(t, bus) - err := sut.sendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}}) + err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}}) require.NoError(t, err) sentMsg := <-sut.mailQueue @@ -192,7 +192,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false - _, mailer, err := createSutWithConfig(bus, cfg) + ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommand{ Subject: "subject", @@ -201,7 +201,7 @@ func TestSendEmailAsync(t *testing.T) { Template: "welcome_on_signup", } - err = bus.Dispatch(context.Background(), cmd) + err = ns.SendEmailCommandHandler(context.Background(), cmd) require.ErrorIs(t, err, models.ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) @@ -210,7 +210,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommand{ Subject: "subject", @@ -245,12 +245,12 @@ func createSut(t *testing.T, bus bus.Bus) (*NotificationService, *FakeMailer) { t.Helper() cfg := createSmtpConfig() - ns, fm, err := createSutWithConfig(bus, cfg) + ns, fm, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) return ns, fm } -func createSutWithConfig(bus bus.Bus, cfg *setting.Cfg) (*NotificationService, *FakeMailer, error) { +func createSutWithConfig(t *testing.T, bus bus.Bus, cfg *setting.Cfg) (*NotificationService, *FakeMailer, error) { smtp := NewFakeMailer() ns, err := ProvideService(bus, cfg, smtp) return ns, smtp, err From c23bc1e7b79d01a1c13d3e66ffa88ebad3ef6be6 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 3 Feb 2022 11:40:19 +0100 Subject: [PATCH 06/34] Prometheus: Show variable options in query builder (#44784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prometheus: Show variable options * Remove lint error * Fix test for CodeQL * Update public/app/plugins/datasource/prometheus/datasource.ts Co-authored-by: Torkel Ödegaard * Update public/app/plugins/datasource/loki/datasource.ts Co-authored-by: Torkel Ödegaard Co-authored-by: Torkel Ödegaard --- .../LokiExploreQueryEditor.test.tsx.snap | 1 + .../app/plugins/datasource/loki/datasource.ts | 8 ++++ .../datasource/loki/language_provider.test.ts | 21 ++++++++++ .../datasource/loki/language_provider.ts | 10 +++-- public/app/plugins/datasource/loki/mocks.ts | 1 + .../components/LokiQueryBuilder.tsx | 18 +++++++-- .../datasource/prometheus/datasource.ts | 8 ++++ .../prometheus/language_provider.test.ts | 40 +++++++++++++++++++ .../prometheus/language_provider.ts | 7 ++-- .../querybuilder/components/MetricSelect.tsx | 10 +---- .../components/PromQueryBuilder.test.tsx | 37 ++++++++++++----- .../components/PromQueryBuilder.tsx | 25 +++++++++--- .../querybuilder/shared/LabelFilterItem.tsx | 8 ++-- .../querybuilder/shared/LabelFilters.test.tsx | 12 +++++- .../querybuilder/shared/LabelFilters.tsx | 5 ++- 15 files changed, 168 insertions(+), 43 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap index c7c3761cb88..7a4bb2db700 100644 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap @@ -54,6 +54,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = ` datasource={ Object { "getTimeRangeParams": [Function], + "interpolateString": [Function], "languageProvider": LokiLanguageProvider { "cleanText": [Function], "datasource": [Circular], diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index cc9aebcfeda..9c14d7a87a0 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -754,6 +754,14 @@ export class LokiDatasource return addLabelToQuery(queryExpr, key, value, operator, true); } } + + interpolateString(string: string) { + return this.templateSrv.replace(string, undefined, this.interpolateQueryExpr); + } + + getVariables(): string[] { + return this.templateSrv.getVariables().map((v) => `$${v.name}`); + } } export function lokiRegularEscape(value: any) { diff --git a/public/app/plugins/datasource/loki/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts index 70fdfd10f8f..61f664bb448 100644 --- a/public/app/plugins/datasource/loki/language_provider.test.ts +++ b/public/app/plugins/datasource/loki/language_provider.test.ts @@ -103,6 +103,27 @@ describe('Language completion provider', () => { }); }); + describe('fetchSeriesLabels', () => { + it('should interpolate variable in series', () => { + const datasource: LokiDatasource = { + metadataRequest: () => ({ data: { data: [] as any[] } }), + getTimeRangeParams: () => ({ start: 0, end: 1 }), + interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'), + } as any as LokiDatasource; + + const languageProvider = new LanguageProvider(datasource); + const fetchSeriesLabels = languageProvider.fetchSeriesLabels; + const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue([]); + fetchSeriesLabels('$stream'); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy).toHaveBeenCalledWith('/loki/api/v1/series', { + end: 1, + 'match[]': 'interpolated-stream', + start: 0, + }); + }); + }); + describe('label key suggestions', () => { it('returns all label suggestions on empty selector', async () => { const datasource = makeMockLokiDatasource({ label1: [], label2: [] }); diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index fce76be1f9b..80bf63afd0d 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -396,15 +396,16 @@ export default class LokiLanguageProvider extends LanguageProvider { * @param name */ fetchSeriesLabels = async (match: string): Promise> => { + const interpolatedMatch = this.datasource.interpolateString(match); const url = '/loki/api/v1/series'; const { start, end } = this.datasource.getTimeRangeParams(); - const cacheKey = this.generateCacheKey(url, start, end, match); + const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch); let value = this.seriesCache.get(cacheKey); if (!value) { // Clear value when requesting new one. Empty object being truthy also makes sure we don't request twice. this.seriesCache.set(cacheKey, {}); - const params = { 'match[]': match, start, end }; + const params = { 'match[]': interpolatedMatch, start, end }; const data = await this.request(url, params); const { values } = processLabels(data); value = values; @@ -442,11 +443,12 @@ export default class LokiLanguageProvider extends LanguageProvider { } async fetchLabelValues(key: string): Promise { - const url = `/loki/api/v1/label/${key}/values`; + const interpolatedKey = this.datasource.interpolateString(key); + const url = `/loki/api/v1/label/${interpolatedKey}/values`; const rangeParams = this.datasource.getTimeRangeParams(); const { start, end } = rangeParams; - const cacheKey = this.generateCacheKey(url, start, end, key); + const cacheKey = this.generateCacheKey(url, start, end, interpolatedKey); const params = { start, end }; let labelValues = this.labelsCache.get(cacheKey); diff --git a/public/app/plugins/datasource/loki/mocks.ts b/public/app/plugins/datasource/loki/mocks.ts index d2849697227..35d37f6e0ef 100644 --- a/public/app/plugins/datasource/loki/mocks.ts +++ b/public/app/plugins/datasource/loki/mocks.ts @@ -43,6 +43,7 @@ export function makeMockLokiDatasource(labelsAndValues: Labels, series?: SeriesF } } }, + interpolateString: (string: string) => string, } as any; } diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 104c84847e4..27f9b722ee8 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -5,7 +5,7 @@ import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/sha import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList'; import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { lokiQueryModeller } from '../LokiQueryModeller'; -import { DataSourceApi } from '@grafana/data'; +import { DataSourceApi, SelectableValue } from '@grafana/data'; import { EditorRow, EditorRows } from '@grafana/experimental'; import { QueryPreview } from './QueryPreview'; @@ -22,6 +22,11 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, onChange({ ...query, labels }); }; + const withTemplateVariableOptions = async (optionsPromise: Promise): Promise => { + const options = await optionsPromise; + return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value })); + }; + const onGetLabelNames = async (forLabel: Partial): Promise => { const labelsToConsider = query.labels.filter((x) => x !== forLabel); @@ -46,15 +51,20 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, const expr = lokiQueryModeller.renderLabels(labelsToConsider); const result = await datasource.languageProvider.fetchSeriesLabels(expr); - return result[forLabel.label] ?? []; + const forLabelInterpolated = datasource.interpolateString(forLabel.label); + return result[forLabelInterpolated] ?? []; }; return ( ) => + withTemplateVariableOptions(onGetLabelNames(forLabel)) + } + onGetLabelValues={(forLabel: Partial) => + withTemplateVariableOptions(onGetLabelValues(forLabel)) + } labelsFilters={query.labels} onChange={onChangeLabels} /> diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index f2417ade731..4e64d73abbe 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -987,6 +987,14 @@ export class PrometheusDatasource interval: this.templateSrv.replace(target.interval, variables), }; } + + getVariables(): string[] { + return this.templateSrv.getVariables().map((v) => `$${v.name}`); + } + + interpolateString(string: string) { + return this.templateSrv.replace(string, undefined, this.interpolateQueryExpr); + } } /** diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index 814a9995fe5..950021b60fc 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -11,6 +11,7 @@ describe('Language completion provider', () => { const datasource: PrometheusDatasource = { metadataRequest: () => ({ data: { data: [] as any[] } }), getTimeRangeParams: () => ({ start: '0', end: '1' }), + interpolateString: (string: string) => string, } as any as PrometheusDatasource; describe('cleanText', () => { @@ -79,6 +80,41 @@ describe('Language completion provider', () => { }); }); + describe('fetchSeriesLabels', () => { + it('should interpolate variable in series', () => { + const languageProvider = new LanguageProvider({ + ...datasource, + interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'), + } as PrometheusDatasource); + const fetchSeriesLabels = languageProvider.fetchSeriesLabels; + const requestSpy = jest.spyOn(languageProvider, 'request'); + fetchSeriesLabels('$metric'); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy).toHaveBeenCalledWith('/api/v1/series', [], { + end: '1', + 'match[]': 'interpolated-metric', + start: '0', + }); + }); + }); + + describe('fetchLabelValues', () => { + it('should interpolate variable in series', () => { + const languageProvider = new LanguageProvider({ + ...datasource, + interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'), + } as PrometheusDatasource); + const fetchLabelValues = languageProvider.fetchLabelValues; + const requestSpy = jest.spyOn(languageProvider, 'request'); + fetchLabelValues('$job'); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy).toHaveBeenCalledWith('/api/v1/label/interpolated-job/values', [], { + end: '1', + start: '0', + }); + }); + }); + describe('empty query suggestions', () => { it('returns no suggestions on empty context', async () => { const instance = new LanguageProvider(datasource); @@ -266,6 +302,7 @@ describe('Language completion provider', () => { const datasources: PrometheusDatasource = { metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }), getTimeRangeParams: () => ({ start: '0', end: '1' }), + interpolateString: (string: string) => string, } as any as PrometheusDatasource; const instance = new LanguageProvider(datasources); const value = Plain.deserialize('metric{}'); @@ -299,6 +336,7 @@ describe('Language completion provider', () => { }, }), getTimeRangeParams: () => ({ start: '0', end: '1' }), + interpolateString: (string: string) => string, } as any as PrometheusDatasource; const instance = new LanguageProvider(datasource); const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}'); @@ -536,6 +574,7 @@ describe('Language completion provider', () => { const datasource: PrometheusDatasource = { metadataRequest: jest.fn(() => ({ data: { data: [] as any[] } })), getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })), + interpolateString: (string: string) => string, } as any as PrometheusDatasource; const instance = new LanguageProvider(datasource); @@ -586,6 +625,7 @@ describe('Language completion provider', () => { metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })), getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })), lookupsDisabled: false, + interpolateString: (string: string) => string, } as any as PrometheusDatasource; const instance = new LanguageProvider(datasource); diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index e1da3ae2163..1e1a63df6b5 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -460,7 +460,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { fetchLabelValues = async (key: string): Promise => { const params = this.datasource.getTimeRangeParams(); - const url = `/api/v1/label/${key}/values`; + const url = `/api/v1/label/${this.datasource.interpolateString(key)}/values`; return await this.request(url, [], params); }; @@ -491,10 +491,11 @@ export default class PromQlLanguageProvider extends LanguageProvider { * @param withName */ fetchSeriesLabels = async (name: string, withName?: boolean): Promise> => { + const interpolatedName = this.datasource.interpolateString(name); const range = this.datasource.getTimeRangeParams(); const urlParams = { ...range, - 'match[]': name, + 'match[]': interpolatedName, }; const url = `/api/v1/series`; // Cache key is a bit different here. We add the `withName` param and also round up to a minute the intervals. @@ -502,7 +503,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { // millisecond while still actually getting all the keys for the correct interval. This still can create problems // when user does not the newest values for a minute if already cached. const cacheParams = new URLSearchParams({ - 'match[]': name, + 'match[]': interpolatedName, start: roundSecToMin(parseInt(range.start, 10)).toString(), end: roundSecToMin(parseInt(range.end, 10)).toString(), withName: withName ? 'true' : 'false', diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx index 7e798ffa987..3951a1a30f2 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx @@ -8,7 +8,7 @@ import { css } from '@emotion/css'; export interface Props { query: PromVisualQuery; onChange: (query: PromVisualQuery) => void; - onGetMetrics: () => Promise; + onGetMetrics: () => Promise; } export function MetricSelect({ query, onChange, onGetMetrics }: Props) { @@ -18,12 +18,6 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) { isLoading?: boolean; }>({}); - const loadMetrics = async () => { - return await onGetMetrics().then((res) => { - return res.map((value) => ({ label: value, value })); - }); - }; - return ( @@ -35,7 +29,7 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) { allowCustomValue onOpenMenu={async () => { setState({ isLoading: true }); - const metrics = await loadMetrics(); + const metrics = await onGetMetrics(); setState({ metrics, isLoading: undefined }); }} isLoading={state.isLoading} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx index ea9ff3fe71b..655a00dd434 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx @@ -87,12 +87,26 @@ describe('PromQueryBuilder', () => { expect(languageProvider.getSeries).toBeCalledWith('{label_name="label_value"}', true); }); + it('tries to load variables in metric field', async () => { + const { datasource } = setup(); + datasource.getVariables = jest.fn().mockReturnValue([]); + openMetricSelect(); + expect(datasource.getVariables).toBeCalled(); + }); + it('tries to load labels when metric selected', async () => { const { languageProvider } = setup(); openLabelNameSelect(); expect(languageProvider.fetchSeriesLabels).toBeCalledWith('{__name__="random_metric"}'); }); + it('tries to load variables in label field', async () => { + const { datasource } = setup(); + datasource.getVariables = jest.fn().mockReturnValue([]); + openLabelNameSelect(); + expect(datasource.getVariables).toBeCalled(); + }); + it('tries to load labels when metric selected and other labels are already present', async () => { const { languageProvider } = setup({ ...defaultQuery, @@ -117,23 +131,24 @@ describe('PromQueryBuilder', () => { function setup(query: PromVisualQuery = defaultQuery) { const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider; + const datasource = new PrometheusDatasource( + { + url: '', + jsonData: {}, + meta: {} as any, + } as any, + undefined, + undefined, + languageProvider + ); const props = { - datasource: new PrometheusDatasource( - { - url: '', - jsonData: {}, - meta: {} as any, - } as any, - undefined, - undefined, - languageProvider - ), + datasource, onRunQuery: () => {}, onChange: () => {}, }; render(); - return { languageProvider }; + return { languageProvider, datasource }; } function getMetricSelect() { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx index ee77f15392b..a681cc534be 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx @@ -9,7 +9,7 @@ import { NestedQueryList } from './NestedQueryList'; import { promQueryModeller } from '../PromQueryModeller'; import { QueryBuilderLabelFilter } from '../shared/types'; import { QueryPreview } from './QueryPreview'; -import { DataSourceApi } from '@grafana/data'; +import { DataSourceApi, SelectableValue } from '@grafana/data'; import { OperationsEditorRow } from '../shared/OperationsEditorRow'; export interface Props { @@ -25,6 +25,12 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange onChange({ ...query, labels }); }; + const withTemplateVariableOptions = async (optionsPromise: Promise): Promise => { + const variables = datasource.getVariables(); + const options = await optionsPromise; + return [...variables, ...options].map((value) => ({ label: value, value })); + }; + const onGetLabelNames = async (forLabel: Partial): Promise => { // If no metric we need to use a different method if (!query.metric) { @@ -58,7 +64,8 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange labelsToConsider.push({ label: '__name__', op: '=', value: query.metric }); const expr = promQueryModeller.renderLabels(labelsToConsider); const result = await datasource.languageProvider.fetchSeriesLabels(expr); - return result[forLabel.label] ?? []; + const forLabelInterpolated = datasource.interpolateString(forLabel.label); + return result[forLabelInterpolated] ?? []; }; const onGetMetrics = async () => { @@ -73,12 +80,20 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange return ( - + withTemplateVariableOptions(onGetMetrics())} + /> ) => + withTemplateVariableOptions(onGetLabelNames(forLabel)) + } + onGetLabelValues={(forLabel: Partial) => + withTemplateVariableOptions(onGetLabelValues(forLabel)) + } /> diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx index f0d238e38c5..dec8b2c6431 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx @@ -8,8 +8,8 @@ export interface Props { defaultOp: string; item: Partial; onChange: (value: QueryBuilderLabelFilter) => void; - onGetLabelNames: (forLabel: Partial) => Promise; - onGetLabelValues: (forLabel: Partial) => Promise; + onGetLabelNames: (forLabel: Partial) => Promise; + onGetLabelValues: (forLabel: Partial) => Promise; onDelete: () => void; } @@ -53,7 +53,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe allowCustomValue onOpenMenu={async () => { setState({ isLoadingLabelNames: true }); - const labelNames = (await onGetLabelNames(item)).map((x) => ({ label: x, value: x })); + const labelNames = await onGetLabelNames(item); setState({ labelNames, isLoadingLabelNames: undefined }); }} isLoading={state.isLoadingLabelNames} @@ -90,7 +90,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe const labelValues = await onGetLabelValues(item); setState({ ...state, - labelValues: labelValues.map((value) => ({ label: value, value })), + labelValues, isLoadingLabelValues: undefined, }); }} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.test.tsx index a80b252f4ef..b13702c4226 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.test.tsx @@ -52,8 +52,16 @@ describe('LabelFilters', () => { function setup(labels: QueryBuilderLabelFilter[] = []) { const props = { onChange: jest.fn(), - onGetLabelNames: async () => ['foo', 'bar', 'baz'], - onGetLabelValues: async () => ['bar', 'qux', 'quux'], + onGetLabelNames: async () => [ + { label: 'foo', value: 'foo' }, + { label: 'bar', value: 'bar' }, + { label: 'baz', value: 'baz' }, + ], + onGetLabelValues: async () => [ + { label: 'bar', value: 'bar' }, + { label: 'qux', value: 'qux' }, + { label: 'quux', value: 'quux' }, + ], }; render(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx index e6fcd229c57..d3c4daf546b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx @@ -1,3 +1,4 @@ +import { SelectableValue } from '@grafana/data'; import { EditorField, EditorFieldGroup, EditorList } from '@grafana/experimental'; import { isEqual } from 'lodash'; import React, { useState } from 'react'; @@ -7,8 +8,8 @@ import { LabelFilterItem } from './LabelFilterItem'; export interface Props { labelsFilters: QueryBuilderLabelFilter[]; onChange: (labelFilters: QueryBuilderLabelFilter[]) => void; - onGetLabelNames: (forLabel: Partial) => Promise; - onGetLabelValues: (forLabel: Partial) => Promise; + onGetLabelNames: (forLabel: Partial) => Promise; + onGetLabelValues: (forLabel: Partial) => Promise; } export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues }: Props) { From a79c048344bddff7a868b040d9a08953917480f9 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 3 Feb 2022 13:53:23 +0200 Subject: [PATCH 07/34] Feature Highlights: move setting to a feature toggle (#44780) * Add toggle * Use the toggle * Cleanup --- docs/sources/enterprise/enterprise-configuration.md | 6 ------ packages/grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/api/frontendsettings.go | 3 --- pkg/services/featuremgmt/registry.go | 5 +++++ pkg/services/featuremgmt/toggles_gen.go | 4 ++++ public/app/features/datasources/state/navModel.ts | 8 ++++---- public/app/features/teams/TeamPages.tsx | 2 +- public/app/features/teams/state/navModel.ts | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index ebee7b16687..1639e9e2abf 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -501,9 +501,3 @@ The org id of the datasource where the query data will be written. If all `default_remote_write_*` properties are set, this information will be populated at startup. If a remote write target has already been configured, nothing will happen. - -## [feature_highlights] - -### enabled - -Whether the feature highlights feature is enabled diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 971014422c9..a3f86909bee 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,4 +36,5 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + featureHighlights?: boolean; } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 0e45d41c620..43d38af0b77 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -273,9 +273,6 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "enabled": hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true), }, "unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled, - "featureHighlights": map[string]bool{ - "enabled": hs.SettingsProvider.Section("feature_highlights").KeyValue("enabled").MustBool(false), - }, } if hs.Cfg.GeomapDefaultBaseLayerConfig != nil { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3763a03f094..9a5a487cbf7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -110,5 +110,10 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, + { + Name: "featureHighlights", + Description: "Highlight Enterprise features", + State: FeatureStateStable, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 430b9fbda56..49faad2bd17 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -82,4 +82,8 @@ const ( // FlagValidatedQueries // only execute the query saved in a panel FlagValidatedQueries = "validatedQueries" + + // FlagFeatureHighlights + // Highlight Enterprise features + FlagFeatureHighlights = "featureHighlights" ) diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 25c88ae6935..4b6e635ed6e 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -8,7 +8,7 @@ import { GenericDataSourcePlugin } from '../settings/PluginSettings'; export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { const pluginMeta = plugin.meta; - + const highlightsEnabled = config.featureToggles.featureHighlights; const navModel: NavModelItem = { img: pluginMeta.info.logos.large, id: 'datasource-' + dataSource.uid, @@ -61,7 +61,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { navModel.children!.push(dsPermissions); } - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...dsPermissions, url: dsPermissions.url + '/upgrade', @@ -79,7 +79,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('analytics')) { navModel.children!.push(analytics); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...analytics, url: analytics.url + '/upgrade', @@ -98,7 +98,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('caching')) { navModel.children!.push(caching); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...caching, url: caching.url + '/upgrade', diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 400dc0079c5..9817ddefe5b 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -130,7 +130,7 @@ export class TeamPages extends PureComponent { case PageTypes.GroupSync: if (isSignedInUserTeamAdmin && isSyncEnabled) { return ; - } else if (config.featureHighlights.enabled) { + } else if (config.featureToggles.featureHighlights) { return ( Date: Thu, 3 Feb 2022 13:26:05 +0100 Subject: [PATCH 08/34] Chore: Remove bus from the alerting service (#44496) * propagate notificationservice down to the notifiers * replace dispatch in result handler * remove dispatch from the rule reader * remove dispatch from eval context * remove dispatch from alerting usage * remove dispatch from alerting usage * remove dispatch from notifier * attempt to fix tests in alerting * hello linter, my old friend; also disable some tests for now * use mocks to fix the tests * resolving wire providers * make linter happy * remove yet another bus.dispatch * fix tests using store mock --- pkg/server/wire.go | 2 + pkg/services/alerting/alerting_usage.go | 4 +- pkg/services/alerting/alerting_usage_test.go | 12 +- pkg/services/alerting/conditions/query.go | 3 +- .../conditions/query_interval_test.go | 9 +- .../alerting/conditions/query_test.go | 9 +- pkg/services/alerting/engine.go | 24 +- .../alerting/engine_integration_test.go | 2 +- pkg/services/alerting/engine_test.go | 64 +++- pkg/services/alerting/eval_context.go | 8 +- pkg/services/alerting/eval_context_test.go | 4 +- pkg/services/alerting/eval_handler_test.go | 28 +- pkg/services/alerting/notifier.go | 36 ++- pkg/services/alerting/notifier_test.go | 22 +- .../alerting/notifiers/alertmanager.go | 8 +- .../alerting/notifiers/alertmanager_test.go | 8 +- pkg/services/alerting/notifiers/base.go | 6 +- pkg/services/alerting/notifiers/base_test.go | 10 +- pkg/services/alerting/notifiers/dingding.go | 8 +- .../alerting/notifiers/dingding_test.go | 6 +- pkg/services/alerting/notifiers/discord.go | 8 +- .../alerting/notifiers/discord_test.go | 4 +- pkg/services/alerting/notifiers/email.go | 8 +- pkg/services/alerting/notifiers/email_test.go | 6 +- pkg/services/alerting/notifiers/googlechat.go | 8 +- .../alerting/notifiers/googlechat_test.go | 4 +- pkg/services/alerting/notifiers/hipchat.go | 8 +- .../alerting/notifiers/hipchat_test.go | 6 +- pkg/services/alerting/notifiers/kafka.go | 8 +- pkg/services/alerting/notifiers/kafka_test.go | 4 +- pkg/services/alerting/notifiers/line.go | 8 +- pkg/services/alerting/notifiers/line_test.go | 4 +- pkg/services/alerting/notifiers/opsgenie.go | 10 +- .../alerting/notifiers/opsgenie_test.go | 65 ++--- pkg/services/alerting/notifiers/pagerduty.go | 8 +- .../alerting/notifiers/pagerduty_test.go | 32 +- pkg/services/alerting/notifiers/pushover.go | 8 +- .../alerting/notifiers/pushover_test.go | 8 +- pkg/services/alerting/notifiers/sensu.go | 8 +- pkg/services/alerting/notifiers/sensu_test.go | 4 +- pkg/services/alerting/notifiers/sensugo.go | 8 +- .../alerting/notifiers/sensugo_test.go | 4 +- pkg/services/alerting/notifiers/slack.go | 8 +- pkg/services/alerting/notifiers/slack_test.go | 12 +- pkg/services/alerting/notifiers/teams.go | 8 +- pkg/services/alerting/notifiers/teams_test.go | 6 +- pkg/services/alerting/notifiers/telegram.go | 8 +- .../alerting/notifiers/telegram_test.go | 12 +- pkg/services/alerting/notifiers/threema.go | 8 +- .../alerting/notifiers/threema_test.go | 10 +- pkg/services/alerting/notifiers/victorops.go | 8 +- .../alerting/notifiers/victorops_test.go | 12 +- pkg/services/alerting/notifiers/webhook.go | 8 +- .../alerting/notifiers/webhook_test.go | 4 +- pkg/services/alerting/reader.go | 11 +- pkg/services/alerting/result_handler.go | 10 +- pkg/services/alerting/service.go | 19 +- pkg/services/alerting/service_test.go | 9 +- pkg/services/alerting/test_notification.go | 4 +- pkg/services/alerting/test_rule.go | 2 +- .../notifiers/alert_notifications.go | 12 +- .../provisioning/notifiers/config_reader.go | 8 +- .../notifiers/config_reader_test.go | 14 +- pkg/services/provisioning/provisioning.go | 11 +- pkg/services/sqlstore/alert_notification.go | 16 + pkg/services/sqlstore/mockstore/mockstore.go | 276 +++++++++--------- 66 files changed, 557 insertions(+), 443 deletions(-) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index a98268e2146..f39a816b895 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -202,6 +202,7 @@ var wireBasicSet = wire.NewSet( var wireSet = wire.NewSet( wireBasicSet, sqlstore.ProvideService, + wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)), ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -214,6 +215,7 @@ var wireTestSet = wire.NewSet( ProvideTestEnv, sqlstore.ProvideServiceForTests, ngmetrics.ProvideServiceForTest, + wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)), notifications.MockNotificationService, wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), diff --git a/pkg/services/alerting/alerting_usage.go b/pkg/services/alerting/alerting_usage.go index 44e1152ab23..441fc85bf48 100644 --- a/pkg/services/alerting/alerting_usage.go +++ b/pkg/services/alerting/alerting_usage.go @@ -28,7 +28,7 @@ type UsageStatsQuerier interface { // configured in Grafana. func (e *AlertEngine) QueryUsageStats(ctx context.Context) (*UsageStats, error) { cmd := &models.GetAllAlertsQuery{} - err := e.Bus.Dispatch(ctx, cmd) + err := e.sqlStore.GetAllAlertQueryHandler(ctx, cmd) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (e *AlertEngine) mapRulesToUsageStats(ctx context.Context, rules []*models. result := map[string]int{} for k, v := range typeCount { query := &models.GetDataSourceQuery{Id: k} - err := e.Bus.Dispatch(ctx, query) + err := e.sqlStore.GetDataSource(ctx, query) if err != nil { return map[string]int{}, nil } diff --git a/pkg/services/alerting/alerting_usage_test.go b/pkg/services/alerting/alerting_usage_test.go index 5fb090ffe7e..582d8c7dd9f 100644 --- a/pkg/services/alerting/alerting_usage_test.go +++ b/pkg/services/alerting/alerting_usage_test.go @@ -7,18 +7,18 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" ) func TestAlertingUsageStats(t *testing.T) { + store := &AlertStoreMock{} ae := &AlertEngine{ - Bus: bus.New(), + sqlStore: store, } - ae.Bus.AddHandler(func(ctx context.Context, query *models.GetAllAlertsQuery) error { + store.getAllAlerts = func(ctx context.Context, query *models.GetAllAlertsQuery) error { var createFake = func(file string) *simplejson.Json { // Ignore gosec warning G304 since it's a test // nolint:gosec @@ -37,9 +37,9 @@ func TestAlertingUsageStats(t *testing.T) { {Id: 3, Settings: createFake("testdata/settings/empty.json")}, } return nil - }) + } - ae.Bus.AddHandler(func(ctx context.Context, query *models.GetDataSourceQuery) error { + store.getDataSource = func(ctx context.Context, query *models.GetDataSourceQuery) error { ds := map[int64]*models.DataSource{ 1: {Type: "influxdb"}, 2: {Type: "graphite"}, @@ -54,7 +54,7 @@ func TestAlertingUsageStats(t *testing.T) { query.Result = r return nil - }) + } result, err := ae.QueryUsageStats(context.Background()) require.NoError(t, err, "getAlertingUsage should not return error") diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index d6363416976..9aee49e5b1b 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -13,7 +13,6 @@ import ( gocontext "context" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -140,7 +139,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange l OrgId: context.Rule.OrgID, } - if err := bus.Dispatch(context.Ctx, getDsInfo); err != nil { + if err := context.Store.GetDataSource(context.Ctx, getDsInfo); err != nil { return nil, fmt.Errorf("could not find datasource: %w", err) } diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go index 2ed470c9906..21e7385613b 100644 --- a/pkg/services/alerting/conditions/query_interval_test.go +++ b/pkg/services/alerting/conditions/query_interval_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" @@ -137,16 +137,15 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo * //nolint: staticcheck // legacydata.DataResponse deprecated func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) { t.Run("desc", func(t *testing.T) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error { - query.Result = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData} - return nil - }) + store := mockstore.NewSQLStoreMock() + store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData} ctx := &queryIntervalTestContext{} ctx.result = &alerting.EvalContext{ Ctx: context.Background(), Rule: &alerting.Rule{}, RequestValidator: &validations.OSSPluginRequestValidator{}, + Store: store, } jsonModel, err := simplejson.NewJson([]byte(`{ diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index bf1d5a895bb..f7e34b49a2f 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -6,13 +6,13 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -35,10 +35,8 @@ func newTimeSeriesPointsFromArgs(values ...float64) legacydata.DataTimeSeriesPoi func TestQueryCondition(t *testing.T) { setup := func() *queryConditionTestContext { ctx := &queryConditionTestContext{} - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error { - query.Result = &models.DataSource{Id: 1, Type: "graphite"} - return nil - }) + store := mockstore.NewSQLStoreMock() + store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite"} ctx.reducer = `{"type":"avg"}` ctx.evaluator = `{"type":"gt","params":[100]}` @@ -46,6 +44,7 @@ func TestQueryCondition(t *testing.T) { Ctx: context.Background(), Rule: &alerting.Rule{}, RequestValidator: &validations.OSSPluginRequestValidator{}, + Store: store, } return ctx } diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index e89996d0b10..54b5e50010d 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -16,11 +16,25 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +// AlertStore is a subset of SQLStore API to satisfy the needs of the alerting service. +// A subset is needed to make it easier to mock during the tests. +type AlertStore interface { + GetAllAlertQueryHandler(context.Context, *models.GetAllAlertsQuery) error + GetDataSource(context.Context, *models.GetDataSourceQuery) error + GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error + SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error + SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error + GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error + GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error + SetAlertState(context.Context, *models.SetAlertStateCommand) error +} + // AlertEngine is the background process that // schedules alert evaluations and makes sure notifications // are sent. @@ -40,6 +54,7 @@ type AlertEngine struct { resultHandler resultHandler usageStatsService usagestats.Service tracer tracing.Tracer + sqlStore AlertStore } // IsDisabled returns true if the alerting service is disabled for this instance. @@ -50,7 +65,7 @@ func (e *AlertEngine) IsDisabled() bool { // ProvideAlertEngine returns a new AlertEngine. func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidator models.PluginRequestValidator, dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal, - cfg *setting.Cfg, tracer tracing.Tracer) *AlertEngine { + notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg) *AlertEngine { e := &AlertEngine{ Cfg: cfg, RenderService: renderer, @@ -59,14 +74,15 @@ func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidato DataService: dataService, usageStatsService: usageStatsService, tracer: tracer, + sqlStore: sqlStore, } e.ticker = NewTicker(time.Now(), time.Second*0, clock.New(), 1) e.execQueue = make(chan *Job, 1000) e.scheduler = newScheduler() e.evalHandler = NewEvalHandler(e.DataService) - e.ruleReader = newRuleReader() + e.ruleReader = newRuleReader(sqlStore) e.log = log.New("alerting.engine") - e.resultHandler = newResultHandler(e.RenderService, encryptionService.GetDecryptedValue) + e.resultHandler = newResultHandler(e.RenderService, sqlStore, notificationService, encryptionService.GetDecryptedValue) e.registerUsageMetrics() @@ -179,7 +195,7 @@ func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout) cancelChan <- cancelFn alertCtx, span := e.tracer.Start(alertCtx, "alert execution") - evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator) + evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator, e.sqlStore) evalContext.Ctx = alertCtx go func() { diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index 29422ea5dc3..f8a308ac5f2 100644 --- a/pkg/services/alerting/engine_integration_test.go +++ b/pkg/services/alerting/engine_integration_test.go @@ -24,7 +24,7 @@ func TestEngineTimeouts(t *testing.T) { usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer) + engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg()) setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 engine.resultHandler = &FakeResultHandler{} diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go index da21e2dcdda..771970efd95 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -43,12 +43,66 @@ func (handler *FakeResultHandler) handle(evalContext *EvalContext) error { return nil } +// A mock implementation of the AlertStore interface, allowing to override certain methods individually +type AlertStoreMock struct { + getAllAlerts func(context.Context, *models.GetAllAlertsQuery) error + getDataSource func(context.Context, *models.GetDataSourceQuery) error + getAlertNotificationsWithUidToSend func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error + getOrCreateNotificationState func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error +} + +func (a *AlertStoreMock) GetDataSource(c context.Context, cmd *models.GetDataSourceQuery) error { + if a.getDataSource != nil { + return a.getDataSource(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models.GetAllAlertsQuery) error { + if a.getAllAlerts != nil { + return a.getAllAlerts(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error { + if a.getAlertNotificationsWithUidToSend != nil { + return a.getAlertNotificationsWithUidToSend(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetOrCreateAlertNotificationState(c context.Context, cmd *models.GetOrCreateNotificationStateQuery) error { + if a.getOrCreateNotificationState != nil { + return a.getOrCreateNotificationState(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetDashboardUIDById(_ context.Context, _ *models.GetDashboardRefByIdQuery) error { + return nil +} + +func (a *AlertStoreMock) SetAlertNotificationStateToCompleteCommand(_ context.Context, _ *models.SetAlertNotificationStateToCompleteCommand) error { + return nil +} + +func (a *AlertStoreMock) SetAlertNotificationStateToPendingCommand(_ context.Context, _ *models.SetAlertNotificationStateToPendingCommand) error { + return nil +} + +func (a *AlertStoreMock) SetAlertState(_ context.Context, _ *models.SetAlertStateCommand) error { + return nil +} + func TestEngineProcessJob(t *testing.T) { bus := bus.New() usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer) + + store := &AlertStoreMock{} + engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg()) setting.AlertingEvaluationTimeout = 30 * time.Second setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 @@ -56,19 +110,19 @@ func TestEngineProcessJob(t *testing.T) { job := &Job{running: true, Rule: &Rule{}} t.Run("Should register usage metrics func", func(t *testing.T) { - bus.AddHandler(func(ctx context.Context, q *models.GetAllAlertsQuery) error { + store.getAllAlerts = func(ctx context.Context, q *models.GetAllAlertsQuery) error { settings, err := simplejson.NewJson([]byte(`{"conditions": [{"query": { "datasourceId": 1}}]}`)) if err != nil { return err } q.Result = []*models.Alert{{Settings: settings}} return nil - }) + } - bus.AddHandler(func(ctx context.Context, q *models.GetDataSourceQuery) error { + store.getDataSource = func(ctx context.Context, q *models.GetDataSourceQuery) error { q.Result = &models.DataSource{Id: 1, Type: models.DS_PROMETHEUS} return nil - }) + } report, err := usMock.GetUsageReport(context.Background()) require.Nil(t, err) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 8d3c5f46faa..de0d2d155ab 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -6,7 +6,6 @@ import ( "regexp" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -36,10 +35,12 @@ type EvalContext struct { RequestValidator models.PluginRequestValidator Ctx context.Context + + Store AlertStore } // NewEvalContext is the EvalContext constructor. -func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator) *EvalContext { +func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator, sqlStore AlertStore) *EvalContext { return &EvalContext{ Ctx: alertCtx, StartTime: time.Now(), @@ -49,6 +50,7 @@ func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator model Log: log.New("alerting.evalContext"), PrevAlertState: rule.State, RequestValidator: requestValidator, + Store: sqlStore, } } @@ -108,7 +110,7 @@ func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) { } uidQuery := &models.GetDashboardRefByIdQuery{Id: c.Rule.DashboardID} - if err := bus.Dispatch(c.Ctx, uidQuery); err != nil { + if err := c.Store.GetDashboardUIDById(c.Ctx, uidQuery); err != nil { return nil, err } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 6aa12287716..30c3114ed15 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -15,7 +15,7 @@ import ( ) func TestStateIsUpdatedWhenNeeded(t *testing.T) { - ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}) + ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil) t.Run("ok -> alerting", func(t *testing.T) { ctx.PrevAlertState = models.AlertStateOK @@ -200,7 +200,7 @@ func TestGetStateFromEvalContext(t *testing.T) { } for _, tc := range tcs { - evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}) + evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil) tc.applyFn(evalContext) newState := evalContext.GetNewState() diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 051830bf1b4..ca11a462dd5 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -29,7 +29,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { Conditions: []Condition{&conditionStub{ firing: true, }}, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -39,7 +39,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { t.Run("Show return triggered with single passing condition2", func(t *testing.T) { context := NewEvalContext(context.Background(), &Rule{ Conditions: []Condition{&conditionStub{firing: true, operator: "and"}}, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -52,7 +52,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}}, &conditionStub{firing: false, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -65,7 +65,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -78,7 +78,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -92,7 +92,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -106,7 +106,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -120,7 +120,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "and"}, &conditionStub{firing: true, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -134,7 +134,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "or"}, &conditionStub{firing: true, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -148,7 +148,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "or"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -163,7 +163,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "or", noData: false}, &conditionStub{operator: "or", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.False(t, context.NoDataFound) @@ -174,7 +174,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { Conditions: []Condition{ &conditionStub{operator: "and", noData: true}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -187,7 +187,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "and", noData: true}, &conditionStub{operator: "and", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.True(t, context.NoDataFound) @@ -199,7 +199,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "or", noData: true}, &conditionStub{operator: "or", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.True(t, context.NoDataFound) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 98e0b135822..9c0626acc9f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -6,11 +6,11 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" ) @@ -83,18 +83,22 @@ type ShowWhen struct { Is string `json:"is"` } -func newNotificationService(renderService rendering.Service, decryptFn GetDecryptedValueFn) *notificationService { +func newNotificationService(renderService rendering.Service, sqlStore AlertStore, notificationSvc *notifications.NotificationService, decryptFn GetDecryptedValueFn) *notificationService { return ¬ificationService{ - log: log.New("alerting.notifier"), - renderService: renderService, - decryptFn: decryptFn, + log: log.New("alerting.notifier"), + renderService: renderService, + sqlStore: sqlStore, + notificationService: notificationSvc, + decryptFn: decryptFn, } } type notificationService struct { - log log.Logger - renderService rendering.Service - decryptFn GetDecryptedValueFn + log log.Logger + renderService rendering.Service + sqlStore AlertStore + notificationService *notifications.NotificationService + decryptFn GetDecryptedValueFn } func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error { @@ -152,7 +156,7 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no Version: notifierState.state.Version, } - return bus.Dispatch(evalContext.Ctx, cmd) + return n.sqlStore.SetAlertNotificationStateToCompleteCommand(evalContext.Ctx, cmd) } func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *notifierState) error { @@ -163,7 +167,7 @@ func (n *notificationService) sendNotification(evalContext *EvalContext, notifie AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges, } - err := bus.Dispatch(evalContext.Ctx, setPendingCmd) + err := n.sqlStore.SetAlertNotificationStateToPendingCommand(evalContext.Ctx, setPendingCmd) if err != nil { if errors.Is(err, models.ErrAlertNotificationStateVersionConflict) { return nil @@ -251,13 +255,13 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) { query := &models.GetAlertNotificationsWithUidToSendQuery{OrgId: orgID, Uids: notificationUids} - if err := bus.Dispatch(evalContext.Ctx, query); err != nil { + if err := n.sqlStore.GetAlertNotificationsWithUidToSend(evalContext.Ctx, query); err != nil { return nil, err } var result notifierStateSlice for _, notification := range query.Result { - not, err := InitNotifier(notification, n.decryptFn) + not, err := InitNotifier(notification, n.decryptFn, n.notificationService) if err != nil { n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err) continue @@ -269,7 +273,7 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [ OrgId: evalContext.Rule.OrgID, } - err = bus.Dispatch(evalContext.Ctx, query) + err = n.sqlStore.GetOrCreateAlertNotificationState(evalContext.Ctx, query) if err != nil { n.log.Error("Could not get notification state.", "notifier", notification.Id, "error", err) continue @@ -287,13 +291,13 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [ } // InitNotifier instantiate a new notifier based on the model. -func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Notifier, error) { +func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn, notificationService *notifications.NotificationService) (Notifier, error) { notifierPlugin, found := notifierFactories[model.Type] if !found { return nil, fmt.Errorf("unsupported notification type %q", model.Type) } - return notifierPlugin.Factory(model, fn) + return notifierPlugin.Factory(model, fn, notificationService) } // GetDecryptedValueFn is a function that returns the decrypted value of @@ -301,7 +305,7 @@ func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Noti type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string, secret string) string // NotifierFactory is a signature for creating notifiers. -type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error) +type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error) var notifierFactories = make(map[string]*NotifierPlugin) diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index a1732eb89b1..b9204541fd7 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/components/simplejson" @@ -21,17 +22,18 @@ import ( func TestNotificationService(t *testing.T) { testRule := &Rule{Name: "Test", Message: "Something is bad"} - evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}) + store := &AlertStoreMock{} + evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store) testRuleTemplated := &Rule{Name: "Test latency ${quantile}", Message: "Something is bad on instance ${instance}"} - evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}) + evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store) evalCtxWithMatch.EvalMatches = []*EvalMatch{{ Tags: map[string]string{ "instance": "localhost:3000", "quantile": "0.99", }, }} - evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}) + evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store) notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification", evalCtx, true, func(sc *scenarioContext) { @@ -177,7 +179,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext evalCtx.dashboardRef = &models.DashboardRef{Uid: "db-uid"} - bus.AddHandler("test", func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { + store := evalCtx.Store.(*AlertStoreMock) + + store.getAlertNotificationsWithUidToSend = func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { query.Result = []*models.AlertNotification{ { Id: 1, @@ -188,9 +192,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext }, } return nil - }) + } - bus.AddHandler("test", func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error { + store.getOrCreateNotificationState = func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error { query.Result = &models.AlertNotificationState{ AlertId: evalCtx.Rule.ID, AlertRuleStateUpdatedVersion: 1, @@ -199,7 +203,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext State: models.AlertNotificationStateUnknown, } return nil - }) + } bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { return nil @@ -263,7 +267,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext }, } - scenarioCtx.notificationService = newNotificationService(renderService, nil) + scenarioCtx.notificationService = newNotificationService(renderService, store, nil, nil) fn(scenarioCtx) }) } @@ -279,7 +283,7 @@ type testNotifier struct { Frequency time.Duration } -func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn) (Notifier, error) { +func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn, ns notifications.Service) (Notifier, error) { uploadImage := true value, exist := model.Settings.CheckGet("uploadImage") if exist { diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d8146b54811..92876853470 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -50,7 +50,7 @@ func init() { } // NewAlertmanagerNotifier returns a new Alertmanager notifier -func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { urlString := model.Settings.Get("url").MustString() if urlString == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -67,7 +67,7 @@ func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDec basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString(), setting.SecretKey) return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, BasicAuthUser: basicAuthUser, BasicAuthPassword: basicAuthPassword, @@ -183,7 +183,7 @@ func (am *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := am.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { am.log.Error("Failed to send alertmanager", "error", err, "alertmanager", am.Name, "url", url) errCnt++ } diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index 0d1d5e3f3a4..29e920a6a2b 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -68,7 +68,7 @@ func TestWhenAlertManagerShouldNotify(t *testing.T) { am := &AlertmanagerNotifier{log: log.New("test.logger")} evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: tc.prevState, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.Rule.State = tc.newState @@ -92,7 +92,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -106,7 +106,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) alertmanagerNotifier := not.(*AlertmanagerNotifier) require.NoError(t, err) @@ -125,7 +125,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) alertmanagerNotifier := not.(*AlertmanagerNotifier) require.NoError(t, err) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 42363a6bdcd..07f5957e06d 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -24,11 +25,13 @@ type NotifierBase struct { DisableResolveMessage bool Frequency time.Duration + NotificationService notifications.Service + log log.Logger } // NewNotifierBase returns a new `NotifierBase`. -func NewNotifierBase(model *models.AlertNotification) NotifierBase { +func NewNotifierBase(model *models.AlertNotification, notificationService notifications.Service) NotifierBase { uploadImage := true if value, exists := model.Settings.CheckGet("uploadImage"); exists { uploadImage = value.MustBool() @@ -43,6 +46,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { SendReminder: model.SendReminder, DisableResolveMessage: model.DisableResolveMessage, Frequency: model.Frequency, + NotificationService: notificationService, log: log.New("alerting.notifier." + model.Name), } } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 32d68376b84..5ac57e2d8ba 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -170,7 +170,7 @@ func TestShouldSendAlertNotification(t *testing.T) { for _, tc := range tcs { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: tc.prevState, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) if tc.state == nil { tc.state = &models.AlertNotificationState{} @@ -197,24 +197,24 @@ func TestBaseNotifier(t *testing.T) { t.Run("can parse false value", func(t *testing.T) { bJSON.Set("uploadImage", false) - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.False(t, base.UploadImage) }) t.Run("can parse true value", func(t *testing.T) { bJSON.Set("uploadImage", true) - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.True(t, base.UploadImage) }) t.Run("default value should be true for backwards compatibility", func(t *testing.T) { - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.True(t, base.UploadImage) }) t.Run("default value should be false for backwards compatibility", func(t *testing.T) { - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.False(t, base.DisableResolveMessage) }) } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 5313700f405..7767f791df2 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -5,10 +5,10 @@ import ( "fmt" "net/url" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const defaultDingdingMsgType = "link" @@ -47,7 +47,7 @@ func init() { }) } -func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -56,7 +56,7 @@ func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecrypte msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType) return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), MsgType: msgType, URL: url, log: log.New("alerting.notifier.dingding"), @@ -91,7 +91,7 @@ func (dd *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := dd.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { dd.log.Error("Failed to send DingDing", "error", err, "dingding", dd.Name) return err } diff --git a/pkg/services/alerting/notifiers/dingding_test.go b/pkg/services/alerting/notifiers/dingding_test.go index 63143fb2631..a1036d949a0 100644 --- a/pkg/services/alerting/notifiers/dingding_test.go +++ b/pkg/services/alerting/notifiers/dingding_test.go @@ -24,7 +24,7 @@ func TestDingDingNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) t.Run("settings should trigger incident", func(t *testing.T) { @@ -37,7 +37,7 @@ func TestDingDingNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) notifier := not.(*DingDingNotifier) require.Nil(t, err) @@ -50,7 +50,7 @@ func TestDingDingNotifier(t *testing.T) { &alerting.Rule{ State: models.AlertStateAlerting, Message: `{host="localhost"}`, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, err = notifier.genBody(evalContext, "") require.Nil(t, err) }) diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 9d2eaf18f5b..99ac48e1193 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -9,11 +9,11 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -57,7 +57,7 @@ func init() { }) } -func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { avatar := model.Settings.Get("avatar_url").MustString() content := model.Settings.Get("content").MustString() url := model.Settings.Get("url").MustString() @@ -67,7 +67,7 @@ func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecrypted useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false) return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Content: content, AvatarURL: avatar, WebhookURL: url, @@ -177,7 +177,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { } } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := dn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { dn.log.Error("Failed to send notification to Discord", "error", err) return err } diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go index 809d03df105..60b13788f56 100644 --- a/pkg/services/alerting/notifiers/discord_test.go +++ b/pkg/services/alerting/notifiers/discord_test.go @@ -22,7 +22,7 @@ func TestDiscordNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -41,7 +41,7 @@ func TestDiscordNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) discordNotifier := not.(*DiscordNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 1e348eb44b4..fb072d59617 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -3,12 +3,12 @@ package notifiers import ( "os" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -48,7 +48,7 @@ type EmailNotifier struct { // NewEmailNotifier is the constructor function // for the EmailNotifier. -func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { addressesString := model.Settings.Get("addresses").MustString() singleEmail := model.Settings.Get("singleEmail").MustBool(false) @@ -60,7 +60,7 @@ func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa addresses := util.SplitEmails(addressesString) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Addresses: addresses, SingleEmail: singleEmail, log: log.New("alerting.notifier.email"), @@ -117,7 +117,7 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { } } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := en.NotificationService.SendEmailCommandHandlerSync(evalContext.Ctx, cmd); err != nil { en.log.Error("Failed to send alert notification email", "error", err) return err } diff --git a/pkg/services/alerting/notifiers/email_test.go b/pkg/services/alerting/notifiers/email_test.go index 2017e4da6c5..27dff78d9fc 100644 --- a/pkg/services/alerting/notifiers/email_test.go +++ b/pkg/services/alerting/notifiers/email_test.go @@ -22,7 +22,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) emailNotifier := not.(*EmailNotifier) require.Nil(t, err) @@ -63,7 +63,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) emailNotifier := not.(*EmailNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go index 24ef91ad398..02c4664a41b 100644 --- a/pkg/services/alerting/notifiers/googlechat.go +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -5,10 +5,10 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -32,14 +32,14 @@ func init() { }) } -func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &GoogleChatNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, log: log.New("alerting.notifier.googlechat"), }, nil @@ -220,7 +220,7 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error { Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := gcn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name) return err } diff --git a/pkg/services/alerting/notifiers/googlechat_test.go b/pkg/services/alerting/notifiers/googlechat_test.go index 8cde1cd3547..6447f7dbe5e 100644 --- a/pkg/services/alerting/notifiers/googlechat_test.go +++ b/pkg/services/alerting/notifiers/googlechat_test.go @@ -22,7 +22,7 @@ func TestGoogleChatNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestGoogleChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) webhookNotifier := not.(*GoogleChatNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 21db0e1b02e..7a21903fb6c 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -7,10 +7,10 @@ import ( "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -53,7 +53,7 @@ const ( // NewHipChatNotifier is the constructor functions // for the HipChatNotifier -func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if strings.HasSuffix(url, "/") { url = url[:len(url)-1] @@ -66,7 +66,7 @@ func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecrypted roomID := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, APIKey: apikey, RoomID: roomID, @@ -177,7 +177,7 @@ func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hc.log.Info("Request payload", "json", string(data)) cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := hc.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name) return err } diff --git a/pkg/services/alerting/notifiers/hipchat_test.go b/pkg/services/alerting/notifiers/hipchat_test.go index b27e941d50f..51b1e2d9e62 100644 --- a/pkg/services/alerting/notifiers/hipchat_test.go +++ b/pkg/services/alerting/notifiers/hipchat_test.go @@ -23,7 +23,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) hipchatNotifier := not.(*HipChatNotifier) require.Nil(t, err) @@ -65,7 +65,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) hipchatNotifier := not.(*HipChatNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index e0511f53468..c9faaea1027 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -5,11 +5,11 @@ import ( "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -41,7 +41,7 @@ func init() { } // NewKafkaNotifier is the constructor function for the Kafka notifier. -func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { endpoint := model.Settings.Get("kafkaRestProxy").MustString() if endpoint == "" { return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"} @@ -52,7 +52,7 @@ func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), @@ -124,7 +124,7 @@ func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := kn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { kn.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/kafka_test.go b/pkg/services/alerting/notifiers/kafka_test.go index 86e0be76b28..18dc7b7e1af 100644 --- a/pkg/services/alerting/notifiers/kafka_test.go +++ b/pkg/services/alerting/notifiers/kafka_test.go @@ -22,7 +22,7 @@ func TestKafkaNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -40,7 +40,7 @@ func TestKafkaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) kafkaNotifier := not.(*KafkaNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index c9e5771459c..9e360ecfecd 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -5,10 +5,10 @@ import ( "fmt" "net/url" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -37,14 +37,14 @@ const ( ) // NewLINENotifier is the constructor for the LINE notifier -func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString(), setting.SecretKey) if token == "" { return nil, alerting.ValidationError{Reason: "Could not find token in settings"} } return &LineNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Token: token, log: log.New("alerting.notifier.line"), }, nil @@ -92,7 +92,7 @@ func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error { Body: form.Encode(), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := ln.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { ln.log.Error("Failed to send notification to LINE", "error", err, "body", body) return err } diff --git a/pkg/services/alerting/notifiers/line_test.go b/pkg/services/alerting/notifiers/line_test.go index be7559a10b2..1720002b2fc 100644 --- a/pkg/services/alerting/notifiers/line_test.go +++ b/pkg/services/alerting/notifiers/line_test.go @@ -21,7 +21,7 @@ func TestLineNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) t.Run("settings should trigger incident", func(t *testing.T) { @@ -36,7 +36,7 @@ func TestLineNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) lineNotifier := not.(*LineNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 6c13ee382d6..8cf3a9ebfe8 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -5,11 +5,11 @@ import ( "fmt" "strconv" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -84,7 +84,7 @@ const ( ) // NewOpsGenieNotifier is the constructor for OpsGenie. -func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { autoClose := model.Settings.Get("autoClose").MustBool(true) overridePriority := model.Settings.Get("overridePriority").MustBool(true) apiKey := fn(context.Background(), model.SecureSettings, "apiKey", model.Settings.Get("apiKey").MustString(), setting.SecretKey) @@ -104,7 +104,7 @@ func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecrypt } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), APIKey: apiKey, APIUrl: apiURL, AutoClose: autoClose, @@ -205,7 +205,7 @@ func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) } @@ -229,7 +229,7 @@ func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go index e83af9cd5cb..08fefd1b86d 100644 --- a/pkg/services/alerting/notifiers/opsgenie_test.go +++ b/pkg/services/alerting/notifiers/opsgenie_test.go @@ -6,11 +6,11 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/validations" "github.com/stretchr/testify/require" @@ -28,7 +28,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -45,7 +45,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) opsgenieNotifier := not.(*OpsGenieNotifier) require.Nil(t, err) @@ -69,7 +69,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(alerting.ValidationError{})) require.True(t, strings.HasSuffix(err.Error(), "Invalid value for sendTagsAs: \"not_a_valid_value\"")) @@ -92,7 +92,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -102,22 +103,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"}) @@ -142,7 +141,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -152,22 +152,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, nil) + }, nil, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{}) @@ -192,7 +190,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -202,22 +201,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, nil) + }, nil, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"}) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 4fc4766a1a2..358a23d14ec 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -76,7 +76,7 @@ var ( ) // NewPagerdutyNotifier is the constructor for the PagerDuty notifier -func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { severity := model.Settings.Get("severity").MustString("critical") autoResolve := model.Settings.Get("autoResolve").MustBool(false) key := fn(context.Background(), model.SecureSettings, "integrationKey", model.Settings.Get("integrationKey").MustString(), setting.SecretKey) @@ -86,7 +86,7 @@ func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryp } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Key: key, Severity: severity, AutoResolve: autoResolve, @@ -240,7 +240,7 @@ func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := pn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { pn.log.Error("Failed to send notification to Pagerduty", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index fc6bcedc0af..52777bc075a 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -39,7 +39,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -55,7 +55,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -78,7 +78,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -105,7 +105,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -130,7 +130,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -139,7 +139,7 @@ func TestPagerdutyNotifier(t *testing.T) { Name: "someRule", Message: "someMessage", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext) @@ -187,7 +187,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -195,7 +195,7 @@ func TestPagerdutyNotifier(t *testing.T) { ID: 0, Name: "someRule", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext) @@ -244,7 +244,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -253,7 +253,7 @@ func TestPagerdutyNotifier(t *testing.T) { Name: "someRule", Message: "someMessage", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true evalContext.EvalMatches = []*alerting.EvalMatch{ { @@ -314,7 +314,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -332,7 +332,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "severity", Value: "warning"}, {Key: "dedup_key", Value: "key-" + strings.Repeat("x", 260)}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true @@ -394,7 +394,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -411,7 +411,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "component", Value: "aComponent"}, {Key: "severity", Value: "info"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true @@ -473,7 +473,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -490,7 +490,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "component", Value: "aComponent"}, {Key: "severity", Value: "llama"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 7586c4bd576..6a6fedce24e 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -11,10 +11,10 @@ import ( "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const pushoverEndpoint = "https://api.pushover.net/1/messages.json" @@ -194,7 +194,7 @@ func init() { } // NewPushoverNotifier is the constructor for the Pushover Notifier -func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { userKey := fn(context.Background(), model.SecureSettings, "userKey", model.Settings.Get("userKey").MustString(), setting.SecretKey) APIToken := fn(context.Background(), model.SecureSettings, "apiToken", model.Settings.Get("apiToken").MustString(), setting.SecretKey) device := model.Settings.Get("device").MustString() @@ -219,7 +219,7 @@ func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecrypt return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), UserKey: userKey, APIToken: APIToken, AlertingPriority: alertingPriority, @@ -287,7 +287,7 @@ func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { Body: uploadBody.String(), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := pn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name) return err } diff --git a/pkg/services/alerting/notifiers/pushover_test.go b/pkg/services/alerting/notifiers/pushover_test.go index fe27b424b0f..c498611af7a 100644 --- a/pkg/services/alerting/notifiers/pushover_test.go +++ b/pkg/services/alerting/notifiers/pushover_test.go @@ -26,7 +26,7 @@ func TestPushoverNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -48,7 +48,7 @@ func TestPushoverNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pushoverNotifier := not.(*PushoverNotifier) require.Nil(t, err) @@ -74,7 +74,7 @@ func TestGenPushoverBody(t *testing.T) { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "") require.Nil(t, err) @@ -85,7 +85,7 @@ func TestGenPushoverBody(t *testing.T) { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "") require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index ec7477867e9..b41cf17ca18 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -5,11 +5,11 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -61,14 +61,14 @@ func init() { } // NewSensuNotifier is the constructor for the Sensu Notifier. -func NewSensuNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSensuNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), @@ -146,7 +146,7 @@ func (sn *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { HttpMethod: "POST", } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { sn.log.Error("Failed to send sensu event", "error", err, "sensu", sn.Name) return err } diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go index 2be6f71d325..dc4376b9f9c 100644 --- a/pkg/services/alerting/notifiers/sensu_test.go +++ b/pkg/services/alerting/notifiers/sensu_test.go @@ -22,7 +22,7 @@ func TestSensuNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -41,7 +41,7 @@ func TestSensuNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) sensuNotifier := not.(*SensuNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/sensugo.go b/pkg/services/alerting/notifiers/sensugo.go index 655014a532c..045484be0bb 100644 --- a/pkg/services/alerting/notifiers/sensugo.go +++ b/pkg/services/alerting/notifiers/sensugo.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -72,7 +72,7 @@ func init() { } // NewSensuGoNotifier is the constructor for the Sensu Go Notifier. -func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() apikey := fn(context.Background(), model.SecureSettings, "apikey", model.Settings.Get("apikey").MustString(), setting.SecretKey) @@ -84,7 +84,7 @@ func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecrypte } return &SensuGoNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, Entity: model.Settings.Get("entity").MustString(), Check: model.Settings.Get("check").MustString(), @@ -197,7 +197,7 @@ func (sn *SensuGoNotifier) Notify(evalContext *alerting.EvalContext) error { "Authorization": fmt.Sprintf("Key %s", sn.APIKey), }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { sn.log.Error("Failed to send Sensu Go event", "error", err, "sensugo", sn.Name) return err } diff --git a/pkg/services/alerting/notifiers/sensugo_test.go b/pkg/services/alerting/notifiers/sensugo_test.go index c1a4eea0a84..2e95208a516 100644 --- a/pkg/services/alerting/notifiers/sensugo_test.go +++ b/pkg/services/alerting/notifiers/sensugo_test.go @@ -21,7 +21,7 @@ func TestSensuGoNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) json = ` @@ -42,7 +42,7 @@ func TestSensuGoNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) sensuGoNotifier := not.(*SensuGoNotifier) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 3f5b90a7305..b88f89e8204 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -16,10 +16,10 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -121,7 +121,7 @@ func init() { const slackAPIEndpoint = "https://slack.com/api/chat.postMessage" // NewSlackNotifier is the constructor for the Slack notifier. -func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { urlStr := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString(), setting.SecretKey) if urlStr == "" { urlStr = slackAPIEndpoint @@ -174,7 +174,7 @@ func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedV return &SlackNotifier{ url: apiURL, - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), recipient: recipient, username: username, iconEmoji: iconEmoji, @@ -418,7 +418,7 @@ func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log cmd := &models.SendWebhookSync{ Url: "https://slack.com/api/files.upload", Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST", } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") return err } diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 2bcc1438e9a..59244765c25 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -27,7 +27,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) assert.EqualError(t, err, "alert validation error: recipient must be specified when using the Slack chat API") }) @@ -45,7 +45,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -83,7 +83,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -131,7 +131,7 @@ func TestSlackNotifier(t *testing.T) { SecureSettings: securedSettingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -162,7 +162,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "1ABCDE", slackNotifier.recipient) @@ -253,7 +253,7 @@ func TestSendSlackRequest(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 6229046c68d..e85f4a12d82 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -3,10 +3,10 @@ package notifiers import ( "encoding/json" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -30,14 +30,14 @@ func init() { } // NewTeamsNotifier is the constructor for Teams notifier. -func NewTeamsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewTeamsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, log: log.New("alerting.notifier.teams"), }, nil @@ -135,7 +135,7 @@ func (tn *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { data, _ := json.Marshal(&body) cmd := &models.SendWebhookSync{Url: tn.URL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send teams notification", "error", err, "webhook", tn.Name) return err } diff --git a/pkg/services/alerting/notifiers/teams_test.go b/pkg/services/alerting/notifiers/teams_test.go index 3c43931ee8e..9bd9a6a14ff 100644 --- a/pkg/services/alerting/notifiers/teams_test.go +++ b/pkg/services/alerting/notifiers/teams_test.go @@ -22,7 +22,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) teamsNotifier := not.(*TeamsNotifier) require.Nil(t, err) @@ -61,7 +61,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) teamsNotifier := not.(*TeamsNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 16b705d4bac..d93d776b853 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -8,10 +8,10 @@ import ( "mime/multipart" "os" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -63,7 +63,7 @@ type TelegramNotifier struct { } // NewTelegramNotifier is the constructor for the Telegram notifier -func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -81,7 +81,7 @@ func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecrypt } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), BotToken: botToken, ChatID: chatID, UploadImage: uploadImage, @@ -271,7 +271,7 @@ func (tn *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send webhook", "error", err, "webhook", tn.Name) return err } diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go index b0b1af92de8..18eb4e4e081 100644 --- a/pkg/services/alerting/notifiers/telegram_test.go +++ b/pkg/services/alerting/notifiers/telegram_test.go @@ -25,7 +25,7 @@ func TestTelegramNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -43,7 +43,7 @@ func TestTelegramNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) telegramNotifier := not.(*TelegramNotifier) require.Nil(t, err) @@ -59,7 +59,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "") require.LessOrEqual(t, len(caption), 1024) @@ -75,7 +75,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -93,7 +93,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/foo", @@ -110,7 +110,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/foo", diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index c1b69e959c8..19b7ecd6324 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -6,10 +6,10 @@ import ( "net/url" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -71,7 +71,7 @@ type ThreemaNotifier struct { } // NewThreemaNotifier is the constructor for the Threema notifier -func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -101,7 +101,7 @@ func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecrypte } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, @@ -158,7 +158,7 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error HttpMethod: "POST", HttpHeader: headers, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := notifier.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) return err } diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go index 4800afbe53e..974ad20eeaf 100644 --- a/pkg/services/alerting/notifiers/threema_test.go +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -24,7 +24,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -43,7 +43,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) threemaNotifier := not.(*ThreemaNotifier) @@ -70,7 +70,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) @@ -92,7 +92,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) @@ -114,7 +114,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index c0d96d6b1b0..9cc1416c911 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -4,11 +4,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -47,7 +47,7 @@ func init() { // NewVictoropsNotifier creates an instance of VictoropsNotifier that // handles posting notifications to Victorops REST API -func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { autoResolve := model.Settings.Get("autoResolve").MustBool(true) url := model.Settings.Get("url").MustString() if url == "" { @@ -56,7 +56,7 @@ func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecrypt noDataAlertType := model.Settings.Get("noDataAlertType").MustString(AlertStateWarning) return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, NoDataAlertType: noDataAlertType, AutoResolve: autoResolve, @@ -156,7 +156,7 @@ func (vn *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { data, _ := bodyJSON.MarshalJSON() cmd := &models.SendWebhookSync{Url: vn.URL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := vn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name) return err } diff --git a/pkg/services/alerting/notifiers/victorops_test.go b/pkg/services/alerting/notifiers/victorops_test.go index 8678e0da1ac..d68629cd82a 100644 --- a/pkg/services/alerting/notifiers/victorops_test.go +++ b/pkg/services/alerting/notifiers/victorops_test.go @@ -35,7 +35,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -52,7 +52,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) victoropsNotifier := not.(*VictoropsNotifier) require.Nil(t, err) @@ -76,7 +76,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) victoropsNotifier := not.(*VictoropsNotifier) @@ -90,7 +90,7 @@ func TestVictoropsNotifier(t *testing.T) { {Key: "keyOnly"}, {Key: "severity", Value: "warning"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payload, err := victoropsNotifier.buildEventPayload(evalContext) @@ -124,7 +124,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) victoropsNotifier := not.(*VictoropsNotifier) @@ -138,7 +138,7 @@ func TestVictoropsNotifier(t *testing.T) { {Key: "keyOnly"}, {Key: "severity", Value: "warning"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payload, err := victoropsNotifier.buildEventPayload(evalContext) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 6d4e4702150..5ec1659718b 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -60,7 +60,7 @@ func init() { // NewWebHookNotifier is the constructor for // the WebHook notifier. -func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -69,7 +69,7 @@ func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecrypte password := fn(context.Background(), model.SecureSettings, "password", model.Settings.Get("password").MustString(), setting.SecretKey) return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, User: model.Settings.Get("username").MustString(), Password: password, @@ -153,7 +153,7 @@ func (wn *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { HttpMethod: wn.HTTPMethod, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := wn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { wn.log.Error("Failed to send webhook", "error", err, "webhook", wn.Name) return err } diff --git a/pkg/services/alerting/notifiers/webhook_test.go b/pkg/services/alerting/notifiers/webhook_test.go index 0054d3f3a4c..39ed35c6395 100644 --- a/pkg/services/alerting/notifiers/webhook_test.go +++ b/pkg/services/alerting/notifiers/webhook_test.go @@ -22,7 +22,7 @@ func TestWebhookNotifier_parsingFromSettings(t *testing.T) { Settings: settingsJSON, } - _, err = NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -37,7 +37,7 @@ func TestWebhookNotifier_parsingFromSettings(t *testing.T) { Settings: settingsJSON, } - not, err := NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) webhookNotifier := not.(*WebhookNotifier) diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index ae8303beca8..6d4cc8dd80f 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -4,7 +4,6 @@ import ( "context" "sync" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" @@ -16,12 +15,14 @@ type ruleReader interface { type defaultRuleReader struct { sync.RWMutex - log log.Logger + sqlStore AlertStore + log log.Logger } -func newRuleReader() *defaultRuleReader { +func newRuleReader(sqlStore AlertStore) *defaultRuleReader { ruleReader := &defaultRuleReader{ - log: log.New("alerting.ruleReader"), + sqlStore: sqlStore, + log: log.New("alerting.ruleReader"), } return ruleReader @@ -30,7 +31,7 @@ func newRuleReader() *defaultRuleReader { func (arr *defaultRuleReader) fetch(ctx context.Context) []*Rule { cmd := &models.GetAllAlertsQuery{} - if err := bus.Dispatch(ctx, cmd); err != nil { + if err := arr.sqlStore.GetAllAlertQueryHandler(ctx, cmd); err != nil { arr.log.Error("Could not load alerts", "error", err) return []*Rule{} } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 401e69409d8..3bb01b92d0e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -5,13 +5,13 @@ import ( "errors" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" ) @@ -21,13 +21,15 @@ type resultHandler interface { type defaultResultHandler struct { notifier *notificationService + sqlStore AlertStore log log.Logger } -func newResultHandler(renderService rendering.Service, decryptFn GetDecryptedValueFn) *defaultResultHandler { +func newResultHandler(renderService rendering.Service, sqlStore AlertStore, notificationService *notifications.NotificationService, decryptFn GetDecryptedValueFn) *defaultResultHandler { return &defaultResultHandler{ log: log.New("alerting.resultHandler"), - notifier: newNotificationService(renderService, decryptFn), + sqlStore: sqlStore, + notifier: newNotificationService(renderService, sqlStore, notificationService, decryptFn), } } @@ -58,7 +60,7 @@ func (handler *defaultResultHandler) handle(evalContext *EvalContext) error { EvalData: annotationData, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := handler.sqlStore.SetAlertState(evalContext.Ctx, cmd); err != nil { if errors.Is(err, models.ErrCannotChangeStateOnPausedAlert) { handler.log.Error("Cannot change state on alert that's paused", "error", err) return err diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index a648950b7a2..22d6bbaf121 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -6,22 +6,25 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) type AlertNotificationService struct { - Bus bus.Bus - SQLStore *sqlstore.SQLStore - EncryptionService encryption.Internal + Bus bus.Bus + SQLStore *sqlstore.SQLStore + EncryptionService encryption.Internal + NotificationService *notifications.NotificationService } func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService encryption.Internal, -) *AlertNotificationService { + notificationService *notifications.NotificationService) *AlertNotificationService { s := &AlertNotificationService{ - Bus: bus, - SQLStore: store, - EncryptionService: encryptionService, + Bus: bus, + SQLStore: store, + EncryptionService: encryptionService, + NotificationService: notificationService, } s.Bus.AddHandler(s.GetAlertNotifications) @@ -153,7 +156,7 @@ func (s *AlertNotificationService) createNotifier(ctx context.Context, model *mo return nil, err } - notifier, err := InitNotifier(model, s.EncryptionService.GetDecryptedValue) + notifier, err := InitNotifier(model, s.EncryptionService.GetDecryptedValue, s.NotificationService) if err != nil { logger.Error("Failed to create notifier", "error", err.Error()) return nil, err diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index 2a3909193a4..020003ffacd 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" @@ -19,7 +20,7 @@ func TestService(t *testing.T) { nType := "test" registerTestNotifier(nType) - s := ProvideService(bus.New(), sqlStore, ossencryption.ProvideService()) + s := ProvideService(bus.New(), sqlStore, ossencryption.ProvideService(), nil) origSecret := setting.SecretKey setting.SecretKey = "alert_notification_service_test" @@ -116,7 +117,9 @@ func TestService(t *testing.T) { func registerTestNotifier(notifierType string) { RegisterNotifier(&NotifierPlugin{ - Type: notifierType, - Factory: func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error) { return nil, nil }, + Type: notifierType, + Factory: func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error) { + return nil, nil + }, }) } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index c4a45f31075..aee76f1b64c 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -29,7 +29,7 @@ var ( ) func (s *AlertNotificationService) HandleNotificationTestCommand(ctx context.Context, cmd *NotificationTestCommand) error { - notificationSvc := newNotificationService(nil, nil) + notificationSvc := newNotificationService(nil, nil, nil, nil) model := models.AlertNotification{ Id: cmd.ID, @@ -57,7 +57,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext { ID: rand.Int63(), } - ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}) + ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}, nil) if cmd.Settings.Get("uploadImage").MustBool(true) { ctx.ImagePublicURL = "https://grafana.com/assets/img/blog/mixed_styles.png" } diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 74968920cbf..3ccee726f6c 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -29,7 +29,7 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID handler := NewEvalHandler(e.DataService) - context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}) + context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}, nil) context.IsTestRun = true context.IsDebug = true diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go index 1341ca9cd01..dff39292842 100644 --- a/pkg/services/provisioning/notifiers/alert_notifications.go +++ b/pkg/services/provisioning/notifiers/alert_notifications.go @@ -5,12 +5,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "golang.org/x/net/context" ) // Provision alert notifiers -func Provision(ctx context.Context, configDirectory string, encryptionService encryption.Internal) error { - dc := newNotificationProvisioner(encryptionService, log.New("provisioning.notifiers")) +func Provision(ctx context.Context, configDirectory string, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { + dc := newNotificationProvisioner(encryptionService, notificationService, log.New("provisioning.notifiers")) return dc.applyChanges(ctx, configDirectory) } @@ -20,12 +21,13 @@ type NotificationProvisioner struct { cfgProvider *configReader } -func newNotificationProvisioner(encryptionService encryption.Internal, log log.Logger) NotificationProvisioner { +func newNotificationProvisioner(encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { return NotificationProvisioner{ log: log, cfgProvider: &configReader{ - encryptionService: encryptionService, - log: log, + encryptionService: encryptionService, + notificationService: notifiationService, + log: log, }, } } diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go index 5c7053109e4..f5680db8cc3 100644 --- a/pkg/services/provisioning/notifiers/config_reader.go +++ b/pkg/services/provisioning/notifiers/config_reader.go @@ -12,14 +12,16 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" "gopkg.in/yaml.v2" ) type configReader struct { - encryptionService encryption.Internal - log log.Logger + encryptionService encryption.Internal + notificationService *notifications.NotificationService + log log.Logger } func (cr *configReader) readConfig(ctx context.Context, path string) ([]*notificationsAsConfig, error) { @@ -175,7 +177,7 @@ func (cr *configReader) validateNotifications(notifications []*notificationsAsCo Settings: notification.SettingsToJSON(), SecureSettings: encryptedSecureSettings, Type: notification.Type, - }, cr.encryptionService.GetDecryptedValue) + }, cr.encryptionService.GetDecryptedValue, cr.notificationService) if err != nil { return err diff --git a/pkg/services/provisioning/notifiers/config_reader_test.go b/pkg/services/provisioning/notifiers/config_reader_test.go index 9d67c18d33d..984421265ce 100644 --- a/pkg/services/provisioning/notifiers/config_reader_test.go +++ b/pkg/services/provisioning/notifiers/config_reader_test.go @@ -139,7 +139,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("One configured notification", func(t *testing.T) { t.Run("no notification in database", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { @@ -170,7 +170,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 1) t.Run("should update one notification", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -194,7 +194,7 @@ func TestNotificationAsConfig(t *testing.T) { }) t.Run("Two notifications with is_default", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), doubleNotificationsConfig) t.Run("should both be inserted", func(t *testing.T) { require.NoError(t, err) @@ -237,7 +237,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 2) t.Run("should have two new notifications", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -271,7 +271,7 @@ func TestNotificationAsConfig(t *testing.T) { err = sqlStore.CreateAlertNotificationCommand(context.Background(), &existingNotificationCmd) require.NoError(t, err) - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), correctPropertiesWithOrgName) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -290,7 +290,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Config doesn't contain required field", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), noRequiredFields) require.NotNil(t, err) @@ -304,7 +304,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Empty yaml file", func(t *testing.T) { t.Run("should have not changed repo", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), emptyFile) if err != nil { t.Fatalf("applyChanges return an error %v", err) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 24ff38ce227..c7210b9279a 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -9,6 +9,7 @@ import ( plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/notifiers" @@ -19,12 +20,13 @@ import ( ) func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store, - encryptionService encryption.Internal) (*ProvisioningServiceImpl, error) { + encryptionService encryption.Internal, notificatonService *notifications.NotificationService) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, SQLStore: sqlStore, pluginStore: pluginStore, EncryptionService: encryptionService, + NotificationService: notificatonService, log: log.New("provisioning"), newDashboardProvisioner: dashboards.New, provisionNotifiers: notifiers.Provision, @@ -59,7 +61,7 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl { // Used for testing purposes func newProvisioningServiceImpl( newDashboardProvisioner dashboards.DashboardProvisionerFactory, - provisionNotifiers func(context.Context, string, encryption.Internal) error, + provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error, provisionDatasources func(context.Context, string) error, provisionPlugins func(context.Context, string, plugifaces.Store) error, ) *ProvisioningServiceImpl { @@ -77,11 +79,12 @@ type ProvisioningServiceImpl struct { SQLStore *sqlstore.SQLStore pluginStore plugifaces.Store EncryptionService encryption.Internal + NotificationService *notifications.NotificationService log log.Logger pollingCtxCancel context.CancelFunc newDashboardProvisioner dashboards.DashboardProvisionerFactory dashboardProvisioner dashboards.DashboardProvisioner - provisionNotifiers func(context.Context, string, encryption.Internal) error + provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error provisionDatasources func(context.Context, string) error provisionPlugins func(context.Context, string, plugifaces.Store) error mutex sync.Mutex @@ -157,7 +160,7 @@ func (ps *ProvisioningServiceImpl) ProvisionPlugins(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) error { alertNotificationsPath := filepath.Join(ps.Cfg.ProvisioningPath, "notifiers") - if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.EncryptionService); err != nil { + if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.EncryptionService, ps.NotificationService); err != nil { err = errutil.Wrap("Alert notification provisioning error", err) ps.log.Error("Failed to provision alert notifications", "error", err) return err diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 518170f411a..043bd57545a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -13,6 +13,22 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type AlertNotificationStore interface { + DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error + DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error + GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error + GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error + GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error + GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error + GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error + CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error + UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error + UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error + SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error + SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error + GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error +} + func (ss *SQLStore) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?" diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index b666447fc5b..4fb230bd1cd 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -9,8 +9,9 @@ import ( ) type SQLStoreMock struct { - ExpectedUser *models.User - ExpectedError error + ExpectedUser *models.User + ExpectedDatasource *models.DataSource + ExpectedError error } func NewSQLStoreMock() *SQLStoreMock { @@ -18,83 +19,83 @@ func NewSQLStoreMock() *SQLStoreMock { } func (m SQLStoreMock) DeleteExpiredSnapshots(ctx context.Context, cmd *models.DeleteExpiredSnapshotsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateDashboardSnapshot(ctx context.Context, cmd *models.CreateDashboardSnapshotCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDashboardSnapshot(ctx context.Context, cmd *models.DeleteDashboardSnapshotCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgByName(name string) (*models.Org, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateOrgWithMember(name string, userID int64) (models.Org, error) { - return models.Org{}, nil // TODO: Implement + return models.Org{}, nil } func (m SQLStoreMock) UpdateOrg(ctx context.Context, cmd *models.UpdateOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgAddress(ctx context.Context, cmd *models.UpdateOrgAddressCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteOrg(ctx context.Context, cmd *models.DeleteOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CloneUserToServiceAccount(ctx context.Context, siUser *models.SignedInUser) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetUserById(ctx context.Context, query *models.GetUserByIdQuery) error { @@ -103,51 +104,51 @@ func (m SQLStoreMock) GetUserById(ctx context.Context, query *models.GetUserById } func (m SQLStoreMock) GetUserByLogin(ctx context.Context, query *models.GetUserByLoginQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserByEmail(ctx context.Context, query *models.GetUserByEmailQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUser(ctx context.Context, cmd *models.UpdateUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ChangeUserPassword(ctx context.Context, cmd *models.ChangeUserPasswordCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserLastSeenAt(ctx context.Context, cmd *models.UpdateUserLastSeenAtCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetUsingOrg(ctx context.Context, cmd *models.SetUsingOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserProfile(ctx context.Context, query *models.GetUserProfileQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserOrgList(ctx context.Context, query *models.GetUserOrgListQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetSignedInUserWithCacheCtx(ctx context.Context, query *models.GetSignedInUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetSignedInUser(ctx context.Context, query *models.GetSignedInUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) BatchDisableUsers(ctx context.Context, cmd *models.BatchDisableUsersCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteUser(ctx context.Context, cmd *models.DeleteUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserPermissions(userID int64, isAdmin bool) error { @@ -155,7 +156,7 @@ func (m SQLStoreMock) UpdateUserPermissions(userID int64, isAdmin bool) error { } func (m SQLStoreMock) SetUserHelpFlag(ctx context.Context, cmd *models.SetUserHelpFlagCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateTeam(name string, email string, orgID int64) (models.Team, error) { @@ -167,409 +168,410 @@ func (m SQLStoreMock) CreateTeam(name string, email string, orgID int64) (models } func (m SQLStoreMock) UpdateTeam(ctx context.Context, cmd *models.UpdateTeamCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteTeam(ctx context.Context, cmd *models.DeleteTeamCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchTeams(ctx context.Context, query *models.SearchTeamsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamById(ctx context.Context, query *models.GetTeamByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamsByUser(ctx context.Context, query *models.GetTeamsByUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddTeamMember(userID int64, orgID int64, teamID int64, isExternal bool, permission models.PermissionType) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTeamMember(ctx context.Context, cmd *models.UpdateTeamMemberCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) { - return false, nil // TODO: Implement + return false, nil } func (m SQLStoreMock) RemoveTeamMember(ctx context.Context, cmd *models.RemoveTeamMemberCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamMembers(ctx context.Context, query *models.GetTeamMembersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) NewSession(ctx context.Context) *sqlstore.DBSession { - return nil // TODO: Implement + return nil } func (m SQLStoreMock) WithDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPreferencesWithDefaults(ctx context.Context, query *models.GetPreferencesWithDefaultsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPreferences(ctx context.Context, query *models.GetPreferencesQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SavePreferences(ctx context.Context, cmd *models.SavePreferencesCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPluginSettings(ctx context.Context, orgID int64) ([]*models.PluginSettingInfoDTO, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePluginSettingVersion(ctx context.Context, cmd *models.UpdatePluginSettingVersionCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) IsStarredByUserCtx(ctx context.Context, query *models.IsStarredByUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) StarDashboard(ctx context.Context, cmd *models.StarDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UnstarDashboard(ctx context.Context, cmd *models.UnstarDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserStars(ctx context.Context, query *models.GetUserStarsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) WithTransactionalDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardVersion(ctx context.Context, query *models.GetDashboardVersionQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardVersions(ctx context.Context, query *models.GetDashboardVersionsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteExpiredVersions(ctx context.Context, cmd *models.DeleteExpiredVersionsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDashboardACLCtx(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) HandleAlertsQuery(ctx context.Context, query *models.GetAlertsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) PauseAlert(ctx context.Context, cmd *models.PauseAlertCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) PauseAllAlerts(ctx context.Context, cmd *models.PauseAllAlertCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertStatesForDashboard(ctx context.Context, query *models.GetAlertStatesForDashboardQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgUser(ctx context.Context, cmd *models.UpdateOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchOrgUsers(ctx context.Context, query *models.SearchOrgUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetDashboard(id int64, orgID int64, uid string, slug string) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { - return false, nil // TODO: Implement + return false, nil } func (m SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { - return nil // TODO: Implement + query.Result = m.ExpectedDatasource + return m.ExpectedError } func (m SQLStoreMock) GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDataSourcesByType(ctx context.Context, query *models.GetDataSourcesByTypeQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Migrate() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Sync() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Reset() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Quote(value string) string { - return "" // TODO: Implement + return "" } func (m SQLStoreMock) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey { - return nil // TODO: Implement + return nil } func (m SQLStoreMock) DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetApiKeyByName(ctx context.Context, query *models.GetApiKeyByNameQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateTempUser(ctx context.Context, cmd *models.CreateTempUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTempUserWithEmailSent(ctx context.Context, cmd *models.UpdateTempUserWithEmailSentCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTempUsersQuery(ctx context.Context, query *models.GetTempUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTempUserByCode(ctx context.Context, query *models.GetTempUserByCodeQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ExpireOldUserInvites(ctx context.Context, cmd *models.ExpireTempUsersCommand) error { - return nil // TODO: Implement + return m.ExpectedError } From 29b97361f70e8991069601a88842080d686dce9f Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 3 Feb 2022 13:35:56 +0100 Subject: [PATCH 09/34] Alerting: load correct unified alerting tab (#44794) --- .../components/PanelEditor/PanelEditorTabs.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 1c04cfc2473..8c903e3fbfa 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -41,7 +41,7 @@ export const PanelEditorTabs: FC = React.memo(({ panel, da {tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert) { - renderAlertTab(tab, panel, dashboard, onChangeTab); + return renderAlertTab(tab, panel, dashboard, onChangeTab); } return ( void ) { - if (!config.alertingEnabled || !config.unifiedAlertingEnabled) { + const alertingDisabled = !config.alertingEnabled && !config.unifiedAlertingEnabled; + + if (alertingDisabled) { return null; - } else if (config.unifiedAlertingEnabled) { + } + + if (config.unifiedAlertingEnabled) { return ( ); - } else if (config.alertingEnabled) { + } + + if (config.alertingEnabled) { return ( Date: Thu, 3 Feb 2022 08:15:55 -0500 Subject: [PATCH 10/34] Prometheus: Set interval on time field (#44802) --- pkg/tsdb/prometheus/framing_test.go | 2 +- pkg/tsdb/prometheus/prometeus_bench_test.go | 2 +- .../testdata/range_infinity.result.golden.txt | 2 +- .../testdata/range_missing.result.golden.txt | 10 +-- .../testdata/range_nan.result.golden.txt | 2 +- .../testdata/range_simple.result.golden.txt | 4 +- pkg/tsdb/prometheus/time_series_query.go | 70 ++----------------- pkg/tsdb/prometheus/time_series_query_test.go | 26 +++---- 8 files changed, 27 insertions(+), 91 deletions(-) diff --git a/pkg/tsdb/prometheus/framing_test.go b/pkg/tsdb/prometheus/framing_test.go index 6db7e7e9c67..c2ddadd0dac 100644 --- a/pkg/tsdb/prometheus/framing_test.go +++ b/pkg/tsdb/prometheus/framing_test.go @@ -132,5 +132,5 @@ func runQuery(response []byte, query PrometheusQuery) (*backend.QueryDataRespons } s := Service{tracer: tracer} - return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } diff --git a/pkg/tsdb/prometheus/prometeus_bench_test.go b/pkg/tsdb/prometheus/prometeus_bench_test.go index 5bf01f8ae57..b7365e30c44 100644 --- a/pkg/tsdb/prometheus/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/prometeus_bench_test.go @@ -28,7 +28,7 @@ func BenchmarkJson(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } } diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt index 2aad902f0db..68c697dd90e 100644 --- a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////KAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAJgAAAADAAAAUAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAABAAAAAFAAAAMSAvIDAAAAAEAAAAbmFtZQAAAACw/v//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAAOwAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAKAAAACgAAAAAAADAaAAAAADAAAAUAAAACwAAAAEAAAAOP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAXP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAB8////CAAAACgAAAAdAAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IjEgLyAwIn0AAAAGAAAAY29uZmlnAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAABEFRTUKckWAA6wT9QpyRYA2EqL1CnJFgAAAAAAAPB/AAAAAAAA8H8AAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAAA4AgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAmAAAAAMAAABQAAAAKAAAAAQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAIz+//8IAAAAEAAAAAUAAAAxIC8gMAAAAAQAAABuYW1lAAAAALD+//8IAAAALAAAACIAAAB7ImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In19AAAEAAAAbWV0YQAAAAACAAAA7AAAABgAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAoAAAAKAAAAAAAAMBoAAAAAMAAABQAAAALAAAAAQAAAA4////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABc////CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAHz///8IAAAAKAAAAB0AAAB7ImRpc3BsYXlOYW1lRnJvbURTIjoiMSAvIDAifQAAAAYAAABjb25maWcAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABQAgAAQVJST1cx +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt index e63cc8828b8..fe85719448f 100644 --- a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt @@ -6,23 +6,17 @@ Frame[0] { } } Name: go_goroutines{job="prometheus"} -Dimensions: 2 Fields by 9 Rows +Dimensions: 2 Fields by 3 Rows +-------------------------------+------------------------------------------------+ | Name: Time | Name: Value | | Labels: | Labels: __name__=go_goroutines, job=prometheus | | Type: []time.Time | Type: []*float64 | +-------------------------------+------------------------------------------------+ -| 2022-01-11 08:25:30 +0000 UTC | null | -| 2022-01-11 08:25:31 +0000 UTC | null | -| 2022-01-11 08:25:32 +0000 UTC | null | | 2022-01-11 08:25:33 +0000 UTC | 21 | | 2022-01-11 08:25:34 +0000 UTC | 32 | -| 2022-01-11 08:25:35 +0000 UTC | null | -| 2022-01-11 08:25:36 +0000 UTC | null | | 2022-01-11 08:25:37 +0000 UTC | 43 | -| 2022-01-11 08:25:38 +0000 UTC | null | +-------------------------------+------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt index f4d34e2a377..78de205a99e 100644 --- a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt index 7866187f0e6..bd1e0d265da 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt @@ -38,5 +38,5 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////QAMAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAApAAAACgAAAAEAAAAUP3//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABw/f//CAAAAGQAAABbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjIwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAEAAAAbmFtZQAAAADo/f//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAALQBAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAGgBAABoAQAAAAADAWgBAAADAAAAvAAAACwAAAAEAAAAcP7//wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAlP7//wgAAAB4AAAAbQAAAHsiX19uYW1lX18iOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWwiLCJjb2RlIjoiMjAwIiwiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAGAAAAbGFiZWxzAAAg////CAAAAIQAAAB5AAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPVwiMjAwXCIsIGhhbmRsZXI9XCIvYXBpL3YxL3F1ZXJ5X3JhbmdlXCIsIGpvYj1cInByb21ldGhldXNcIn0ifQAAAAYAAABjb25maWcAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAARBUU1CnJFgAOsE/UKckWANhKi9QpyRYAAAAAAAA1QAAAAAAAAEBAAAAAAACARUAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAAAwABAAAAUAMAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAADsAAAAAwAAAKQAAAAoAAAABAAAAFD9//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAcP3//wgAAABkAAAAWwAAAHByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPSIyMDAiLCBoYW5kbGVyPSIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwgam9iPSJwcm9tZXRoZXVzIn0ABAAAAG5hbWUAAAAA6P3//wgAAAAsAAAAIgAAAHsiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJtYXRyaXgifX0AAAQAAABtZXRhAAAAAAIAAAC0AQAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABoAQAAaAEAAAAAAwFoAQAAAwAAALwAAAAsAAAABAAAAHD+//8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAJT+//8IAAAAeAAAAG0AAAB7Il9fbmFtZV9fIjoicHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFsIiwiY29kZSI6IjIwMCIsImhhbmRsZXIiOiIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAABgAAAGxhYmVscwAAIP///wgAAACEAAAAeQAAAHsiZGlzcGxheU5hbWVGcm9tRFMiOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT1cIjIwMFwiLCBoYW5kbGVyPVwiL2FwaS92MS9xdWVyeV9yYW5nZVwiLCBqb2I9XCJwcm9tZXRoZXVzXCJ9In0AAAAGAAAAY29uZmlnAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAcAMAAEFSUk9XMQ== -FRAME=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 +FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/time_series_query.go b/pkg/tsdb/prometheus/time_series_query.go index f86638771b8..ae7abf12406 100644 --- a/pkg/tsdb/prometheus/time_series_query.go +++ b/pkg/tsdb/prometheus/time_series_query.go @@ -47,7 +47,7 @@ const ( ExemplarQueryType TimeSeriesQueryType = "exemplar" ) -func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery, fillNulls bool) (*backend.QueryDataResponse, error) { +func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery) (*backend.QueryDataResponse, error) { result := backend.QueryDataResponse{ Responses: backend.Responses{}, } @@ -101,7 +101,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P } } - frames, err := parseTimeSeriesResponse(response, query, fillNulls) + frames, err := parseTimeSeriesResponse(response, query) if err != nil { return &result, err } @@ -128,12 +128,7 @@ func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.Query return &result, err } - fillNulls := true - if req.Headers["FromAlert"] == "true" { - fillNulls = false - } - - return s.runQueries(ctx, client, queries, fillNulls) + return s.runQueries(ctx, client, queries) } func formatLegend(metric model.Metric, query *PrometheusQuery) string { @@ -207,7 +202,7 @@ func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, d return qs, nil } -func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery, fillNulls bool) (data.Frames, error) { +func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery) (data.Frames, error) { var ( frames = data.Frames{} nextFrames = data.Frames{} @@ -219,11 +214,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P switch v := value.(type) { case model.Matrix: - if fillNulls { - nextFrames = matrixToDataFramesWithNullFill(v, query, nextFrames) - } else { - nextFrames = matrixToDataFrames(v, query, nextFrames) - } + nextFrames = matrixToDataFrames(v, query, nextFrames) case model.Vector: nextFrames = vectorToDataFrames(v, query, nextFrames) case *model.Scalar: @@ -317,56 +308,6 @@ func interpolateVariables(model *QueryModel, interval time.Duration, timeRange t return expr } -func matrixToDataFramesWithNullFill(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { - for _, v := range matrix { - tags := make(map[string]string, len(v.Metric)) - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - - baseTimestamp := alignTimeRange(query.Start, query.Step, query.UtcOffsetSec).UnixMilli() - endTimestamp := alignTimeRange(query.End, query.Step, query.UtcOffsetSec).UnixMilli() - // For each step we create 1 data point. This results in range / step + 1 data points. - datapointsCount := int((endTimestamp-baseTimestamp)/query.Step.Milliseconds()) + 1 - - timeField := data.NewFieldFromFieldType(data.FieldTypeTime, datapointsCount) - valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, datapointsCount) - idx := 0 - - for _, pair := range v.Values { - timestamp := int64(pair.Timestamp) - value := float64(pair.Value) - - for t := baseTimestamp; t < timestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - timeField.Set(idx, time.Unix(pair.Timestamp.Unix(), 0).UTC()) - if !math.IsNaN(value) { - valueField.Set(idx, &value) - } - baseTimestamp = timestamp + query.Step.Milliseconds() - idx++ - } - - for t := baseTimestamp; t <= endTimestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - name := formatLegend(v.Metric, query) - timeField.Name = data.TimeSeriesTimeFieldName - valueField.Name = data.TimeSeriesValueFieldName - valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} - valueField.Labels = tags - - frames = append(frames, newDataFrame(name, "matrix", timeField, valueField)) - } - - return frames -} - func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { for _, v := range matrix { tags := make(map[string]string, len(v.Metric)) @@ -387,6 +328,7 @@ func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data name := formatLegend(v.Metric, query) timeField.Name = data.TimeSeriesTimeFieldName + timeField.Config = &data.FieldConfig{Interval: float64(query.Step.Milliseconds())} valueField.Name = data.TimeSeriesValueFieldName valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} valueField.Labels = tags diff --git a/pkg/tsdb/prometheus/time_series_query_test.go b/pkg/tsdb/prometheus/time_series_query_test.go index 2f5ec14f160..fc50c3fb78f 100644 --- a/pkg/tsdb/prometheus/time_series_query_test.go +++ b/pkg/tsdb/prometheus/time_series_query_test.go @@ -556,7 +556,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) // Test fields @@ -594,7 +594,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(5, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -631,16 +631,16 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) - require.Equal(t, res[0].Fields[0].Len(), 4) - require.Equal(t, res[0].Fields[0].At(1), time.Unix(2, 0).UTC()) - require.Equal(t, res[0].Fields[0].At(2), time.Unix(3, 0).UTC()) - require.Equal(t, res[0].Fields[1].Len(), 4) - require.Nil(t, res[0].Fields[1].At(1)) - require.Nil(t, res[0].Fields[1].At(2)) + require.Equal(t, res[0].Fields[0].Len(), 2) + require.Equal(t, time.Unix(1, 0).UTC(), res[0].Fields[0].At(0)) + require.Equal(t, time.Unix(4, 0).UTC(), res[0].Fields[0].At(1)) + require.Equal(t, res[0].Fields[1].Len(), 2) + require.Equal(t, float64(1), *res[0].Fields[1].At(0).(*float64)) + require.Equal(t, float64(4), *res[0].Fields[1].At(1).(*float64)) }) t.Run("matrix response with from alerting missed data points should be parsed correctly", func(t *testing.T) { @@ -662,7 +662,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, false) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -693,7 +693,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) var nilPointer *float64 @@ -713,7 +713,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -740,7 +740,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } query := &PrometheusQuery{} - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) From bb88cf683cefc750004032c5c518501269a01694 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 3 Feb 2022 14:17:05 +0100 Subject: [PATCH 11/34] Alerting: Fix alert notification template (#44761) * Wrap the inner template into div to prevent premailer from breaking the HTML structure * Remove test row * Add wrapper explanation * Remove redundant code * Add empty line --- emails/templates/ng_alert_notification.html | 116 ++++++++--------- public/emails/ng_alert_notification.html | 133 ++++++++++---------- 2 files changed, 128 insertions(+), 121 deletions(-) diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html index 1f967e53353..b7c7f6156dd 100644 --- a/emails/templates/ng_alert_notification.html +++ b/emails/templates/ng_alert_notification.html @@ -1,11 +1,14 @@ + +
+ [[Subject .Subject "[[.Title]]"]] [[ define "alert" ]] - - - Value: [[ .ValueString ]] - - + + + Value: [[ .ValueString ]] + + [[ if gt (len .Annotations.SortedPairs) 0 ]] @@ -187,62 +190,63 @@ - [[ if gt (len .Alerts.Firing) 0 ]] - - - - [[ range .Alerts.Firing ]] + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ range .Alerts.Firing ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] + [[ if gt (len .Alerts.Resolved) 0 ]] + + + + [[ range .Alerts.Resolved ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - [[ if gt (len .Alerts.Resolved) 0 ]] - - - - [[ range .Alerts.Resolved ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - - - -
- Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
+ Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Firing + + [[ .Labels.alertname ]] +
+ Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Resolved + + [[ .Labels.alertname ]] +
- Firing - - [[ .Labels.alertname ]] + + Go to alerts page
- Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Resolved - - [[ .Labels.alertname ]] -
- Go to alerts page -
[[ end ]] + +
diff --git a/public/emails/ng_alert_notification.html b/public/emails/ng_alert_notification.html index a873e1299b7..8f0f2828b8a 100644 --- a/public/emails/ng_alert_notification.html +++ b/public/emails/ng_alert_notification.html @@ -183,7 +183,7 @@ text-decoration: underline; @@ -200,18 +200,21 @@ text-decoration: underline;
- +
- + - - + + + {{ if gt (len .Annotations.SortedPairs) 0 }}
- {{Subject .Subject "{{.Title}}"}} + +
+ +{{Subject .Subject "{{.Title}}"}} {{ define "alert" }} -
- Value: {{ .ValueString }} -
+ Value: {{ .ValueString }} +
@@ -233,25 +236,25 @@ text-decoration: underline; {{ if .SilenceURL }} - + Silence {{ end }} {{ if .Annotations.runbook_url }} - + View Runbook {{ end }} {{ if .DashboardURL}} - + Go to Dashboard {{ end }} {{ if .PanelURL}} - + Go to Panel {{ end }} @@ -277,62 +280,62 @@ text-decoration: underline;
- {{ if gt (len .Alerts.Firing) 0 }} - - - - {{ range .Alerts.Firing }} + {{ if gt (len .Alerts.Firing) 0 }} + + + + {{ range .Alerts.Firing }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} + {{ if gt (len .Alerts.Resolved) 0 }} + + + + {{ range .Alerts.Resolved }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} - - - {{ template "alert" . }} - {{ end }} - {{ end }} - {{ if gt (len .Alerts.Resolved) 0 }} - - - - {{ range .Alerts.Resolved }} - - - - - {{ template "alert" . }} - {{ end }} - {{ end }} - - -
- Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
+ Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
+ Firing + + {{ .Labels.alertname }} +
+ Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
+ Resolved + + {{ .Labels.alertname }} +
- Firing - - {{ .Labels.alertname }} + + Go to alerts page
- Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
- Resolved - - {{ .Labels.alertname }} -
- Go to alerts page -
-
- - + + {{ end }} +
- + @@ -341,7 +344,7 @@ text-decoration: underline; -
+

Sent by Grafana v{{.BuildVersion}} @@ -355,9 +358,9 @@ text-decoration: underline;

- - - - + + + + From 6415b9a54dec6e9763f2faee076ce6578564b072 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Thu, 3 Feb 2022 13:45:29 +0000 Subject: [PATCH 12/34] Explore: avoid locking timepicker when range is inverted (#44790) * Explore: avoid locking timepicker when range is inverted * Explore: prevent time picker to lock if from & to search parameters are present --- public/app/core/utils/explore.test.ts | 14 ++++++++++++++ public/app/core/utils/explore.ts | 12 +++++++----- public/app/features/explore/Wrapper.test.tsx | 10 +++++++++- public/app/features/explore/Wrapper.tsx | 15 +++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a25e8f75a54..97fde9b8820 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -11,6 +11,7 @@ import { getExploreUrl, GetExploreUrlArguments, getTimeRangeFromUrl, + getTimeRange, } from './explore'; import store from 'app/core/store'; import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data'; @@ -361,6 +362,19 @@ describe('getTimeRangeFromUrl', () => { }); }); +describe('getTimeRange', () => { + describe('should flip from and to when from is after to', () => { + const rawRange = { + from: 'now', + to: 'now-6h', + }; + + const range = getTimeRange('utc', rawRange, 0); + + expect(range.from.isBefore(range.to)).toBe(true); + }); +}); + describe('getRefIds', () => { describe('when called with a null value', () => { it('then it should return empty array', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 77a29040e18..f10af2037f1 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -358,11 +358,13 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSour }; export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange, fiscalYearStartMonth: number): TimeRange => { - return { - from: dateMath.parse(rawRange.from, false, timeZone as any, fiscalYearStartMonth)!, - to: dateMath.parse(rawRange.to, true, timeZone as any, fiscalYearStartMonth)!, - raw: rawRange, - }; + let range = rangeUtil.convertRawToRange(rawRange, timeZone, fiscalYearStartMonth); + + if (range.to.isBefore(range.from)) { + range = rangeUtil.convertRawToRange({ from: range.raw.to, to: range.raw.from }, timeZone, fiscalYearStartMonth); + } + + return range; }; const parseRawTime = (value: string | DateTime): TimeFragment | null => { diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 89518be6e2d..1c964abef98 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -314,12 +314,20 @@ describe('Wrapper', () => { store.dispatch(splitOpen({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any); await waitFor(() => expect(document.title).toEqual('Explore - loki | elastic - Grafana')); }); + + it('removes `from` and `to` parameters from url when first mounted', () => { + setup({ searchParams: 'from=1&to=2&orgId=1' }); + + expect(locationService.getSearchObject()).toEqual(expect.not.objectContaining({ from: '1', to: '2' })); + expect(locationService.getSearchObject()).toEqual(expect.objectContaining({ orgId: '1' })); + }); }); type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type SetupOptions = { datasources?: DatasourceSetup[]; query?: any; + searchParams?: string; }; function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi }; store: EnhancedStore } { @@ -368,7 +376,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou }, }; - locationService.push({ pathname: '/explore' }); + locationService.push({ pathname: '/explore', search: options?.searchParams }); if (options?.query) { locationService.partial(options.query); diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index abc725c59fb..f38a2e6616e 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -10,6 +10,7 @@ import { Branding } from '../../core/components/Branding/Branding'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from 'app/types'; +import { locationService } from '@grafana/runtime'; interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {} interface OwnProps {} @@ -38,6 +39,20 @@ class WrapperUnconnected extends PureComponent { lastSavedUrl.left = undefined; lastSavedUrl.right = undefined; + // timeSrv (which is used internally) on init reads `from` and `to` param from the URL and updates itself + // using those value regardless of what is passed to the init method. + // The updated value is then used by Explore to get the range for each pane. + // This means that if `from` and `to` parameters are present in the URL, + // it would be impossible to change the time range in Explore. + // We are only doing this on mount for 2 reasons: + // 1: Doing it on update means we'll enter a render loop. + // 2: when parsing time in Explore (before feeding it to timeSrv) we make sure `from` is before `to` inside + // each pane state in order to not trigger un URL update from timeSrv. + const searchParams = locationService.getSearchObject(); + if (searchParams.from || searchParams.to) { + locationService.partial({ from: undefined, to: undefined }, true); + } + const richHistory = getRichHistory(); this.props.richHistoryUpdatedAction({ richHistory }); } From a943bf996323de90549d08f07dbd6e9402a49349 Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Thu, 3 Feb 2022 15:13:14 +0100 Subject: [PATCH 13/34] Improve prettier:check output (#44816) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e421573e495..66ebbda259d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "packages:typecheck": "lerna run typecheck", "packages:clean": "lerna run clean", "precommit": "yarn run lint-staged", - "prettier:check": "prettier --list-different \"**/*.{scss,md,mdx}\"", + "prettier:check": "prettier --check --list-different=false --loglevel=warn \"**/*.{scss,md,mdx}\"", "prettier:write": "prettier --list-different \"**/*.{scss,md,mdx}\" --write", "start": "yarn themes:generate && yarn dev --watch", "start:noTsCheck": "yarn start --env noTsCheck=1", From afac7701cb15e323312e16f201039aabe6e2b660 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 15:15:48 +0100 Subject: [PATCH 14/34] ReleaseNotes: Updated changelog and release notes for 8.4.0-beta1 (#44822) --- CHANGELOG.md | 20 ++++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes-8-4-0-beta1.md | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-4-0-beta1.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e10774dee..a2b75759278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ + + +# 8.4.0-beta1 (2022-02-02) + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) + + # 8.3.4 (2022-01-17) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 2ae655180d9..907d9f47afd 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) - [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) - [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}}) - [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-0-beta1.md b/docs/sources/release-notes/release-notes-8-4-0-beta1.md new file mode 100644 index 00000000000..1d9aaf557bb --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-0-beta1.md @@ -0,0 +1,23 @@ ++++ +title = "Release notes for Grafana 8.4.0-beta1" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.0-beta1 + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) From 0c2ba819a7a0772fdec7358c80f2e31e2246eac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 3 Feb 2022 15:23:38 +0100 Subject: [PATCH 15/34] Loki: use generic grafana null-insertion mechanism (#44826) * loki: refactor: return dataframes instead of timeseries * fixed unit test * removed unused import --- .../datasource/loki/datasource.test.ts | 19 ++++--- .../loki/result_transformer.test.ts | 6 +-- .../datasource/loki/result_transformer.ts | 50 ++++++++++++------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index da697baefa1..2c75f36b5da 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -10,7 +10,6 @@ import { FieldType, LogRowModel, MutableDataFrame, - TimeSeries, toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, config } from '@grafana/runtime'; @@ -334,7 +333,7 @@ describe('LokiDatasource', () => { expect(ds.runRangeQuery).toBeCalled(); }); - it('should return series data for metrics range queries', async () => { + it('should return dataframe data for metrics range queries', async () => { const ds = createLokiDSForTests(); const options = getQueryOptions({ targets: [{ expr: metricsQuery, refId: 'B', range: true }], @@ -345,11 +344,19 @@ describe('LokiDatasource', () => { await expect(ds.query(options)).toEmitValuesWith((received) => { const result = received[0]; - const timeSeries = result.data[0] as TimeSeries; + const frame = result.data[0] as DataFrame; - expect(timeSeries.meta?.preferredVisualisationType).toBe('graph'); - expect(timeSeries.refId).toBe('B'); - expect(timeSeries.datapoints[0]).toEqual([1.1, 1605715380000]); + expect(frame.meta?.preferredVisualisationType).toBe('graph'); + expect(frame.refId).toBe('B'); + frame.fields.forEach((field) => { + const value = field.values.get(0); + + if (field.type === FieldType.time) { + expect(value).toBe(1605715380000); + } else { + expect(value).toBe(1.1); + } + }); }); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index b2fb3682d4e..d9d0e0b99b8 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -290,8 +290,6 @@ describe('enhanceDataFrame', () => { * NOTE on time parameters: * - Input time series data has timestamps in sec (like Prometheus) * - Output time series has timestamps in ms (as expected for the chart lib) - * - Start/end parameters are in ns (as expected for Loki) - * - Step is in sec (like in Prometheus) */ const data: Array<[number, string]> = [ [1, '1'], @@ -300,12 +298,10 @@ describe('enhanceDataFrame', () => { ]; it('returns data as is if step, start, and end align', () => { - const options: Partial = { start: 1 * 1e9, end: 4 * 1e9, step: 1 }; - const result = ResultTransformer.lokiPointsToTimeseriesPoints(data, options as TransformerOptions); + const result = ResultTransformer.lokiPointsToTimeseriesPoints(data); expect(result).toEqual([ [1, 1000], [0, 2000], - [null, 3000], [1, 4000], ]); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 970cb1ddc45..ae7ef9ce46c 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -17,6 +17,7 @@ import { QueryResultMeta, TimeSeriesValue, ScopedVars, + toDataFrame, } from '@grafana/data'; import { getTemplateSrv, getDataSourceSrv } from '@grafana/runtime'; @@ -184,21 +185,16 @@ function lokiMatrixToTimeSeries(matrixResult: LokiMatrixResult, options: Transfo return { target: name, title: name, - datapoints: lokiPointsToTimeseriesPoints(matrixResult.values, options), + datapoints: lokiPointsToTimeseriesPoints(matrixResult.values), tags: matrixResult.metric, meta: options.meta, refId: options.refId, }; } -export function lokiPointsToTimeseriesPoints( - data: Array<[number, string]>, - options: TransformerOptions -): TimeSeriesValue[][] { - const stepMs = options.step * 1000; +export function lokiPointsToTimeseriesPoints(data: Array<[number, string]>): TimeSeriesValue[][] { const datapoints: TimeSeriesValue[][] = []; - let baseTimestampMs = options.start / 1e6; for (const [time, value] of data) { let datapointValue: TimeSeriesValue = parseFloat(value); @@ -207,19 +203,10 @@ export function lokiPointsToTimeseriesPoints( } const timestamp = time * 1000; - for (let t = baseTimestampMs; t < timestamp; t += stepMs) { - datapoints.push([null, t]); - } - baseTimestampMs = timestamp + stepMs; datapoints.push([datapointValue, timestamp]); } - const endTimestamp = options.end / 1e6; - for (let t = baseTimestampMs; t <= endTimestamp; t += stepMs) { - datapoints.push([null, t]); - } - return datapoints; } @@ -454,7 +441,7 @@ function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): }; } -export function rangeQueryResponseToTimeSeries( +function rangeQueryResponseToTimeSeries( response: LokiResponse, query: LokiRangeQueryRequest, target: LokiQuery, @@ -491,6 +478,33 @@ export function rangeQueryResponseToTimeSeries( } } +export function rangeQueryResponseToDataFrames( + response: LokiResponse, + query: LokiRangeQueryRequest, + target: LokiQuery, + responseListLength: number, + scopedVars: ScopedVars +): DataFrame[] { + const series = rangeQueryResponseToTimeSeries(response, query, target, responseListLength, scopedVars); + const frames = series.map((s) => toDataFrame(s)); + + const { step } = query; + + if (step != null) { + const intervalMs = step * 1000; + + frames.forEach((frame) => { + frame.fields.forEach((field) => { + if (field.type === FieldType.time) { + field.config.interval = intervalMs; + } + }); + }); + } + + return frames; +} + export function processRangeQueryResponse( response: LokiResponse, target: LokiQuery, @@ -511,7 +525,7 @@ export function processRangeQueryResponse( case LokiResultType.Vector: case LokiResultType.Matrix: return of({ - data: rangeQueryResponseToTimeSeries( + data: rangeQueryResponseToDataFrames( response, query, { From 5f16e4cedc39678ec80dadd6dd7c3be258ef6d9b Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Thu, 3 Feb 2022 16:23:50 +0200 Subject: [PATCH 16/34] Rename build-e2e-publish pipelines (#44836) --- .drone.yml | 10 +++++----- scripts/drone/pipelines/release.star | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.drone.yml b/.drone.yml index f8d744be837..2b826efa4a0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1094,7 +1094,7 @@ type: docker --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release +name: oss-build-e2e-publish-release node: type: no-parallel platform: @@ -1599,7 +1599,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release +- oss-build-e2e-publish-release - oss-test-release - oss-integration-tests-release kind: pipeline @@ -2858,7 +2858,7 @@ volumes: --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release-branch +name: oss-build-e2e-publish-release-branch node: type: no-parallel platform: @@ -3305,7 +3305,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release-branch +- oss-build-e2e-publish-release-branch - oss-test-release-branch - oss-integration-tests-release-branch kind: pipeline @@ -4191,6 +4191,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 4d1a5696bf1e510fb51a021c07e240c50cb913724ce08ed52cce037ff02dd8de +hmac: f26fc6de1d7ec3cf5608b70c851c8cf2b998e07abad8c52714e72ad072492387 ... diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 1c592e9d730..88be5a4a675 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -285,7 +285,7 @@ def get_oss_pipelines(trigger, ver_mode): ) pipelines = [ pipeline( - name='oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], + name='oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps + package_steps + publish_steps, volumes=volumes, @@ -308,7 +308,7 @@ def get_oss_pipelines(trigger, ver_mode): ]) deps = { 'depends_on': [ - 'oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), + 'oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), 'oss-test-{}'.format(ver_mode), 'oss-integration-tests-{}'.format(ver_mode) ] From bc7e55d99bb43f3f870c3ebe7b960e147462b9dd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 3 Feb 2022 16:20:02 +0100 Subject: [PATCH 17/34] Chore: Fix log filters (#44681) --- pkg/api/frontend_logging_test.go | 2 +- pkg/infra/log/composite_logger.go | 25 ++ pkg/infra/log/interface.go | 2 +- pkg/infra/log/log.go | 335 ++++++++++++------ pkg/infra/log/syslog.go | 2 +- pkg/infra/usagestats/service/service.go | 2 +- pkg/login/social/generic_oauth_test.go | 2 +- pkg/middleware/recovery.go | 2 +- .../loader/initializer/initializer_test.go | 6 +- pkg/plugins/manager/loader/loader_test.go | 4 +- .../login/loginservice/loginservice_test.go | 2 +- pkg/setting/setting.go | 2 + 12 files changed, 261 insertions(+), 125 deletions(-) create mode 100644 pkg/infra/log/composite_logger.go diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index a8e204fdd25..84d8953f7aa 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -42,7 +42,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro })) origHandler := frontendLogger.GetLogger() - frontendLogger.AddLogger(newfrontendLogger, "info", map[string]level.Option{}) + frontendLogger.SetLogger(level.NewFilter(newfrontendLogger, level.AllowInfo())) sourceMapReads := []SourceMapReadRecord{} t.Cleanup(func() { diff --git a/pkg/infra/log/composite_logger.go b/pkg/infra/log/composite_logger.go new file mode 100644 index 00000000000..5c238673347 --- /dev/null +++ b/pkg/infra/log/composite_logger.go @@ -0,0 +1,25 @@ +package log + +import gokitlog "github.com/go-kit/log" + +type compositeLogger struct { + loggers []gokitlog.Logger +} + +func newCompositeLogger(loggers ...gokitlog.Logger) *compositeLogger { + if len(loggers) == 0 { + loggers = []gokitlog.Logger{} + } + + return &compositeLogger{loggers: loggers} +} + +func (l *compositeLogger) Log(keyvals ...interface{}) error { + for _, logger := range l.loggers { + if err := logger.Log(keyvals...); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/infra/log/interface.go b/pkg/infra/log/interface.go index 523f9a3bc33..c0909d682cb 100644 --- a/pkg/infra/log/interface.go +++ b/pkg/infra/log/interface.go @@ -12,7 +12,7 @@ const ( type Logger interface { // New returns a new Logger that has this logger's context plus the given context - New(ctx ...interface{}) MultiLoggers + New(ctx ...interface{}) *ConcreteLogger Log(keyvals ...interface{}) error diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index ba531ca633f..a4d38d84256 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -7,9 +7,12 @@ package log import ( "fmt" "io" + "log" "os" "path/filepath" + "sort" "strings" + "sync" "time" gokitlog "github.com/go-kit/log" @@ -25,8 +28,7 @@ import ( var loggersToClose []DisposableHandler var loggersToReload []ReloadableHandler -var filters map[string]level.Option -var Root MultiLoggers +var root *logManager const ( // top 7 calls in the stack are within logger @@ -37,11 +39,212 @@ const ( func init() { loggersToClose = make([]DisposableHandler, 0) loggersToReload = make([]ReloadableHandler, 0) - filters = map[string]level.Option{} // Use console by default format := getLogFormat("console") - Root.AddLogger(format(os.Stderr), "info", filters) + logger := level.NewFilter(format(os.Stderr), level.AllowInfo()) + root = newManager(logger) +} + +// logManager manage loggers +type logManager struct { + *ConcreteLogger + loggersByName map[string]*ConcreteLogger + logFilters []LogWithFilters + mutex sync.RWMutex +} + +func newManager(logger gokitlog.Logger) *logManager { + return &logManager{ + ConcreteLogger: newConcreteLogger(logger), + loggersByName: map[string]*ConcreteLogger{}, + } +} + +func (lm *logManager) initialize(loggers []LogWithFilters) { + lm.mutex.Lock() + defer lm.mutex.Unlock() + + defaultLoggers := make([]gokitlog.Logger, len(loggers)) + for index, logger := range loggers { + defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } + + lm.ConcreteLogger.SetLogger(&compositeLogger{loggers: defaultLoggers}) + lm.logFilters = loggers + + loggersByName := []string{} + for k := range lm.loggersByName { + loggersByName = append(loggersByName, k) + } + sort.Strings(loggersByName) + + for _, name := range loggersByName { + ctxLoggers := make([]gokitlog.Logger, len(loggers)) + + for index, logger := range loggers { + if filterLevel, exists := logger.filters[name]; !exists { + ctxLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } else { + ctxLoggers[index] = level.NewFilter(logger.val, filterLevel) + } + } + + lm.loggersByName[name].SetLogger(&compositeLogger{loggers: ctxLoggers}) + } +} + +func (lm *logManager) SetLogger(logger gokitlog.Logger) { + lm.ConcreteLogger.SetLogger(logger) +} + +func (lm *logManager) GetLogger() gokitlog.Logger { + return lm.ConcreteLogger.GetLogger() +} + +func (lm *logManager) Log(args ...interface{}) error { + lm.mutex.RLock() + defer lm.mutex.RUnlock() + if err := lm.ConcreteLogger.Log(args...); err != nil { + log.Println("Logging error", "error", err) + } + + return nil +} + +func (lm *logManager) New(ctx ...interface{}) *ConcreteLogger { + lm.mutex.Lock() + defer lm.mutex.Unlock() + if len(ctx) == 0 { + return lm.ConcreteLogger + } + + loggerName, ok := ctx[0].(string) + if !ok { + return lm.ConcreteLogger + } + + if logger, exists := lm.loggersByName[loggerName]; exists { + return logger + } + + ctx = append([]interface{}{"logger"}, ctx...) + + if len(lm.logFilters) == 0 { + ctxLogger := newConcreteLogger(lm.logger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger + } + + compositeLogger := newCompositeLogger() + for _, logWithFilter := range lm.logFilters { + filterLevel, ok := logWithFilter.filters[loggerName] + if ok { + logWithFilter.val = level.NewFilter(logWithFilter.val, filterLevel) + } else { + logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) + } + + compositeLogger.loggers = append(compositeLogger.loggers, logWithFilter.val) + } + + ctxLogger := newConcreteLogger(compositeLogger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger +} + +type ConcreteLogger struct { + ctx []interface{} + logger gokitlog.Logger + mutex sync.RWMutex +} + +func newConcreteLogger(logger gokitlog.Logger, ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + ctx = []interface{}{} + } else { + logger = gokitlog.With(logger, ctx...) + } + + return &ConcreteLogger{ + ctx: ctx, + logger: logger, + } +} + +func (cl *ConcreteLogger) SetLogger(logger gokitlog.Logger) { + cl.mutex.Lock() + cl.logger = gokitlog.With(logger, cl.ctx...) + cl.mutex.Unlock() +} + +func (cl *ConcreteLogger) GetLogger() gokitlog.Logger { + cl.mutex.Lock() + defer cl.mutex.Unlock() + return cl.logger +} + +func (cl *ConcreteLogger) Warn(msg string, args ...interface{}) { + _ = cl.log(msg, level.WarnValue(), args...) +} + +func (cl *ConcreteLogger) Debug(msg string, args ...interface{}) { + // args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) + _ = cl.log(msg, level.DebugValue(), args...) +} + +func (cl *ConcreteLogger) Error(msg string, args ...interface{}) { + _ = cl.log(msg, level.ErrorValue(), args...) +} + +func (cl *ConcreteLogger) Info(msg string, args ...interface{}) { + _ = cl.log(msg, level.InfoValue(), args...) +} + +func (cl *ConcreteLogger) log(msg string, logLevel level.Value, args ...interface{}) error { + cl.mutex.RLock() + logger := gokitlog.With(cl.logger, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) + cl.mutex.RUnlock() + + args = append([]interface{}{level.Key(), logLevel, "msg", msg}, args...) + + return logger.Log(args...) +} + +func (cl *ConcreteLogger) Log(keyvals ...interface{}) error { + cl.mutex.RLock() + defer cl.mutex.RUnlock() + return cl.logger.Log(keyvals...) +} + +func (cl *ConcreteLogger) New(ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + root.New() + } + + keyvals := []interface{}{} + + if len(cl.ctx)%2 == 1 { + cl.ctx = append(cl.ctx, nil) + } + + for i := 0; i < len(cl.ctx); i += 2 { + k, v := cl.ctx[i], cl.ctx[i+1] + + if k == "logger" { + continue + } + + keyvals = append(keyvals, k, v) + } + + keyvals = append(keyvals, ctx...) + + return root.New(keyvals...) +} + +func New(ctx ...interface{}) *ConcreteLogger { + return root.New(ctx...) } type LogWithFilters struct { @@ -50,111 +253,23 @@ type LogWithFilters struct { maxLevel level.Option } -type MultiLoggers struct { - loggers []LogWithFilters -} - -func (ml *MultiLoggers) AddLogger(val gokitlog.Logger, levelName string, filters map[string]level.Option) { - logger := LogWithFilters{val: val, filters: filters, maxLevel: getLogLevelFromString(levelName)} - ml.loggers = append(ml.loggers, logger) -} - -func (ml *MultiLoggers) SetLogger(des MultiLoggers) { - ml.loggers = des.loggers -} - -func (ml *MultiLoggers) GetLogger() MultiLoggers { - return *ml -} - -func (ml MultiLoggers) Warn(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.WarnValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Debug(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Error(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.ErrorValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Info(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.InfoValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Log(keyvals ...interface{}) error { - for _, multilogger := range ml.loggers { - multilogger.val = gokitlog.With(multilogger.val, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) - if err := multilogger.val.Log(keyvals...); err != nil { - return err - } - } - return nil -} - -// New creates a new logger from the existing one with additional context -func (ml MultiLoggers) New(ctx ...interface{}) MultiLoggers { - return with(ml, gokitlog.With, ctx) -} - -// New creates MultiLoggers with the provided context and caller that is added as a suffix. -// The first element of the context must be the logger name -func New(ctx ...interface{}) MultiLoggers { +func with(ctxLogger *ConcreteLogger, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) *ConcreteLogger { if len(ctx) == 0 { - return Root + return ctxLogger } - var newloger MultiLoggers - ctx = append([]interface{}{"logger"}, ctx...) - for _, logWithFilter := range Root.loggers { - logWithFilter.val = gokitlog.With(logWithFilter.val, ctx...) - v, ok := logWithFilter.filters[ctx[0].(string)] - if ok { - logWithFilter.val = level.NewFilter(logWithFilter.val, v) - } else { - logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) - } - newloger.loggers = append(newloger.loggers, logWithFilter) - } - return newloger -} -func with(loggers MultiLoggers, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) MultiLoggers { - if len(ctx) == 0 { - return loggers - } - var newloger MultiLoggers - for _, l := range loggers.loggers { - l.val = withFunc(l.val, ctx...) - newloger.loggers = append(newloger.loggers, l) - } - return newloger + ctxLogger.logger = withFunc(ctxLogger.logger, ctx...) + return ctxLogger } // WithPrefix adds context that will be added to the log message -func WithPrefix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithPrefix, ctx) +func WithPrefix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithPrefix, ctx) } // WithSuffix adds context that will be appended at the end of the log message -func WithSuffix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithSuffix, ctx) +func WithSuffix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithSuffix, ctx) } var logLevels = map[string]level.Option{ @@ -177,7 +292,7 @@ func getLogLevelFromString(levelName string) level.Option { loglevel, ok := logLevels[levelName] if !ok { - _ = level.Error(Root).Log("Unknown log level", "level", levelName) + _ = level.Error(root).Log("Unknown log level", "level", levelName) return level.AllowError() } @@ -282,7 +397,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { mode = strings.TrimSpace(mode) sec, err := cfg.GetSection("log." + mode) if err != nil { - _ = level.Error(Root).Log("Unknown log mode", "mode", mode) + _ = level.Error(root).Log("Unknown log mode", "mode", mode) return errutil.Wrapf(err, "failed to get config section log.%s", mode) } @@ -301,7 +416,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log")) dpath := filepath.Dir(fileName) if err := os.MkdirAll(dpath, os.ModePerm); err != nil { - _ = level.Error(Root).Log("Failed to create directory", "dpath", dpath, "err", err) + _ = level.Error(root).Log("Failed to create directory", "dpath", dpath, "err", err) return errutil.Wrapf(err, "failed to create log directory %q", dpath) } fileHandler := NewFileWriter() @@ -313,7 +428,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileHandler.Daily = sec.Key("daily_rotate").MustBool(true) fileHandler.Maxdays = sec.Key("max_days").MustInt64(7) if err := fileHandler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) + _ = level.Error(root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) return errutil.Wrapf(err, "failed to initialize file handler") } @@ -336,20 +451,14 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { } } - // copy joined default + mode filters into filters - for key, value := range modeFilters { - if _, exist := filters[key]; !exist { - filters[key] = value - } - } - handler.filters = modeFilters handler.maxLevel = leveloption - // handler = LogFilterHandler(leveloption, modeFilters, handler) configLoggers = append(configLoggers, handler) } + if len(configLoggers) > 0 { - Root.loggers = configLoggers + root.initialize(configLoggers) } + return nil } diff --git a/pkg/infra/log/syslog.go b/pkg/infra/log/syslog.go index b1d818f5d58..150a8c6fae3 100644 --- a/pkg/infra/log/syslog.go +++ b/pkg/infra/log/syslog.go @@ -62,7 +62,7 @@ func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler { handler.Tag = sec.Key("tag").MustString("") if err := handler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to init syslog log handler", "error", err) + _ = level.Error(root).Log("Failed to init syslog log handler", "error", err) os.Exit(1) } handler.logger = gokitsyslog.NewSyslogLogger(handler.syslog, format, gokitsyslog.PrioritySelectorOption(selector)) diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 13ac15b2be1..bea01a403b4 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -25,7 +25,7 @@ type UsageStats struct { kvStore *kvstore.NamespacedKVStore RouteRegister routing.RouteRegister - log log.MultiLoggers + log log.Logger oauthProviders map[string]bool externalMetrics []usagestats.MetricsFunc diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index 9be66f1e3ac..ab9990b4f16 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -17,7 +17,7 @@ import ( func newLogger(name string, lev string) log.Logger { logger := log.New(name) - logger.AddLogger(logger, lev, map[string]level.Option{}) + logger.SetLogger(level.NewFilter(logger.GetLogger(), level.AllowInfo())) return logger } diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index ea8483a6911..fe5d3a22597 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -107,7 +107,7 @@ func Recovery(cfg *setting.Cfg) web.Handler { defer func() { if r := recover(); r != nil { var panicLogger log.Logger - panicLogger = log.Root + panicLogger = log.New("recovery") // try to get request logger ctx := contexthandler.FromContext(c.Req.Context()) if ctx != nil { diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index 8b6412e8372..d63a34384c3 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -212,11 +212,11 @@ func (*testLicensingService) FeatureEnabled(feature string) bool { } type fakeLogger struct { - log.MultiLoggers + *log.ConcreteLogger } -func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (f fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (f fakeLogger) Warn(_ string, _ ...interface{}) { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 59cb67d8e86..b21a9b6f02f 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -1128,8 +1128,8 @@ type fakeLogger struct { log.Logger } -func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (fl fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (fl fakeLogger) Info(_ string, _ ...interface{}) { diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 5cbd5acb510..14f1c146b0f 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -46,7 +46,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { buf := &bytes.Buffer{} - logger.AddLogger(log.NewLogfmtLogger(buf), "info", map[string]level.Option{}) + logger.SetLogger(level.NewFilter(log.NewLogfmtLogger(buf), level.AllowInfo())) user := createSimpleUser() externalUser := createSimpleExternalUser() diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 713bfc31d3f..aa7375f0e33 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -772,6 +772,8 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) { return nil, err } + cfg.Logger.Info(fmt.Sprintf("Starting %s", ApplicationName), "version", BuildVersion, "commit", BuildCommit, "branch", BuildBranch, "compiled", time.Unix(BuildStamp, 0)) + return parsedFile, err } From 602d62ebcc322bbb5d2fa3e65814781affcc3904 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 3 Feb 2022 15:27:05 +0000 Subject: [PATCH 18/34] Access control: FGAC for team sync endpoints (#44673) * add actions for team group sync * extend the hook to allow specifying whether the user is external * move user struct to type package * interface for permission service to allow mocking it * reuse existing permissions * test fix * refactor * linting --- pkg/api/team_members.go | 5 +-- pkg/services/accesscontrol/accesscontrol.go | 7 +++- .../accesscontrol/database/database_test.go | 3 +- .../database/resource_permissions.go | 8 ++--- .../resource_permissions_bench_test.go | 2 +- .../database/resource_permissions_test.go | 6 ++-- .../accesscontrol/resourcepermissions/api.go | 2 +- .../resourcepermissions/api_test.go | 2 +- .../resourcepermissions/options.go | 3 +- .../resourcepermissions/service.go | 9 ++--- .../resourcepermissions/service_mock.go | 33 +++++++++++++++++++ .../resourcepermissions/service_test.go | 4 +-- .../resourcepermissions/types/hook.go | 12 +++++-- .../resourceservices/resource_services.go | 8 ++--- 14 files changed, 76 insertions(+), 28 deletions(-) create mode 100644 pkg/services/accesscontrol/resourcepermissions/service_mock.go diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index cc0cd45daf4..3c094622086 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" @@ -152,7 +153,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { } teamIDString := strconv.FormatInt(teamId, 10) - if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, userId, teamIDString, ""); err != nil { + if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil { if errors.Is(err, models.ErrTeamNotFound) { return response.Error(404, "Team not found", nil) } @@ -171,7 +172,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { // Stubbable by tests. var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error { teamIDString := strconv.FormatInt(teamID, 10) - if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, userID, teamIDString, permission); err != nil { + if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil { return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err) } return nil diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 83fbeeaf4d3..3bf514d6371 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -37,13 +37,18 @@ type ResourcePermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) // SetUserPermission sets permission on resource for a user - SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*ResourcePermission, error) + SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) // SetTeamPermission sets permission on resource for a team SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*ResourcePermission, error) // SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error) } +type User struct { + ID int64 + IsExternal bool +} + // Metadata contains user accesses for a given resource // Ex: map[string]bool{"create":true, "delete": true} type Metadata map[string]bool diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index d4fa790178f..441b0deea29 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" @@ -81,7 +80,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { user, team := createUserAndTeam(t, sql, tt.orgID) for _, id := range tt.userPermissions { - _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{ + _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:write"}, Resource: "dashboards", ResourceID: id, diff --git a/pkg/services/accesscontrol/database/resource_permissions.go b/pkg/services/accesscontrol/database/resource_permissions.go index 436d6179df5..651fad1ee12 100644 --- a/pkg/services/accesscontrol/database/resource_permissions.go +++ b/pkg/services/accesscontrol/database/resource_permissions.go @@ -34,20 +34,20 @@ func (p *flatResourcePermission) Managed() bool { } func (s *AccessControlStore) SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) { - if userID == 0 { + if user.ID == 0 { return nil, models.ErrUserNotFound } var err error var permission *accesscontrol.ResourcePermission err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd) + permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd) if err == nil && hook != nil { - return hook(sess, orgID, userID, cmd.ResourceID, cmd.Permission) + return hook(sess, orgID, user, cmd.ResourceID, cmd.Permission) } return err diff --git a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go index 77a6a9fa398..27047433870 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go @@ -93,7 +93,7 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce _, err := ac.SetUserResourcePermission( context.Background(), accesscontrol.GlobalOrgID, - userIds[i], + accesscontrol.User{ID: userIds[i]}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{dsAction}, Resource: dsResource, diff --git a/pkg/services/accesscontrol/database/resource_permissions_test.go b/pkg/services/accesscontrol/database/resource_permissions_test.go index e029da2b1f5..00d6a5bfae9 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_test.go @@ -70,11 +70,11 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s, nil) + _, err := store.SetUserResourcePermission(context.Background(), test.orgID, accesscontrol.User{ID: test.userID}, s, nil) require.NoError(t, err) } - added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{ + added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, accesscontrol.SetResourcePermissionCommand{ Actions: test.actions, Resource: test.resource, ResourceID: test.resourceID, @@ -352,7 +352,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst }) require.NoError(t, err) - _, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{ + _, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Resource: resource, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 84a93834854..987a57948fd 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -131,7 +131,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, cmd.Permission) + _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: userID}, resourceID, cmd.Permission) if err != nil { return response.Error(http.StatusBadRequest, "failed to set user permission", err) } diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 6e596875cd4..bf64c93d0d4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -160,7 +160,7 @@ func TestApi_getPermissions(t *testing.T) { // seed user 1 with "View" permission on dashboard 1 u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) require.NoError(t, err) - _, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, "View") + _, err = service.SetUserPermission(context.Background(), u.OrgId, accesscontrol.User{ID: u.Id}, tt.resourceID, "View") require.NoError(t, err) // seed built in role Admin with "Edit" permission on dashboard 1 diff --git a/pkg/services/accesscontrol/resourcepermissions/options.go b/pkg/services/accesscontrol/resourcepermissions/options.go index 907a202ad5c..12fcb5acdb9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/options.go +++ b/pkg/services/accesscontrol/resourcepermissions/options.go @@ -3,6 +3,7 @@ package resourcepermissions import ( "context" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -28,7 +29,7 @@ type Options struct { // RoleGroup is the group name for the generated fixed roles RoleGroup string // OnSetUser if configured will be called each time a permission is set for a user - OnSetUser func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error + OnSetUser func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error // OnSetTeam if configured will be called each time a permission is set for a team OnSetTeam func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error // OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 6ab670a313e..ca5a587baa9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -16,7 +16,8 @@ import ( type Store interface { // SetUserResourcePermission sets permission for managed user role on a resource SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, + user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) @@ -100,7 +101,7 @@ func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID st }) } -func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { +func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { if !s.options.Assignments.Users { return nil, ErrInvalidAssignment } @@ -114,11 +115,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, re return nil, err } - if err := s.validateUser(ctx, orgID, userID); err != nil { + if err := s.validateUser(ctx, orgID, user.ID); err != nil { return nil, err } - return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{ + return s.store.SetUserResourcePermission(ctx, orgID, user, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Permission: permission, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/service_mock.go b/pkg/services/accesscontrol/resourcepermissions/service_mock.go new file mode 100644 index 00000000000..a1ad509e7cd --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/service_mock.go @@ -0,0 +1,33 @@ +package resourcepermissions + +import ( + "context" + + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +type MockService struct { + mock.Mock +} + +func (m *MockService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, resourceID) + return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, user, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index dbdcd3e4b5e..cec11542dd4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -46,13 +46,13 @@ func TestService_SetUserPermission(t *testing.T) { var hookCalled bool if tt.callHook { - service.options.OnSetUser = func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + service.options.OnSetUser = func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { hookCalled = true return nil } } - _, err = service.SetUserPermission(context.Background(), user.OrgId, user.Id, "1", "") + _, err = service.SetUserPermission(context.Background(), user.OrgId, accesscontrol.User{ID: user.Id}, "1", "") require.NoError(t, err) assert.Equal(t, tt.callHook, hookCalled) }) diff --git a/pkg/services/accesscontrol/resourcepermissions/types/hook.go b/pkg/services/accesscontrol/resourcepermissions/types/hook.go index 5e86cc3893c..b389e8d101c 100644 --- a/pkg/services/accesscontrol/resourcepermissions/types/hook.go +++ b/pkg/services/accesscontrol/resourcepermissions/types/hook.go @@ -1,7 +1,15 @@ package types -import "github.com/grafana/grafana/pkg/services/sqlstore" +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) -type UserResourceHookFunc func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error +type UserResourceHookFunc func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error type TeamResourceHookFunc func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error type BuiltinResourceHookFunc func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error + +type User struct { + ID int64 + IsExternal bool +} diff --git a/pkg/services/accesscontrol/resourceservices/resource_services.go b/pkg/services/accesscontrol/resourceservices/resource_services.go index 077e6cd93a5..3cf2adfb95f 100644 --- a/pkg/services/accesscontrol/resourceservices/resource_services.go +++ b/pkg/services/accesscontrol/resourceservices/resource_services.go @@ -77,20 +77,20 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore ReaderRoleName: "Team permission reader", WriterRoleName: "Team permission writer", RoleGroup: "Teams", - OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + OnSetUser: func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { teamId, err := strconv.ParseInt(resourceID, 10, 64) if err != nil { return err } switch permission { case "Member": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, 0) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) case "Admin": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN) case "": return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{ OrgId: orgID, - UserId: userID, + UserId: user.ID, TeamId: teamId, }) default: From 86756ee3e5dba1b374edcc9801257d44967a5e7b Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 3 Feb 2022 16:27:53 +0100 Subject: [PATCH 19/34] AccessControl: introduce a different accesscontrol check (licensed or not) (#44777) Co-authored-by: ievaVasiljeva --- public/app/core/services/context_srv.ts | 16 ++++++++++------ public/app/features/admin/UserOrgs.tsx | 2 +- public/app/features/teams/TeamList.tsx | 6 +++--- public/app/features/users/UsersTable.test.tsx | 2 +- public/app/features/users/UsersTable.tsx | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 262ac14abd4..507ebcaf60f 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,8 +1,8 @@ import config from '../../core/config'; import { extend } from 'lodash'; import { rangeUtil, WithAccessControlMetadata } from '@grafana/data'; -import { featureEnabled } from '@grafana/runtime'; import { AccessControlAction, UserPermission } from 'app/types'; +import { featureEnabled } from '@grafana/runtime'; export class User { id: number; @@ -83,13 +83,17 @@ export class ContextSrv { } accessControlEnabled(): boolean { + return Boolean(config.featureToggles['accesscontrol']); + } + + licensedAccessControlEnabled(): boolean { return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); } // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -99,7 +103,7 @@ export class ContextSrv { // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -126,14 +130,14 @@ export class ContextSrv { } hasAccessToExplore() { - if (config.featureToggles['accesscontrol']) { + if (this.accessControlEnabled()) { return this.hasPermission(AccessControlAction.DataSourcesExplore); } return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } hasAccess(action: string, fallBack: boolean) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallBack; } return this.hasPermission(action); @@ -141,7 +145,7 @@ export class ContextSrv { // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallback(); } if (actions.some((action) => this.hasPermission(action))) { diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 8cff0b30c8c..5af09928063 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -176,7 +176,7 @@ class UnThemedOrgRow extends PureComponent { - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? (
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 27c6af5be0b..9c40f286826 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,7 +43,7 @@ export class TeamList extends PureComponent { componentDidMount() { this.fetchTeams(); - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { this.fetchRoleOptions(); } } @@ -89,7 +89,7 @@ export class TeamList extends PureComponent { {team.memberCount} - {contextSrv.accessControlEnabled() && ( + {contextSrv.licensedAccessControlEnabled() && ( this.state.roleOptions} /> @@ -155,7 +155,7 @@ export class TeamList extends PureComponent { Name Email Members - {contextSrv.accessControlEnabled() && Roles} + {contextSrv.licensedAccessControlEnabled() && Roles} diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx index 68bba275f15..f71e0517eb7 100644 --- a/public/app/features/users/UsersTable.test.tsx +++ b/public/app/features/users/UsersTable.test.tsx @@ -9,7 +9,7 @@ jest.mock('app/core/core', () => ({ contextSrv: { hasPermission: () => true, hasPermissionInMetadata: () => true, - accessControlEnabled: () => false, + licensedAccessControlEnabled: () => false, }, })); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 427a32ef53a..97ae76998d5 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -40,7 +40,7 @@ const UsersTable: FC = (props) => { console.error('Error loading options'); } } - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { fetchOptions(); } }, [orgId]); @@ -88,7 +88,7 @@ const UsersTable: FC = (props) => { {user.lastSeenAtAge} - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? ( Date: Thu, 3 Feb 2022 16:52:22 +0100 Subject: [PATCH 20/34] Chore: Update latest.json (#44854) --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 91df80dab6b..707ba83022e 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "stable": "8.3.4", - "testing": "8.3.4" + "testing": "8.4.0-beta1" } From 71e3e6ec93b9f14cca07a6a1ef0f8d8aa388b892 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 3 Feb 2022 16:52:53 +0100 Subject: [PATCH 21/34] Chore: Update pre version (#44855) --- lerna.json | 2 +- package.json | 2 +- packages/grafana-data/package.json | 4 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 4 +- packages/grafana-runtime/package.json | 8 ++-- packages/grafana-schema/package.json | 2 +- packages/grafana-toolkit/package.json | 6 +-- packages/grafana-ui/package.json | 8 ++-- packages/jaeger-ui-components/package.json | 6 +-- .../internal/input-datasource/package.json | 8 ++-- yarn.lock | 40 +++++++++---------- 12 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lerna.json b/lerna.json index ea302e8c442..a4f8c6aa934 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "npmClient": "yarn", "useWorkspaces": true, "packages": ["packages/*"], - "version": "8.4.0-pre" + "version": "8.5.0-pre" } diff --git a/package.json b/package.json index 66ebbda259d..2f59a592b2d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 1360df99d8d..105b1884e06 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "5.0.2", - "@grafana/schema": "8.4.0-pre", + "@grafana/schema": "8.5.0-pre", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 195c0b8d964..417fe206b8a 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 337a953666e..92b4aa6e7a1 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.16.7", "@babel/preset-env": "7.16.7", "@cypress/webpack-preprocessor": "5.11.0", - "@grafana/e2e-selectors": "8.4.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", "@grafana/tsconfig": "^1.0.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.3", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 8de4dbe08e2..7e670e72903 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "@sentry/browser": "6.17.2", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 204550b89ed..ca2cd054338 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 061a034b8c8..c8d0562dbdb 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -28,10 +28,10 @@ "dependencies": { "@babel/core": "7.13.14", "@babel/preset-env": "7.13.12", - "@grafana/data": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", "@grafana/eslint-config": "2.5.2", "@grafana/tsconfig": "^1.0.0-rc1", - "@grafana/ui": "8.4.0-pre", + "@grafana/ui": "8.5.0-pre", "@jest/core": "26.6.3", "@rushstack/eslint-patch": "1.0.6", "@types/command-exists": "^1.2.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6f51f50ec0e..01a9fadac02 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.7.1", "@emotion/react": "11.7.1", "@grafana/aws-sdk": "0.0.31", - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/schema": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", + "@grafana/schema": "8.5.0-pre", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.2", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 5de63be37d2..1be78ccbcd0 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,8 +27,8 @@ }, "dependencies": { "@emotion/css": "11.7.1", - "@grafana/data": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 67bc2ced531..ce4d3ebc8aa 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Input Datasource", "private": true, "repository": { @@ -24,9 +24,9 @@ "webpack": "5.58.1" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/toolkit": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/toolkit": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/yarn.lock b/yarn.lock index e5dcf69b0f4..2cf0fe30081 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3642,9 +3642,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 8.4.0-pre - "@grafana/toolkit": 8.4.0-pre - "@grafana/ui": 8.4.0-pre + "@grafana/data": 8.5.0-pre + "@grafana/toolkit": 8.5.0-pre + "@grafana/ui": 8.5.0-pre "@types/jest": 26.0.15 "@types/lodash": 4.14.149 "@types/react": 17.0.30 @@ -3685,12 +3685,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@8.4.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@8.5.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": 5.0.2 - "@grafana/schema": 8.4.0-pre + "@grafana/schema": 8.5.0-pre "@grafana/tsconfig": ^1.0.0-rc1 "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-json": 4.1.0 @@ -3742,7 +3742,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@8.4.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@8.5.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3766,7 +3766,7 @@ __metadata: "@babel/core": 7.16.7 "@babel/preset-env": 7.16.7 "@cypress/webpack-preprocessor": 5.11.0 - "@grafana/e2e-selectors": 8.4.0-pre + "@grafana/e2e-selectors": 8.5.0-pre "@grafana/tsconfig": ^1.0.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-commonjs": 21.0.1 @@ -3846,10 +3846,10 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 8.4.0-pre - "@grafana/e2e-selectors": 8.4.0-pre + "@grafana/data": 8.5.0-pre + "@grafana/e2e-selectors": 8.5.0-pre "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.5.0-pre "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.17.2 @@ -3878,7 +3878,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/schema@8.4.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@8.5.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -3925,16 +3925,16 @@ __metadata: languageName: node linkType: hard -"@grafana/toolkit@8.4.0-pre, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": +"@grafana/toolkit@8.5.0-pre, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": version: 0.0.0-use.local resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit" dependencies: "@babel/core": 7.13.14 "@babel/preset-env": 7.13.12 - "@grafana/data": 8.4.0-pre + "@grafana/data": 8.5.0-pre "@grafana/eslint-config": 2.5.2 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.5.0-pre "@jest/core": 26.6.3 "@rushstack/eslint-patch": 1.0.6 "@types/command-exists": ^1.2.0 @@ -4025,7 +4025,7 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@8.4.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@8.5.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: @@ -4033,9 +4033,9 @@ __metadata: "@emotion/css": 11.7.1 "@emotion/react": 11.7.1 "@grafana/aws-sdk": 0.0.31 - "@grafana/data": 8.4.0-pre - "@grafana/e2e-selectors": 8.4.0-pre - "@grafana/schema": 8.4.0-pre + "@grafana/data": 8.5.0-pre + "@grafana/e2e-selectors": 8.5.0-pre + "@grafana/schema": 8.5.0-pre "@grafana/slate-react": 0.22.10-grafana "@grafana/tsconfig": ^1.0.0-rc1 "@mdx-js/react": 1.6.22 @@ -4251,9 +4251,9 @@ __metadata: resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components" dependencies: "@emotion/css": 11.7.1 - "@grafana/data": 8.4.0-pre + "@grafana/data": 8.5.0-pre "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.5.0-pre "@types/classnames": ^2.2.7 "@types/deep-freeze": ^0.1.1 "@types/grafana__slate-react": "npm:@types/slate-react@0.22.5" From 6d931226d89d5d972fff47c389ab1f8dc88322c7 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 3 Feb 2022 16:59:25 +0100 Subject: [PATCH 22/34] AccessControl: Show UserPicker based on `canListUsers` (#44843) * AccessControl: Show UserPicker based on canListUser * Update public/app/core/components/AccessControl/AddPermission.tsx Co-authored-by: Ieva --- .../components/AccessControl/AddPermission.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index b2da564b1fe..aef87a20de6 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; -import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; +import { Alert, Button, Form, HorizontalGroup, Input, Select } from '@grafana/ui'; import { OrgRole } from 'app/types/acl'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { Assignments, PermissionTarget, SetPermission } from './types'; @@ -54,10 +54,22 @@ export const AddPermission = ({ (target === PermissionTarget.User && userId > 0) || (PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); + const renderMissingListUserRights = () => { + return ( + + You are missing the permission to list users (org.users:read). Please contact your administrator to get this + resolved. + + ); + }; + return (
{title}
+ + {target === PermissionTarget.User && !canListUsers && renderMissingListUserRights()} +
setPermissionTarget(v.value!)} + disabled={targetOptions.length === 0} menuShouldPortal /> - {target === PermissionTarget.User && ( + {target === PermissionTarget.User && canListUsers && ( setUserId(u.value || 0)} className={'width-20'} /> )} + {target === PermissionTarget.User && !canListUsers && } {target === PermissionTarget.Team && ( setTeamId(t.value?.id || 0)} className={'width-20'} /> From 85ea1a5d6449b0e68b2683a19cd4514a98883c2e Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Thu, 3 Feb 2022 08:06:31 -0800 Subject: [PATCH 23/34] Prometheus: Fix Azure authentication support (#44407) Re-adding back Azure authentication support to Prometheus datasource after the datasource query logic was rewritten from plugin.json routes to Go backend. Ref #35857 --- pkg/tsdb/prometheus/promclient/provider.go | 22 +++--- .../prometheus/promclient/provider_azure.go | 32 ++++++++ .../prometheus/promclient/provider_test.go | 2 +- pkg/tsdb/prometheus/prometheus.go | 10 ++- pkg/util/maputil/maputil.go | 73 +++++++++++++++++++ 5 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 pkg/tsdb/prometheus/promclient/provider_azure.go create mode 100644 pkg/util/maputil/maputil.go diff --git a/pkg/tsdb/prometheus/promclient/provider.go b/pkg/tsdb/prometheus/promclient/provider.go index c84a9651773..c33d6a14c11 100644 --- a/pkg/tsdb/prometheus/promclient/provider.go +++ b/pkg/tsdb/prometheus/promclient/provider.go @@ -4,8 +4,8 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/middleware" + "github.com/grafana/grafana/pkg/util/maputil" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -16,30 +16,28 @@ import ( type Provider struct { settings backend.DataSourceInstanceSettings - jsonData JsonData + jsonData map[string]interface{} + httpMethod string clientProvider httpclient.Provider log log.Logger } func NewProvider( settings backend.DataSourceInstanceSettings, - jsonData JsonData, + jsonData map[string]interface{}, clientProvider httpclient.Provider, log log.Logger, ) *Provider { + httpMethod, _ := maputil.GetStringOptional(jsonData, "httpMethod") return &Provider{ settings: settings, jsonData: jsonData, + httpMethod: httpMethod, clientProvider: clientProvider, log: log, } } -type JsonData struct { - Method string `json:"httpMethod"` - TimeInterval string `json:"timeInterval"` -} - func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts, err := p.settings.HTTPClientOptions() if err != nil { @@ -54,6 +52,12 @@ func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts.SigV4.Service = "aps" } + // Azure authentication + err = p.configureAzureAuthentication(opts) + if err != nil { + return nil, err + } + roundTripper, err := p.clientProvider.GetTransport(opts) if err != nil { return nil, err @@ -77,7 +81,7 @@ func (p *Provider) middlewares() []sdkhttpclient.Middleware { middleware.CustomQueryParameters(p.log), sdkhttpclient.CustomHeadersMiddleware(), } - if strings.ToLower(p.jsonData.Method) == "get" { + if strings.ToLower(p.httpMethod) == "get" { middlewares = append(middlewares, middleware.ForceHttpGet(p.log)) } diff --git a/pkg/tsdb/prometheus/promclient/provider_azure.go b/pkg/tsdb/prometheus/promclient/provider_azure.go new file mode 100644 index 00000000000..920f253a75d --- /dev/null +++ b/pkg/tsdb/prometheus/promclient/provider_azure.go @@ -0,0 +1,32 @@ +package promclient + +import ( + "fmt" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" + "github.com/grafana/grafana/pkg/util/maputil" +) + +func (p *Provider) configureAzureAuthentication(opts sdkhttpclient.Options) error { + credentials, err := azcredentials.FromDatasourceData(p.jsonData, p.settings.DecryptedSecureJSONData) + if err != nil { + err = fmt.Errorf("invalid Azure credentials: %s", err) + return err + } + + if credentials != nil { + opts.CustomOptions["_azureCredentials"] = credentials + + resourceId, err := maputil.GetStringOptional(p.jsonData, "azureEndpointResourceId") + if err != nil { + return err + } + + if resourceId != "" { + opts.CustomOptions["azureEndpointResourceId"] = resourceId + } + } + + return nil +} diff --git a/pkg/tsdb/prometheus/promclient/provider_test.go b/pkg/tsdb/prometheus/promclient/provider_test.go index 935b3d07c5a..43acc14b362 100644 --- a/pkg/tsdb/prometheus/promclient/provider_test.go +++ b/pkg/tsdb/prometheus/promclient/provider_test.go @@ -135,7 +135,7 @@ func setup(jsonData ...string) *testContext { rawData = []byte(jsonData[0]) } - var jd promclient.JsonData + var jd map[string]interface{} _ = json.Unmarshal(rawData, &jd) settings := backend.DataSourceInstanceSettings{URL: "test-url", JSONData: rawData} diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e16597c50fb..9ba855411ad 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana/pkg/util/maputil" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -42,7 +43,7 @@ func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Trace func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - var jsonData promclient.JsonData + var jsonData map[string]interface{} err := json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) @@ -54,10 +55,15 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } + timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval") + if err != nil { + return nil, err + } + mdl := DatasourceInfo{ ID: settings.ID, URL: settings.URL, - TimeInterval: jsonData.TimeInterval, + TimeInterval: timeInterval, getClient: pc.GetClient, } diff --git a/pkg/util/maputil/maputil.go b/pkg/util/maputil/maputil.go new file mode 100644 index 00000000000..becd6d81e0c --- /dev/null +++ b/pkg/util/maputil/maputil.go @@ -0,0 +1,73 @@ +package maputil + +import "fmt" + +func GetMap(obj map[string]interface{}, key string) (map[string]interface{}, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(map[string]interface{}); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be an object", key) + return nil, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return nil, err + } +} + +func GetBool(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return false, err + } +} + +func GetBoolOptional(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + // Value optional, not error + return false, nil + } +} + +func GetString(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return "", err + } +} + +func GetStringOptional(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + // Value optional, not error + return "", nil + } +} From 8217d6d206692a411f5055780a3a6518711ca365 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 3 Feb 2022 17:49:39 +0100 Subject: [PATCH 24/34] AccessControl: Change teams permissions page when accesscontrol is enabled (#43971) * AccessControl: Change teams permissions page when frontend is hit * Implement frontend changes for group sync * Changing the org/teams/edit permissions Co-authored-by: ievaVasiljeva * Fixing routes Co-authored-by: ievaVasiljeva * Use props straight away no need to go through the state Co-authored-by: Alex Khomenko * Update public/app/features/teams/TeamPages.tsx Co-authored-by: ievaVasiljeva Co-authored-by: Alex Khomenko --- pkg/api/api.go | 5 +- pkg/api/common_test.go | 7 +-- pkg/api/index.go | 4 +- pkg/api/roles.go | 19 +++++++ pkg/api/team.go | 45 ++++++++++++++++ pkg/models/team.go | 15 +++--- public/app/core/services/context_srv.ts | 7 +++ .../app/features/teams/TeamGroupSync.test.tsx | 1 + public/app/features/teams/TeamGroupSync.tsx | 17 ++++-- public/app/features/teams/TeamList.tsx | 6 ++- public/app/features/teams/TeamPages.test.tsx | 1 + public/app/features/teams/TeamPages.tsx | 52 ++++++++++++++---- public/app/features/teams/TeamPermissions.tsx | 31 +++++++++++ .../app/features/teams/TeamSettings.test.tsx | 6 +++ public/app/features/teams/TeamSettings.tsx | 8 ++- .../__snapshots__/TeamGroupSync.test.tsx.snap | 7 +++ .../__snapshots__/TeamPages.test.tsx.snap | 4 +- .../__snapshots__/TeamSettings.test.tsx.snap | 2 + public/app/features/teams/state/actions.ts | 8 ++- public/app/features/teams/state/navModel.ts | 54 +++++++++++++------ public/app/routes/routes.tsx | 19 +++++-- public/app/types/accessControl.ts | 5 ++ public/app/types/teams.ts | 3 +- 23 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 public/app/features/teams/TeamPermissions.tsx diff --git a/pkg/api/api.go b/pkg/api/api.go index 497251aafa1..7ca90636284 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -57,8 +57,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) - r.Get("/org/teams", reqCanAccessTeams, hs.Index) - r.Get("/org/teams/*", reqCanAccessTeams, hs.Index) + r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) + r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) + r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 4b337918132..0bba9e04d3e 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -311,9 +311,10 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { cfg.IsFeatureToggleEnabled = features.IsEnabled return &HTTPServer{ - Cfg: cfg, - Features: features, - Bus: bus.GetBus(), + Cfg: cfg, + Features: features, + Bus: bus.GetBus(), + AccessControl: accesscontrolmock.New().WithDisabled(), } } diff --git a/pkg/api/index.go b/pkg/api/index.go index 25565347c10..da0c9c45bf1 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -148,7 +148,7 @@ func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) } -func enableTeams(hs *HTTPServer, c *models.ReqContext) bool { +func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) } @@ -263,7 +263,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if enableTeams(hs, c) { + if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) { configNodes = append(configNodes, &dtos.NavLink{ Text: "Teams", Id: "teams", diff --git a/pkg/api/roles.go b/pkg/api/roles.go index e8f3fc9fe53..0fc5d75723e 100644 --- a/pkg/api/roles.go +++ b/pkg/api/roles.go @@ -298,3 +298,22 @@ var orgsCreateAccessEvaluator = accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionOrgsRead), accesscontrol.EvalPermission(ActionOrgsCreate), ) + +// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access +var teamsAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) + +// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access +var teamsEditAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) diff --git a/pkg/api/team.go b/pkg/api/team.go index 5db290459b2..b880bb4ba2e 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -2,12 +2,14 @@ package api import ( "errors" + "fmt" "net/http" "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -103,6 +105,20 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { return response.Success("Team deleted") } +func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err) + return nil, err + } + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil +} + // GET /api/teams/search func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { perPage := c.QueryInt("perpage") @@ -134,8 +150,17 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.Error(500, "Failed to search Teams", err) } + teamIDs := map[string]bool{} for _, team := range query.Result.Teams { team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + teamIDs[strconv.FormatInt(team.Id, 10)] = true + } + + metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs) + if err == nil && len(metadata) != 0 { + for _, team := range query.Result.Teams { + team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)] + } } query.Result.Page = page @@ -144,6 +169,23 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.JSON(200, query.Result) } +func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err) + return nil, err + } + + key := fmt.Sprintf("%d", teamID) + teamIDs := map[string]bool{key: true} + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil +} + // GET /api/teams/:teamId func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) @@ -165,6 +207,9 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get Team", err) } + metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id) + query.Result.AccessControl = metadata + query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return response.JSON(200, &query.Result) } diff --git a/pkg/models/team.go b/pkg/models/team.go index 328e1815b90..38f434458af 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -77,13 +77,14 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - MemberCount int64 `json:"memberCount"` - Permission PermissionType `json:"permission"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + MemberCount int64 `json:"memberCount"` + Permission PermissionType `json:"permission"` + AccessControl map[string]bool `json:"accessControl"` } type SearchTeamQueryResult struct { diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 507ebcaf60f..bf719821487 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -143,6 +143,13 @@ export class ContextSrv { return this.hasPermission(action); } + hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) { + if (!config.featureToggles['accesscontrol']) { + return fallBack; + } + return this.hasPermissionInMetadata(action, object); + } + // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { if (!this.accessControlEnabled()) { diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index f3deb62c77b..6ba6f050278 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -6,6 +6,7 @@ import { getMockTeamGroups } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { + isReadOnly: false, groups: [] as TeamGroup[], loadTeamGroups: jest.fn(), addTeamGroup: jest.fn(), diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index aeedfb12f16..a550b0cd9df 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -23,13 +23,17 @@ const mapDispatchToProps = { removeTeamGroup, }; +interface OwnProps { + isReadOnly: boolean; +} + interface State { isAdding: boolean; newGroupId: string; } const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; +export type Props = OwnProps & ConnectedProps; const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; @@ -70,11 +74,12 @@ export class TeamGroupSync extends PureComponent { } renderGroup(group: TeamGroup) { + const { isReadOnly } = this.props; return ( {group.groupId} - @@ -84,7 +89,7 @@ export class TeamGroupSync extends PureComponent { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.groups; + const { groups, isReadOnly } = this.props; return (
@@ -95,7 +100,7 @@ export class TeamGroupSync extends PureComponent {
{groups.length > 0 && ( - )} @@ -113,11 +118,12 @@ export class TeamGroupSync extends PureComponent { value={newGroupId} onChange={this.onNewGroupIdChanged} placeholder="cn=ops,ou=groups,dc=grafana,dc=org" + disabled={isReadOnly} />
-
@@ -135,6 +141,7 @@ export class TeamGroupSync extends PureComponent { proTipLinkTitle="Learn more" proTipLink="http://docs.grafana.org/auth/enhanced_ldap/" proTipTarget="_blank" + buttonDisabled={isReadOnly} /> )} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 9c40f286826..83894dc5e46 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -69,7 +69,11 @@ export class TeamList extends PureComponent { const { editorsCanAdmin, signedInUser } = this.props; const permission = team.permission; const teamUrl = `org/teams/edit/${team.id}`; - const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); + const canDelete = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsDelete, + team, + isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }) + ); return ( diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 8f5fa7ec666..c4f852b2bf6 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime/src/config', () => ({ licenseInfo: { enabledFeatures: { teamsync: true }, }, + featureToggles: { accesscontrol: false }, }, })); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 9817ddefe5b..65430e5e898 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -4,9 +4,10 @@ import { includes } from 'lodash'; import config from 'app/core/config'; import Page from 'app/core/components/Page/Page'; import TeamMembers from './TeamMembers'; +import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { StoreState } from 'app/types'; +import { AccessControlAction, StoreState } from 'app/types'; import { loadTeam, loadTeamMembers } from './state/actions'; import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeamLoadingNav } from './state/navModel'; @@ -37,10 +38,17 @@ enum PageTypes { function mapStateToProps(state: StoreState, props: OwnProps) { const teamId = parseInt(props.match.params.id, 10); - const pageName = props.match.params.page ?? 'members'; + const team = getTeam(state.team, teamId); + let defaultPage = 'members'; + if (contextSrv.accessControlEnabled()) { + // With FGAC the settings page will always be available + if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { + defaultPage = 'settings'; + } + } + const pageName = props.match.params.page ?? defaultPage; const teamLoadingNav = getTeamLoadingNav(pageName as string); const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); - const team = getTeam(state.team, teamId); const members = getTeamMembers(state.team); return { @@ -81,7 +89,10 @@ export class TeamPages extends PureComponent { const { loadTeam, teamId } = this.props; this.setState({ isLoading: true }); const team = await loadTeam(teamId); - await this.props.loadTeamMembers(); + // With accesscontrol, the TeamPermissions will fetch team members + if (!contextSrv.accessControlEnabled()) { + await this.props.loadTeamMembers(); + } this.setState({ isLoading: false }); return team; } @@ -105,6 +116,10 @@ export class TeamPages extends PureComponent { }; hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + if (contextSrv.accessControlEnabled()) { + return navModel; + } + if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { navModel.main.children .filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members)) @@ -121,15 +136,34 @@ export class TeamPages extends PureComponent { const { members, team } = this.props; const currentPage = this.getCurrentPage(); + const canReadTeam = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsRead, + team!, + isSignedInUserTeamAdmin + ); + const canReadTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsRead, + team!, + isSignedInUserTeamAdmin + ); + const canWriteTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + team!, + isSignedInUserTeamAdmin + ); + switch (currentPage) { case PageTypes.Members: - return ; - + if (contextSrv.accessControlEnabled()) { + return ; + } else { + return ; + } case PageTypes.Settings: - return isSignedInUserTeamAdmin && ; + return canReadTeam && ; case PageTypes.GroupSync: - if (isSignedInUserTeamAdmin && isSyncEnabled) { - return ; + if (canReadTeamPermissions && isSyncEnabled) { + return ; } else if (config.featureToggles.featureHighlights) { return ( { + const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + props.team + ); + + return ( + + ); +}; + +export default TeamPermissions; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index b261116718e..4d4cfc0e83c 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -3,6 +3,12 @@ import { shallow } from 'enzyme'; import { Props, TeamSettings } from './TeamSettings'; import { getMockTeam } from './__mocks__/teamMocks'; +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermissionInMetadata: () => true, + }, +})); + const setup = (propOverrides?: object) => { const props: Props = { team: getMockTeam(), diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 8ef7dd288f5..1d34aee41e9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -4,7 +4,8 @@ import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; -import { Team } from 'app/types'; +import { AccessControlAction, Team } from 'app/types'; +import { contextSrv } from 'app/core/core'; const mapDispatchToProps = { updateTeam, @@ -18,6 +19,8 @@ interface OwnProps { export type Props = ConnectedProps & OwnProps; export const TeamSettings: FC = ({ team, updateTeam }) => { + const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); + return (
@@ -26,6 +29,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { onSubmit={(formTeam: Team) => { updateTeam(formTeam.name, formTeam.email); }} + disabled={!canWriteTeamSettings} > {({ register }) => ( <> @@ -44,7 +48,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { )}
- +
); }; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 35719d0908d..3abd26f7778 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -44,6 +44,7 @@ exports[`Render should render component 1`] = ` >