Compare commits
24 Commits
v8.4.0
...
v8.4.0-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f16e4cedc | |||
| 0c2ba819a7 | |||
| afac7701cb | |||
| a943bf9963 | |||
| 6415b9a54d | |||
| bb88cf683c | |||
| c8bb4c733e | |||
| 29b97361f7 | |||
| 43b15b92ad | |||
| a79c048344 | |||
| c23bc1e7b7 | |||
| f582e6c86a | |||
| f36ed878e9 | |||
| 3314178a0a | |||
| f38f10416a | |||
| 8c6a5f043a | |||
| 1b286e6bb5 | |||
| b2655750e8 | |||
| 16f0c6617a | |||
| f8105efff3 | |||
| ce4d8646fd | |||
| 984c95de63 | |||
| 0092d10764 | |||
| 3504844ad7 |
+5
-5
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+40
-8
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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, "/")
|
||||
|
||||
@@ -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{}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -83,18 +83,22 @@ type ShowWhen struct {
|
||||
Is string `json:"is"`
|
||||
}
|
||||
|
||||
func newNotificationService(renderService rendering.Service, decryptFn GetDecryptedValueFn) *notificationService {
|
||||
func newNotificationService(renderService rendering.Service, sqlStore AlertStore, notificationSvc *notifications.NotificationService, decryptFn GetDecryptedValueFn) *notificationService {
|
||||
return ¬ificationService{
|
||||
log: log.New("alerting.notifier"),
|
||||
renderService: renderService,
|
||||
decryptFn: decryptFn,
|
||||
log: log.New("alerting.notifier"),
|
||||
renderService: renderService,
|
||||
sqlStore: sqlStore,
|
||||
notificationService: notificationSvc,
|
||||
decryptFn: decryptFn,
|
||||
}
|
||||
}
|
||||
|
||||
type notificationService struct {
|
||||
log log.Logger
|
||||
renderService rendering.Service
|
||||
decryptFn GetDecryptedValueFn
|
||||
log log.Logger
|
||||
renderService rendering.Service
|
||||
sqlStore AlertStore
|
||||
notificationService *notifications.NotificationService
|
||||
decryptFn GetDecryptedValueFn
|
||||
}
|
||||
|
||||
func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error {
|
||||
@@ -152,7 +156,7 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no
|
||||
Version: notifierState.state.Version,
|
||||
}
|
||||
|
||||
return bus.Dispatch(evalContext.Ctx, cmd)
|
||||
return n.sqlStore.SetAlertNotificationStateToCompleteCommand(evalContext.Ctx, cmd)
|
||||
}
|
||||
|
||||
func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *notifierState) error {
|
||||
@@ -163,7 +167,7 @@ func (n *notificationService) sendNotification(evalContext *EvalContext, notifie
|
||||
AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(evalContext.Ctx, setPendingCmd)
|
||||
err := n.sqlStore.SetAlertNotificationStateToPendingCommand(evalContext.Ctx, setPendingCmd)
|
||||
if err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationStateVersionConflict) {
|
||||
return nil
|
||||
@@ -251,13 +255,13 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
|
||||
func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) {
|
||||
query := &models.GetAlertNotificationsWithUidToSendQuery{OrgId: orgID, Uids: notificationUids}
|
||||
|
||||
if err := bus.Dispatch(evalContext.Ctx, query); err != nil {
|
||||
if err := n.sqlStore.GetAlertNotificationsWithUidToSend(evalContext.Ctx, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result notifierStateSlice
|
||||
for _, notification := range query.Result {
|
||||
not, err := InitNotifier(notification, n.decryptFn)
|
||||
not, err := InitNotifier(notification, n.decryptFn, n.notificationService)
|
||||
if err != nil {
|
||||
n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err)
|
||||
continue
|
||||
@@ -269,7 +273,7 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [
|
||||
OrgId: evalContext.Rule.OrgID,
|
||||
}
|
||||
|
||||
err = bus.Dispatch(evalContext.Ctx, query)
|
||||
err = n.sqlStore.GetOrCreateAlertNotificationState(evalContext.Ctx, query)
|
||||
if err != nil {
|
||||
n.log.Error("Could not get notification state.", "notifier", notification.Id, "error", err)
|
||||
continue
|
||||
@@ -287,13 +291,13 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [
|
||||
}
|
||||
|
||||
// InitNotifier instantiate a new notifier based on the model.
|
||||
func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Notifier, error) {
|
||||
func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn, notificationService *notifications.NotificationService) (Notifier, error) {
|
||||
notifierPlugin, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("unsupported notification type %q", model.Type)
|
||||
}
|
||||
|
||||
return notifierPlugin.Factory(model, fn)
|
||||
return notifierPlugin.Factory(model, fn, notificationService)
|
||||
}
|
||||
|
||||
// GetDecryptedValueFn is a function that returns the decrypted value of
|
||||
@@ -301,7 +305,7 @@ func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Noti
|
||||
type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string, secret string) string
|
||||
|
||||
// NotifierFactory is a signature for creating notifiers.
|
||||
type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error)
|
||||
type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error)
|
||||
|
||||
var notifierFactories = make(map[string]*NotifierPlugin)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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"})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user