Compare commits

...

24 Commits

Author SHA1 Message Date
Dimitris Sotirakis 5f16e4cedc Rename build-e2e-publish pipelines (#44836) 2022-02-03 14:23:50 +00:00
Gábor Farkas 0c2ba819a7 Loki: use generic grafana null-insertion mechanism (#44826)
* loki: refactor: return dataframes instead of timeseries

* fixed unit test

* removed unused import
2022-02-03 15:23:38 +01:00
Grot (@grafanabot) afac7701cb ReleaseNotes: Updated changelog and release notes for 8.4.0-beta1 (#44822) 2022-02-03 15:15:48 +01:00
Armand Grillet a943bf9963 Improve prettier:check output (#44816) 2022-02-03 15:13:14 +01:00
Giordano Ricci 6415b9a54d 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
2022-02-03 13:45:29 +00:00
Konrad Lalik bb88cf683c 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
2022-02-03 14:17:05 +01:00
Todd Treece c8bb4c733e Prometheus: Set interval on time field (#44802) 2022-02-03 08:15:55 -05:00
Gilles De Mey 29b97361f7 Alerting: load correct unified alerting tab (#44794) 2022-02-03 13:35:56 +01:00
Serge Zaitsev 43b15b92ad 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
2022-02-03 13:26:05 +01:00
Alex Khomenko a79c048344 Feature Highlights: move setting to a feature toggle (#44780)
* Add toggle

* Use the toggle

* Cleanup
2022-02-03 13:53:23 +02:00
Ivana Huckova c23bc1e7b7 Prometheus: Show variable options in query builder (#44784)
* Prometheus: Show variable options

* Remove lint error

* Fix test for CodeQL

* Update public/app/plugins/datasource/prometheus/datasource.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>

* Update public/app/plugins/datasource/loki/datasource.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2022-02-03 11:40:19 +01:00
Kat Yang f582e6c86a 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 <serge.zaitsev@grafana.com>
2022-02-03 10:33:46 +01:00
Gábor Farkas f36ed878e9 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>
2022-02-03 10:25:37 +01:00
Peter Holmberg 3314178a0a grafana/ui: Fix RelativeTimeRange supported formats (#44535)
* remove link to docs site

* extract tooltip to component

* text and formatting

* use div instead of p
2022-02-03 10:13:19 +01:00
Vardan Torosyan f38f10416a Revert fixed roles and service accounts (#44778)
* Revert fixed roles and service accounts

* Leave the fixed role for service accounts
2022-02-03 09:59:26 +01:00
Armand Grillet 8c6a5f043a [docs] Clarify legacy alerting deprecation (#44759)
* Clarify legacy alerting deprecation

* Lint Markdown
2022-02-03 09:59:02 +01:00
idafurjes 1b286e6bb5 Remove bus from quota, preferences, plugins, user_token (#44762)
* Remove bus from quota, preferences, plugins, user_token

* Bind sqlstore.Store to *sqlstore.SQLStore

* Fix test

* Fix sqlstore wire injection, dependency
2022-02-03 09:20:20 +01:00
Joan López de la Franca Beltran b2655750e8 Encryption: Add support for data keys re-encryption (#43548)
* Encryption: Add support for data keys re-encryption

* Add tests for data keys re-encryption

* Update code after refactorings

Co-authored-by: Leonard Gram <leo@xlson.com>
2022-02-03 09:15:38 +01:00
JM 16f0c6617a Update docs library element http api (#44493) 2022-02-03 08:46:03 +01:00
Joan López de la Franca Beltran f8105efff3 Encryption: CLI rollback command (#43935)
* Encryption: CLI rollback command

* Update flag reference to 'featuremgmt' pkg

* Update feature toggles usage

* Clean up data keys table after envelope encryption rollback
2022-02-03 07:19:18 +01:00
Josh Hunt ce4d8646fd Chore: E2E tests for various variables types (#44747)
* Pass data-testid into VariableTextEditorField

* Add e2e tests for custom variables

* Rename query variable specs to match others

* Add e2e tests for Text Box variables

* manually remove id: null

* Add tests for constant variables
2022-02-03 09:58:56 +11:00
Yuriy Tseretyan 984c95de63 Do not store EvaluationString in Evaluation. (#44606)
* do not store evaluation string in Evaluation.
* reduce number of buckets to store for a single state
2022-02-02 19:18:20 +01:00
Marcus Efraimsson 0092d10764 Instrumentation: Fix HTTP request instrumentation of authentication failures (#44234)
Moves the request tracing middleware earlier in the chain, just after the tracing middleware 
and before the log middleware. With these changes we'll be able to track 
authentication/authorization status failures that currently exits early and don't execute the 
request tracing middleware. In addition, there might be some other routes now being tracked 
with this that we didn't do before.

Fixes #39590
2022-02-02 18:48:46 +01:00
Leon Sorokin 3504844ad7 DataFrame: insert null values along interval (#44622) 2022-02-02 10:25:49 -06:00
184 changed files with 2460 additions and 1288 deletions
+5 -5
View File
@@ -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
...
+20
View File
@@ -1,3 +1,23 @@
<!-- 8.4.0-beta1 START -->
# 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.4.0-beta1 END -->
<!-- 8.3.4 START -->
# 8.3.4 (2022-01-17)
@@ -0,0 +1,76 @@
{
"__inputs": [],
"__elements": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "8.4.0-pre"
},
{
"type": "panel",
"id": "text",
"name": "Text",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"mode": "markdown",
"content": "VariableUnderTest: $VariableUnderTest"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 35,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Test variable output",
"uid": "kVi2Gex7z",
"version": 2,
"weekStart": ""
}
+5 -4
View File
@@ -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.
+1 -1
View File
@@ -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:
@@ -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
+2 -2
View File
@@ -269,8 +269,8 @@ Creates a new library element.
JSON Body schema:
- **folderId** ID of the folder where the library element is stored.
- **name** Name of the library element.
- **folderId** Optional, the ID of the folder where the library element is stored.
- **name** Optional, the name of the library element.
- **model** The JSON model for the library element.
- **kind** Kind of element to create, Use `1` for library panels or `2` for library variables.
- **uid** Optional, the [unique identifier](/http_api/library_element/#identifier-id-vs-unique-identifier-uid).
+1
View File
@@ -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" >}})
@@ -0,0 +1,23 @@
+++
title = "Release notes for Grafana 8.4.0-beta1"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# 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)
@@ -0,0 +1,31 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
describe('Variables - Constant', () => {
it('can add a new text box variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Constant" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Constant{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type('pesto').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.components.RefreshPicker.runButtonV2().click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: pesto');
// Assert the variable is not visible in the dashboard nav
e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').should('not.exist');
});
});
@@ -0,0 +1,60 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
function fillInCustomVariable(name: string, label: string, value: string) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type(name).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput().type(value).blur();
}
function assertPreviewValues(expectedValues: string[]) {
for (const expected of expectedValues) {
const index = expectedValues.indexOf(expected);
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(index).should('have.text', expected);
}
}
describe('Variables - Custom', () => {
it('can add a custom template variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'one,two,three');
assertPreviewValues(['one', 'two', 'three']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('two').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: two');
});
it('can add a custom template variable with labels', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Custom{enter}');
// Set it's name, label, and content
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'One : 1,Two : 2, Three : 3');
assertPreviewValues(['One', 'Two', 'Three']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: 2');
});
});
@@ -2,7 +2,7 @@ import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
describe('Variables - Add variable', () => {
describe('Variables - Query - Add variable', () => {
it('query variable should be default and default fields should be correct', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
@@ -0,0 +1,28 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
describe('Variables - Text box', () => {
it('can add a new text box variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Text box{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput().type('cat-dog').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e().get('#VariableUnderTest').clear().type('dog-cat').blur();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat');
});
});
+60 -56
View File
@@ -1,11 +1,14 @@
<!-- The template body needs to be wrapped in div to prevent Premailer from adding </tr></td> tags which breaks the HTML structure -->
<div>
[[Subject .Subject "[[.Title]]"]]
[[ define "alert" ]]
<tr>
<td colspan="2" class="value">
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
</td>
</tr>
<tr>
<td colspan="2" class="value">
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
</td>
</tr>
[[ if gt (len .Annotations.SortedPairs) 0 ]]
<tr>
<td colspan="2" class="annotations">
@@ -187,62 +190,63 @@
<tr>
<td class="twelve">
<table>
[[ if gt (len .Alerts.Firing) 0 ]]
<tr>
<td colspan="2" class="section-heading">
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 ]]
</td>
</tr>
[[ range .Alerts.Firing ]]
[[ if gt (len .Alerts.Firing) 0 ]]
<tr>
<td colspan="2" class="section-heading">
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 ]]
</td>
</tr>
[[ range .Alerts.Firing ]]
<tr>
<td
class="status-tag status-firing"
width="68"
>
Firing
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
[[ if gt (len .Alerts.Resolved) 0 ]]
<tr>
<td colspan="2" class="section-heading">
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 ]]
</td>
</tr>
[[ range .Alerts.Resolved ]]
<tr>
<td
class="status-tag status-resolved"
width="68"
>
Resolved
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
<tr>
<td
class="status-tag status-firing"
width="68"
>
Firing
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
<td colspan="2">
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
[[ if gt (len .Alerts.Resolved) 0 ]]
<tr>
<td colspan="2" class="section-heading">
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 ]]
</td>
</tr>
[[ range .Alerts.Resolved ]]
<tr>
<td
class="status-tag status-resolved"
width="68"
>
Resolved
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
<tr>
<td colspan="2">
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
</td>
</tr>
</table>
</table>
</td>
</tr>
</table>
[[ end ]]
</div>
+1 -1
View File
@@ -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",
@@ -67,6 +67,12 @@ export interface FieldConfig<TOptions = any> {
min?: number | null;
max?: number | null;
// Interval indicates the expected regular step between values in the series.
// When an interval exists, consumers can identify "missing" values when the expected value is not present.
// The grafana timeseries visualization will render disconnected values when missing values are found it the time field.
// The interval uses the same units as the values. For time.Time, this is defined in milliseconds.
interval?: number | null;
// Convert input values into a display string
mappings?: ValueMapping[];
@@ -36,4 +36,5 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
disable_http_request_histogram?: boolean;
validatedQueries?: boolean;
featureHighlights?: boolean;
}
@@ -135,6 +135,9 @@ export const Pages = {
TextBoxVariable: {
textBoxOptionsQueryInput: 'Variable editor Form TextBox Query field',
},
CustomVariable: {
customValueInput: 'data-testid custom-variable-input',
},
},
},
},
@@ -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
</CustomScrollbar>
<div className={styles.rightSide}>
<div className={styles.title}>
<TimePickerTitle>Specify time range</TimePickerTitle>
<div className={styles.description}>
Specify a relative time range, for more information see{' '}
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/">
docs <Icon name="external-link-alt" />
</a>
.
</div>
<TimePickerTitle>
<Tooltip content={<TooltipContent />} placement="bottom" theme="info">
<div>
Specify time range <Icon name="info-circle" />
</div>
</Tooltip>
</TimePickerTitle>
</div>
<Field label="From" invalid={!from.validation.isValid} error={from.validation.errorMessage}>
<Input
@@ -152,6 +152,38 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
);
}
const TooltipContent = () => {
const styles = useStyles2(toolTipStyles);
return (
<>
<div className={styles.supported}>
Supported formats: <code className={styles.tooltip}>now-[digit]s/m/h/d/w</code>
</div>
<div>Example: to select a time range from 10 minutes ago to now</div>
<code className={styles.tooltip}>From: now-10m To: now</code>
<div className={styles.link}>
For more information see{' '}
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/">
docs <Icon name="external-link-alt" />
</a>
.
</div>
</>
);
};
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;
@@ -0,0 +1,215 @@
import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data';
import { applyNullInsertThreshold } from './nullInsertThreshold';
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function genFrame() {
let fieldCount = 10;
let valueCount = 3000;
let step = 1000;
let skipProb = 0.5;
let skipSteps = [1, 5]; // min, max
let allValues = Array(fieldCount);
allValues[0] = Array(valueCount);
for (let i = 0, curStep = Date.now(); i < valueCount; i++) {
curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1);
}
for (let fi = 1; fi < fieldCount; fi++) {
let values = Array(valueCount);
for (let i = 0; i < valueCount; i++) {
values[i] = Math.random() * 100;
}
allValues[fi] = values;
}
return {
length: valueCount,
fields: allValues.map((values, i) => {
return {
name: 'A-' + i,
type: i === 0 ? FieldType.time : FieldType.number,
config: {
interval: i === 0 ? step : null,
},
values: new ArrayVector(values),
};
}),
};
}
describe('nullInsertThreshold Transformer', () => {
test('should insert nulls at +threshold between adjacent > threshold: 1', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold(df);
expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']);
});
test('should insert nulls at +threshold between adjacent > threshold: 2', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold(df);
expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']);
});
test('should insert nulls at +interval between adjacent > interval: 1', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold(df);
expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']);
});
// TODO: make this work
test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold(df);
expect(result.fields[0].values.toArray()).toStrictEqual([5, 6, 7, 8, 11]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']);
});
test('should insert nulls at midpoints between adjacent > interval: 2', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold(df);
expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']);
});
test('should noop on fewer than two values', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'Value', type: FieldType.number, values: [1] },
],
});
const result = applyNullInsertThreshold(df);
expect(result).toBe(df);
});
test('should noop on invalid threshold', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold(df);
expect(result).toBe(df);
});
test('should noop on invalid interval', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold(df);
expect(result).toBe(df);
});
test('should noop when no missing steps', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold(df);
expect(result).toBe(df);
});
test('should noop when refFieldName not found', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold(df, 'Time2');
expect(result).toBe(df);
});
test('perf stress test should be <= 10ms', () => {
// 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values)
let bigFrameA = genFrame();
// eslint-disable-next-line no-console
console.time('insertValues-10x3k');
applyNullInsertThreshold(bigFrameA);
// eslint-disable-next-line no-console
console.timeEnd('insertValues-10x3k');
});
});
@@ -0,0 +1,115 @@
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
type InsertMode = (prev: number, next: number, threshold: number) => number;
const INSERT_MODES = {
threshold: (prev: number, next: number, threshold: number) => prev + threshold,
midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2,
// previous time + 1ms to prevent StateTimeline from forward-interpolating prior state
plusone: (prev: number, next: number, threshold: number) => prev + 1,
};
export function applyNullInsertThreshold(
frame: DataFrame,
refFieldName?: string | null,
insertMode: InsertMode = INSERT_MODES.threshold
): DataFrame {
if (frame.length < 2) {
return frame;
}
const refField = frame.fields.find((field) => {
// note: getFieldDisplayName() would require full DF[]
return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time;
});
if (refField == null) {
return frame;
}
const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls ?? refField.config.interval ?? null);
const uniqueThresholds = new Set<number>(thresholds);
uniqueThresholds.delete(null as any);
if (uniqueThresholds.size === 0) {
return frame;
}
if (uniqueThresholds.size === 1) {
const threshold = uniqueThresholds.values().next().value;
if (threshold <= 0) {
return frame;
}
const refValues = refField.values.toArray();
const frameValues = frame.fields.map((field) => field.values.toArray());
const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, insertMode);
if (filledFieldValues === frameValues) {
return frame;
}
return {
...frame,
length: filledFieldValues[0].length,
fields: frame.fields.map((field, i) => ({
...field,
values: new ArrayVector(filledFieldValues[i]),
})),
};
}
// TODO: unique threshold-per-field (via overrides) is unimplemented
// should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...)
// as a separate nullInsertThreshold() dataset, then re-join into single dataset via join()
return frame;
}
function nullInsertThreshold(refValues: number[], frameValues: any[][], threshold: number, getInsertValue: InsertMode) {
const len = refValues.length;
let prevValue: number = refValues[0];
const refValuesNew: number[] = [prevValue];
for (let i = 1; i < len; i++) {
const curValue = refValues[i];
if (curValue - prevValue > threshold) {
refValuesNew.push(getInsertValue(prevValue, curValue, threshold));
}
refValuesNew.push(curValue);
prevValue = curValue;
}
const filledLen = refValuesNew.length;
if (filledLen === len) {
return frameValues;
}
const filledFieldValues: any[][] = [];
for (let fieldValues of frameValues) {
let filledValues;
if (fieldValues !== refValues) {
filledValues = Array(filledLen);
for (let i = 0, j = 0; i < filledLen; i++) {
filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null;
}
} else {
filledValues = refValuesNew;
}
filledFieldValues.push(filledValues);
}
return filledFieldValues;
}
@@ -1,6 +1,7 @@
import { XYFieldMatchers } from './types';
import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames } from '@grafana/data';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
import { FIXED_UNIT } from './GraphNG';
@@ -32,7 +33,7 @@ function applySpanNullsThresholds(frame: DataFrame) {
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) {
let alignedFrame = outerJoinDataFrames({
frames: frames,
frames: frames.map((frame) => applyNullInsertThreshold(frame)),
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
@@ -1,5 +1,6 @@
import { DataFrame, FieldConfig, FieldSparkline, IndexVector } from '@grafana/data';
import { GraphFieldConfig } from '@grafana/schema';
import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold';
/** @internal
* Given a sparkline config returns a DataFrame ready to be turned into Plot data set
@@ -11,7 +12,7 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig
...config,
};
return {
return applyNullInsertThreshold({
refId: 'sparkline',
fields: [
sparkline.x ?? IndexVector.newField(length),
@@ -21,5 +22,5 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig
},
],
length,
};
});
}
+1 -9
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -112,7 +111,7 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *models.ReqContext) response.
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
err = updateUserPermissions(hs.SQLStore, userID, form.IsGrafanaAdmin)
err = hs.SQLStore.UpdateUserPermissions(userID, form.IsGrafanaAdmin)
if err != nil {
if errors.Is(err, models.ErrLastGrafanaAdmin) {
return response.Error(400, models.ErrLastGrafanaAdmin.Error(), nil)
@@ -230,10 +229,3 @@ func (hs *HTTPServer) AdminRevokeUserAuthToken(c *models.ReqContext) response.Re
}
return hs.revokeUserAuthTokenInternal(c, userID, cmd)
}
// updateUserPermissions updates the user's permissions.
//
// Stubbable by tests.
var updateUserPermissions = func(sqlStore *sqlstore.SQLStore, userID int64, isAdmin bool) error {
return sqlStore.UpdateUserPermissions(userID, isAdmin)
}
+28 -43
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -34,25 +35,18 @@ func TestAdminAPIEndpoint(t *testing.T) {
updateCmd := dtos.AdminUpdateUserPermissionsForm{
IsGrafanaAdmin: false,
}
mock := mockstore.SQLStoreMock{
ExpectedError: models.ErrLastGrafanaAdmin,
}
putAdminScenario(t, "When calling PUT on", "/api/admin/users/1/permissions",
"/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) {
// TODO: Use a fake SQLStore when it's represented by an interface
origUpdateUserPermissions := updateUserPermissions
t.Cleanup(func() {
updateUserPermissions = origUpdateUserPermissions
})
updateUserPermissions = func(sqlStore *sqlstore.SQLStore, userID int64, isAdmin bool) error {
return models.ErrLastGrafanaAdmin
}
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
})
}, mock)
})
t.Run("When a server admin attempts to logout himself from all devices", func(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
adminLogoutUserScenario(t, "Should not be allowed when calling POST on",
"/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
@@ -62,54 +56,41 @@ func TestAdminAPIEndpoint(t *testing.T) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
})
}, mock)
})
t.Run("When a server admin attempts to logout a non-existing user from all devices", func(t *testing.T) {
mock := mockstore.SQLStoreMock{
ExpectedError: models.ErrUserNotFound,
}
adminLogoutUserScenario(t, "Should return not found when calling POST on", "/api/admin/users/200/logout",
"/api/admin/users/:id/logout", func(sc *scenarioContext) {
userID := int64(0)
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
userID = cmd.Id
return models.ErrUserNotFound
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
assert.Equal(t, int64(200), userID)
})
}, mock)
})
t.Run("When a server admin attempts to revoke an auth token for a non-existing user", func(t *testing.T) {
cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2}
mock := mockstore.SQLStoreMock{
ExpectedError: models.ErrUserNotFound,
}
adminRevokeUserAuthTokenScenario(t, "Should return not found when calling POST on",
"/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) {
var userID int64
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
userID = cmd.Id
return models.ErrUserNotFound
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
assert.Equal(t, int64(200), userID)
})
}, mock)
})
t.Run("When a server admin gets auth tokens for a non-existing user", func(t *testing.T) {
mock := mockstore.SQLStoreMock{
ExpectedError: models.ErrUserNotFound,
}
adminGetUserAuthTokensScenario(t, "Should return not found when calling GET on",
"/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) {
var userID int64
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
userID = cmd.Id
return models.ErrUserNotFound
})
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
assert.Equal(t, int64(200), userID)
})
}, mock)
})
t.Run("When a server admin attempts to enable/disable a nonexistent user", func(t *testing.T) {
@@ -305,12 +286,13 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := &HTTPServer{
Cfg: setting.NewCfg(),
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
@@ -330,13 +312,14 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
})
}
func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) {
func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
@@ -357,7 +340,7 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern
})
}
func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc) {
func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -366,6 +349,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
@@ -386,7 +370,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
})
}
func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) {
func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -395,6 +379,7 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
+5 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -81,6 +82,7 @@ func TestAlertingAPIEndpoint(t *testing.T) {
})
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts",
models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
@@ -102,7 +104,7 @@ func TestAlertingAPIEndpoint(t *testing.T) {
require.Nil(t, searchQuery)
assert.NotNil(t, getAlertsQuery)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET",
"/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery",
@@ -140,7 +142,7 @@ func TestAlertingAPIEndpoint(t *testing.T) {
assert.Equal(t, int64(2), getAlertsQuery.DashboardIDs[1])
assert.Equal(t, int64(5), getAlertsQuery.Limit)
assert.Equal(t, "alertQuery", getAlertsQuery.Query)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/alert-notifications/1",
"/alert-notifications/:notificationId", models.ROLE_ADMIN, func(sc *scenarioContext) {
@@ -149,7 +151,7 @@ func TestAlertingAPIEndpoint(t *testing.T) {
sc.handlerFunc = GetAlertNotificationByID
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
})
}, mock)
}
func callPauseAlert(sc *scenarioContext) {
+9 -7
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
)
@@ -55,6 +56,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -62,7 +64,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
})
@@ -84,7 +86,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -92,7 +94,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
})
})
@@ -165,7 +167,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
setUp()
@@ -174,7 +176,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
})
@@ -198,7 +200,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
setUp()
@@ -207,7 +209,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
})
+10 -11
View File
@@ -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")
@@ -127,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)
@@ -154,7 +153,7 @@ func (hs *HTTPServer) registerRoutes() {
userRoute.Delete("/stars/dashboard/:id", routing.Wrap(hs.UnstarDashboard))
userRoute.Put("/password", routing.Wrap(hs.ChangeUserPassword))
userRoute.Get("/quotas", routing.Wrap(GetUserQuotas))
userRoute.Get("/quotas", routing.Wrap(hs.GetUserQuotas))
userRoute.Put("/helpflags/:id", routing.Wrap(hs.SetHelpFlag))
// For dev purpose
userRoute.Get("/helpflags/clear", routing.Wrap(hs.ClearHelpFlags))
@@ -256,15 +255,15 @@ 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
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
prefRoute.Post("/set-home-dash", routing.Wrap(SetHomeDashboard))
prefRoute.Post("/set-home-dash", routing.Wrap(hs.SetHomeDashboard))
})
// Data sources
@@ -493,8 +492,8 @@ func (hs *HTTPServer) registerRoutes() {
adminUserRoute.Delete("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(AdminDeleteUser))
adminUserRoute.Post("/:id/disable", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
adminUserRoute.Post("/:id/enable", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(AdminEnableUser))
adminUserRoute.Get("/:id/quotas", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(UpdateUserQuota))
adminUserRoute.Get("/:id/quotas", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(hs.GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(hs.UpdateUserQuota))
adminUserRoute.Post("/:id/logout", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
+5 -3
View File
@@ -40,15 +40,16 @@ import (
"github.com/stretchr/testify/require"
)
func loggedInUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) {
loggedInUserScenarioWithRole(t, desc, "GET", url, routePattern, models.ROLE_EDITOR, fn)
func loggedInUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
loggedInUserScenarioWithRole(t, desc, "GET", url, routePattern, models.ROLE_EDITOR, fn, sqlStore)
}
func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
sc := setupScenarioContext(t, url)
sc.sqlStore = sqlStore
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
sc.context = c
sc.context.UserId = testUserID
@@ -156,6 +157,7 @@ type scenarioContext struct {
req *http.Request
url string
userAuthTokenService *auth.FakeUserAuthTokenService
sqlStore sqlstore.Store
}
func (sc *scenarioContext) exec() {
+9 -7
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
)
@@ -30,13 +31,13 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
return models.ErrDashboardNotFound
})
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
callGetDashboardPermissions(sc, hs)
assert.Equal(t, 404, sc.resp.Code)
})
}, mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -73,13 +74,13 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
return nil
})
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
callGetDashboardPermissions(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -125,6 +126,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
return nil
})
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
@@ -139,7 +141,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
assert.Len(t, resp, 5)
assert.Equal(t, int64(2), resp[0].UserId)
assert.Equal(t, models.PERMISSION_VIEW, resp[0].Permission)
})
}, mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -341,7 +343,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
return nil
})
}
mock := mockstore.NewSQLStoreMock()
var resp []*models.DashboardAclInfoDTO
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
@@ -357,7 +359,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
assert.Equal(t, models.PERMISSION_EDIT, resp[0].Permission)
assert.Equal(t, int64(4), resp[1].UserId)
assert.Equal(t, models.PERMISSION_ADMIN, resp[1].Permission)
})
}, mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
+12 -8
View File
@@ -12,6 +12,7 @@ import (
"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/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -69,6 +70,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
}
t.Run("When user has editor role and is not in the ACL", func(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "Should not be able to delete snapshot when calling DELETE on",
"DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockSnapshotResult := setUpSnapshotTest(t)
@@ -84,7 +86,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
require.Nil(t, externalRequest)
})
}, mock)
})
t.Run("When user is anonymous", func(t *testing.T) {
@@ -120,7 +122,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
{Role: &viewerRole, Permission: models.PERMISSION_VIEW},
{Role: &editorRole, Permission: models.PERMISSION_EDIT},
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE",
"/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockSnapshotResult := setUpSnapshotTest(t)
@@ -143,11 +145,12 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
assert.Equal(t, 1, respJSON.Get("id").MustInt())
assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host))
assert.Equal(t, "/", externalRequest.URL.EscapedPath())
})
}, mock)
})
t.Run("When user is editor and creator of the snapshot", func(t *testing.T) {
aclMockResp = []*models.DashboardAclInfoDTO{}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on",
"DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockSnapshotResult := setUpSnapshotTest(t)
@@ -164,11 +167,12 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted"))
assert.Equal(t, 1, respJSON.Get("id").MustInt())
})
}, mock)
})
t.Run("When deleting an external snapshot", func(t *testing.T) {
aclMockResp = []*models.DashboardAclInfoDTO{}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t,
"Should gracefully delete local snapshot when remote snapshot has already been removed when calling DELETE on",
"DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
@@ -192,7 +196,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted"))
assert.Equal(t, 1, respJSON.Get("id").MustInt())
})
}, mock)
loggedInUserScenarioWithRole(t,
"Should fail to delete local snapshot when an unexpected 500 error occurs when calling DELETE on", "DELETE",
@@ -213,7 +217,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
require.NoError(t, writeErr)
assert.Equal(t, 500, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t,
"Should fail to delete local snapshot when an unexpected remote error occurs when calling DELETE on",
@@ -230,7 +234,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, 500, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "Should be able to read a snapshot's unencrypted data when calling GET on",
"GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
@@ -247,6 +251,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
id := dashboard.Get("id")
assert.Equal(t, int64(100), id.MustInt64())
})
}, mock)
})
}
+49 -43
View File
@@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
@@ -159,7 +160,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
t.Run("When user is an Org Viewer", func(t *testing.T) {
role := models.ROLE_VIEWER
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp()
@@ -171,7 +172,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.False(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
assert.False(t, dash.Meta.CanAdmin)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
@@ -185,7 +186,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1",
"/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -193,7 +194,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersion(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
@@ -201,12 +202,12 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersions(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
t.Run("When user is an Org Editor", func(t *testing.T) {
role := models.ROLE_EDITOR
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp()
@@ -217,7 +218,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
assert.False(t, dash.Meta.CanAdmin)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
@@ -227,10 +228,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mock,
})
assert.Equal(t, 200, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1",
"/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -238,7 +240,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersion(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
@@ -246,7 +248,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersions(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
})
@@ -256,6 +258,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Live: newTestLive(t),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mockstore.NewSQLStoreMock(),
}
setUp := func() *testState {
@@ -315,7 +318,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
t.Run("When user is an Org Viewer and has no permissions for this dashboard", func(t *testing.T) {
role := models.ROLE_VIEWER
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp()
@@ -324,7 +327,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
@@ -333,7 +336,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1",
"/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -341,7 +344,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersion(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
@@ -349,12 +352,12 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersions(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
t.Run("When user is an Org Editor and has no permissions for this dashboard", func(t *testing.T) {
role := models.ROLE_EDITOR
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp()
@@ -364,7 +367,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
@@ -373,7 +376,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1",
"/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -381,7 +384,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersion(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
@@ -389,7 +392,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callGetDashboardVersions(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
t.Run("When user is an Org Viewer but has an edit permission", func(t *testing.T) {
@@ -407,7 +410,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
})
return state
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi",
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -418,29 +421,28 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
assert.False(t, dash.Meta.CanAdmin)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 200, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersion(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersions(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
t.Run("When user is an Org Viewer and viewers can edit", func(t *testing.T) {
@@ -466,7 +468,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
return state
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -477,7 +479,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.True(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
assert.False(t, dash.Meta.CanAdmin)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -485,7 +487,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
})
t.Run("When user is an Org Viewer but has an admin permission", func(t *testing.T) {
@@ -503,7 +505,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
})
return state
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -513,7 +515,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
assert.True(t, dash.Meta.CanAdmin)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -521,21 +523,21 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 200, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersion(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersions(sc)
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
t.Run("When user is an Org Editor but has a view permission", func(t *testing.T) {
@@ -553,7 +555,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
})
return state
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -561,7 +563,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
assert.False(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUpInner()
@@ -569,21 +571,21 @@ func TestDashboardAPIEndpoint(t *testing.T) {
callDeleteDashboardByUID(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersion(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
setUpInner()
callGetDashboardVersions(sc)
assert.Equal(t, 403, sc.resp.Code)
})
}, mock)
})
})
@@ -964,7 +966,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
return nil
})
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
@@ -972,12 +974,13 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mock,
})
assert.Equal(t, 400, sc.resp.Code)
result := sc.ToJSON()
assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Error(), result.Get("error").MustString())
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
@@ -990,8 +993,9 @@ func TestDashboardAPIEndpoint(t *testing.T) {
dash := getDashboardShouldReturn200WithConfig(sc, mock)
assert.Equal(t, filepath.Join("test", "dashboard1.json"), dash.Meta.ProvisionedExternalId)
})
}, mock)
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
@@ -1008,6 +1012,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
ProvisioningService: mock,
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mockSQLStore,
}
callGetDashboard(sc, hs)
@@ -1018,7 +1023,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, false, dash.Meta.Provisioned)
})
}, mock)
})
}
@@ -1036,6 +1041,7 @@ func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ
LibraryPanelService: &libraryPanelsService,
LibraryElementService: &libraryElementsService,
ProvisioningService: provisioningService,
SQLStore: sc.sqlStore,
}
callGetDashboard(sc, hs)
+5 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -27,6 +28,7 @@ const (
)
func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
loggedInUserScenario(t, "When calling GET on", "/api/datasources/", "/api/datasources/", func(sc *scenarioContext) {
// Stubs the database query
bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourcesQuery) error {
@@ -45,6 +47,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
SQLStore: mock,
}
sc.handlerFunc = hs.GetDataSources
sc.fakeReq("GET", "/api/datasources").exec()
@@ -57,7 +60,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
assert.Equal(t, "BBB", respJSON[1]["name"])
assert.Equal(t, "mmm", respJSON[2]["name"])
assert.Equal(t, "ZZZ", respJSON[3]["name"])
})
}, mock)
loggedInUserScenario(t, "Should be able to save a data source when calling DELETE on non-existing",
"/api/datasources/name/12345", "/api/datasources/name/:name", func(sc *scenarioContext) {
@@ -70,7 +73,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
sc.handlerFunc = hs.DeleteDataSourceByName
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
})
}, mock)
}
// Adding data sources with invalid URLs should lead to an error.
+9 -7
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
)
@@ -34,11 +35,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
dashboards.NewFolderService = origNewFolderService
})
mockFolderService(mock)
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 404, sc.resp.Code)
})
}, mockSQLStore)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -77,11 +78,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
}
mockFolderService(mock)
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
})
}, mockSQLStore)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -130,7 +131,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
}
mockFolderService(mock)
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 200, sc.resp.Code)
@@ -142,7 +143,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
assert.Len(t, resp, 5)
assert.Equal(t, int64(2), resp[0].UserId)
assert.Equal(t, models.PERMISSION_VIEW, resp[0].Permission)
})
}, mockSQLStore)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -325,6 +326,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
mockFolderService(mock)
var resp []*models.DashboardAclInfoDTO
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 200, sc.resp.Code)
@@ -337,7 +339,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
assert.Equal(t, models.PERMISSION_EDIT, resp[0].Permission)
assert.Equal(t, int64(4), resp[1].UserId)
assert.Equal(t, models.PERMISSION_ADMIN, resp[1].Permission)
})
}, mockSQLStore)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
-3
View File
@@ -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 {
+7 -2
View File
@@ -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"
@@ -107,7 +108,7 @@ type HTTPServer struct {
LivePushGateway *pushhttp.Gateway
ThumbService thumbs.Service
ContextHandler *contexthandler.ContextHandler
SQLStore *sqlstore.SQLStore
SQLStore sqlstore.Store
AlertEngine *alerting.AlertEngine
LoadSchemaService *schemaloader.SchemaLoaderService
AlertNG *ngalert.AlertNG
@@ -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")
@@ -432,6 +436,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.web
m.Use(middleware.RequestTracing(hs.tracer))
m.Use(middleware.RequestMetrics(hs.Features))
m.Use(middleware.Logger(hs.Cfg))
+7 -6
View File
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -41,7 +42,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
sqlStore.Cfg = settings
hs.SQLStore = sqlStore
mock := mockstore.NewSQLStoreMock()
loggedInUserScenario(t, "When calling GET on", "api/org/users", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
@@ -54,7 +55,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 3)
})
}, mock)
loggedInUserScenario(t, "When calling GET on", "api/org/users/search", "api/org/users/search", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
@@ -72,7 +73,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, int64(3), resp.TotalCount)
assert.Equal(t, 1000, resp.PerPage)
assert.Equal(t, 1, resp.Page)
})
}, mock)
loggedInUserScenario(t, "When calling GET with page and limit query parameters on", "api/org/users/search", "api/org/users/search", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
@@ -90,7 +91,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, int64(3), resp.TotalCount)
assert.Equal(t, 2, resp.PerPage)
assert.Equal(t, 2, resp.Page)
})
}, mock)
t.Run("Given there are two hidden users", func(t *testing.T) {
settings.HiddenUsers = map[string]struct{}{
@@ -113,7 +114,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Len(t, resp, 2)
assert.Equal(t, testUserLogin, resp[0].Login)
assert.Equal(t, "user2", resp[1].Login)
})
}, mock)
loggedInUserScenarioWithRole(t, "When calling GET as an admin on", "GET", "api/org/users/lookup",
"api/org/users/lookup", models.ROLE_ADMIN, func(sc *scenarioContext) {
@@ -130,7 +131,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Len(t, resp, 2)
assert.Equal(t, testUserLogin, resp[0].Login)
assert.Equal(t, "user2", resp[1].Login)
})
}, mock)
})
}
+13 -7
View File
@@ -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)
}
+2 -3
View File
@@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"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/infra/fs"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -143,7 +142,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
if err := bus.Dispatch(c.Req.Context(), &query); err != nil {
if err := hs.SQLStore.GetPluginSettingById(c.Req.Context(), &query); err != nil {
if !errors.Is(err, models.ErrPluginSettingNotFound) {
return response.Error(500, "Failed to get login settings", nil)
}
@@ -175,7 +174,7 @@ func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Respons
cmd.OrgId = c.OrgId
cmd.PluginId = pluginID
if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.UpdatePluginSetting(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update plugin setting", err)
}
+2 -3
View File
@@ -6,7 +6,6 @@ import (
"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/web"
)
@@ -18,7 +17,7 @@ const (
)
// POST /api/preferences/set-home-dash
func SetHomeDashboard(c *models.ReqContext) response.Response {
func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response {
cmd := models.SavePreferencesCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -26,7 +25,7 @@ func SetHomeDashboard(c *models.ReqContext) response.Response {
cmd.UserId = c.UserId
cmd.OrgId = c.OrgId
if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.SavePreferences(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to set home dashboard", err)
}
+4 -5
View File
@@ -5,7 +5,6 @@ import (
"strconv"
"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/web"
@@ -61,7 +60,7 @@ func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response {
return response.Success("Organization quota updated")
}
func GetUserQuotas(c *models.ReqContext) response.Response {
func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response {
if !setting.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
@@ -73,14 +72,14 @@ func GetUserQuotas(c *models.ReqContext) response.Response {
query := models.GetUserQuotasQuery{UserId: id}
if err := bus.Dispatch(c.Req.Context(), &query); err != nil {
if err := hs.SQLStore.GetUserQuotas(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
}
return response.JSON(200, query.Result)
}
func UpdateUserQuota(c *models.ReqContext) response.Response {
func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response {
cmd := models.UpdateUserQuotaCmd{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
@@ -99,7 +98,7 @@ func UpdateUserQuota(c *models.ReqContext) response.Response {
return response.Error(404, "Invalid quota target", nil)
}
if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.UpdateUserQuota(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")
+2 -3
View File
@@ -5,7 +5,6 @@ import (
"strings"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
@@ -52,8 +51,8 @@ type RouteRegister interface {
type RegisterNamedMiddleware func(name string) web.Handler
func ProvideRegister(features featuremgmt.FeatureToggles) *RouteRegisterImpl {
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
func ProvideRegister() *RouteRegisterImpl {
return NewRouteRegister(middleware.ProvideRouteOperationName)
}
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
+1 -10
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -25,7 +24,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
return response.Error(403, "Not allowed to create team.", nil)
}
team, err := createTeam(hs.SQLStore, cmd.Name, cmd.Email, c.OrgId)
team, err := hs.SQLStore.CreateTeam(cmd.Name, cmd.Email, c.OrgId)
if err != nil {
if errors.Is(err, models.ErrTeamNameTaken) {
return response.Error(409, "Team name taken", err)
@@ -45,7 +44,6 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
c.Logger.Warn("Could not add creator to team because is not a real user")
}
}
return response.JSON(200, &util.DynMap{
"teamId": team.Id,
"message": "Team created",
@@ -211,10 +209,3 @@ func (hs *HTTPServer) UpdateTeamPreferences(c *models.ReqContext) response.Respo
return hs.updatePreferencesFor(c.Req.Context(), orgId, 0, teamId, &dtoCmd)
}
// createTeam creates a team.
//
// Stubbable by tests.
var createTeam = func(sqlStore *sqlstore.SQLStore, name, email string, orgID int64) (models.Team, error) {
return sqlStore.CreateTeam(name, email, orgID)
}
+4 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/teamguardian/database"
"github.com/grafana/grafana/pkg/services/teamguardian/manager"
"github.com/grafana/grafana/pkg/setting"
@@ -47,6 +48,7 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
License: &licensing.OSSLicensingService{},
SQLStore: sqlStore,
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "api/teams/1/members",
"api/teams/:teamId/members", models.ROLE_ADMIN, func(sc *scenarioContext) {
@@ -61,7 +63,7 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
err := json.Unmarshal(sc.resp.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Len(t, resp, 3)
})
}, mock)
t.Run("Given there is two hidden users", func(t *testing.T) {
settings.HiddenUsers = map[string]struct{}{
@@ -86,7 +88,7 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, "loginuser0", resp[0].Login)
assert.Equal(t, "loginuser1", resp[1].Login)
assert.Equal(t, "loginuser2", resp[2].Login)
})
}, mock)
})
}
+10 -25
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
@@ -34,7 +35,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
t.Run("Given two teams", func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = sqlstore.InitTestDB(t)
mock := mockstore.SQLStoreMock{}
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
require.NoError(t, err)
@@ -50,7 +51,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
assert.EqualValues(t, 2, resp.TotalCount)
assert.Equal(t, 2, len(resp.Teams))
})
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
@@ -67,29 +68,15 @@ func TestTeamAPIEndpoint(t *testing.T) {
assert.EqualValues(t, 2, resp.TotalCount)
assert.Equal(t, 0, len(resp.Teams))
})
}, mock)
})
t.Run("When creating team with API key", func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.Cfg.EditorsCanAdmin = true
hs.SQLStore = mockstore.NewSQLStoreMock()
teamName := "team foo"
// TODO: Use a fake SQLStore when it's represented by an interface
orgCreateTeam := createTeam
orgAddTeamMember := addOrUpdateTeamMember
t.Cleanup(func() {
createTeam = orgCreateTeam
addOrUpdateTeamMember = orgAddTeamMember
})
createTeamCalled := 0
createTeam = func(sqlStore *sqlstore.SQLStore, name, email string, orgID int64) (models.Team, error) {
createTeamCalled++
return models.Team{Name: teamName, Id: 42}, nil
}
addTeamMemberCalled := 0
addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64,
permission string) error {
@@ -109,9 +96,9 @@ func TestTeamAPIEndpoint(t *testing.T) {
}
c.OrgRole = models.ROLE_EDITOR
c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName})
hs.CreateTeam(c)
assert.Equal(t, createTeamCalled, 1)
assert.Equal(t, addTeamMemberCalled, 0)
r := hs.CreateTeam(c)
assert.Equal(t, 200, r.Status())
assert.True(t, stub.warnCalled)
assert.Equal(t, stub.warnMessage, "Could not add creator to team because is not a real user")
})
@@ -125,10 +112,8 @@ func TestTeamAPIEndpoint(t *testing.T) {
}
c.OrgRole = models.ROLE_EDITOR
c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName})
createTeamCalled, addTeamMemberCalled = 0, 0
hs.CreateTeam(c)
assert.Equal(t, createTeamCalled, 1)
assert.Equal(t, addTeamMemberCalled, 1)
r := hs.CreateTeam(c)
assert.Equal(t, 200, r.Status())
assert.False(t, stub.warnCalled)
})
})
+8 -7
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/oauth2"
@@ -41,7 +42,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
},
TotalCount: 2,
}
mock := mockstore.NewSQLStoreMock()
loggedInUserScenario(t, "When calling GET on", "api/users/1", "api/users/:id", func(sc *scenarioContext) {
fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC)
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
@@ -100,7 +101,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
resp.UpdatedAt = fakeNow
resp.AvatarUrl = avatarUrl
require.EqualValues(t, expected, resp)
})
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) {
fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC)
@@ -141,7 +142,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
require.Equal(t, "admin", resp.Login)
require.Equal(t, "admin@test.com", resp.Email)
require.True(t, resp.IsGrafanaAdmin)
})
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/users", "/api/users", func(sc *scenarioContext) {
var sentLimit int
@@ -165,7 +166,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
require.NoError(t, err)
assert.Equal(t, 2, len(respJSON.MustArray()))
})
}, mock)
loggedInUserScenario(t, "When calling GET with page and limit querystring parameters on", "/api/users", "/api/users", func(sc *scenarioContext) {
var sentLimit int
@@ -185,7 +186,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, 10, sentLimit)
assert.Equal(t, 2, sendPage)
})
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
@@ -211,7 +212,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, 2, respJSON.Get("totalCount").MustInt())
assert.Equal(t, 2, len(respJSON.Get("users").MustArray()))
})
}, mock)
loggedInUserScenario(t, "When calling GET with page and perpage querystring parameters on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
@@ -231,5 +232,5 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
assert.Equal(t, 10, sentLimit)
assert.Equal(t, 2, sendPage)
})
}, mock)
}
+3 -5
View File
@@ -8,7 +8,6 @@ import (
"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/util"
"github.com/grafana/grafana/pkg/web"
@@ -32,7 +31,7 @@ func (hs *HTTPServer) RevokeUserAuthToken(c *models.ReqContext) response.Respons
func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, userID int64) response.Response {
userQuery := models.GetUserByIdQuery{Id: userID}
if err := bus.Dispatch(ctx, &userQuery); err != nil {
if err := hs.SQLStore.GetUserById(ctx, &userQuery); err != nil {
if errors.Is(err, models.ErrUserNotFound) {
return response.Error(404, "User not found", err)
}
@@ -52,7 +51,7 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user
func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) response.Response {
userQuery := models.GetUserByIdQuery{Id: userID}
if err := bus.Dispatch(c.Req.Context(), &userQuery); err != nil {
if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil {
if errors.Is(err, models.ErrUserNotFound) {
return response.Error(404, "User not found", err)
}
@@ -118,8 +117,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int
func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) response.Response {
userQuery := models.GetUserByIdQuery{Id: userID}
if err := bus.Dispatch(c.Req.Context(), &userQuery); err != nil {
if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil {
if errors.Is(err, models.ErrUserNotFound) {
return response.Error(404, "User not found", err)
}
+34 -50
View File
@@ -11,87 +11,73 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
)
func TestUserTokenAPIEndpoint(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
t.Run("When current user attempts to revoke an auth token for a non-existing user", func(t *testing.T) {
cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2}
mock.ExpectedError = models.ErrUserNotFound
revokeUserAuthTokenScenario(t, "Should return not found when calling POST on", "/api/user/revoke-auth-token",
"/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) {
var userID int64
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
userID = cmd.Id
return models.ErrUserNotFound
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
assert.Equal(t, int64(200), userID)
})
}, mock)
})
t.Run("When current user gets auth tokens for a non-existing user", func(t *testing.T) {
mock := mockstore.SQLStoreMock{
ExpectedUser: &models.User{Id: 200},
ExpectedError: models.ErrUserNotFound,
}
getUserAuthTokensScenario(t, "Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) {
var userID int64
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
userID = cmd.Id
return models.ErrUserNotFound
})
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
assert.Equal(t, int64(200), userID)
})
}, mock)
})
t.Run("When logging out an existing user from all devices", func(t *testing.T) {
mock := mockstore.SQLStoreMock{
ExpectedUser: &models.User{Id: 200},
}
logoutUserFromAllDevicesInternalScenario(t, "Should be successful", 1, func(sc *scenarioContext) {
const userID int64 = 200
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: userID}
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
t.Run("When logout a non-existing user from all devices", func(t *testing.T) {
logoutUserFromAllDevicesInternalScenario(t, "Should return not found", testUserID, func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
return models.ErrUserNotFound
})
mock.ExpectedError = models.ErrUserNotFound
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
})
}, mock)
})
t.Run("When revoke an auth token for a user", func(t *testing.T) {
cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2}
token := &models.UserToken{Id: 1}
mock := mockstore.SQLStoreMock{
ExpectedUser: &models.User{Id: 200},
}
revokeUserAuthTokenInternalScenario(t, "Should be successful", cmd, 200, token, func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: 200}
return nil
})
sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) {
return &models.UserToken{Id: 2}, nil
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
}, mock)
})
t.Run("When revoke the active auth token used by himself", func(t *testing.T) {
cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2}
token := &models.UserToken{Id: 2}
mock := mockstore.NewSQLStoreMock()
revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: testUserID}
@@ -103,18 +89,13 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
})
}, mock)
})
t.Run("When gets auth tokens for a user", func(t *testing.T) {
currentToken := &models.UserToken{Id: 1}
mock := mockstore.NewSQLStoreMock()
getUserAuthTokensInternalScenario(t, "Should be successful", currentToken, func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: testUserID}
return nil
})
tokens := []*models.UserToken{
{
Id: 1,
@@ -165,12 +146,12 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
assert.Equal(t, "11.0", resultTwo.Get("browserVersion").MustString())
assert.Equal(t, "iOS", resultTwo.Get("os").MustString())
assert.Equal(t, "11.0", resultTwo.Get("osVersion").MustString())
})
}, mock)
})
}
func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd,
userId int64, fn scenarioFunc) {
userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -179,6 +160,7 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
@@ -199,7 +181,7 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat
})
}
func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc) {
func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -208,6 +190,7 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, url)
@@ -227,13 +210,14 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte
})
}
func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc) {
func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, "/")
@@ -253,7 +237,7 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId
}
func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64,
token *models.UserToken, fn scenarioFunc) {
token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -262,6 +246,7 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, "/")
@@ -275,14 +260,12 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R
return hs.revokeUserAuthTokenInternal(c, userId, cmd)
})
sc.m.Post("/", sc.defaultHandler)
fn(sc)
})
}
func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc) {
func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -291,6 +274,7 @@ func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: fakeAuthTokenService,
SQLStore: sqlStore,
}
sc := setupScenarioContext(t, "/")
+10
View File
@@ -183,6 +183,16 @@ var adminCommands = []*cli.Command{
Usage: "Re-encrypts secrets by decrypting and re-encrypting them with the currently configured encryption. Returns ok unless there is an error. Safe to execute multiple times.",
Action: runRunnerCommand(secretsmigrations.ReEncryptSecrets),
},
{
Name: "rollback",
Usage: "Rolls back secrets to legacy encryption. Returns ok unless there is an error. Safe to execute multiple times.",
Action: runRunnerCommand(secretsmigrations.RollBackSecrets),
},
{
Name: "re-encrypt-data-keys",
Usage: "Rotates persisted data encryption keys. Returns ok unless there is an error. Safe to execute multiple times.",
Action: runRunnerCommand(secretsmigrations.ReEncryptDEKS),
},
},
},
}
@@ -0,0 +1,19 @@
package secretsmigrations
import (
"context"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func ReEncryptDEKS(_ utils.CommandLine, runner runner.Runner) error {
if !runner.Features.IsEnabled(featuremgmt.FlagEnvelopeEncryption) {
logger.Warn("Envelope encryption is not enabled, quitting...")
return nil
}
return runner.SecretsService.ReEncryptDataKeys(context.Background())
}
@@ -17,12 +17,6 @@ import (
"xorm.io/xorm"
)
type simpleSecret struct {
tableName string
columnName string
isBase64Encoded bool
}
func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
var rows []struct {
Id int
@@ -76,10 +70,6 @@ func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.S
return nil
}
type jsonSecret struct {
tableName string
}
func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
var rows []struct {
Id int
@@ -119,8 +109,6 @@ func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Ses
return nil
}
type alertingSecret struct{}
func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error {
var results []struct {
Id int
@@ -0,0 +1,220 @@
package secretsmigrations
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"xorm.io/xorm"
)
func (s simpleSecret) rollback(
secretsSrv *manager.SecretsService,
encryptionSrv encryption.Internal,
sess *xorm.Session,
secretKey string,
) error {
var rows []struct {
Id int
Secret string
}
if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil {
return err
}
for _, row := range rows {
if len(row.Secret) == 0 {
continue
}
var (
err error
decoded = []byte(row.Secret)
)
if s.isBase64Encoded {
decoded, err = base64.StdEncoding.DecodeString(row.Secret)
if err != nil {
return err
}
}
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
if err != nil {
return err
}
encrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey)
if err != nil {
return err
}
encoded := string(encrypted)
if s.isBase64Encoded {
encoded = base64.StdEncoding.EncodeToString(encrypted)
}
updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
if _, err := sess.Exec(updateSQL, encoded, row.Id); err != nil {
return err
}
}
logger.Infof("Column %s from %s have been rolled back successfully\n", s.columnName, s.tableName)
return nil
}
func (s jsonSecret) rollback(
secretsSrv *manager.SecretsService,
encryptionSrv encryption.Internal,
sess *xorm.Session,
secretKey string,
) error {
var rows []struct {
Id int
SecureJsonData map[string][]byte
}
if err := sess.Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil {
return err
}
for _, row := range rows {
if len(row.SecureJsonData) == 0 {
continue
}
decrypted, err := secretsSrv.DecryptJsonData(context.Background(), row.SecureJsonData)
if err != nil {
return err
}
var toUpdate struct {
SecureJsonData map[string][]byte
}
toUpdate.SecureJsonData, err = encryptionSrv.EncryptJsonData(context.Background(), decrypted, secretKey)
if err != nil {
return err
}
if _, err := sess.Table(s.tableName).Where("id = ?", row.Id).Update(toUpdate); err != nil {
return err
}
}
logger.Infof("Secure json data from %s have been rolled back successfully\n", s.tableName)
return nil
}
func (s alertingSecret) rollback(
secretsSrv *manager.SecretsService,
encryptionSrv encryption.Internal,
sess *xorm.Session,
secretKey string,
) error {
var results []struct {
Id int
AlertmanagerConfiguration string
}
selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration"
if err := sess.SQL(selectSQL).Find(&results); err != nil {
return err
}
for _, result := range results {
result := result
postableUserConfig, err := notifier.Load([]byte(result.AlertmanagerConfiguration))
if err != nil {
return err
}
for _, receiver := range postableUserConfig.AlertmanagerConfig.Receivers {
for _, gmr := range receiver.GrafanaManagedReceivers {
for k, v := range gmr.SecureSettings {
decoded, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return err
}
decrypted, err := secretsSrv.Decrypt(context.Background(), decoded)
if err != nil {
return err
}
reencrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey)
if err != nil {
return err
}
gmr.SecureSettings[k] = base64.StdEncoding.EncodeToString(reencrypted)
}
}
}
marshalled, err := json.Marshal(postableUserConfig)
if err != nil {
return err
}
result.AlertmanagerConfiguration = string(marshalled)
if _, err := sess.Table("alert_configuration").Where("id = ?", result.Id).Update(&result); err != nil {
return err
}
}
logger.Info("Alerting secrets have rolled re-encrypted successfully\n")
return nil
}
func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error {
if !runner.Features.IsEnabled(featuremgmt.FlagEnvelopeEncryption) {
logger.Warn("Envelope encryption is not enabled, quitting...")
return nil
}
toMigrate := []interface {
rollback(*manager.SecretsService, encryption.Internal, *xorm.Session, string) error
}{
simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted", isBase64Encoded: false},
simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token", isBase64Encoded: true},
simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token", isBase64Encoded: true},
simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type", isBase64Encoded: true},
jsonSecret{tableName: "data_source"},
jsonSecret{tableName: "plugin_setting"},
alertingSecret{},
}
return runner.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
for _, m := range toMigrate {
if err := m.rollback(
runner.SecretsService,
runner.EncryptionService,
sess.Session,
runner.Cfg.SecretKey); err != nil {
return err
}
}
if _, err := sess.Exec("DELETE FROM data_keys"); err != nil {
logger.Warn("Error while cleaning up data keys table...", "err", err)
}
return nil
})
}
@@ -0,0 +1,13 @@
package secretsmigrations
type simpleSecret struct {
tableName string
columnName string
isBase64Encoded bool
}
type jsonSecret struct {
tableName string
}
type alertingSecret struct{}
+48 -38
View File
@@ -45,50 +45,60 @@ func init() {
}
// RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(features featuremgmt.FeatureToggles) func(handler string) web.Handler {
return func(handler string) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
rw := res.(web.ResponseWriter)
now := time.Now()
httpRequestsInFlight.Inc()
defer httpRequestsInFlight.Dec()
func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/public/") || c.Req.URL.Path == "robots.txt" || c.Req.URL.Path == "/metrics" {
c.Next()
return
}
status := rw.Status()
rw := res.(web.ResponseWriter)
now := time.Now()
httpRequestsInFlight.Inc()
defer httpRequestsInFlight.Dec()
c.Map(c.Req)
c.Next()
code := sanitizeCode(status)
method := sanitizeMethod(req.Method)
handler := "unknown"
// enable histogram and disable summaries + counters for http requests.
if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) {
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
} else {
// avoiding the sanitize functions for in the new instrumentation
// since they dont make much sense. We should remove them later.
histogram := httpRequestDurationHistogram.
WithLabelValues(handler, strconv.Itoa(rw.Status()), req.Method)
if traceID, ok := cw.ExtractSampledTraceID(c.Req.Context()); ok {
// Need to type-convert the Observer to an
// ExemplarObserver. This will always work for a
// HistogramVec.
histogram.(prometheus.ExemplarObserver).ObserveWithExemplar(
time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID},
)
return
}
histogram.Observe(time.Since(now).Seconds())
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
handler = routeOperation
}
status := rw.Status()
code := sanitizeCode(status)
method := sanitizeMethod(req.Method)
// enable histogram and disable summaries + counters for http requests.
if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) {
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
} else {
// avoiding the sanitize functions for in the new instrumentation
// since they dont make much sense. We should remove them later.
histogram := httpRequestDurationHistogram.
WithLabelValues(handler, code, req.Method)
if traceID, ok := cw.ExtractSampledTraceID(c.Req.Context()); ok {
// Need to type-convert the Observer to an
// ExemplarObserver. This will always work for a
// HistogramVec.
histogram.(prometheus.ExemplarObserver).ObserveWithExemplar(
time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID},
)
return
}
histogram.Observe(time.Since(now).Seconds())
}
switch {
case strings.HasPrefix(req.RequestURI, "/api/datasources/proxy"):
countProxyRequests(status)
case strings.HasPrefix(req.RequestURI, "/api/"):
countApiRequests(status)
default:
countPageRequests(status)
}
switch {
case strings.HasPrefix(req.RequestURI, "/api/datasources/proxy"):
countProxyRequests(status)
case strings.HasPrefix(req.RequestURI, "/api/"):
countApiRequests(status)
default:
countPageRequests(status)
}
}
}
+6
View File
@@ -65,6 +65,7 @@ import (
serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/teamguardian"
teamguardianDatabase "github.com/grafana/grafana/pkg/services/teamguardian/database"
teamguardianManager "github.com/grafana/grafana/pkg/services/teamguardian/manager"
@@ -201,10 +202,12 @@ 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)),
wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)),
wire.Bind(new(sqlstore.Store), new(*sqlstore.SQLStore)),
)
var wireTestSet = wire.NewSet(
@@ -212,11 +215,14 @@ 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)),
wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)),
wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)),
mockstore.NewSQLStoreMock,
wire.Bind(new(sqlstore.Store), new(*mockstore.SQLStoreMock)),
)
func Initialize(cla setting.CommandLineArgs, opts Options, apiOpts api.ServerOptions) (*Server, error) {
@@ -91,7 +91,7 @@ func buildScopeParams(c *models.ReqContext) accesscontrol.ScopeParams {
type OrgIDGetter func(c *models.ReqContext) (int64, error)
func AuthorizeInOrgMiddleware(ac accesscontrol.AccessControl, db *sqlstore.SQLStore) func(web.Handler, OrgIDGetter, accesscontrol.Evaluator) web.Handler {
func AuthorizeInOrgMiddleware(ac accesscontrol.AccessControl, db sqlstore.Store) func(web.Handler, OrgIDGetter, accesscontrol.Evaluator) web.Handler {
return func(fallback web.Handler, getTargetOrg OrgIDGetter, evaluator accesscontrol.Evaluator) web.Handler {
if ac.IsDisabled() {
return fallback
+2 -2
View File
@@ -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
}
+6 -6
View File
@@ -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")
+1 -2
View File
@@ -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)
}
@@ -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(`{
@@ -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
}
+20 -4
View File
@@ -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() {
@@ -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{}
+59 -5
View File
@@ -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)
+5 -3
View File
@@ -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
}
+2 -2
View File
@@ -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()
+14 -14
View File
@@ -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)
+20 -16
View File
@@ -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 &notificationService{
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)
+13 -9
View File
@@ -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 {
@@ -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++
}
@@ -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)
+5 -1
View File
@@ -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),
}
}
+5 -5
View File
@@ -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)
})
}
+4 -4
View File
@@ -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
}
@@ -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)
})
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
+2 -2
View File
@@ -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)
+5 -5
View File
@@ -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
}
@@ -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"})
+4 -4
View File
@@ -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
}
@@ -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
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}
@@ -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)
+4 -4
View File
@@ -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
}

Some files were not shown because too many files have changed in this diff Show More