diff --git a/.drone.yml b/.drone.yml index f8d744be837..2b826efa4a0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1094,7 +1094,7 @@ type: docker --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release +name: oss-build-e2e-publish-release node: type: no-parallel platform: @@ -1599,7 +1599,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release +- oss-build-e2e-publish-release - oss-test-release - oss-integration-tests-release kind: pipeline @@ -2858,7 +2858,7 @@ volumes: --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release-branch +name: oss-build-e2e-publish-release-branch node: type: no-parallel platform: @@ -3305,7 +3305,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release-branch +- oss-build-e2e-publish-release-branch - oss-test-release-branch - oss-integration-tests-release-branch kind: pipeline @@ -4191,6 +4191,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 4d1a5696bf1e510fb51a021c07e240c50cb913724ce08ed52cce037ff02dd8de +hmac: f26fc6de1d7ec3cf5608b70c851c8cf2b998e07abad8c52714e72ad072492387 ... diff --git a/.vscode/launch.json b/.vscode/launch.json index 18e31305b45..5eaae3ff47b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,7 @@ "mode": "auto", "program": "${workspaceFolder}/pkg/cmd/grafana-server/", "env": {}, + "cwd": "${workspaceFolder}", "args": ["--homepath", "${workspaceFolder}", "--packaging", "dev"] }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e10774dee..a2b75759278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ + + +# 8.4.0-beta1 (2022-02-02) + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) + + # 8.3.4 (2022-01-17) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index ac3a4398e98..d94d09cda63 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -15,10 +15,11 @@ Grafana 8.0 introduced new and improved alerting that centralizes alerting infor Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature. -| Release | Cloud | Enterprise | OSS | -| ----------- | ------------- | ---------- | -------------------------------- | -| Grafana 8.2 | On by default | Opt-in | Opt-in | -| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Release | Cloud | Enterprise | OSS | +| ------------------------ | ------------- | ------------- | -------------------------------- | +| Grafana 8.2 | On by default | Opt-in | Opt-in | +| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | +| Grafana 9.0 (unreleased) | On by default | On by default | On by default | > **Note:** New installs include existing installs which do not have any alerts configured. diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/alerting/old-alerting/_index.md index 32695e28573..8d0cd673290 100644 --- a/docs/sources/alerting/old-alerting/_index.md +++ b/docs/sources/alerting/old-alerting/_index.md @@ -7,7 +7,7 @@ weight = 114 Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in.md" >}}) feature. -> **Note**: Legacy dashboard alerts is deprecated and will be removed in a future release. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. +> **Note**: Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) for all existing installations. Legacy dashboard alerts have two main components: diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index ebee7b16687..1639e9e2abf 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -501,9 +501,3 @@ The org id of the datasource where the query data will be written. If all `default_remote_write_*` properties are set, this information will be populated at startup. If a remote write target has already been configured, nothing will happen. - -## [feature_highlights] - -### enabled - -Whether the feature highlights feature is enabled diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 2ae655180d9..907d9f47afd 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) - [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) - [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}}) - [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-0-beta1.md b/docs/sources/release-notes/release-notes-8-4-0-beta1.md new file mode 100644 index 00000000000..1d9aaf557bb --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-0-beta1.md @@ -0,0 +1,23 @@ ++++ +title = "Release notes for Grafana 8.4.0-beta1" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.0-beta1 + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) diff --git a/e2e/dashboards-suite/new-constant-variable.spec.ts b/e2e/dashboards-suite/new-constant-variable.spec.ts index a173a5cf6fb..9df1d069df1 100644 --- a/e2e/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e/dashboards-suite/new-constant-variable.spec.ts @@ -3,23 +3,23 @@ import { e2e } from '@grafana/e2e'; const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; describe('Variables - Constant', () => { - it('can add a new text box variable', () => { + it('can add a new constant 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.generalTypeSelectV2().type('Constant{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().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.BackButton.backArrow().click({ force: true }); e2e.components.RefreshPicker.runButtonV2().click(); // Assert it was rendered diff --git a/e2e/dashboards-suite/new-custom-variable.spec.ts b/e2e/dashboards-suite/new-custom-variable.spec.ts index 28aaca032b9..a9c1408c69d 100644 --- a/e2e/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e/dashboards-suite/new-custom-variable.spec.ts @@ -3,9 +3,9 @@ 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.General.generalTypeSelectV2().type('Custom{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name).blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label).blur(); e2e.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput().type(value).blur(); } @@ -42,7 +42,7 @@ describe('Variables - Custom', () => { // Create a new "Custom" variable e2e.components.CallToActionCard.buttonV2('Add variable').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Custom{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Custom{enter}'); // Set it's name, label, and content fillInCustomVariable('VariableUnderTest', 'Variable under test', 'One : 1,Two : 2, Three : 3'); @@ -50,7 +50,7 @@ describe('Variables - Custom', () => { // 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.BackButton.backArrow().click({ force: true }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click(); diff --git a/e2e/dashboards-suite/new-datasource-variable.spec.ts b/e2e/dashboards-suite/new-datasource-variable.spec.ts new file mode 100644 index 00000000000..0818e7ff4db --- /dev/null +++ b/e2e/dashboards-suite/new-datasource-variable.spec.ts @@ -0,0 +1,37 @@ +import { e2e } from '@grafana/e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; + +describe('Variables - Datasource', () => { + it('can add a new datasource variable', () => { + e2e.flows.login('admin', 'admin'); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + + // Create a new "Datasource" variable + e2e.components.CallToActionCard.buttonV2('Add variable').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Data source{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); + + // If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" + // Or, just update is to match some gdev datasources to test with :) + e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().type('Prometheus{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption() + .eq(0) + .should('have.text', 'gdev-prometheus'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption() + .eq(1) + .should('have.text', 'gdev-slow-prometheus'); + + // Navigate back to the homepage and change the selected variable value + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.components.BackButton.backArrow().click({ force: true }); + e2e.components.RefreshPicker.runButtonV2().click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('gdev-slow-prometheus').click(); + + // Assert it was rendered + e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus'); + }); +}); diff --git a/e2e/dashboards-suite/new-interval-variable.spec.ts b/e2e/dashboards-suite/new-interval-variable.spec.ts new file mode 100644 index 00000000000..c22f40ba865 --- /dev/null +++ b/e2e/dashboards-suite/new-interval-variable.spec.ts @@ -0,0 +1,41 @@ +import { e2e } from '@grafana/e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; + +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 - Interval', () => { + it('can add a new interval variable', () => { + e2e.flows.login('admin', 'admin'); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + + // Create a new "Interval" variable + e2e.components.CallToActionCard.buttonV2('Add variable').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Interval{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); + + e2e.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput() + .clear() + .type('10s,10m,60m,90m,1h30m') + .blur(); + + assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']); + + // Navigate back to the homepage and change the selected variable value + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.components.BackButton.backArrow().click({ force: true }); + e2e.components.RefreshPicker.runButtonV2().click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click(); + + // Assert it was rendered + e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m'); + }); +}); diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index 206e014a15b..dbc5d16f068 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -9,18 +9,18 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2() .should('be.visible') .within((input) => { expect(input.attr('placeholder')).equals('name'); expect(input.val()).equals('query0'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2() .should('be.visible') .within((select) => { e2e.components.Select.singleValue().should('have.text', 'Query'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') .within((input) => { expect(input.attr('placeholder')).equals('optional display name'); @@ -33,7 +33,7 @@ describe('Variables - Query - Add variable', () => { expect(input.attr('placeholder')).equals('descriptive text'); expect(input.val()).equals(''); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelectV2() .should('be.visible') .within((select) => { e2e.components.Select.singleValue().should('have.text', ''); @@ -45,19 +45,19 @@ describe('Variables - Query - Add variable', () => { e2e.components.Select.singleValue().should('have.text', 'gdev-testdata'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect() + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2() .should('be.visible') .within((select) => { e2e.components.Select.singleValue().should('have.text', 'On dashboard load'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput() + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2() .should('be.visible') .within((input) => { const placeholder = '/.*-(?.*)-(?.*)-.*/'; expect(input.attr('placeholder')).equals(placeholder); expect(input.val()).equals(''); }); - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelect() + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2() .should('be.visible') .within((select) => { e2e.components.Select.singleValue().should('have.text', 'Disabled'); @@ -66,7 +66,7 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch().should('not.be.checked'); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); - e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().should('not.exist'); }); it('adding a single value query variable', () => { @@ -75,7 +75,7 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') .clear() .type('a label'); @@ -89,7 +89,7 @@ describe('Variables - Query - Add variable', () => { .type('*') .blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput() + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2() .should('be.visible') .type('/.*C.*/') .blur(); @@ -125,7 +125,7 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') .clear() .type('a label'); @@ -139,7 +139,7 @@ describe('Variables - Query - Add variable', () => { .type('*') .blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput() + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2() .should('be.visible') .type('/.*C.*/') .blur(); @@ -152,7 +152,7 @@ describe('Variables - Query - Add variable', () => { .click({ force: true }) .should('be.checked'); - e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().within((input) => { + e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().within((input) => { expect(input.attr('placeholder')).equals('blank = auto'); expect(input.val()).equals(''); }); diff --git a/e2e/dashboards-suite/new-text-box-variable.spec.ts b/e2e/dashboards-suite/new-text-box-variable.spec.ts index bb19b03e4bc..0f5dddb8a04 100644 --- a/e2e/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/dashboards-suite/new-text-box-variable.spec.ts @@ -7,19 +7,19 @@ describe('Variables - Text box', () => { e2e.flows.login('admin', 'admin'); e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); - // Create a new "Custom" variable + // Create a new "text box" 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.generalTypeSelectV2().type('Text box{enter}'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().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.components.BackButton.backArrow().click({ force: true }); e2e().get('#VariableUnderTest').clear().type('dog-cat').blur(); // Assert it was rendered diff --git a/e2e/dashboards-suite/textbox-variables.spec.ts b/e2e/dashboards-suite/textbox-variables.spec.ts index 5f056cda09b..0f59bf6a9a9 100644 --- a/e2e/dashboards-suite/textbox-variables.spec.ts +++ b/e2e/dashboards-suite/textbox-variables.spec.ts @@ -201,7 +201,7 @@ function validateVariable(value: string) { e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput() + e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2() .should('be.visible') .should('have.value', value); } @@ -234,7 +234,7 @@ function changeQueryInput() { e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click(); - e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput() + e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2() .should('be.visible') .clear() .type('changed value') diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html index 1f967e53353..b7c7f6156dd 100644 --- a/emails/templates/ng_alert_notification.html +++ b/emails/templates/ng_alert_notification.html @@ -1,11 +1,14 @@ + +
+ [[Subject .Subject "[[.Title]]"]] [[ define "alert" ]] - - - Value: [[ .ValueString ]] - - + + + Value: [[ .ValueString ]] + + [[ if gt (len .Annotations.SortedPairs) 0 ]] @@ -187,62 +190,63 @@ - [[ if gt (len .Alerts.Firing) 0 ]] - - - - [[ range .Alerts.Firing ]] + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ range .Alerts.Firing ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] + [[ if gt (len .Alerts.Resolved) 0 ]] + + + + [[ range .Alerts.Resolved ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - [[ if gt (len .Alerts.Resolved) 0 ]] - - - - [[ range .Alerts.Resolved ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - - - -
- Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
+ Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Firing + + [[ .Labels.alertname ]] +
+ Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Resolved + + [[ .Labels.alertname ]] +
- Firing - - [[ .Labels.alertname ]] + + Go to alerts page
- Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Resolved - - [[ .Labels.alertname ]] -
- Go to alerts page -
[[ end ]] + +
diff --git a/latest.json b/latest.json index 91df80dab6b..707ba83022e 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "stable": "8.3.4", - "testing": "8.3.4" + "testing": "8.4.0-beta1" } diff --git a/lerna.json b/lerna.json index ea302e8c442..a4f8c6aa934 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "npmClient": "yarn", "useWorkspaces": true, "packages": ["packages/*"], - "version": "8.4.0-pre" + "version": "8.5.0-pre" } diff --git a/package.json b/package.json index e421573e495..2f59a592b2d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", @@ -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", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 1360df99d8d..105b1884e06 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "5.0.2", - "@grafana/schema": "8.4.0-pre", + "@grafana/schema": "8.5.0-pre", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 971014422c9..a3f86909bee 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,4 +36,5 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + featureHighlights?: boolean; } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 195c0b8d964..417fe206b8a 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ae1ca74046a..6ac5d7fbe6e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -110,20 +110,28 @@ export const Pages = { */ modeLabelEdit: 'Variable editor Header mode Edit', generalNameInput: 'Variable editor Form Name field', + generalNameInputV2: 'data-testid Variable editor Form Name field', generalTypeSelect: 'Variable editor Form Type select', + generalTypeSelectV2: 'data-testid Variable editor Form Type select', generalLabelInput: 'Variable editor Form Label field', + generalLabelInputV2: 'data-testid Variable editor Form Label field', generalHideSelect: 'Variable editor Form Hide select', + generalHideSelectV2: 'data-testid Variable editor Form Hide select', selectionOptionsMultiSwitch: 'Variable editor Form Multi switch', selectionOptionsIncludeAllSwitch: 'Variable editor Form IncludeAll switch', selectionOptionsCustomAllInput: 'Variable editor Form IncludeAll field', + selectionOptionsCustomAllInputV2: 'data-testid Variable editor Form IncludeAll field', previewOfValuesOption: 'Variable editor Preview of Values option', submitButton: 'Variable editor Submit button', }, QueryVariable: { queryOptionsDataSourceSelect: Components.DataSourcePicker.container, queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select', + queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select', queryOptionsRegExInput: 'Variable editor Form Query RegEx field', + queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field', queryOptionsSortSelect: 'Variable editor Form Query Sort select', + queryOptionsSortSelectV2: 'data-testid Variable editor Form Query Sort select', queryOptionsQueryInput: 'Variable editor Form Default Variable Query Editor textarea', valueGroupsTagsEnabledSwitch: 'Variable editor Form Query UseTags switch', valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field', @@ -131,13 +139,21 @@ export const Pages = { }, ConstantVariable: { constantOptionsQueryInput: 'Variable editor Form Constant Query field', + constantOptionsQueryInputV2: 'data-testid Variable editor Form Constant Query field', + }, + DatasourceVariable: { + datasourceSelect: 'data-testid datasource variable datasource type', }, TextBoxVariable: { textBoxOptionsQueryInput: 'Variable editor Form TextBox Query field', + textBoxOptionsQueryInputV2: 'data-testid Variable editor Form TextBox Query field', }, CustomVariable: { customValueInput: 'data-testid custom-variable-input', }, + IntervalVariable: { + intervalsValueInput: 'data-testid interval variable intervals input', + }, }, }, }, diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 337a953666e..92b4aa6e7a1 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.16.7", "@babel/preset-env": "7.16.7", "@cypress/webpack-preprocessor": "5.11.0", - "@grafana/e2e-selectors": "8.4.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", "@grafana/tsconfig": "^1.0.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.3", diff --git a/packages/grafana-e2e/src/flows/addDashboard.ts b/packages/grafana-e2e/src/flows/addDashboard.ts index 2d4b089c6b4..d7efeb9c9ba 100644 --- a/packages/grafana-e2e/src/flows/addDashboard.ts +++ b/packages/grafana-e2e/src/flows/addDashboard.ts @@ -162,7 +162,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar // This field is key to many reactive changes if (type !== VARIABLE_TYPE_QUERY) { - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect() + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2() .should('be.visible') .within(() => { e2e.components.Select.singleValue().should('have.text', 'Query').click(); @@ -171,10 +171,10 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar } if (label) { - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label); } - e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type(name); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name); if ( dataSource && @@ -188,7 +188,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar } if (constantValue && type === VARIABLE_TYPE_CONSTANT) { - e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(constantValue); + e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type(constantValue); } if (type === VARIABLE_TYPE_QUERY) { @@ -197,7 +197,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar } if (regex) { - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput().type(regex); + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2().type(regex); } } diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 8de4dbe08e2..7e670e72903 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "@sentry/browser": "6.17.2", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 204550b89ed..ca2cd054338 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 061a034b8c8..c8d0562dbdb 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -28,10 +28,10 @@ "dependencies": { "@babel/core": "7.13.14", "@babel/preset-env": "7.13.12", - "@grafana/data": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", "@grafana/eslint-config": "2.5.2", "@grafana/tsconfig": "^1.0.0-rc1", - "@grafana/ui": "8.4.0-pre", + "@grafana/ui": "8.5.0-pre", "@jest/core": "26.6.3", "@rushstack/eslint-patch": "1.0.6", "@types/command-exists": "^1.2.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6f51f50ec0e..01a9fadac02 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.7.1", "@emotion/react": "11.7.1", "@grafana/aws-sdk": "0.0.31", - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/schema": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/e2e-selectors": "8.5.0-pre", + "@grafana/schema": "8.5.0-pre", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.2", diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index 939ac33c704..02f7945ceb5 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -18,6 +18,7 @@ import { import { Field } from '../../Forms/Field'; import { getInputStyles, Input } from '../../Input/Input'; import { Icon } from '../../Icon/Icon'; +import { Tooltip } from '../../Tooltip/Tooltip'; /** * @internal @@ -115,14 +116,13 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
- Specify time range -
- Specify a relative time range, for more information see{' '} - - docs - - . -
+ + } placement="bottom" theme="info"> +
+ Specify time range +
+
+
{ + const styles = useStyles2(toolTipStyles); + return ( + <> +
+ Supported formats: now-[digit]s/m/h/d/w +
+
Example: to select a time range from 10 minutes ago to now
+ From: now-10m To: now +
+ For more information see{' '} + + docs + + . +
+ + ); +}; + +const toolTipStyles = (theme: GrafanaTheme2) => ({ + supported: css` + margin-bottom: ${theme.spacing(1)}; + `, + tooltip: css` + margin: 0; + `, + link: css` + margin-top: ${theme.spacing(1)}; + `, +}); + const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme2) => { const inputStyles = getInputStyles({ theme, invalid: false }); const bodyMinimumHeight = 250; diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 5de63be37d2..1be78ccbcd0 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,8 +27,8 @@ }, "dependencies": { "@emotion/css": "11.7.1", - "@grafana/data": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/pkg/api/api.go b/pkg/api/api.go index e1aff6d4591..d25e0a44dfc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,7 +13,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" "github.com/grafana/grafana/pkg/services/featuremgmt" - sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" ) var plog = log.New("api") @@ -58,8 +57,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) - r.Get("/org/teams", reqCanAccessTeams, hs.Index) - r.Get("/org/teams/*", reqCanAccessTeams, hs.Index) + r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) + r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) + r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) @@ -127,8 +127,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) @@ -256,10 +256,10 @@ func (hs *HTTPServer) registerRoutes() { // auth api keys apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) { - keysRoute.Get("/", authorize(reqOrgAdmin, sa.ActionApikeyListEv), routing.Wrap(GetAPIKeys)) - keysRoute.Post("/", authorize(reqOrgAdmin, sa.ActionApikeyAddEv), quota("api_key"), routing.Wrap(hs.AddAPIKey)) - keysRoute.Post("/additional", authorize(reqOrgAdmin, sa.ActionApikeyAddAdditionalEv), quota("api_key"), routing.Wrap(hs.AdditionalAPIKey)) - keysRoute.Delete("/:id", authorize(reqOrgAdmin, sa.ActionApikeyRemoveEv), routing.Wrap(DeleteAPIKey)) + keysRoute.Get("/", routing.Wrap(GetAPIKeys)) + keysRoute.Post("/", quota("api_key"), routing.Wrap(hs.AddAPIKey)) + keysRoute.Post("/additional", quota("api_key"), routing.Wrap(hs.AdditionalAPIKey)) + keysRoute.Delete("/:id", routing.Wrap(DeleteAPIKey)) }, reqOrgAdmin) // Preferences @@ -373,7 +373,7 @@ func (hs *HTTPServer) registerRoutes() { // Search apiRoute.Get("/search/sorting", routing.Wrap(hs.ListSortOptions)) - apiRoute.Get("/search/", routing.Wrap(Search)) + apiRoute.Get("/search/", routing.Wrap(hs.Search)) // metrics apiRoute.Post("/tsdb/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), routing.Wrap(hs.QueryMetrics)) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 4b337918132..0bba9e04d3e 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -311,9 +311,10 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { cfg.IsFeatureToggleEnabled = features.IsEnabled return &HTTPServer{ - Cfg: cfg, - Features: features, - Bus: bus.GetBus(), + Cfg: cfg, + Features: features, + Bus: bus.GetBus(), + AccessControl: accesscontrolmock.New().WithDisabled(), } } diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index a8e204fdd25..84d8953f7aa 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -42,7 +42,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro })) origHandler := frontendLogger.GetLogger() - frontendLogger.AddLogger(newfrontendLogger, "info", map[string]level.Option{}) + frontendLogger.SetLogger(level.NewFilter(newfrontendLogger, level.AllowInfo())) sourceMapReads := []SourceMapReadRecord{} t.Cleanup(func() { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 0e45d41c620..43d38af0b77 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -273,9 +273,6 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "enabled": hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true), }, "unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled, - "featureHighlights": map[string]bool{ - "enabled": hs.SettingsProvider.Section("feature_highlights").KeyValue("enabled").MustBool(false), - }, } if hs.Cfg.GeomapDefaultBaseLayerConfig != nil { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 3c84c2bf73f..73012c9ffa0 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -44,8 +44,8 @@ import ( "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/pushhttp" "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" @@ -127,8 +127,9 @@ type HTTPServer struct { teamGuardian teamguardian.TeamGuardian queryDataService *query.Service serviceAccountsService serviceaccounts.Service - authInfoService authinfoservice.Service + authInfoService login.AuthInfoService TeamPermissionsService *resourcepermissions.Service + NotificationService *notifications.NotificationService } type ServerOptions struct { @@ -155,7 +156,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 login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices, + notificationService *notifications.NotificationService) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -215,6 +217,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi serviceAccountsService: serviceaccountsService, authInfoService: authInfoService, TeamPermissionsService: resourcePermissionServices.GetTeamService(), + NotificationService: notificationService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/index.go b/pkg/api/index.go index 25565347c10..4668e58a805 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/navlinks" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -148,7 +147,7 @@ func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) } -func enableTeams(hs *HTTPServer, c *models.ReqContext) bool { +func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) } @@ -263,7 +262,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if enableTeams(hs, c) { + if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) { configNodes = append(configNodes, &dtos.NavLink{ Text: "Teams", Id: "teams", @@ -540,7 +539,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink { func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := bus.Dispatch(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { + if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { return nil, err } hasEditPerm := hasEditPermissionInFoldersQuery.Result @@ -553,7 +552,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat settings["dateFormats"] = hs.Cfg.DateFormats prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser} - if err := bus.Dispatch(c.Req.Context(), &prefsQuery); err != nil { + if err := hs.SQLStore.GetPreferencesWithDefaults(c.Req.Context(), &prefsQuery); err != nil { return nil, err } prefs := prefsQuery.Result diff --git a/pkg/api/password.go b/pkg/api/password.go index 04c09a7470b..487c1e1c33d 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -1,19 +1,19 @@ package api import ( + "context" "errors" "net/http" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func SendResetPasswordEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Response { form := dtos.SendResetPasswordEmailForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -27,27 +27,33 @@ func SendResetPasswordEmail(c *models.ReqContext) response.Response { userQuery := models.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} - if err := bus.Dispatch(c.Req.Context(), &userQuery); err != nil { + if err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery); err != nil { c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail) return response.Error(200, "Email sent", err) } emailCmd := models.SendResetPasswordEmailCommand{User: userQuery.Result} - if err := bus.Dispatch(c.Req.Context(), &emailCmd); err != nil { + if err := hs.NotificationService.SendResetPasswordEmail(c.Req.Context(), &emailCmd); err != nil { return response.Error(500, "Failed to send email", err) } return response.Success("Email sent") } -func ResetPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { form := dtos.ResetUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } query := models.ValidateResetPasswordCodeQuery{Code: form.Code} - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + getUserByLogin := func(ctx context.Context, login string) (*models.User, error) { + userQuery := models.GetUserByLoginQuery{LoginOrEmail: login} + err := hs.SQLStore.GetUserByLogin(ctx, &userQuery) + return userQuery.Result, err + } + + if err := hs.NotificationService.ValidateResetPasswordCode(c.Req.Context(), &query, getUserByLogin); err != nil { if errors.Is(err, models.ErrInvalidEmailCode) { return response.Error(400, "Invalid or expired reset password code", nil) } @@ -66,7 +72,7 @@ func ResetPassword(c *models.ReqContext) response.Response { return response.Error(500, "Failed to encode password", err) } - if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil { + if err := hs.SQLStore.ChangeUserPassword(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to change user password", err) } diff --git a/pkg/api/roles.go b/pkg/api/roles.go index e8f3fc9fe53..0fc5d75723e 100644 --- a/pkg/api/roles.go +++ b/pkg/api/roles.go @@ -298,3 +298,22 @@ var orgsCreateAccessEvaluator = accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionOrgsRead), accesscontrol.EvalPermission(ActionOrgsCreate), ) + +// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access +var teamsAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) + +// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access +var teamsEditAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) diff --git a/pkg/api/search.go b/pkg/api/search.go index fa691138967..2f1a2cbf407 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -7,13 +7,12 @@ import ( "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/api/response" - "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/search" ) -func Search(c *models.ReqContext) response.Response { +func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { query := c.Query("query") tags := c.QueryStrings("tag") starred := c.Query("starred") @@ -62,7 +61,7 @@ func Search(c *models.ReqContext) response.Response { Sort: sort, } - err := bus.Dispatch(c.Req.Context(), &searchQuery) + err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery) if err != nil { return response.Error(500, "Search failed", err) } diff --git a/pkg/api/team.go b/pkg/api/team.go index 5db290459b2..b880bb4ba2e 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -2,12 +2,14 @@ package api import ( "errors" + "fmt" "net/http" "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -103,6 +105,20 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { return response.Success("Team deleted") } +func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err) + return nil, err + } + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil +} + // GET /api/teams/search func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { perPage := c.QueryInt("perpage") @@ -134,8 +150,17 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.Error(500, "Failed to search Teams", err) } + teamIDs := map[string]bool{} for _, team := range query.Result.Teams { team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + teamIDs[strconv.FormatInt(team.Id, 10)] = true + } + + metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs) + if err == nil && len(metadata) != 0 { + for _, team := range query.Result.Teams { + team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)] + } } query.Result.Page = page @@ -144,6 +169,23 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.JSON(200, query.Result) } +func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err) + return nil, err + } + + key := fmt.Sprintf("%d", teamID) + teamIDs := map[string]bool{key: true} + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil +} + // GET /api/teams/:teamId func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) @@ -165,6 +207,9 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get Team", err) } + metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id) + query.Result.AccessControl = metadata + query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return response.JSON(200, &query.Result) } diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index cc0cd45daf4..3c094622086 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" @@ -152,7 +153,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { } teamIDString := strconv.FormatInt(teamId, 10) - if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, userId, teamIDString, ""); err != nil { + if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil { if errors.Is(err, models.ErrTeamNotFound) { return response.Error(404, "Team not found", nil) } @@ -171,7 +172,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { // Stubbable by tests. var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error { teamIDString := strconv.FormatInt(teamID, 10) - if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, userID, teamIDString, permission); err != nil { + if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil { return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err) } return nil diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 727c08ccbad..2841d5bd5d2 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/services/login/authinfoservice" + authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" @@ -46,7 +47,8 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { 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)) - srv := authinfoservice.ProvideAuthInfoService(bus.New(), sqlStore, &authinfoservice.OSSUserProtectionImpl{}, secretsService) + authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, bus.New(), secretsService) + srv := authinfoservice.ProvideAuthInfoService(&authinfoservice.OSSUserProtectionImpl{}, authInfoStore) hs.authInfoService = srv createUserCmd := models.CreateUserCommand{ diff --git a/pkg/infra/log/composite_logger.go b/pkg/infra/log/composite_logger.go new file mode 100644 index 00000000000..5c238673347 --- /dev/null +++ b/pkg/infra/log/composite_logger.go @@ -0,0 +1,25 @@ +package log + +import gokitlog "github.com/go-kit/log" + +type compositeLogger struct { + loggers []gokitlog.Logger +} + +func newCompositeLogger(loggers ...gokitlog.Logger) *compositeLogger { + if len(loggers) == 0 { + loggers = []gokitlog.Logger{} + } + + return &compositeLogger{loggers: loggers} +} + +func (l *compositeLogger) Log(keyvals ...interface{}) error { + for _, logger := range l.loggers { + if err := logger.Log(keyvals...); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/infra/log/interface.go b/pkg/infra/log/interface.go index 523f9a3bc33..c0909d682cb 100644 --- a/pkg/infra/log/interface.go +++ b/pkg/infra/log/interface.go @@ -12,7 +12,7 @@ const ( type Logger interface { // New returns a new Logger that has this logger's context plus the given context - New(ctx ...interface{}) MultiLoggers + New(ctx ...interface{}) *ConcreteLogger Log(keyvals ...interface{}) error diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index ba531ca633f..da9395c410d 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -7,9 +7,12 @@ package log import ( "fmt" "io" + "log" "os" "path/filepath" + "sort" "strings" + "sync" "time" gokitlog "github.com/go-kit/log" @@ -25,8 +28,7 @@ import ( var loggersToClose []DisposableHandler var loggersToReload []ReloadableHandler -var filters map[string]level.Option -var Root MultiLoggers +var root *logManager const ( // top 7 calls in the stack are within logger @@ -37,11 +39,212 @@ const ( func init() { loggersToClose = make([]DisposableHandler, 0) loggersToReload = make([]ReloadableHandler, 0) - filters = map[string]level.Option{} // Use console by default format := getLogFormat("console") - Root.AddLogger(format(os.Stderr), "info", filters) + logger := level.NewFilter(format(os.Stderr), level.AllowInfo()) + root = newManager(logger) +} + +// logManager manage loggers +type logManager struct { + *ConcreteLogger + loggersByName map[string]*ConcreteLogger + logFilters []LogWithFilters + mutex sync.RWMutex +} + +func newManager(logger gokitlog.Logger) *logManager { + return &logManager{ + ConcreteLogger: newConcreteLogger(logger), + loggersByName: map[string]*ConcreteLogger{}, + } +} + +func (lm *logManager) initialize(loggers []LogWithFilters) { + lm.mutex.Lock() + defer lm.mutex.Unlock() + + defaultLoggers := make([]gokitlog.Logger, len(loggers)) + for index, logger := range loggers { + defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } + + lm.ConcreteLogger.SetLogger(&compositeLogger{loggers: defaultLoggers}) + lm.logFilters = loggers + + loggersByName := []string{} + for k := range lm.loggersByName { + loggersByName = append(loggersByName, k) + } + sort.Strings(loggersByName) + + for _, name := range loggersByName { + ctxLoggers := make([]gokitlog.Logger, len(loggers)) + + for index, logger := range loggers { + if filterLevel, exists := logger.filters[name]; !exists { + ctxLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } else { + ctxLoggers[index] = level.NewFilter(logger.val, filterLevel) + } + } + + lm.loggersByName[name].SetLogger(&compositeLogger{loggers: ctxLoggers}) + } +} + +func (lm *logManager) SetLogger(logger gokitlog.Logger) { + lm.ConcreteLogger.SetLogger(logger) +} + +func (lm *logManager) GetLogger() gokitlog.Logger { + return lm.ConcreteLogger.GetLogger() +} + +func (lm *logManager) Log(args ...interface{}) error { + lm.mutex.RLock() + defer lm.mutex.RUnlock() + if err := lm.ConcreteLogger.Log(args...); err != nil { + log.Println("Logging error", "error", err) + } + + return nil +} + +func (lm *logManager) New(ctx ...interface{}) *ConcreteLogger { + lm.mutex.Lock() + defer lm.mutex.Unlock() + if len(ctx) == 0 { + return lm.ConcreteLogger + } + + loggerName, ok := ctx[0].(string) + if !ok { + return lm.ConcreteLogger + } + + if logger, exists := lm.loggersByName[loggerName]; exists { + return logger + } + + ctx = append([]interface{}{"logger"}, ctx...) + + if len(lm.logFilters) == 0 { + ctxLogger := newConcreteLogger(lm.logger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger + } + + compositeLogger := newCompositeLogger() + for _, logWithFilter := range lm.logFilters { + filterLevel, ok := logWithFilter.filters[loggerName] + if ok { + logWithFilter.val = level.NewFilter(logWithFilter.val, filterLevel) + } else { + logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) + } + + compositeLogger.loggers = append(compositeLogger.loggers, logWithFilter.val) + } + + ctxLogger := newConcreteLogger(compositeLogger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger +} + +type ConcreteLogger struct { + ctx []interface{} + logger gokitlog.Logger + mutex sync.RWMutex +} + +func newConcreteLogger(logger gokitlog.Logger, ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + ctx = []interface{}{} + } else { + logger = gokitlog.With(logger, ctx...) + } + + return &ConcreteLogger{ + ctx: ctx, + logger: logger, + } +} + +func (cl *ConcreteLogger) SetLogger(logger gokitlog.Logger) { + cl.mutex.Lock() + cl.logger = gokitlog.With(logger, cl.ctx...) + cl.mutex.Unlock() +} + +func (cl *ConcreteLogger) GetLogger() gokitlog.Logger { + cl.mutex.Lock() + defer cl.mutex.Unlock() + return cl.logger +} + +func (cl *ConcreteLogger) Warn(msg string, args ...interface{}) { + _ = cl.log(msg, level.WarnValue(), args...) +} + +func (cl *ConcreteLogger) Debug(msg string, args ...interface{}) { + // args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) + _ = cl.log(msg, level.DebugValue(), args...) +} + +func (cl *ConcreteLogger) Error(msg string, args ...interface{}) { + _ = cl.log(msg, level.ErrorValue(), args...) +} + +func (cl *ConcreteLogger) Info(msg string, args ...interface{}) { + _ = cl.log(msg, level.InfoValue(), args...) +} + +func (cl *ConcreteLogger) log(msg string, logLevel level.Value, args ...interface{}) error { + cl.mutex.RLock() + logger := gokitlog.With(cl.logger, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) + cl.mutex.RUnlock() + + args = append([]interface{}{level.Key(), logLevel, "msg", msg}, args...) + + return logger.Log(args...) +} + +func (cl *ConcreteLogger) Log(keyvals ...interface{}) error { + cl.mutex.RLock() + defer cl.mutex.RUnlock() + return cl.logger.Log(keyvals...) +} + +func (cl *ConcreteLogger) New(ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + root.New() + } + + keyvals := []interface{}{} + + if len(cl.ctx)%2 == 1 { + cl.ctx = append(cl.ctx, nil) + } + + for i := 0; i < len(cl.ctx); i += 2 { + k, v := cl.ctx[i], cl.ctx[i+1] + + if k == "logger" { + continue + } + + keyvals = append(keyvals, k, v) + } + + keyvals = append(keyvals, ctx...) + + return root.New(keyvals...) +} + +func New(ctx ...interface{}) *ConcreteLogger { + return root.New(ctx...) } type LogWithFilters struct { @@ -50,111 +253,23 @@ type LogWithFilters struct { maxLevel level.Option } -type MultiLoggers struct { - loggers []LogWithFilters -} - -func (ml *MultiLoggers) AddLogger(val gokitlog.Logger, levelName string, filters map[string]level.Option) { - logger := LogWithFilters{val: val, filters: filters, maxLevel: getLogLevelFromString(levelName)} - ml.loggers = append(ml.loggers, logger) -} - -func (ml *MultiLoggers) SetLogger(des MultiLoggers) { - ml.loggers = des.loggers -} - -func (ml *MultiLoggers) GetLogger() MultiLoggers { - return *ml -} - -func (ml MultiLoggers) Warn(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.WarnValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Debug(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Error(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.ErrorValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Info(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.InfoValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Log(keyvals ...interface{}) error { - for _, multilogger := range ml.loggers { - multilogger.val = gokitlog.With(multilogger.val, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) - if err := multilogger.val.Log(keyvals...); err != nil { - return err - } - } - return nil -} - -// New creates a new logger from the existing one with additional context -func (ml MultiLoggers) New(ctx ...interface{}) MultiLoggers { - return with(ml, gokitlog.With, ctx) -} - -// New creates MultiLoggers with the provided context and caller that is added as a suffix. -// The first element of the context must be the logger name -func New(ctx ...interface{}) MultiLoggers { +func with(ctxLogger *ConcreteLogger, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) *ConcreteLogger { if len(ctx) == 0 { - return Root + return ctxLogger } - var newloger MultiLoggers - ctx = append([]interface{}{"logger"}, ctx...) - for _, logWithFilter := range Root.loggers { - logWithFilter.val = gokitlog.With(logWithFilter.val, ctx...) - v, ok := logWithFilter.filters[ctx[0].(string)] - if ok { - logWithFilter.val = level.NewFilter(logWithFilter.val, v) - } else { - logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) - } - newloger.loggers = append(newloger.loggers, logWithFilter) - } - return newloger -} -func with(loggers MultiLoggers, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) MultiLoggers { - if len(ctx) == 0 { - return loggers - } - var newloger MultiLoggers - for _, l := range loggers.loggers { - l.val = withFunc(l.val, ctx...) - newloger.loggers = append(newloger.loggers, l) - } - return newloger + ctxLogger.logger = withFunc(ctxLogger.logger, ctx...) + return ctxLogger } // WithPrefix adds context that will be added to the log message -func WithPrefix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithPrefix, ctx) +func WithPrefix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithPrefix, ctx) } // WithSuffix adds context that will be appended at the end of the log message -func WithSuffix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithSuffix, ctx) +func WithSuffix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithSuffix, ctx) } var logLevels = map[string]level.Option{ @@ -177,7 +292,7 @@ func getLogLevelFromString(levelName string) level.Option { loglevel, ok := logLevels[levelName] if !ok { - _ = level.Error(Root).Log("Unknown log level", "level", levelName) + _ = level.Error(root).Log("Unknown log level", "level", levelName) return level.AllowError() } @@ -282,7 +397,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { mode = strings.TrimSpace(mode) sec, err := cfg.GetSection("log." + mode) if err != nil { - _ = level.Error(Root).Log("Unknown log mode", "mode", mode) + _ = level.Error(root).Log("Unknown log mode", "mode", mode) return errutil.Wrapf(err, "failed to get config section log.%s", mode) } @@ -301,8 +416,8 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log")) dpath := filepath.Dir(fileName) if err := os.MkdirAll(dpath, os.ModePerm); err != nil { - _ = level.Error(Root).Log("Failed to create directory", "dpath", dpath, "err", err) - return errutil.Wrapf(err, "failed to create log directory %q", dpath) + _ = level.Error(root).Log("Failed to create directory", "dpath", dpath, "err", err) + continue } fileHandler := NewFileWriter() fileHandler.Filename = fileName @@ -313,8 +428,8 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileHandler.Daily = sec.Key("daily_rotate").MustBool(true) fileHandler.Maxdays = sec.Key("max_days").MustInt64(7) if err := fileHandler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) - return errutil.Wrapf(err, "failed to initialize file handler") + _ = level.Error(root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) + continue } loggersToClose = append(loggersToClose, fileHandler) @@ -336,20 +451,14 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { } } - // copy joined default + mode filters into filters - for key, value := range modeFilters { - if _, exist := filters[key]; !exist { - filters[key] = value - } - } - handler.filters = modeFilters handler.maxLevel = leveloption - // handler = LogFilterHandler(leveloption, modeFilters, handler) configLoggers = append(configLoggers, handler) } + if len(configLoggers) > 0 { - Root.loggers = configLoggers + root.initialize(configLoggers) } + return nil } diff --git a/pkg/infra/log/syslog.go b/pkg/infra/log/syslog.go index b1d818f5d58..150a8c6fae3 100644 --- a/pkg/infra/log/syslog.go +++ b/pkg/infra/log/syslog.go @@ -62,7 +62,7 @@ func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler { handler.Tag = sec.Key("tag").MustString("") if err := handler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to init syslog log handler", "error", err) + _ = level.Error(root).Log("Failed to init syslog log handler", "error", err) os.Exit(1) } handler.logger = gokitsyslog.NewSyslogLogger(handler.syslog, format, gokitsyslog.PrioritySelectorOption(selector)) diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 13ac15b2be1..bea01a403b4 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -25,7 +25,7 @@ type UsageStats struct { kvStore *kvstore.NamespacedKVStore RouteRegister routing.RouteRegister - log log.MultiLoggers + log log.Logger oauthProviders map[string]bool externalMetrics []usagestats.MetricsFunc diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index 9be66f1e3ac..ab9990b4f16 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -17,7 +17,7 @@ import ( func newLogger(name string, lev string) log.Logger { logger := log.New(name) - logger.AddLogger(logger, lev, map[string]level.Option{}) + logger.SetLogger(level.NewFilter(logger.GetLogger(), level.AllowInfo())) return logger } diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index ea8483a6911..fe5d3a22597 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -107,7 +107,7 @@ func Recovery(cfg *setting.Cfg) web.Handler { defer func() { if r := recover(); r != nil { var panicLogger log.Logger - panicLogger = log.Root + panicLogger = log.New("recovery") // try to get request logger ctx := contexthandler.FromContext(c.Req.Context()) if ctx != nil { diff --git a/pkg/models/team.go b/pkg/models/team.go index 328e1815b90..38f434458af 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -77,13 +77,14 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - MemberCount int64 `json:"memberCount"` - Permission PermissionType `json:"permission"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + MemberCount int64 `json:"memberCount"` + Permission PermissionType `json:"permission"` + AccessControl map[string]bool `json:"accessControl"` } type SearchTeamQueryResult struct { diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index 8b6412e8372..d63a34384c3 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -212,11 +212,11 @@ func (*testLicensingService) FeatureEnabled(feature string) bool { } type fakeLogger struct { - log.MultiLoggers + *log.ConcreteLogger } -func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (f fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (f fakeLogger) Warn(_ string, _ ...interface{}) { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 59cb67d8e86..b21a9b6f02f 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -1128,8 +1128,8 @@ type fakeLogger struct { log.Logger } -func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (fl fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (fl fakeLogger) Info(_ string, _ ...interface{}) { diff --git a/pkg/server/wire.go b/pkg/server/wire.go index a98268e2146..263a3844ea7 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -45,6 +45,7 @@ import ( "github.com/grafana/grafana/pkg/services/live/pushhttp" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" + authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/ngalert" ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -144,6 +145,8 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(login.Service), new(*loginservice.Implementation)), authinfoservice.ProvideAuthInfoService, wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)), + authinfodatabase.ProvideAuthInfoStore, + wire.Bind(new(login.Store), new(*authinfodatabase.AuthInfoStore)), datasourceproxy.ProvideService, search.ProvideService, live.ProvideService, @@ -190,7 +193,6 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)), teamguardianManager.ProvideService, wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)), - wire.Bind(new(authinfoservice.Service), new(*authinfoservice.Implementation)), featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, resourceservices.ProvideResourceServices, @@ -202,6 +204,7 @@ var wireBasicSet = wire.NewSet( var wireSet = wire.NewSet( wireBasicSet, sqlstore.ProvideService, + wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)), ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -214,6 +217,7 @@ var wireTestSet = wire.NewSet( ProvideTestEnv, sqlstore.ProvideServiceForTests, ngmetrics.ProvideServiceForTest, + wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)), notifications.MockNotificationService, wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 83fbeeaf4d3..3bf514d6371 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -37,13 +37,18 @@ type ResourcePermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) // SetUserPermission sets permission on resource for a user - SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*ResourcePermission, error) + SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) // SetTeamPermission sets permission on resource for a team SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*ResourcePermission, error) // SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error) } +type User struct { + ID int64 + IsExternal bool +} + // Metadata contains user accesses for a given resource // Ex: map[string]bool{"create":true, "delete": true} type Metadata map[string]bool diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index d4fa790178f..441b0deea29 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" @@ -81,7 +80,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { user, team := createUserAndTeam(t, sql, tt.orgID) for _, id := range tt.userPermissions { - _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{ + _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:write"}, Resource: "dashboards", ResourceID: id, diff --git a/pkg/services/accesscontrol/database/resource_permissions.go b/pkg/services/accesscontrol/database/resource_permissions.go index 436d6179df5..651fad1ee12 100644 --- a/pkg/services/accesscontrol/database/resource_permissions.go +++ b/pkg/services/accesscontrol/database/resource_permissions.go @@ -34,20 +34,20 @@ func (p *flatResourcePermission) Managed() bool { } func (s *AccessControlStore) SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) { - if userID == 0 { + if user.ID == 0 { return nil, models.ErrUserNotFound } var err error var permission *accesscontrol.ResourcePermission err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd) + permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd) if err == nil && hook != nil { - return hook(sess, orgID, userID, cmd.ResourceID, cmd.Permission) + return hook(sess, orgID, user, cmd.ResourceID, cmd.Permission) } return err diff --git a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go index 77a6a9fa398..27047433870 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go @@ -93,7 +93,7 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce _, err := ac.SetUserResourcePermission( context.Background(), accesscontrol.GlobalOrgID, - userIds[i], + accesscontrol.User{ID: userIds[i]}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{dsAction}, Resource: dsResource, diff --git a/pkg/services/accesscontrol/database/resource_permissions_test.go b/pkg/services/accesscontrol/database/resource_permissions_test.go index e029da2b1f5..00d6a5bfae9 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_test.go @@ -70,11 +70,11 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s, nil) + _, err := store.SetUserResourcePermission(context.Background(), test.orgID, accesscontrol.User{ID: test.userID}, s, nil) require.NoError(t, err) } - added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{ + added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, accesscontrol.SetResourcePermissionCommand{ Actions: test.actions, Resource: test.resource, ResourceID: test.resourceID, @@ -352,7 +352,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst }) require.NoError(t, err) - _, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{ + _, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Resource: resource, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 84a93834854..987a57948fd 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -131,7 +131,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, cmd.Permission) + _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: userID}, resourceID, cmd.Permission) if err != nil { return response.Error(http.StatusBadRequest, "failed to set user permission", err) } diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 6e596875cd4..bf64c93d0d4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -160,7 +160,7 @@ func TestApi_getPermissions(t *testing.T) { // seed user 1 with "View" permission on dashboard 1 u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) require.NoError(t, err) - _, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, "View") + _, err = service.SetUserPermission(context.Background(), u.OrgId, accesscontrol.User{ID: u.Id}, tt.resourceID, "View") require.NoError(t, err) // seed built in role Admin with "Edit" permission on dashboard 1 diff --git a/pkg/services/accesscontrol/resourcepermissions/options.go b/pkg/services/accesscontrol/resourcepermissions/options.go index 907a202ad5c..12fcb5acdb9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/options.go +++ b/pkg/services/accesscontrol/resourcepermissions/options.go @@ -3,6 +3,7 @@ package resourcepermissions import ( "context" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -28,7 +29,7 @@ type Options struct { // RoleGroup is the group name for the generated fixed roles RoleGroup string // OnSetUser if configured will be called each time a permission is set for a user - OnSetUser func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error + OnSetUser func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error // OnSetTeam if configured will be called each time a permission is set for a team OnSetTeam func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error // OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 6ab670a313e..ca5a587baa9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -16,7 +16,8 @@ import ( type Store interface { // SetUserResourcePermission sets permission for managed user role on a resource SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, + user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) @@ -100,7 +101,7 @@ func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID st }) } -func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { +func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { if !s.options.Assignments.Users { return nil, ErrInvalidAssignment } @@ -114,11 +115,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, re return nil, err } - if err := s.validateUser(ctx, orgID, userID); err != nil { + if err := s.validateUser(ctx, orgID, user.ID); err != nil { return nil, err } - return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{ + return s.store.SetUserResourcePermission(ctx, orgID, user, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Permission: permission, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/service_mock.go b/pkg/services/accesscontrol/resourcepermissions/service_mock.go new file mode 100644 index 00000000000..a1ad509e7cd --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/service_mock.go @@ -0,0 +1,33 @@ +package resourcepermissions + +import ( + "context" + + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +type MockService struct { + mock.Mock +} + +func (m *MockService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, resourceID) + return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, user, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index dbdcd3e4b5e..cec11542dd4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -46,13 +46,13 @@ func TestService_SetUserPermission(t *testing.T) { var hookCalled bool if tt.callHook { - service.options.OnSetUser = func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + service.options.OnSetUser = func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { hookCalled = true return nil } } - _, err = service.SetUserPermission(context.Background(), user.OrgId, user.Id, "1", "") + _, err = service.SetUserPermission(context.Background(), user.OrgId, accesscontrol.User{ID: user.Id}, "1", "") require.NoError(t, err) assert.Equal(t, tt.callHook, hookCalled) }) diff --git a/pkg/services/accesscontrol/resourcepermissions/types/hook.go b/pkg/services/accesscontrol/resourcepermissions/types/hook.go index 5e86cc3893c..b389e8d101c 100644 --- a/pkg/services/accesscontrol/resourcepermissions/types/hook.go +++ b/pkg/services/accesscontrol/resourcepermissions/types/hook.go @@ -1,7 +1,15 @@ package types -import "github.com/grafana/grafana/pkg/services/sqlstore" +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) -type UserResourceHookFunc func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error +type UserResourceHookFunc func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error type TeamResourceHookFunc func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error type BuiltinResourceHookFunc func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error + +type User struct { + ID int64 + IsExternal bool +} diff --git a/pkg/services/accesscontrol/resourceservices/resource_services.go b/pkg/services/accesscontrol/resourceservices/resource_services.go index 077e6cd93a5..3cf2adfb95f 100644 --- a/pkg/services/accesscontrol/resourceservices/resource_services.go +++ b/pkg/services/accesscontrol/resourceservices/resource_services.go @@ -77,20 +77,20 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore ReaderRoleName: "Team permission reader", WriterRoleName: "Team permission writer", RoleGroup: "Teams", - OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + OnSetUser: func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { teamId, err := strconv.ParseInt(resourceID, 10, 64) if err != nil { return err } switch permission { case "Member": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, 0) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) case "Admin": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN) case "": return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{ OrgId: orgID, - UserId: userID, + UserId: user.ID, TeamId: teamId, }) default: diff --git a/pkg/services/alerting/alerting_usage.go b/pkg/services/alerting/alerting_usage.go index 44e1152ab23..441fc85bf48 100644 --- a/pkg/services/alerting/alerting_usage.go +++ b/pkg/services/alerting/alerting_usage.go @@ -28,7 +28,7 @@ type UsageStatsQuerier interface { // configured in Grafana. func (e *AlertEngine) QueryUsageStats(ctx context.Context) (*UsageStats, error) { cmd := &models.GetAllAlertsQuery{} - err := e.Bus.Dispatch(ctx, cmd) + err := e.sqlStore.GetAllAlertQueryHandler(ctx, cmd) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (e *AlertEngine) mapRulesToUsageStats(ctx context.Context, rules []*models. result := map[string]int{} for k, v := range typeCount { query := &models.GetDataSourceQuery{Id: k} - err := e.Bus.Dispatch(ctx, query) + err := e.sqlStore.GetDataSource(ctx, query) if err != nil { return map[string]int{}, nil } diff --git a/pkg/services/alerting/alerting_usage_test.go b/pkg/services/alerting/alerting_usage_test.go index 5fb090ffe7e..582d8c7dd9f 100644 --- a/pkg/services/alerting/alerting_usage_test.go +++ b/pkg/services/alerting/alerting_usage_test.go @@ -7,18 +7,18 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" ) func TestAlertingUsageStats(t *testing.T) { + store := &AlertStoreMock{} ae := &AlertEngine{ - Bus: bus.New(), + sqlStore: store, } - ae.Bus.AddHandler(func(ctx context.Context, query *models.GetAllAlertsQuery) error { + store.getAllAlerts = func(ctx context.Context, query *models.GetAllAlertsQuery) error { var createFake = func(file string) *simplejson.Json { // Ignore gosec warning G304 since it's a test // nolint:gosec @@ -37,9 +37,9 @@ func TestAlertingUsageStats(t *testing.T) { {Id: 3, Settings: createFake("testdata/settings/empty.json")}, } return nil - }) + } - ae.Bus.AddHandler(func(ctx context.Context, query *models.GetDataSourceQuery) error { + store.getDataSource = func(ctx context.Context, query *models.GetDataSourceQuery) error { ds := map[int64]*models.DataSource{ 1: {Type: "influxdb"}, 2: {Type: "graphite"}, @@ -54,7 +54,7 @@ func TestAlertingUsageStats(t *testing.T) { query.Result = r return nil - }) + } result, err := ae.QueryUsageStats(context.Background()) require.NoError(t, err, "getAlertingUsage should not return error") diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index d6363416976..9aee49e5b1b 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -13,7 +13,6 @@ import ( gocontext "context" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -140,7 +139,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange l OrgId: context.Rule.OrgID, } - if err := bus.Dispatch(context.Ctx, getDsInfo); err != nil { + if err := context.Store.GetDataSource(context.Ctx, getDsInfo); err != nil { return nil, fmt.Errorf("could not find datasource: %w", err) } diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go index 2ed470c9906..21e7385613b 100644 --- a/pkg/services/alerting/conditions/query_interval_test.go +++ b/pkg/services/alerting/conditions/query_interval_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" @@ -137,16 +137,15 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo * //nolint: staticcheck // legacydata.DataResponse deprecated func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) { t.Run("desc", func(t *testing.T) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error { - query.Result = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData} - return nil - }) + store := mockstore.NewSQLStoreMock() + store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData} ctx := &queryIntervalTestContext{} ctx.result = &alerting.EvalContext{ Ctx: context.Background(), Rule: &alerting.Rule{}, RequestValidator: &validations.OSSPluginRequestValidator{}, + Store: store, } jsonModel, err := simplejson.NewJson([]byte(`{ diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index bf1d5a895bb..f7e34b49a2f 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -6,13 +6,13 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -35,10 +35,8 @@ func newTimeSeriesPointsFromArgs(values ...float64) legacydata.DataTimeSeriesPoi func TestQueryCondition(t *testing.T) { setup := func() *queryConditionTestContext { ctx := &queryConditionTestContext{} - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error { - query.Result = &models.DataSource{Id: 1, Type: "graphite"} - return nil - }) + store := mockstore.NewSQLStoreMock() + store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite"} ctx.reducer = `{"type":"avg"}` ctx.evaluator = `{"type":"gt","params":[100]}` @@ -46,6 +44,7 @@ func TestQueryCondition(t *testing.T) { Ctx: context.Background(), Rule: &alerting.Rule{}, RequestValidator: &validations.OSSPluginRequestValidator{}, + Store: store, } return ctx } diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index e89996d0b10..54b5e50010d 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -16,11 +16,25 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +// AlertStore is a subset of SQLStore API to satisfy the needs of the alerting service. +// A subset is needed to make it easier to mock during the tests. +type AlertStore interface { + GetAllAlertQueryHandler(context.Context, *models.GetAllAlertsQuery) error + GetDataSource(context.Context, *models.GetDataSourceQuery) error + GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error + SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error + SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error + GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error + GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error + SetAlertState(context.Context, *models.SetAlertStateCommand) error +} + // AlertEngine is the background process that // schedules alert evaluations and makes sure notifications // are sent. @@ -40,6 +54,7 @@ type AlertEngine struct { resultHandler resultHandler usageStatsService usagestats.Service tracer tracing.Tracer + sqlStore AlertStore } // IsDisabled returns true if the alerting service is disabled for this instance. @@ -50,7 +65,7 @@ func (e *AlertEngine) IsDisabled() bool { // ProvideAlertEngine returns a new AlertEngine. func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidator models.PluginRequestValidator, dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal, - cfg *setting.Cfg, tracer tracing.Tracer) *AlertEngine { + notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg) *AlertEngine { e := &AlertEngine{ Cfg: cfg, RenderService: renderer, @@ -59,14 +74,15 @@ func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidato DataService: dataService, usageStatsService: usageStatsService, tracer: tracer, + sqlStore: sqlStore, } e.ticker = NewTicker(time.Now(), time.Second*0, clock.New(), 1) e.execQueue = make(chan *Job, 1000) e.scheduler = newScheduler() e.evalHandler = NewEvalHandler(e.DataService) - e.ruleReader = newRuleReader() + e.ruleReader = newRuleReader(sqlStore) e.log = log.New("alerting.engine") - e.resultHandler = newResultHandler(e.RenderService, encryptionService.GetDecryptedValue) + e.resultHandler = newResultHandler(e.RenderService, sqlStore, notificationService, encryptionService.GetDecryptedValue) e.registerUsageMetrics() @@ -179,7 +195,7 @@ func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout) cancelChan <- cancelFn alertCtx, span := e.tracer.Start(alertCtx, "alert execution") - evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator) + evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator, e.sqlStore) evalContext.Ctx = alertCtx go func() { diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index 29422ea5dc3..f8a308ac5f2 100644 --- a/pkg/services/alerting/engine_integration_test.go +++ b/pkg/services/alerting/engine_integration_test.go @@ -24,7 +24,7 @@ func TestEngineTimeouts(t *testing.T) { usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer) + engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg()) setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 engine.resultHandler = &FakeResultHandler{} diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go index da21e2dcdda..771970efd95 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -43,12 +43,66 @@ func (handler *FakeResultHandler) handle(evalContext *EvalContext) error { return nil } +// A mock implementation of the AlertStore interface, allowing to override certain methods individually +type AlertStoreMock struct { + getAllAlerts func(context.Context, *models.GetAllAlertsQuery) error + getDataSource func(context.Context, *models.GetDataSourceQuery) error + getAlertNotificationsWithUidToSend func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error + getOrCreateNotificationState func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error +} + +func (a *AlertStoreMock) GetDataSource(c context.Context, cmd *models.GetDataSourceQuery) error { + if a.getDataSource != nil { + return a.getDataSource(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models.GetAllAlertsQuery) error { + if a.getAllAlerts != nil { + return a.getAllAlerts(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error { + if a.getAlertNotificationsWithUidToSend != nil { + return a.getAlertNotificationsWithUidToSend(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetOrCreateAlertNotificationState(c context.Context, cmd *models.GetOrCreateNotificationStateQuery) error { + if a.getOrCreateNotificationState != nil { + return a.getOrCreateNotificationState(c, cmd) + } + return nil +} + +func (a *AlertStoreMock) GetDashboardUIDById(_ context.Context, _ *models.GetDashboardRefByIdQuery) error { + return nil +} + +func (a *AlertStoreMock) SetAlertNotificationStateToCompleteCommand(_ context.Context, _ *models.SetAlertNotificationStateToCompleteCommand) error { + return nil +} + +func (a *AlertStoreMock) SetAlertNotificationStateToPendingCommand(_ context.Context, _ *models.SetAlertNotificationStateToPendingCommand) error { + return nil +} + +func (a *AlertStoreMock) SetAlertState(_ context.Context, _ *models.SetAlertStateCommand) error { + return nil +} + func TestEngineProcessJob(t *testing.T) { bus := bus.New() usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer) + + store := &AlertStoreMock{} + engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg()) setting.AlertingEvaluationTimeout = 30 * time.Second setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 @@ -56,19 +110,19 @@ func TestEngineProcessJob(t *testing.T) { job := &Job{running: true, Rule: &Rule{}} t.Run("Should register usage metrics func", func(t *testing.T) { - bus.AddHandler(func(ctx context.Context, q *models.GetAllAlertsQuery) error { + store.getAllAlerts = func(ctx context.Context, q *models.GetAllAlertsQuery) error { settings, err := simplejson.NewJson([]byte(`{"conditions": [{"query": { "datasourceId": 1}}]}`)) if err != nil { return err } q.Result = []*models.Alert{{Settings: settings}} return nil - }) + } - bus.AddHandler(func(ctx context.Context, q *models.GetDataSourceQuery) error { + store.getDataSource = func(ctx context.Context, q *models.GetDataSourceQuery) error { q.Result = &models.DataSource{Id: 1, Type: models.DS_PROMETHEUS} return nil - }) + } report, err := usMock.GetUsageReport(context.Background()) require.Nil(t, err) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 8d3c5f46faa..de0d2d155ab 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -6,7 +6,6 @@ import ( "regexp" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -36,10 +35,12 @@ type EvalContext struct { RequestValidator models.PluginRequestValidator Ctx context.Context + + Store AlertStore } // NewEvalContext is the EvalContext constructor. -func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator) *EvalContext { +func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator, sqlStore AlertStore) *EvalContext { return &EvalContext{ Ctx: alertCtx, StartTime: time.Now(), @@ -49,6 +50,7 @@ func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator model Log: log.New("alerting.evalContext"), PrevAlertState: rule.State, RequestValidator: requestValidator, + Store: sqlStore, } } @@ -108,7 +110,7 @@ func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) { } uidQuery := &models.GetDashboardRefByIdQuery{Id: c.Rule.DashboardID} - if err := bus.Dispatch(c.Ctx, uidQuery); err != nil { + if err := c.Store.GetDashboardUIDById(c.Ctx, uidQuery); err != nil { return nil, err } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 6aa12287716..30c3114ed15 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -15,7 +15,7 @@ import ( ) func TestStateIsUpdatedWhenNeeded(t *testing.T) { - ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}) + ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil) t.Run("ok -> alerting", func(t *testing.T) { ctx.PrevAlertState = models.AlertStateOK @@ -200,7 +200,7 @@ func TestGetStateFromEvalContext(t *testing.T) { } for _, tc := range tcs { - evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}) + evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil) tc.applyFn(evalContext) newState := evalContext.GetNewState() diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 051830bf1b4..ca11a462dd5 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -29,7 +29,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { Conditions: []Condition{&conditionStub{ firing: true, }}, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -39,7 +39,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { t.Run("Show return triggered with single passing condition2", func(t *testing.T) { context := NewEvalContext(context.Background(), &Rule{ Conditions: []Condition{&conditionStub{firing: true, operator: "and"}}, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -52,7 +52,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}}, &conditionStub{firing: false, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -65,7 +65,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -78,7 +78,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -92,7 +92,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: true, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -106,7 +106,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "and"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -120,7 +120,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "and"}, &conditionStub{firing: true, operator: "and"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -134,7 +134,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "or"}, &conditionStub{firing: true, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, true, context.Firing) @@ -148,7 +148,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{firing: false, operator: "or"}, &conditionStub{firing: false, operator: "or"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -163,7 +163,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "or", noData: false}, &conditionStub{operator: "or", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.False(t, context.NoDataFound) @@ -174,7 +174,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { Conditions: []Condition{ &conditionStub{operator: "and", noData: true}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.Equal(t, false, context.Firing) @@ -187,7 +187,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "and", noData: true}, &conditionStub{operator: "and", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.True(t, context.NoDataFound) @@ -199,7 +199,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { &conditionStub{operator: "or", noData: true}, &conditionStub{operator: "or", noData: false}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) handler.Eval(context) require.True(t, context.NoDataFound) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 98e0b135822..9c0626acc9f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -6,11 +6,11 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" ) @@ -83,18 +83,22 @@ type ShowWhen struct { Is string `json:"is"` } -func newNotificationService(renderService rendering.Service, decryptFn GetDecryptedValueFn) *notificationService { +func newNotificationService(renderService rendering.Service, sqlStore AlertStore, notificationSvc *notifications.NotificationService, decryptFn GetDecryptedValueFn) *notificationService { return ¬ificationService{ - log: log.New("alerting.notifier"), - renderService: renderService, - decryptFn: decryptFn, + log: log.New("alerting.notifier"), + renderService: renderService, + sqlStore: sqlStore, + notificationService: notificationSvc, + decryptFn: decryptFn, } } type notificationService struct { - log log.Logger - renderService rendering.Service - decryptFn GetDecryptedValueFn + log log.Logger + renderService rendering.Service + sqlStore AlertStore + notificationService *notifications.NotificationService + decryptFn GetDecryptedValueFn } func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error { @@ -152,7 +156,7 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no Version: notifierState.state.Version, } - return bus.Dispatch(evalContext.Ctx, cmd) + return n.sqlStore.SetAlertNotificationStateToCompleteCommand(evalContext.Ctx, cmd) } func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *notifierState) error { @@ -163,7 +167,7 @@ func (n *notificationService) sendNotification(evalContext *EvalContext, notifie AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges, } - err := bus.Dispatch(evalContext.Ctx, setPendingCmd) + err := n.sqlStore.SetAlertNotificationStateToPendingCommand(evalContext.Ctx, setPendingCmd) if err != nil { if errors.Is(err, models.ErrAlertNotificationStateVersionConflict) { return nil @@ -251,13 +255,13 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) { query := &models.GetAlertNotificationsWithUidToSendQuery{OrgId: orgID, Uids: notificationUids} - if err := bus.Dispatch(evalContext.Ctx, query); err != nil { + if err := n.sqlStore.GetAlertNotificationsWithUidToSend(evalContext.Ctx, query); err != nil { return nil, err } var result notifierStateSlice for _, notification := range query.Result { - not, err := InitNotifier(notification, n.decryptFn) + not, err := InitNotifier(notification, n.decryptFn, n.notificationService) if err != nil { n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err) continue @@ -269,7 +273,7 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [ OrgId: evalContext.Rule.OrgID, } - err = bus.Dispatch(evalContext.Ctx, query) + err = n.sqlStore.GetOrCreateAlertNotificationState(evalContext.Ctx, query) if err != nil { n.log.Error("Could not get notification state.", "notifier", notification.Id, "error", err) continue @@ -287,13 +291,13 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [ } // InitNotifier instantiate a new notifier based on the model. -func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Notifier, error) { +func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn, notificationService *notifications.NotificationService) (Notifier, error) { notifierPlugin, found := notifierFactories[model.Type] if !found { return nil, fmt.Errorf("unsupported notification type %q", model.Type) } - return notifierPlugin.Factory(model, fn) + return notifierPlugin.Factory(model, fn, notificationService) } // GetDecryptedValueFn is a function that returns the decrypted value of @@ -301,7 +305,7 @@ func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Noti type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string, secret string) string // NotifierFactory is a signature for creating notifiers. -type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error) +type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error) var notifierFactories = make(map[string]*NotifierPlugin) diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index a1732eb89b1..b9204541fd7 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/components/simplejson" @@ -21,17 +22,18 @@ import ( func TestNotificationService(t *testing.T) { testRule := &Rule{Name: "Test", Message: "Something is bad"} - evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}) + store := &AlertStoreMock{} + evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store) testRuleTemplated := &Rule{Name: "Test latency ${quantile}", Message: "Something is bad on instance ${instance}"} - evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}) + evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store) evalCtxWithMatch.EvalMatches = []*EvalMatch{{ Tags: map[string]string{ "instance": "localhost:3000", "quantile": "0.99", }, }} - evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}) + evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store) notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification", evalCtx, true, func(sc *scenarioContext) { @@ -177,7 +179,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext evalCtx.dashboardRef = &models.DashboardRef{Uid: "db-uid"} - bus.AddHandler("test", func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { + store := evalCtx.Store.(*AlertStoreMock) + + store.getAlertNotificationsWithUidToSend = func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { query.Result = []*models.AlertNotification{ { Id: 1, @@ -188,9 +192,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext }, } return nil - }) + } - bus.AddHandler("test", func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error { + store.getOrCreateNotificationState = func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error { query.Result = &models.AlertNotificationState{ AlertId: evalCtx.Rule.ID, AlertRuleStateUpdatedVersion: 1, @@ -199,7 +203,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext State: models.AlertNotificationStateUnknown, } return nil - }) + } bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { return nil @@ -263,7 +267,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext }, } - scenarioCtx.notificationService = newNotificationService(renderService, nil) + scenarioCtx.notificationService = newNotificationService(renderService, store, nil, nil) fn(scenarioCtx) }) } @@ -279,7 +283,7 @@ type testNotifier struct { Frequency time.Duration } -func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn) (Notifier, error) { +func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn, ns notifications.Service) (Notifier, error) { uploadImage := true value, exist := model.Settings.CheckGet("uploadImage") if exist { diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d8146b54811..92876853470 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -50,7 +50,7 @@ func init() { } // NewAlertmanagerNotifier returns a new Alertmanager notifier -func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { urlString := model.Settings.Get("url").MustString() if urlString == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -67,7 +67,7 @@ func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDec basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString(), setting.SecretKey) return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, BasicAuthUser: basicAuthUser, BasicAuthPassword: basicAuthPassword, @@ -183,7 +183,7 @@ func (am *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := am.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { am.log.Error("Failed to send alertmanager", "error", err, "alertmanager", am.Name, "url", url) errCnt++ } diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index 0d1d5e3f3a4..29e920a6a2b 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -68,7 +68,7 @@ func TestWhenAlertManagerShouldNotify(t *testing.T) { am := &AlertmanagerNotifier{log: log.New("test.logger")} evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: tc.prevState, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.Rule.State = tc.newState @@ -92,7 +92,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -106,7 +106,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) alertmanagerNotifier := not.(*AlertmanagerNotifier) require.NoError(t, err) @@ -125,7 +125,7 @@ func TestAlertmanagerNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) alertmanagerNotifier := not.(*AlertmanagerNotifier) require.NoError(t, err) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 42363a6bdcd..07f5957e06d 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const ( @@ -24,11 +25,13 @@ type NotifierBase struct { DisableResolveMessage bool Frequency time.Duration + NotificationService notifications.Service + log log.Logger } // NewNotifierBase returns a new `NotifierBase`. -func NewNotifierBase(model *models.AlertNotification) NotifierBase { +func NewNotifierBase(model *models.AlertNotification, notificationService notifications.Service) NotifierBase { uploadImage := true if value, exists := model.Settings.CheckGet("uploadImage"); exists { uploadImage = value.MustBool() @@ -43,6 +46,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { SendReminder: model.SendReminder, DisableResolveMessage: model.DisableResolveMessage, Frequency: model.Frequency, + NotificationService: notificationService, log: log.New("alerting.notifier." + model.Name), } } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 32d68376b84..5ac57e2d8ba 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -170,7 +170,7 @@ func TestShouldSendAlertNotification(t *testing.T) { for _, tc := range tcs { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: tc.prevState, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) if tc.state == nil { tc.state = &models.AlertNotificationState{} @@ -197,24 +197,24 @@ func TestBaseNotifier(t *testing.T) { t.Run("can parse false value", func(t *testing.T) { bJSON.Set("uploadImage", false) - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.False(t, base.UploadImage) }) t.Run("can parse true value", func(t *testing.T) { bJSON.Set("uploadImage", true) - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.True(t, base.UploadImage) }) t.Run("default value should be true for backwards compatibility", func(t *testing.T) { - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.True(t, base.UploadImage) }) t.Run("default value should be false for backwards compatibility", func(t *testing.T) { - base := NewNotifierBase(model) + base := NewNotifierBase(model, nil) require.False(t, base.DisableResolveMessage) }) } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 5313700f405..7767f791df2 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -5,10 +5,10 @@ import ( "fmt" "net/url" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const defaultDingdingMsgType = "link" @@ -47,7 +47,7 @@ func init() { }) } -func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -56,7 +56,7 @@ func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecrypte msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType) return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), MsgType: msgType, URL: url, log: log.New("alerting.notifier.dingding"), @@ -91,7 +91,7 @@ func (dd *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := dd.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { dd.log.Error("Failed to send DingDing", "error", err, "dingding", dd.Name) return err } diff --git a/pkg/services/alerting/notifiers/dingding_test.go b/pkg/services/alerting/notifiers/dingding_test.go index 63143fb2631..a1036d949a0 100644 --- a/pkg/services/alerting/notifiers/dingding_test.go +++ b/pkg/services/alerting/notifiers/dingding_test.go @@ -24,7 +24,7 @@ func TestDingDingNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) t.Run("settings should trigger incident", func(t *testing.T) { @@ -37,7 +37,7 @@ func TestDingDingNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) notifier := not.(*DingDingNotifier) require.Nil(t, err) @@ -50,7 +50,7 @@ func TestDingDingNotifier(t *testing.T) { &alerting.Rule{ State: models.AlertStateAlerting, Message: `{host="localhost"}`, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, err = notifier.genBody(evalContext, "") require.Nil(t, err) }) diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 9d2eaf18f5b..99ac48e1193 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -9,11 +9,11 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -57,7 +57,7 @@ func init() { }) } -func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { avatar := model.Settings.Get("avatar_url").MustString() content := model.Settings.Get("content").MustString() url := model.Settings.Get("url").MustString() @@ -67,7 +67,7 @@ func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecrypted useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false) return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Content: content, AvatarURL: avatar, WebhookURL: url, @@ -177,7 +177,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { } } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := dn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { dn.log.Error("Failed to send notification to Discord", "error", err) return err } diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go index 809d03df105..60b13788f56 100644 --- a/pkg/services/alerting/notifiers/discord_test.go +++ b/pkg/services/alerting/notifiers/discord_test.go @@ -22,7 +22,7 @@ func TestDiscordNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -41,7 +41,7 @@ func TestDiscordNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) discordNotifier := not.(*DiscordNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 1e348eb44b4..fb072d59617 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -3,12 +3,12 @@ package notifiers import ( "os" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -48,7 +48,7 @@ type EmailNotifier struct { // NewEmailNotifier is the constructor function // for the EmailNotifier. -func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { addressesString := model.Settings.Get("addresses").MustString() singleEmail := model.Settings.Get("singleEmail").MustBool(false) @@ -60,7 +60,7 @@ func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa addresses := util.SplitEmails(addressesString) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Addresses: addresses, SingleEmail: singleEmail, log: log.New("alerting.notifier.email"), @@ -117,7 +117,7 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { } } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := en.NotificationService.SendEmailCommandHandlerSync(evalContext.Ctx, cmd); err != nil { en.log.Error("Failed to send alert notification email", "error", err) return err } diff --git a/pkg/services/alerting/notifiers/email_test.go b/pkg/services/alerting/notifiers/email_test.go index 2017e4da6c5..27dff78d9fc 100644 --- a/pkg/services/alerting/notifiers/email_test.go +++ b/pkg/services/alerting/notifiers/email_test.go @@ -22,7 +22,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) emailNotifier := not.(*EmailNotifier) require.Nil(t, err) @@ -63,7 +63,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) emailNotifier := not.(*EmailNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go index 24ef91ad398..02c4664a41b 100644 --- a/pkg/services/alerting/notifiers/googlechat.go +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -5,10 +5,10 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -32,14 +32,14 @@ func init() { }) } -func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &GoogleChatNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, log: log.New("alerting.notifier.googlechat"), }, nil @@ -220,7 +220,7 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error { Body: string(body), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := gcn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name) return err } diff --git a/pkg/services/alerting/notifiers/googlechat_test.go b/pkg/services/alerting/notifiers/googlechat_test.go index 8cde1cd3547..6447f7dbe5e 100644 --- a/pkg/services/alerting/notifiers/googlechat_test.go +++ b/pkg/services/alerting/notifiers/googlechat_test.go @@ -22,7 +22,7 @@ func TestGoogleChatNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestGoogleChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) webhookNotifier := not.(*GoogleChatNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 21db0e1b02e..7a21903fb6c 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -7,10 +7,10 @@ import ( "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -53,7 +53,7 @@ const ( // NewHipChatNotifier is the constructor functions // for the HipChatNotifier -func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if strings.HasSuffix(url, "/") { url = url[:len(url)-1] @@ -66,7 +66,7 @@ func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecrypted roomID := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, APIKey: apikey, RoomID: roomID, @@ -177,7 +177,7 @@ func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hc.log.Info("Request payload", "json", string(data)) cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := hc.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name) return err } diff --git a/pkg/services/alerting/notifiers/hipchat_test.go b/pkg/services/alerting/notifiers/hipchat_test.go index b27e941d50f..51b1e2d9e62 100644 --- a/pkg/services/alerting/notifiers/hipchat_test.go +++ b/pkg/services/alerting/notifiers/hipchat_test.go @@ -23,7 +23,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) hipchatNotifier := not.(*HipChatNotifier) require.Nil(t, err) @@ -65,7 +65,7 @@ func TestHipChatNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) hipchatNotifier := not.(*HipChatNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index e0511f53468..c9faaea1027 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -5,11 +5,11 @@ import ( "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -41,7 +41,7 @@ func init() { } // NewKafkaNotifier is the constructor function for the Kafka notifier. -func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { endpoint := model.Settings.Get("kafkaRestProxy").MustString() if endpoint == "" { return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"} @@ -52,7 +52,7 @@ func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), @@ -124,7 +124,7 @@ func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := kn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { kn.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/kafka_test.go b/pkg/services/alerting/notifiers/kafka_test.go index 86e0be76b28..18dc7b7e1af 100644 --- a/pkg/services/alerting/notifiers/kafka_test.go +++ b/pkg/services/alerting/notifiers/kafka_test.go @@ -22,7 +22,7 @@ func TestKafkaNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -40,7 +40,7 @@ func TestKafkaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) kafkaNotifier := not.(*KafkaNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index c9e5771459c..9e360ecfecd 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -5,10 +5,10 @@ import ( "fmt" "net/url" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -37,14 +37,14 @@ const ( ) // NewLINENotifier is the constructor for the LINE notifier -func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString(), setting.SecretKey) if token == "" { return nil, alerting.ValidationError{Reason: "Could not find token in settings"} } return &LineNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Token: token, log: log.New("alerting.notifier.line"), }, nil @@ -92,7 +92,7 @@ func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error { Body: form.Encode(), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := ln.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { ln.log.Error("Failed to send notification to LINE", "error", err, "body", body) return err } diff --git a/pkg/services/alerting/notifiers/line_test.go b/pkg/services/alerting/notifiers/line_test.go index be7559a10b2..1720002b2fc 100644 --- a/pkg/services/alerting/notifiers/line_test.go +++ b/pkg/services/alerting/notifiers/line_test.go @@ -21,7 +21,7 @@ func TestLineNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) t.Run("settings should trigger incident", func(t *testing.T) { @@ -36,7 +36,7 @@ func TestLineNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) lineNotifier := not.(*LineNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 6c13ee382d6..8cf3a9ebfe8 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -5,11 +5,11 @@ import ( "fmt" "strconv" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -84,7 +84,7 @@ const ( ) // NewOpsGenieNotifier is the constructor for OpsGenie. -func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { autoClose := model.Settings.Get("autoClose").MustBool(true) overridePriority := model.Settings.Get("overridePriority").MustBool(true) apiKey := fn(context.Background(), model.SecureSettings, "apiKey", model.Settings.Get("apiKey").MustString(), setting.SecretKey) @@ -104,7 +104,7 @@ func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecrypt } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), APIKey: apiKey, APIUrl: apiURL, AutoClose: autoClose, @@ -205,7 +205,7 @@ func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) } @@ -229,7 +229,7 @@ func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go index e83af9cd5cb..08fefd1b86d 100644 --- a/pkg/services/alerting/notifiers/opsgenie_test.go +++ b/pkg/services/alerting/notifiers/opsgenie_test.go @@ -6,11 +6,11 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/validations" "github.com/stretchr/testify/require" @@ -28,7 +28,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -45,7 +45,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) opsgenieNotifier := not.(*OpsGenieNotifier) require.Nil(t, err) @@ -69,7 +69,7 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(alerting.ValidationError{})) require.True(t, strings.HasSuffix(err.Error(), "Invalid value for sendTagsAs: \"not_a_valid_value\"")) @@ -92,7 +92,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -102,22 +103,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"}) @@ -142,7 +141,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -152,22 +152,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, nil) + }, nil, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{}) @@ -192,7 +190,8 @@ func TestOpsGenieNotifier(t *testing.T) { Settings: settingsJSON, } - notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error + notificationService := notifications.MockNotificationService() + notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error opsgenieNotifier := notifier.(*OpsGenieNotifier) @@ -202,22 +201,20 @@ func TestOpsGenieNotifier(t *testing.T) { Message: "someMessage", State: models.AlertStateAlerting, AlertRuleTags: tagPairs, - }, nil) + }, nil, nil) evalContext.IsTestRun = true tags := make([]string, 0) details := make(map[string]interface{}) - bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error { - bodyJSON, err := simplejson.NewJson([]byte(cmd.Body)) - if err == nil { - tags = bodyJSON.Get("tags").MustStringArray([]string{}) - details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) - } - return err - }) alertErr := opsgenieNotifier.createAlert(evalContext) + bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body)) + if err == nil { + tags = bodyJSON.Get("tags").MustStringArray([]string{}) + details = bodyJSON.Get("details").MustMap(map[string]interface{}{}) + } + require.Nil(t, notifierErr) require.Nil(t, alertErr) require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"}) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 4fc4766a1a2..358a23d14ec 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -76,7 +76,7 @@ var ( ) // NewPagerdutyNotifier is the constructor for the PagerDuty notifier -func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { severity := model.Settings.Get("severity").MustString("critical") autoResolve := model.Settings.Get("autoResolve").MustBool(false) key := fn(context.Background(), model.SecureSettings, "integrationKey", model.Settings.Get("integrationKey").MustString(), setting.SecretKey) @@ -86,7 +86,7 @@ func NewPagerdutyNotifier(model *models.AlertNotification, fn alerting.GetDecryp } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), Key: key, Severity: severity, AutoResolve: autoResolve, @@ -240,7 +240,7 @@ func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := pn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { pn.log.Error("Failed to send notification to Pagerduty", "error", err, "body", string(body)) return err } diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index fc6bcedc0af..52777bc075a 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -39,7 +39,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -55,7 +55,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -78,7 +78,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -105,7 +105,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pagerdutyNotifier := not.(*PagerdutyNotifier) require.Nil(t, err) @@ -130,7 +130,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -139,7 +139,7 @@ func TestPagerdutyNotifier(t *testing.T) { Name: "someRule", Message: "someMessage", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext) @@ -187,7 +187,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -195,7 +195,7 @@ func TestPagerdutyNotifier(t *testing.T) { ID: 0, Name: "someRule", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext) @@ -244,7 +244,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -253,7 +253,7 @@ func TestPagerdutyNotifier(t *testing.T) { Name: "someRule", Message: "someMessage", State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true evalContext.EvalMatches = []*alerting.EvalMatch{ { @@ -314,7 +314,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -332,7 +332,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "severity", Value: "warning"}, {Key: "dedup_key", Value: "key-" + strings.Repeat("x", 260)}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true @@ -394,7 +394,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -411,7 +411,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "component", Value: "aComponent"}, {Key: "severity", Value: "info"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true @@ -473,7 +473,7 @@ func TestPagerdutyNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) pagerdutyNotifier := not.(*PagerdutyNotifier) @@ -490,7 +490,7 @@ func TestPagerdutyNotifier(t *testing.T) { {Key: "component", Value: "aComponent"}, {Key: "severity", Value: "llama"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png" evalContext.IsTestRun = true diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 7586c4bd576..6a6fedce24e 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -11,10 +11,10 @@ import ( "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) const pushoverEndpoint = "https://api.pushover.net/1/messages.json" @@ -194,7 +194,7 @@ func init() { } // NewPushoverNotifier is the constructor for the Pushover Notifier -func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { userKey := fn(context.Background(), model.SecureSettings, "userKey", model.Settings.Get("userKey").MustString(), setting.SecretKey) APIToken := fn(context.Background(), model.SecureSettings, "apiToken", model.Settings.Get("apiToken").MustString(), setting.SecretKey) device := model.Settings.Get("device").MustString() @@ -219,7 +219,7 @@ func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecrypt return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), UserKey: userKey, APIToken: APIToken, AlertingPriority: alertingPriority, @@ -287,7 +287,7 @@ func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { Body: uploadBody.String(), } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := pn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name) return err } diff --git a/pkg/services/alerting/notifiers/pushover_test.go b/pkg/services/alerting/notifiers/pushover_test.go index fe27b424b0f..c498611af7a 100644 --- a/pkg/services/alerting/notifiers/pushover_test.go +++ b/pkg/services/alerting/notifiers/pushover_test.go @@ -26,7 +26,7 @@ func TestPushoverNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -48,7 +48,7 @@ func TestPushoverNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) pushoverNotifier := not.(*PushoverNotifier) require.Nil(t, err) @@ -74,7 +74,7 @@ func TestGenPushoverBody(t *testing.T) { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: models.AlertStateAlerting, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "") require.Nil(t, err) @@ -85,7 +85,7 @@ func TestGenPushoverBody(t *testing.T) { evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{ State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) _, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "") require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index ec7477867e9..b41cf17ca18 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -5,11 +5,11 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -61,14 +61,14 @@ func init() { } // NewSensuNotifier is the constructor for the Sensu Notifier. -func NewSensuNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSensuNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), @@ -146,7 +146,7 @@ func (sn *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { HttpMethod: "POST", } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { sn.log.Error("Failed to send sensu event", "error", err, "sensu", sn.Name) return err } diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go index 2be6f71d325..dc4376b9f9c 100644 --- a/pkg/services/alerting/notifiers/sensu_test.go +++ b/pkg/services/alerting/notifiers/sensu_test.go @@ -22,7 +22,7 @@ func TestSensuNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -41,7 +41,7 @@ func TestSensuNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) sensuNotifier := not.(*SensuNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/sensugo.go b/pkg/services/alerting/notifiers/sensugo.go index 655014a532c..045484be0bb 100644 --- a/pkg/services/alerting/notifiers/sensugo.go +++ b/pkg/services/alerting/notifiers/sensugo.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -72,7 +72,7 @@ func init() { } // NewSensuGoNotifier is the constructor for the Sensu Go Notifier. -func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() apikey := fn(context.Background(), model.SecureSettings, "apikey", model.Settings.Get("apikey").MustString(), setting.SecretKey) @@ -84,7 +84,7 @@ func NewSensuGoNotifier(model *models.AlertNotification, fn alerting.GetDecrypte } return &SensuGoNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, Entity: model.Settings.Get("entity").MustString(), Check: model.Settings.Get("check").MustString(), @@ -197,7 +197,7 @@ func (sn *SensuGoNotifier) Notify(evalContext *alerting.EvalContext) error { "Authorization": fmt.Sprintf("Key %s", sn.APIKey), }, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { sn.log.Error("Failed to send Sensu Go event", "error", err, "sensugo", sn.Name) return err } diff --git a/pkg/services/alerting/notifiers/sensugo_test.go b/pkg/services/alerting/notifiers/sensugo_test.go index c1a4eea0a84..2e95208a516 100644 --- a/pkg/services/alerting/notifiers/sensugo_test.go +++ b/pkg/services/alerting/notifiers/sensugo_test.go @@ -21,7 +21,7 @@ func TestSensuGoNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) json = ` @@ -42,7 +42,7 @@ func TestSensuGoNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSensuGoNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) sensuGoNotifier := not.(*SensuGoNotifier) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 3f5b90a7305..b88f89e8204 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -16,10 +16,10 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -121,7 +121,7 @@ func init() { const slackAPIEndpoint = "https://slack.com/api/chat.postMessage" // NewSlackNotifier is the constructor for the Slack notifier. -func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { urlStr := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString(), setting.SecretKey) if urlStr == "" { urlStr = slackAPIEndpoint @@ -174,7 +174,7 @@ func NewSlackNotifier(model *models.AlertNotification, fn alerting.GetDecryptedV return &SlackNotifier{ url: apiURL, - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), recipient: recipient, username: username, iconEmoji: iconEmoji, @@ -418,7 +418,7 @@ func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log cmd := &models.SendWebhookSync{ Url: "https://slack.com/api/files.upload", Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST", } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") return err } diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 2bcc1438e9a..59244765c25 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -27,7 +27,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - _, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) assert.EqualError(t, err, "alert validation error: recipient must be specified when using the Slack chat API") }) @@ -45,7 +45,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -83,7 +83,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -131,7 +131,7 @@ func TestSlackNotifier(t *testing.T) { SecureSettings: securedSettingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "ops", slackNotifier.Name) @@ -162,7 +162,7 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) assert.Equal(t, "1ABCDE", slackNotifier.recipient) @@ -253,7 +253,7 @@ func TestSendSlackRequest(t *testing.T) { Settings: settingsJSON, } - not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) slackNotifier := not.(*SlackNotifier) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 6229046c68d..e85f4a12d82 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -3,10 +3,10 @@ package notifiers import ( "encoding/json" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" ) func init() { @@ -30,14 +30,14 @@ func init() { } // NewTeamsNotifier is the constructor for Teams notifier. -func NewTeamsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewTeamsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, log: log.New("alerting.notifier.teams"), }, nil @@ -135,7 +135,7 @@ func (tn *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { data, _ := json.Marshal(&body) cmd := &models.SendWebhookSync{Url: tn.URL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send teams notification", "error", err, "webhook", tn.Name) return err } diff --git a/pkg/services/alerting/notifiers/teams_test.go b/pkg/services/alerting/notifiers/teams_test.go index 3c43931ee8e..9bd9a6a14ff 100644 --- a/pkg/services/alerting/notifiers/teams_test.go +++ b/pkg/services/alerting/notifiers/teams_test.go @@ -22,7 +22,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -39,7 +39,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) teamsNotifier := not.(*TeamsNotifier) require.Nil(t, err) @@ -61,7 +61,7 @@ func TestTeamsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) teamsNotifier := not.(*TeamsNotifier) require.Nil(t, err) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 16b705d4bac..d93d776b853 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -8,10 +8,10 @@ import ( "mime/multipart" "os" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -63,7 +63,7 @@ type TelegramNotifier struct { } // NewTelegramNotifier is the constructor for the Telegram notifier -func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -81,7 +81,7 @@ func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecrypt } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), BotToken: botToken, ChatID: chatID, UploadImage: uploadImage, @@ -271,7 +271,7 @@ func (tn *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send webhook", "error", err, "webhook", tn.Name) return err } diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go index b0b1af92de8..18eb4e4e081 100644 --- a/pkg/services/alerting/notifiers/telegram_test.go +++ b/pkg/services/alerting/notifiers/telegram_test.go @@ -25,7 +25,7 @@ func TestTelegramNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -43,7 +43,7 @@ func TestTelegramNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) telegramNotifier := not.(*TelegramNotifier) require.Nil(t, err) @@ -59,7 +59,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "") require.LessOrEqual(t, len(caption), 1024) @@ -75,7 +75,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -93,7 +93,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/foo", @@ -110,7 +110,7 @@ func TestTelegramNotifier(t *testing.T) { Name: "This is an alarm", Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri", State: models.AlertStateOK, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) caption := generateImageCaption(evalContext, "http://grafa.url/foo", diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index c1b69e959c8..19b7ecd6324 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -6,10 +6,10 @@ import ( "net/url" "strings" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -71,7 +71,7 @@ type ThreemaNotifier struct { } // NewThreemaNotifier is the constructor for the Threema notifier -func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { if model.Settings == nil { return nil, alerting.ValidationError{Reason: "No Settings Supplied"} } @@ -101,7 +101,7 @@ func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecrypte } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, @@ -158,7 +158,7 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error HttpMethod: "POST", HttpHeader: headers, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := notifier.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) return err } diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go index 4800afbe53e..974ad20eeaf 100644 --- a/pkg/services/alerting/notifiers/threema_test.go +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -24,7 +24,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -43,7 +43,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) threemaNotifier := not.(*ThreemaNotifier) @@ -70,7 +70,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) @@ -92,7 +92,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) @@ -114,7 +114,7 @@ func TestThreemaNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, not) var valErr alerting.ValidationError require.True(t, errors.As(err, &valErr)) diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index c0d96d6b1b0..9cc1416c911 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -4,11 +4,11 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -47,7 +47,7 @@ func init() { // NewVictoropsNotifier creates an instance of VictoropsNotifier that // handles posting notifications to Victorops REST API -func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { autoResolve := model.Settings.Get("autoResolve").MustBool(true) url := model.Settings.Get("url").MustString() if url == "" { @@ -56,7 +56,7 @@ func NewVictoropsNotifier(model *models.AlertNotification, _ alerting.GetDecrypt noDataAlertType := model.Settings.Get("noDataAlertType").MustString(AlertStateWarning) return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, NoDataAlertType: noDataAlertType, AutoResolve: autoResolve, @@ -156,7 +156,7 @@ func (vn *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { data, _ := bodyJSON.MarshalJSON() cmd := &models.SendWebhookSync{Url: vn.URL, Body: string(data)} - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := vn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name) return err } diff --git a/pkg/services/alerting/notifiers/victorops_test.go b/pkg/services/alerting/notifiers/victorops_test.go index 8678e0da1ac..d68629cd82a 100644 --- a/pkg/services/alerting/notifiers/victorops_test.go +++ b/pkg/services/alerting/notifiers/victorops_test.go @@ -35,7 +35,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -52,7 +52,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) victoropsNotifier := not.(*VictoropsNotifier) require.Nil(t, err) @@ -76,7 +76,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) victoropsNotifier := not.(*VictoropsNotifier) @@ -90,7 +90,7 @@ func TestVictoropsNotifier(t *testing.T) { {Key: "keyOnly"}, {Key: "severity", Value: "warning"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payload, err := victoropsNotifier.buildEventPayload(evalContext) @@ -124,7 +124,7 @@ func TestVictoropsNotifier(t *testing.T) { Settings: settingsJSON, } - not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Nil(t, err) victoropsNotifier := not.(*VictoropsNotifier) @@ -138,7 +138,7 @@ func TestVictoropsNotifier(t *testing.T) { {Key: "keyOnly"}, {Key: "severity", Value: "warning"}, }, - }, &validations.OSSPluginRequestValidator{}) + }, &validations.OSSPluginRequestValidator{}, nil) evalContext.IsTestRun = true payload, err := victoropsNotifier.buildEventPayload(evalContext) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 6d4e4702150..5ec1659718b 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -60,7 +60,7 @@ func init() { // NewWebHookNotifier is the constructor for // the WebHook notifier. -func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { +func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} @@ -69,7 +69,7 @@ func NewWebHookNotifier(model *models.AlertNotification, fn alerting.GetDecrypte password := fn(context.Background(), model.SecureSettings, "password", model.Settings.Get("password").MustString(), setting.SecretKey) return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model), + NotifierBase: NewNotifierBase(model, ns), URL: url, User: model.Settings.Get("username").MustString(), Password: password, @@ -153,7 +153,7 @@ func (wn *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { HttpMethod: wn.HTTPMethod, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := wn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { wn.log.Error("Failed to send webhook", "error", err, "webhook", wn.Name) return err } diff --git a/pkg/services/alerting/notifiers/webhook_test.go b/pkg/services/alerting/notifiers/webhook_test.go index 0054d3f3a4c..39ed35c6395 100644 --- a/pkg/services/alerting/notifiers/webhook_test.go +++ b/pkg/services/alerting/notifiers/webhook_test.go @@ -22,7 +22,7 @@ func TestWebhookNotifier_parsingFromSettings(t *testing.T) { Settings: settingsJSON, } - _, err = NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + _, err = NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.Error(t, err) }) @@ -37,7 +37,7 @@ func TestWebhookNotifier_parsingFromSettings(t *testing.T) { Settings: settingsJSON, } - not, err := NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue) + not, err := NewWebHookNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil) require.NoError(t, err) webhookNotifier := not.(*WebhookNotifier) diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index ae8303beca8..6d4cc8dd80f 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -4,7 +4,6 @@ import ( "context" "sync" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" @@ -16,12 +15,14 @@ type ruleReader interface { type defaultRuleReader struct { sync.RWMutex - log log.Logger + sqlStore AlertStore + log log.Logger } -func newRuleReader() *defaultRuleReader { +func newRuleReader(sqlStore AlertStore) *defaultRuleReader { ruleReader := &defaultRuleReader{ - log: log.New("alerting.ruleReader"), + sqlStore: sqlStore, + log: log.New("alerting.ruleReader"), } return ruleReader @@ -30,7 +31,7 @@ func newRuleReader() *defaultRuleReader { func (arr *defaultRuleReader) fetch(ctx context.Context) []*Rule { cmd := &models.GetAllAlertsQuery{} - if err := bus.Dispatch(ctx, cmd); err != nil { + if err := arr.sqlStore.GetAllAlertQueryHandler(ctx, cmd); err != nil { arr.log.Error("Could not load alerts", "error", err) return []*Rule{} } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 401e69409d8..3bb01b92d0e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -5,13 +5,13 @@ import ( "errors" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" ) @@ -21,13 +21,15 @@ type resultHandler interface { type defaultResultHandler struct { notifier *notificationService + sqlStore AlertStore log log.Logger } -func newResultHandler(renderService rendering.Service, decryptFn GetDecryptedValueFn) *defaultResultHandler { +func newResultHandler(renderService rendering.Service, sqlStore AlertStore, notificationService *notifications.NotificationService, decryptFn GetDecryptedValueFn) *defaultResultHandler { return &defaultResultHandler{ log: log.New("alerting.resultHandler"), - notifier: newNotificationService(renderService, decryptFn), + sqlStore: sqlStore, + notifier: newNotificationService(renderService, sqlStore, notificationService, decryptFn), } } @@ -58,7 +60,7 @@ func (handler *defaultResultHandler) handle(evalContext *EvalContext) error { EvalData: annotationData, } - if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil { + if err := handler.sqlStore.SetAlertState(evalContext.Ctx, cmd); err != nil { if errors.Is(err, models.ErrCannotChangeStateOnPausedAlert) { handler.log.Error("Cannot change state on alert that's paused", "error", err) return err diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index a648950b7a2..22d6bbaf121 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -6,22 +6,25 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) type AlertNotificationService struct { - Bus bus.Bus - SQLStore *sqlstore.SQLStore - EncryptionService encryption.Internal + Bus bus.Bus + SQLStore *sqlstore.SQLStore + EncryptionService encryption.Internal + NotificationService *notifications.NotificationService } func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService encryption.Internal, -) *AlertNotificationService { + notificationService *notifications.NotificationService) *AlertNotificationService { s := &AlertNotificationService{ - Bus: bus, - SQLStore: store, - EncryptionService: encryptionService, + Bus: bus, + SQLStore: store, + EncryptionService: encryptionService, + NotificationService: notificationService, } s.Bus.AddHandler(s.GetAlertNotifications) @@ -153,7 +156,7 @@ func (s *AlertNotificationService) createNotifier(ctx context.Context, model *mo return nil, err } - notifier, err := InitNotifier(model, s.EncryptionService.GetDecryptedValue) + notifier, err := InitNotifier(model, s.EncryptionService.GetDecryptedValue, s.NotificationService) if err != nil { logger.Error("Failed to create notifier", "error", err.Error()) return nil, err diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index 2a3909193a4..020003ffacd 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" @@ -19,7 +20,7 @@ func TestService(t *testing.T) { nType := "test" registerTestNotifier(nType) - s := ProvideService(bus.New(), sqlStore, ossencryption.ProvideService()) + s := ProvideService(bus.New(), sqlStore, ossencryption.ProvideService(), nil) origSecret := setting.SecretKey setting.SecretKey = "alert_notification_service_test" @@ -116,7 +117,9 @@ func TestService(t *testing.T) { func registerTestNotifier(notifierType string) { RegisterNotifier(&NotifierPlugin{ - Type: notifierType, - Factory: func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error) { return nil, nil }, + Type: notifierType, + Factory: func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error) { + return nil, nil + }, }) } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index c4a45f31075..aee76f1b64c 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -29,7 +29,7 @@ var ( ) func (s *AlertNotificationService) HandleNotificationTestCommand(ctx context.Context, cmd *NotificationTestCommand) error { - notificationSvc := newNotificationService(nil, nil) + notificationSvc := newNotificationService(nil, nil, nil, nil) model := models.AlertNotification{ Id: cmd.ID, @@ -57,7 +57,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext { ID: rand.Int63(), } - ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}) + ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}, nil) if cmd.Settings.Get("uploadImage").MustBool(true) { ctx.ImagePublicURL = "https://grafana.com/assets/img/blog/mixed_styles.png" } diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 74968920cbf..3ccee726f6c 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -29,7 +29,7 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID handler := NewEvalHandler(e.DataService) - context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}) + context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}, nil) context.IsTestRun = true context.IsDebug = true diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3763a03f094..9a5a487cbf7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -110,5 +110,10 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, + { + Name: "featureHighlights", + Description: "Highlight Enterprise features", + State: FeatureStateStable, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 430b9fbda56..49faad2bd17 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -82,4 +82,8 @@ const ( // FlagValidatedQueries // only execute the query saved in a panel FlagValidatedQueries = "validatedQueries" + + // FlagFeatureHighlights + // Highlight Enterprise features + FlagFeatureHighlights = "featureHighlights" ) diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 5b350760136..447ca6f6afc 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -8,4 +8,5 @@ import ( type AuthInfoService interface { LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*models.User, error) + GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error } diff --git a/pkg/services/login/authinfoservice/database.go b/pkg/services/login/authinfoservice/database/database.go similarity index 63% rename from pkg/services/login/authinfoservice/database.go rename to pkg/services/login/authinfoservice/database/database.go index 230d22abe26..777f9a7f8ab 100644 --- a/pkg/services/login/authinfoservice/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -1,27 +1,54 @@ -package authinfoservice +package database import ( "context" "encoding/base64" "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/secrets" "github.com/grafana/grafana/pkg/services/sqlstore" - - "github.com/grafana/grafana/pkg/models" ) -var getTime = time.Now +var GetTime = time.Now -func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +type AuthInfoStore struct { + sqlStore sqlstore.Store + bus bus.Bus + secretsService secrets.Service + logger log.Logger +} + +func ProvideAuthInfoStore(sqlStore sqlstore.Store, bus bus.Bus, secretsService secrets.Service) *AuthInfoStore { + store := &AuthInfoStore{ + sqlStore: sqlStore, + bus: bus, + secretsService: secretsService, + logger: log.New("login.authinfo.store"), + } + store.registerBusHandlers() + return store +} + +func (s *AuthInfoStore) registerBusHandlers() { + s.bus.AddHandler(s.GetExternalUserInfoByLogin) + s.bus.AddHandler(s.GetAuthInfo) + s.bus.AddHandler(s.SetAuthInfo) + s.bus.AddHandler(s.UpdateAuthInfo) + s.bus.AddHandler(s.DeleteAuthInfo) +} + +func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { userQuery := models.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail} - err := s.Bus.Dispatch(ctx, &userQuery) + err := s.bus.Dispatch(ctx, &userQuery) if err != nil { return err } authInfoQuery := &models.GetAuthInfoQuery{UserId: userQuery.Result.Id} - if err := s.Bus.Dispatch(ctx, authInfoQuery); err != nil { + if err := s.bus.Dispatch(ctx, authInfoQuery); err != nil { return err } @@ -37,7 +64,7 @@ func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query * return nil } -func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { if query.UserId == 0 && query.AuthId == "" { return models.ErrUserNotFound } @@ -51,7 +78,7 @@ func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthI var has bool var err error - err = s.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + err = s.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { has, err = sess.Desc("created").Get(userAuth) return err }) @@ -88,12 +115,12 @@ func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthI return nil } -func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { authUser := &models.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, - Created: getTime(), + Created: GetTime(), } if cmd.OAuthToken != nil { @@ -125,18 +152,18 @@ func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInf authUser.OAuthExpiry = cmd.OAuthToken.Expiry } - return s.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Insert(authUser) return err }) } -func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { authUser := &models.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, - Created: getTime(), + Created: GetTime(), } if cmd.OAuthToken != nil { @@ -173,22 +200,51 @@ func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateA AuthModule: cmd.AuthModule, } - return s.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { upd, err := sess.Update(authUser, cond) s.logger.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_module", cmd.AuthModule, "rows", upd) return err }) } -func (s *Implementation) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { - return s.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { +func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Delete(cmd.UserAuth) return err }) } +func (s *AuthInfoStore) GetUserById(id int64) (bool, *models.User, error) { + var ( + has bool + err error + ) + user := &models.User{} + err = s.sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + has, err = sess.ID(id).Get(user) + return err + }) + if err != nil { + return false, nil, err + } + + return has, user, nil +} + +func (s *AuthInfoStore) GetUser(user *models.User) (bool, error) { + var err error + var has bool + + err = s.sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + has, err = sess.Get(user) + return err + }) + + return has, err +} + // decodeAndDecrypt will decode the string with the standard base64 decoder and then decrypt it -func (s *Implementation) decodeAndDecrypt(str string) (string, error) { +func (s *AuthInfoStore) decodeAndDecrypt(str string) (string, error) { // Bail out if empty string since it'll cause a segfault in Decrypt if str == "" { return "", nil @@ -197,7 +253,7 @@ func (s *Implementation) decodeAndDecrypt(str string) (string, error) { if err != nil { return "", err } - decrypted, err := s.SecretsService.Decrypt(context.Background(), decoded) + decrypted, err := s.secretsService.Decrypt(context.Background(), decoded) if err != nil { return "", err } @@ -206,8 +262,8 @@ func (s *Implementation) decodeAndDecrypt(str string) (string, error) { // encryptAndEncode will encrypt a string with grafana's secretKey, and // then encode it with the standard bas64 encoder -func (s *Implementation) encryptAndEncode(str string) (string, error) { - encrypted, err := s.SecretsService.Encrypt(context.Background(), []byte(str), secrets.WithoutScope()) +func (s *AuthInfoStore) encryptAndEncode(str string) (string, error) { + encrypted, err := s.secretsService.Encrypt(context.Background(), []byte(str), secrets.WithoutScope()) if err != nil { return "", err } diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go index e1a13cea8c2..b72c257d434 100644 --- a/pkg/services/login/authinfoservice/service.go +++ b/pkg/services/login/authinfoservice/service.go @@ -4,76 +4,29 @@ import ( "context" "errors" - "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/login" - "github.com/grafana/grafana/pkg/services/secrets" - "github.com/grafana/grafana/pkg/services/sqlstore" ) const genericOAuthModule = "oauth_generic_oauth" type Implementation struct { - Bus bus.Bus - SQLStore *sqlstore.SQLStore UserProtectionService login.UserProtectionService - SecretsService secrets.Service + authInfoStore login.Store logger log.Logger } -type Service interface { - GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error -} - -func ProvideAuthInfoService(bus bus.Bus, store *sqlstore.SQLStore, userProtectionService login.UserProtectionService, - secretsService secrets.Service) *Implementation { +func ProvideAuthInfoService(userProtectionService login.UserProtectionService, authInfoStore login.Store) *Implementation { s := &Implementation{ - Bus: bus, - SQLStore: store, UserProtectionService: userProtectionService, - SecretsService: secretsService, + authInfoStore: authInfoStore, logger: log.New("login.authinfo"), } - s.Bus.AddHandler(s.GetExternalUserInfoByLogin) - s.Bus.AddHandler(s.GetAuthInfo) - s.Bus.AddHandler(s.SetAuthInfo) - s.Bus.AddHandler(s.UpdateAuthInfo) - s.Bus.AddHandler(s.DeleteAuthInfo) - return s } -func (s *Implementation) getUserById(id int64) (bool, *models.User, error) { - var ( - has bool - err error - ) - user := &models.User{} - err = s.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - has, err = sess.ID(id).Get(user) - return err - }) - if err != nil { - return false, nil, err - } - - return has, user, nil -} - -func (s *Implementation) getUser(user *models.User) (bool, error) { - var err error - var has bool - - err = s.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - has, err = sess.Get(user) - return err - }) - - return has, err -} - func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUserByAuthInfoQuery) (bool, *models.User, *models.UserAuth, error) { authQuery := &models.GetAuthInfoQuery{} @@ -82,7 +35,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser authQuery.AuthModule = query.AuthModule authQuery.AuthId = query.AuthId - err := s.GetAuthInfo(ctx, authQuery) + err := s.authInfoStore.GetAuthInfo(ctx, authQuery) if !errors.Is(err, models.ErrUserNotFound) { if err != nil { return false, nil, nil, err @@ -90,7 +43,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser // if user id was specified and doesn't match the user_auth entry, remove it if query.UserId != 0 && query.UserId != authQuery.Result.UserId { - err := s.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + err := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }) if err != nil { @@ -99,14 +52,14 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser return false, nil, nil, models.ErrUserNotFound } else { - has, user, err := s.getUserById(authQuery.Result.UserId) + has, user, err := s.authInfoStore.GetUserById(authQuery.Result.UserId) if err != nil { return false, nil, nil, err } if !has { // if the user has been deleted then remove the entry - err = s.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + err = s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }) if err != nil { @@ -131,7 +84,7 @@ func (s *Implementation) LookupByOneOf(userId int64, email string, login string) // If not found, try to find the user by id if userId != 0 { - foundUser, user, err = s.getUserById(userId) + foundUser, user, err = s.authInfoStore.GetUserById(userId) if err != nil { return false, nil, err } @@ -140,7 +93,7 @@ func (s *Implementation) LookupByOneOf(userId int64, email string, login string) // If not found, try to find the user by email address if !foundUser && email != "" { user = &models.User{Email: email} - foundUser, err = s.getUser(user) + foundUser, err = s.authInfoStore.GetUser(user) if err != nil { return false, nil, err } @@ -149,7 +102,7 @@ func (s *Implementation) LookupByOneOf(userId int64, email string, login string) // If not found, try to find the user by login if !foundUser && login != "" { user = &models.User{Login: login} - foundUser, err = s.getUser(user) + foundUser, err = s.authInfoStore.GetUser(user) if err != nil { return false, nil, err } @@ -168,7 +121,7 @@ func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule stri authQuery.AuthModule = authModule authQuery.AuthId = authId authQuery.UserId = userID - err := s.GetAuthInfo(ctx, authQuery) + err := s.authInfoStore.GetAuthInfo(ctx, authQuery) if err != nil { return nil, err } @@ -215,10 +168,18 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU AuthModule: query.AuthModule, AuthId: query.AuthId, } - if err := s.SetAuthInfo(ctx, cmd); err != nil { + if err := s.authInfoStore.SetAuthInfo(ctx, cmd); err != nil { return nil, err } } return user, nil } + +func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { + return s.authInfoStore.GetAuthInfo(ctx, query) +} + +func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { + return s.authInfoStore.UpdateAuthInfo(ctx, cmd) +} diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index a5cf659db1b..7465654d8a0 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -1,6 +1,3 @@ -//go:build integration -// +build integration - package authinfoservice import ( @@ -11,7 +8,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/secrets/database" + "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" + secretstore "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/stretchr/testify/require" @@ -21,8 +19,9 @@ import ( //nolint:goconst func TestUserAuth(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) - srv := ProvideAuthInfoService(bus.New(), sqlStore, &OSSUserProtectionImpl{}, secretsService) + secretsService := secretsManager.SetupTestService(t, secretstore.ProvideSecretsStore(sqlStore)) + authInfoStore := database.ProvideAuthInfoStore(sqlStore, bus.New(), secretsService) + srv := ProvideAuthInfoService(&OSSUserProtectionImpl{}, authInfoStore) t.Run("Given 5 users", func(t *testing.T) { for i := 0; i < 5; i++ { @@ -31,7 +30,7 @@ func TestUserAuth(t *testing.T) { Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - _, err := srv.SQLStore.CreateUser(context.Background(), cmd) + _, err := sqlStore.CreateUser(context.Background(), cmd) require.Nil(t, err) } @@ -111,12 +110,11 @@ func TestUserAuth(t *testing.T) { require.Equal(t, user.Login, "loginuser1") // remove user - srv.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - sess.Exec("DELETE FROM "+srv.SQLStore.Dialect.Quote("user")+" WHERE id=?", user.Id) - require.NoError(t, err) - - return nil + err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := sess.Exec("DELETE FROM "+sqlStore.Dialect.Quote("user")+" WHERE id=?", user.Id) + return err }) + require.NoError(t, err) // get via user_auth for deleted user query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} @@ -152,7 +150,7 @@ func TestUserAuth(t *testing.T) { AuthModule: query.AuthModule, OAuthToken: token, } - err = srv.UpdateAuthInfo(context.Background(), cmd) + err = srv.authInfoStore.UpdateAuthInfo(context.Background(), cmd) require.Nil(t, err) @@ -160,7 +158,7 @@ func TestUserAuth(t *testing.T) { UserId: user.Id, } - err = srv.GetAuthInfo(context.Background(), getAuthQuery) + err = srv.authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) require.Nil(t, err) require.Equal(t, token.AccessToken, getAuthQuery.Result.OAuthAccessToken) @@ -188,20 +186,20 @@ func TestUserAuth(t *testing.T) { // Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table // Make the first log-in during the past - getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"} user, err := srv.LookupAndUpdate(context.Background(), query) - getTime = time.Now + database.GetTime = time.Now require.Nil(t, err) require.Equal(t, user.Login, login) // Add a second auth module for this user // Have this module's last log-in be more recent - getTime = func() time.Time { return time.Now().AddDate(0, 0, -1) } + database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -1) } query = &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test2", AuthId: "test2"} user, err = srv.LookupAndUpdate(context.Background(), query) - getTime = time.Now + database.GetTime = time.Now require.Nil(t, err) require.Equal(t, user.Login, login) @@ -211,14 +209,14 @@ func TestUserAuth(t *testing.T) { UserId: user.Id, } - err = srv.GetAuthInfo(context.Background(), getAuthQuery) + err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) require.Nil(t, err) require.Equal(t, getAuthQuery.Result.AuthModule, "test2") // "log in" again with the first auth module updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.Id, AuthModule: "test1", AuthId: "test1"} - err = srv.UpdateAuthInfo(context.Background(), updateAuthCmd) + err = authInfoStore.UpdateAuthInfo(context.Background(), updateAuthCmd) require.Nil(t, err) @@ -227,7 +225,7 @@ func TestUserAuth(t *testing.T) { UserId: user.Id, } - err = srv.GetAuthInfo(context.Background(), getAuthQuery) + err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) require.Nil(t, err) require.Equal(t, getAuthQuery.Result.AuthModule, "test1") @@ -238,19 +236,19 @@ func TestUserAuth(t *testing.T) { login := "loginuser0" // Expect to pass since there's a matching login user - getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: genericOAuthModule, AuthId: ""} user, err := srv.LookupAndUpdate(context.Background(), query) - getTime = time.Now + database.GetTime = time.Now require.Nil(t, err) require.Equal(t, user.Login, login) // Should throw a "user not found" error since there's no matching login user - getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } query = &models.GetUserByAuthInfoQuery{Login: "aloginuser", AuthModule: genericOAuthModule, AuthId: ""} user, err = srv.LookupAndUpdate(context.Background(), query) - getTime = time.Now + database.GetTime = time.Now require.NotNil(t, err) require.Nil(t, user) diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 5cbd5acb510..480c6376fc8 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -46,7 +46,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { buf := &bytes.Buffer{} - logger.AddLogger(log.NewLogfmtLogger(buf), "info", map[string]level.Option{}) + logger.SetLogger(level.NewFilter(log.NewLogfmtLogger(buf), level.AllowInfo())) user := createSimpleUser() externalUser := createSimpleExternalUser() @@ -84,6 +84,10 @@ func (a *authInfoServiceMock) LookupAndUpdate(ctx context.Context, query *models return a.user, a.err } +func (a *authInfoServiceMock) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { + return nil +} + func Test_teamSync(t *testing.T) { b := bus.New() authInfoMock := &authInfoServiceMock{} diff --git a/pkg/services/login/userprotection.go b/pkg/services/login/userprotection.go index dcc7d026d24..1bc65f9bb83 100644 --- a/pkg/services/login/userprotection.go +++ b/pkg/services/login/userprotection.go @@ -1,7 +1,22 @@ package login -import "github.com/grafana/grafana/pkg/models" +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) type UserProtectionService interface { AllowUserMapping(user *models.User, authModule string) error } + +type Store interface { + GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error + GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error + SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error + UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error + + GetUserById(id int64) (bool, *models.User, error) + GetUser(user *models.User) (bool, error) +} diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go index 4f95316d322..32cd5dd7cd1 100644 --- a/pkg/services/notifications/codes.go +++ b/pkg/services/notifications/codes.go @@ -70,7 +70,6 @@ func validateUserEmailCode(cfg *setting.Cfg, user *models.User, code string) (bo if err != nil { return false, err } - fmt.Printf("code : %s\ncode2: %s", retCode, code) if retCode == code && minutes > 0 { // check time is expired or not before, _ := time.ParseInLocation("200601021504", start, time.Local) diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index eae202d6a04..3ed26249234 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -29,6 +29,10 @@ type Service interface { EmailSender } +type Store interface { + GetUserByLogin(context.Context, *models.GetUserByLoginQuery) error +} + var mailTemplates *template.Template var tmplResetPassword = "reset_password" var tmplSignUpStarted = "signup_started" @@ -44,8 +48,8 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer) (*Notification mailer: mailer, } - ns.Bus.AddHandler(ns.sendResetPasswordEmail) - ns.Bus.AddHandler(ns.validateResetPasswordCode) + ns.Bus.AddHandler(ns.SendResetPasswordEmail) + ns.Bus.AddHandler(ns.ValidateResetPasswordCode) ns.Bus.AddHandler(ns.SendEmailCommandHandler) ns.Bus.AddHandler(ns.SendEmailCommandHandlerSync) @@ -163,7 +167,7 @@ func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd return nil } -func (ns *NotificationService) sendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { +func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { code, err := createUserEmailCode(ns.Cfg, cmd.User, nil) if err != nil { return err @@ -178,18 +182,20 @@ func (ns *NotificationService) sendResetPasswordEmail(ctx context.Context, cmd * }) } -func (ns *NotificationService) validateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery) error { +type GetUserByLoginFunc = func(c context.Context, login string) (*models.User, error) + +func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { login := getLoginForEmailCode(query.Code) if login == "" { return models.ErrInvalidEmailCode } - userQuery := models.GetUserByLoginQuery{LoginOrEmail: login} - if err := bus.Dispatch(ctx, &userQuery); err != nil { + user, err := userByLogin(ctx, login) + if err != nil { return err } - validEmailCode, err := validateUserEmailCode(ns.Cfg, userQuery.Result, query.Code) + validEmailCode, err := validateUserEmailCode(ns.Cfg, user, query.Code) if err != nil { return err } @@ -197,7 +203,7 @@ func (ns *NotificationService) validateResetPasswordCode(ctx context.Context, qu return models.ErrInvalidEmailCode } - query.Result = userQuery.Result + query.Result = user return nil } diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 3e40661dfab..ac5e1551ae0 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -17,7 +17,7 @@ func TestProvideService(t *testing.T) { t.Run("When invalid from_address in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.FromAddress = "@notanemail@" - _, _, err := createSutWithConfig(bus, cfg) + _, _, err := createSutWithConfig(t, bus, cfg) require.Error(t, err) }) @@ -25,7 +25,7 @@ func TestProvideService(t *testing.T) { t.Run("When template_patterns fails to parse", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.TemplatesPatterns = append(cfg.Smtp.TemplatesPatterns, "/usr/not-a-dir/**") - _, _, err := createSutWithConfig(bus, cfg) + _, _, err := createSutWithConfig(t, bus, cfg) require.Error(t, err) }) @@ -119,7 +119,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -139,7 +139,7 @@ func TestSendEmailSync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ @@ -178,7 +178,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When sending reset email password", func(t *testing.T) { sut, _ := createSut(t, bus) - err := sut.sendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}}) + err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}}) require.NoError(t, err) sentMsg := <-sut.mailQueue @@ -192,7 +192,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false - _, mailer, err := createSutWithConfig(bus, cfg) + ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommand{ Subject: "subject", @@ -201,7 +201,7 @@ func TestSendEmailAsync(t *testing.T) { Template: "welcome_on_signup", } - err = bus.Dispatch(context.Background(), cmd) + err = ns.SendEmailCommandHandler(context.Background(), cmd) require.ErrorIs(t, err, models.ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) @@ -210,7 +210,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When invalid content type in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") - _, mailer, err := createSutWithConfig(bus, cfg) + _, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) cmd := &models.SendEmailCommand{ Subject: "subject", @@ -245,12 +245,12 @@ func createSut(t *testing.T, bus bus.Bus) (*NotificationService, *FakeMailer) { t.Helper() cfg := createSmtpConfig() - ns, fm, err := createSutWithConfig(bus, cfg) + ns, fm, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) return ns, fm } -func createSutWithConfig(bus bus.Bus, cfg *setting.Cfg) (*NotificationService, *FakeMailer, error) { +func createSutWithConfig(t *testing.T, bus bus.Bus, cfg *setting.Cfg) (*NotificationService, *FakeMailer, error) { smtp := NewFakeMailer() ns, err := ProvideService(bus, cfg, smtp) return ns, smtp, err diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go index 1341ca9cd01..dff39292842 100644 --- a/pkg/services/provisioning/notifiers/alert_notifications.go +++ b/pkg/services/provisioning/notifiers/alert_notifications.go @@ -5,12 +5,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "golang.org/x/net/context" ) // Provision alert notifiers -func Provision(ctx context.Context, configDirectory string, encryptionService encryption.Internal) error { - dc := newNotificationProvisioner(encryptionService, log.New("provisioning.notifiers")) +func Provision(ctx context.Context, configDirectory string, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { + dc := newNotificationProvisioner(encryptionService, notificationService, log.New("provisioning.notifiers")) return dc.applyChanges(ctx, configDirectory) } @@ -20,12 +21,13 @@ type NotificationProvisioner struct { cfgProvider *configReader } -func newNotificationProvisioner(encryptionService encryption.Internal, log log.Logger) NotificationProvisioner { +func newNotificationProvisioner(encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { return NotificationProvisioner{ log: log, cfgProvider: &configReader{ - encryptionService: encryptionService, - log: log, + encryptionService: encryptionService, + notificationService: notifiationService, + log: log, }, } } diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go index 5c7053109e4..f5680db8cc3 100644 --- a/pkg/services/provisioning/notifiers/config_reader.go +++ b/pkg/services/provisioning/notifiers/config_reader.go @@ -12,14 +12,16 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" "gopkg.in/yaml.v2" ) type configReader struct { - encryptionService encryption.Internal - log log.Logger + encryptionService encryption.Internal + notificationService *notifications.NotificationService + log log.Logger } func (cr *configReader) readConfig(ctx context.Context, path string) ([]*notificationsAsConfig, error) { @@ -175,7 +177,7 @@ func (cr *configReader) validateNotifications(notifications []*notificationsAsCo Settings: notification.SettingsToJSON(), SecureSettings: encryptedSecureSettings, Type: notification.Type, - }, cr.encryptionService.GetDecryptedValue) + }, cr.encryptionService.GetDecryptedValue, cr.notificationService) if err != nil { return err diff --git a/pkg/services/provisioning/notifiers/config_reader_test.go b/pkg/services/provisioning/notifiers/config_reader_test.go index 9d67c18d33d..984421265ce 100644 --- a/pkg/services/provisioning/notifiers/config_reader_test.go +++ b/pkg/services/provisioning/notifiers/config_reader_test.go @@ -139,7 +139,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("One configured notification", func(t *testing.T) { t.Run("no notification in database", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { @@ -170,7 +170,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 1) t.Run("should update one notification", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -194,7 +194,7 @@ func TestNotificationAsConfig(t *testing.T) { }) t.Run("Two notifications with is_default", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), doubleNotificationsConfig) t.Run("should both be inserted", func(t *testing.T) { require.NoError(t, err) @@ -237,7 +237,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 2) t.Run("should have two new notifications", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -271,7 +271,7 @@ func TestNotificationAsConfig(t *testing.T) { err = sqlStore.CreateAlertNotificationCommand(context.Background(), &existingNotificationCmd) require.NoError(t, err) - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), correctPropertiesWithOrgName) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -290,7 +290,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Config doesn't contain required field", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), noRequiredFields) require.NotNil(t, err) @@ -304,7 +304,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Empty yaml file", func(t *testing.T) { t.Run("should have not changed repo", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), logger) + dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), emptyFile) if err != nil { t.Fatalf("applyChanges return an error %v", err) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 24ff38ce227..c7210b9279a 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -9,6 +9,7 @@ import ( plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/notifiers" @@ -19,12 +20,13 @@ import ( ) func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store, - encryptionService encryption.Internal) (*ProvisioningServiceImpl, error) { + encryptionService encryption.Internal, notificatonService *notifications.NotificationService) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, SQLStore: sqlStore, pluginStore: pluginStore, EncryptionService: encryptionService, + NotificationService: notificatonService, log: log.New("provisioning"), newDashboardProvisioner: dashboards.New, provisionNotifiers: notifiers.Provision, @@ -59,7 +61,7 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl { // Used for testing purposes func newProvisioningServiceImpl( newDashboardProvisioner dashboards.DashboardProvisionerFactory, - provisionNotifiers func(context.Context, string, encryption.Internal) error, + provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error, provisionDatasources func(context.Context, string) error, provisionPlugins func(context.Context, string, plugifaces.Store) error, ) *ProvisioningServiceImpl { @@ -77,11 +79,12 @@ type ProvisioningServiceImpl struct { SQLStore *sqlstore.SQLStore pluginStore plugifaces.Store EncryptionService encryption.Internal + NotificationService *notifications.NotificationService log log.Logger pollingCtxCancel context.CancelFunc newDashboardProvisioner dashboards.DashboardProvisionerFactory dashboardProvisioner dashboards.DashboardProvisioner - provisionNotifiers func(context.Context, string, encryption.Internal) error + provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error provisionDatasources func(context.Context, string) error provisionPlugins func(context.Context, string, plugifaces.Store) error mutex sync.Mutex @@ -157,7 +160,7 @@ func (ps *ProvisioningServiceImpl) ProvisionPlugins(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) error { alertNotificationsPath := filepath.Join(ps.Cfg.ProvisioningPath, "notifiers") - if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.EncryptionService); err != nil { + if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.EncryptionService, ps.NotificationService); err != nil { err = errutil.Wrap("Alert notification provisioning error", err) ps.log.Error("Failed to provision alert notifications", "error", err) return err diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 42e0a224ec1..77bf654f02c 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -60,9 +60,8 @@ type FindPersistedDashboardsQuery struct { } type SearchService struct { - Bus bus.Bus - Cfg *setting.Cfg - + Bus bus.Bus + Cfg *setting.Cfg sortOptions map[string]SortOption } diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go index e8038241322..a51f6c63bd3 100644 --- a/pkg/services/serviceaccounts/manager/roles.go +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -5,22 +5,6 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" ) -var ( - ActionApikeyList = "apikey:list" - ActionApikeyAdd = "apikey:add" - ActionApikeyRemove = "apikey:remove" - ActionApikeyAddAdditional = "apikey:addadditional" - - apikeyWriter = "fixed:apikey:writer" - apikeyReader = "fixed:apikey:reader" - - //API key actions - ActionApikeyListEv = accesscontrol.EvalPermission(ActionApikeyList) - ActionApikeyAddEv = accesscontrol.EvalPermission(ActionApikeyAdd) - ActionApikeyRemoveEv = accesscontrol.EvalPermission(ActionApikeyRemove) //Improvement:Check here or in database layer that user has permissiono modify the service account attached to this api key - ActionApikeyAddAdditionalEv = accesscontrol.EvalPermission(ActionApikeyAddAdditional) -) - func RegisterRoles(ac accesscontrol.AccessControl) error { role := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ @@ -50,49 +34,5 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { return err } - apikeyAdminReadRole := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Version: 1, - Name: apikeyReader, - DisplayName: "Apikeys reader", - Description: "Gives access to list apikeys.", - Group: "Service accounts", - Permissions: []accesscontrol.Permission{ - { - Action: ActionApikeyList, - Scope: accesscontrol.ScopeUsersAll, - }, - }, - }, - Grants: []string{"Admin"}, - } - if err := ac.DeclareFixedRoles(apikeyAdminReadRole); err != nil { - return err - } - - apikeyAdminEditRole := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Version: 1, - Name: apikeyWriter, - DisplayName: "Apikeys writer", - Description: "Gives access to add and delete api keys.", - Group: "Service accounts", - Permissions: accesscontrol.ConcatPermissions(apikeyAdminReadRole.Role.Permissions, []accesscontrol.Permission{ - { - Action: ActionApikeyAdd, - Scope: accesscontrol.ScopeUsersAll, - }, - { - Action: ActionApikeyRemove, - Scope: accesscontrol.ScopeUsersAll, - }, - }), - }, - Grants: []string{"Admin"}, - } - if err := ac.DeclareFixedRoles(apikeyAdminEditRole); err != nil { - return err - } - return nil } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 747a3ee3c2e..447093b3266 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -36,8 +36,10 @@ func ProvideServiceAccountsService( log: log.New("serviceaccounts"), } - if err := RegisterRoles(ac); err != nil { - s.log.Error("Failed to register roles", "error", err) + if features.IsEnabled(featuremgmt.FlagServiceAccounts) { + if err := RegisterRoles(ac); err != nil { + s.log.Error("Failed to register roles", "error", err) + } } serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 518170f411a..043bd57545a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -13,6 +13,22 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type AlertNotificationStore interface { + DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error + DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error + GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error + GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error + GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error + GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error + GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error + CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error + UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error + UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error + SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error + SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error + GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error +} + func (ss *SQLStore) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?" diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index ba2f812a22f..9cc57173906 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -33,7 +33,6 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) bus.AddHandler("sql", GetDashboardPermissionsForUser) bus.AddHandler("sql", GetDashboardsBySlug) - bus.AddHandler("sql", HasEditPermissionInFolders) bus.AddHandler("sql", HasAdminPermissionInFolders) prometheus.MustRegister(shadowSearchCounter) @@ -44,6 +43,7 @@ func (ss *SQLStore) addDashboardQueryAndCommandHandlers() { bus.AddHandler("sql", ss.SearchDashboards) bus.AddHandler("sql", ss.DeleteDashboard) bus.AddHandler("sql", ss.GetDashboards) + bus.AddHandler("sql", ss.HasEditPermissionInFolders) } var generateNewUid func() string = util.GenerateShortUID @@ -800,7 +800,7 @@ func (ss *SQLStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, ove } // HasEditPermissionInFolders validates that an user have access to a certain folder -func HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { +func (ss *SQLStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { return withDbSession(ctx, x, func(dbSession *DBSession) error { if query.SignedInUser.HasRole(models.ROLE_EDITOR) { query.Result = true diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index cdef363fb95..3b7007594f0 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -327,7 +327,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_ADMIN}, } - err := HasEditPermissionInFolders(context.Background(), query) + err := sqlStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) require.True(t, query.Result) }) @@ -393,7 +393,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: models.ROLE_EDITOR}, } - err := HasEditPermissionInFolders(context.Background(), query) + err := sqlStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) require.True(t, query.Result) }) @@ -461,7 +461,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } - err := HasEditPermissionInFolders(context.Background(), query) + err := sqlStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) require.False(t, query.Result) }) @@ -485,7 +485,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } - err := HasEditPermissionInFolders(context.Background(), query) + err := sqlStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) require.True(t, query.Result) }) @@ -501,7 +501,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } - err := HasEditPermissionInFolders(context.Background(), query) + err := sqlStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) require.True(t, query.Result) }) diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index b666447fc5b..827e94d56e2 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -9,8 +9,9 @@ import ( ) type SQLStoreMock struct { - ExpectedUser *models.User - ExpectedError error + ExpectedUser *models.User + ExpectedDatasource *models.DataSource + ExpectedError error } func NewSQLStoreMock() *SQLStoreMock { @@ -18,83 +19,87 @@ func NewSQLStoreMock() *SQLStoreMock { } func (m SQLStoreMock) DeleteExpiredSnapshots(ctx context.Context, cmd *models.DeleteExpiredSnapshotsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateDashboardSnapshot(ctx context.Context, cmd *models.CreateDashboardSnapshotCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDashboardSnapshot(ctx context.Context, cmd *models.DeleteDashboardSnapshotCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error { - return nil // TODO: Implement + return m.ExpectedError +} + +func (m SQLStoreMock) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { + return m.ExpectedError } func (m SQLStoreMock) SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgByName(name string) (*models.Org, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateOrgWithMember(name string, userID int64) (models.Org, error) { - return models.Org{}, nil // TODO: Implement + return models.Org{}, nil } func (m SQLStoreMock) UpdateOrg(ctx context.Context, cmd *models.UpdateOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgAddress(ctx context.Context, cmd *models.UpdateOrgAddressCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteOrg(ctx context.Context, cmd *models.DeleteOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CloneUserToServiceAccount(ctx context.Context, siUser *models.SignedInUser) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetUserById(ctx context.Context, query *models.GetUserByIdQuery) error { @@ -103,51 +108,51 @@ func (m SQLStoreMock) GetUserById(ctx context.Context, query *models.GetUserById } func (m SQLStoreMock) GetUserByLogin(ctx context.Context, query *models.GetUserByLoginQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserByEmail(ctx context.Context, query *models.GetUserByEmailQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUser(ctx context.Context, cmd *models.UpdateUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ChangeUserPassword(ctx context.Context, cmd *models.ChangeUserPasswordCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserLastSeenAt(ctx context.Context, cmd *models.UpdateUserLastSeenAtCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetUsingOrg(ctx context.Context, cmd *models.SetUsingOrgCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserProfile(ctx context.Context, query *models.GetUserProfileQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserOrgList(ctx context.Context, query *models.GetUserOrgListQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetSignedInUserWithCacheCtx(ctx context.Context, query *models.GetSignedInUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetSignedInUser(ctx context.Context, query *models.GetSignedInUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) BatchDisableUsers(ctx context.Context, cmd *models.BatchDisableUsersCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteUser(ctx context.Context, cmd *models.DeleteUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserPermissions(userID int64, isAdmin bool) error { @@ -155,7 +160,7 @@ func (m SQLStoreMock) UpdateUserPermissions(userID int64, isAdmin bool) error { } func (m SQLStoreMock) SetUserHelpFlag(ctx context.Context, cmd *models.SetUserHelpFlagCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateTeam(name string, email string, orgID int64) (models.Team, error) { @@ -167,409 +172,410 @@ func (m SQLStoreMock) CreateTeam(name string, email string, orgID int64) (models } func (m SQLStoreMock) UpdateTeam(ctx context.Context, cmd *models.UpdateTeamCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteTeam(ctx context.Context, cmd *models.DeleteTeamCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchTeams(ctx context.Context, query *models.SearchTeamsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamById(ctx context.Context, query *models.GetTeamByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamsByUser(ctx context.Context, query *models.GetTeamsByUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddTeamMember(userID int64, orgID int64, teamID int64, isExternal bool, permission models.PermissionType) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTeamMember(ctx context.Context, cmd *models.UpdateTeamMemberCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) { - return false, nil // TODO: Implement + return false, nil } func (m SQLStoreMock) RemoveTeamMember(ctx context.Context, cmd *models.RemoveTeamMemberCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTeamMembers(ctx context.Context, query *models.GetTeamMembersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) NewSession(ctx context.Context) *sqlstore.DBSession { - return nil // TODO: Implement + return nil } func (m SQLStoreMock) WithDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPreferencesWithDefaults(ctx context.Context, query *models.GetPreferencesWithDefaultsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPreferences(ctx context.Context, query *models.GetPreferencesQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SavePreferences(ctx context.Context, cmd *models.SavePreferencesCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPluginSettings(ctx context.Context, orgID int64) ([]*models.PluginSettingInfoDTO, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePluginSettingVersion(ctx context.Context, cmd *models.UpdatePluginSettingVersionCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) IsStarredByUserCtx(ctx context.Context, query *models.IsStarredByUserQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) StarDashboard(ctx context.Context, cmd *models.StarDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UnstarDashboard(ctx context.Context, cmd *models.UnstarDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserStars(ctx context.Context, query *models.GetUserStarsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) WithTransactionalDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardVersion(ctx context.Context, query *models.GetDashboardVersionQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardVersions(ctx context.Context, query *models.GetDashboardVersionsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteExpiredVersions(ctx context.Context, cmd *models.DeleteExpiredVersionsCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDashboardACLCtx(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) HandleAlertsQuery(ctx context.Context, query *models.GetAlertsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) PauseAlert(ctx context.Context, cmd *models.PauseAlertCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) PauseAllAlerts(ctx context.Context, cmd *models.PauseAllAlertCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertStatesForDashboard(ctx context.Context, query *models.GetAlertStatesForDashboardQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateOrgUser(ctx context.Context, cmd *models.UpdateOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SearchOrgUsers(ctx context.Context, query *models.SearchOrgUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetDashboard(id int64, orgID int64, uid string, slug string) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { - return nil, nil // TODO: Implement + return nil, m.ExpectedError } func (m SQLStoreMock) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { - return false, nil // TODO: Implement + return false, nil } func (m SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { - return nil // TODO: Implement + query.Result = m.ExpectedDatasource + return m.ExpectedError } func (m SQLStoreMock) GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDataSourcesByType(ctx context.Context, query *models.GetDataSourcesByTypeQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Migrate() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Sync() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Reset() error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) Quote(value string) string { - return "" // TODO: Implement + return "" } func (m SQLStoreMock) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey { - return nil // TODO: Implement + return nil } func (m SQLStoreMock) DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetApiKeyByName(ctx context.Context, query *models.GetApiKeyByNameQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) CreateTempUser(ctx context.Context, cmd *models.CreateTempUserCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) UpdateTempUserWithEmailSent(ctx context.Context, cmd *models.UpdateTempUserWithEmailSentCommand) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTempUsersQuery(ctx context.Context, query *models.GetTempUsersQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) GetTempUserByCode(ctx context.Context, query *models.GetTempUserByCodeQuery) error { - return nil // TODO: Implement + return m.ExpectedError } func (m SQLStoreMock) ExpireOldUserInvites(ctx context.Context, cmd *models.ExpireTempUsersCommand) error { - return nil // TODO: Implement + return m.ExpectedError } diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index ddb8595582d..1c6c3da67fb 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -12,6 +12,7 @@ type Store interface { CreateDashboardSnapshot(ctx context.Context, cmd *models.CreateDashboardSnapshotCommand) error DeleteDashboardSnapshot(ctx context.Context, cmd *models.DeleteDashboardSnapshotCommand) error GetDashboardSnapshot(query *models.GetDashboardSnapshotQuery) error + HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error SearchDashboardSnapshots(query *models.GetDashboardSnapshotsQuery) error GetOrgByName(name string) (*models.Org, error) CreateOrgWithMember(name string, userID int64) (models.Org, error) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 713bfc31d3f..aa7375f0e33 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -772,6 +772,8 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) { return nil, err } + cfg.Logger.Info(fmt.Sprintf("Starting %s", ApplicationName), "version", BuildVersion, "commit", BuildCommit, "branch", BuildBranch, "compiled", time.Unix(BuildStamp, 0)) + return parsedFile, err } diff --git a/pkg/tsdb/prometheus/framing_test.go b/pkg/tsdb/prometheus/framing_test.go index 6db7e7e9c67..c2ddadd0dac 100644 --- a/pkg/tsdb/prometheus/framing_test.go +++ b/pkg/tsdb/prometheus/framing_test.go @@ -132,5 +132,5 @@ func runQuery(response []byte, query PrometheusQuery) (*backend.QueryDataRespons } s := Service{tracer: tracer} - return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } diff --git a/pkg/tsdb/prometheus/promclient/provider.go b/pkg/tsdb/prometheus/promclient/provider.go index c84a9651773..c33d6a14c11 100644 --- a/pkg/tsdb/prometheus/promclient/provider.go +++ b/pkg/tsdb/prometheus/promclient/provider.go @@ -4,8 +4,8 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/middleware" + "github.com/grafana/grafana/pkg/util/maputil" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -16,30 +16,28 @@ import ( type Provider struct { settings backend.DataSourceInstanceSettings - jsonData JsonData + jsonData map[string]interface{} + httpMethod string clientProvider httpclient.Provider log log.Logger } func NewProvider( settings backend.DataSourceInstanceSettings, - jsonData JsonData, + jsonData map[string]interface{}, clientProvider httpclient.Provider, log log.Logger, ) *Provider { + httpMethod, _ := maputil.GetStringOptional(jsonData, "httpMethod") return &Provider{ settings: settings, jsonData: jsonData, + httpMethod: httpMethod, clientProvider: clientProvider, log: log, } } -type JsonData struct { - Method string `json:"httpMethod"` - TimeInterval string `json:"timeInterval"` -} - func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts, err := p.settings.HTTPClientOptions() if err != nil { @@ -54,6 +52,12 @@ func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts.SigV4.Service = "aps" } + // Azure authentication + err = p.configureAzureAuthentication(opts) + if err != nil { + return nil, err + } + roundTripper, err := p.clientProvider.GetTransport(opts) if err != nil { return nil, err @@ -77,7 +81,7 @@ func (p *Provider) middlewares() []sdkhttpclient.Middleware { middleware.CustomQueryParameters(p.log), sdkhttpclient.CustomHeadersMiddleware(), } - if strings.ToLower(p.jsonData.Method) == "get" { + if strings.ToLower(p.httpMethod) == "get" { middlewares = append(middlewares, middleware.ForceHttpGet(p.log)) } diff --git a/pkg/tsdb/prometheus/promclient/provider_azure.go b/pkg/tsdb/prometheus/promclient/provider_azure.go new file mode 100644 index 00000000000..920f253a75d --- /dev/null +++ b/pkg/tsdb/prometheus/promclient/provider_azure.go @@ -0,0 +1,32 @@ +package promclient + +import ( + "fmt" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" + "github.com/grafana/grafana/pkg/util/maputil" +) + +func (p *Provider) configureAzureAuthentication(opts sdkhttpclient.Options) error { + credentials, err := azcredentials.FromDatasourceData(p.jsonData, p.settings.DecryptedSecureJSONData) + if err != nil { + err = fmt.Errorf("invalid Azure credentials: %s", err) + return err + } + + if credentials != nil { + opts.CustomOptions["_azureCredentials"] = credentials + + resourceId, err := maputil.GetStringOptional(p.jsonData, "azureEndpointResourceId") + if err != nil { + return err + } + + if resourceId != "" { + opts.CustomOptions["azureEndpointResourceId"] = resourceId + } + } + + return nil +} diff --git a/pkg/tsdb/prometheus/promclient/provider_test.go b/pkg/tsdb/prometheus/promclient/provider_test.go index 935b3d07c5a..43acc14b362 100644 --- a/pkg/tsdb/prometheus/promclient/provider_test.go +++ b/pkg/tsdb/prometheus/promclient/provider_test.go @@ -135,7 +135,7 @@ func setup(jsonData ...string) *testContext { rawData = []byte(jsonData[0]) } - var jd promclient.JsonData + var jd map[string]interface{} _ = json.Unmarshal(rawData, &jd) settings := backend.DataSourceInstanceSettings{URL: "test-url", JSONData: rawData} diff --git a/pkg/tsdb/prometheus/prometeus_bench_test.go b/pkg/tsdb/prometheus/prometeus_bench_test.go index 5bf01f8ae57..b7365e30c44 100644 --- a/pkg/tsdb/prometheus/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/prometeus_bench_test.go @@ -28,7 +28,7 @@ func BenchmarkJson(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e16597c50fb..9ba855411ad 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana/pkg/util/maputil" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -42,7 +43,7 @@ func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Trace func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - var jsonData promclient.JsonData + var jsonData map[string]interface{} err := json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) @@ -54,10 +55,15 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } + timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval") + if err != nil { + return nil, err + } + mdl := DatasourceInfo{ ID: settings.ID, URL: settings.URL, - TimeInterval: jsonData.TimeInterval, + TimeInterval: timeInterval, getClient: pc.GetClient, } diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt index 2aad902f0db..68c697dd90e 100644 --- a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////KAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAJgAAAADAAAAUAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAABAAAAAFAAAAMSAvIDAAAAAEAAAAbmFtZQAAAACw/v//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAAOwAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAKAAAACgAAAAAAADAaAAAAADAAAAUAAAACwAAAAEAAAAOP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAXP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAB8////CAAAACgAAAAdAAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IjEgLyAwIn0AAAAGAAAAY29uZmlnAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAABEFRTUKckWAA6wT9QpyRYA2EqL1CnJFgAAAAAAAPB/AAAAAAAA8H8AAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAAA4AgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAmAAAAAMAAABQAAAAKAAAAAQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAIz+//8IAAAAEAAAAAUAAAAxIC8gMAAAAAQAAABuYW1lAAAAALD+//8IAAAALAAAACIAAAB7ImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In19AAAEAAAAbWV0YQAAAAACAAAA7AAAABgAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAoAAAAKAAAAAAAAMBoAAAAAMAAABQAAAALAAAAAQAAAA4////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABc////CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAHz///8IAAAAKAAAAB0AAAB7ImRpc3BsYXlOYW1lRnJvbURTIjoiMSAvIDAifQAAAAYAAABjb25maWcAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABQAgAAQVJST1cx +FRAME=QVJST1cxAAD/////WAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAJgAAAADAAAAUAAAACgAAAAEAAAARP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABk/v//CAAAABAAAAAFAAAAMSAvIDAAAAAEAAAAbmFtZQAAAACI/v//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAAOwAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAKAAAACgAAAAAAADAaAAAAADAAAAUAAAACwAAAAEAAAAEP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAANP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAABU////CAAAACgAAAAdAAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IjEgLyAwIn0AAAAGAAAAY29uZmlnAAAAAAAAVv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAAB4AAAAgAAAAAAAAAqAAAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABwAAAARAAAAeyJpbnRlcnZhbCI6MTAwMH0AAAAGAAAAY29uZmlnAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAABEFRTUKckWAA6wT9QpyRYA2EqL1CnJFgAAAAAAAPB/AAAAAAAA8H8AAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAABoAgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAJgAAAADAAAAUAAAACgAAAAEAAAARP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABk/v//CAAAABAAAAAFAAAAMSAvIDAAAAAEAAAAbmFtZQAAAACI/v//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAAOwAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAKAAAACgAAAAAAADAaAAAAADAAAAUAAAACwAAAAEAAAAEP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAANP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAABU////CAAAACgAAAAdAAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IjEgLyAwIn0AAAAGAAAAY29uZmlnAAAAAAAAVv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAAB4AAAAgAAAAAAAAAqAAAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABwAAAARAAAAeyJpbnRlcnZhbCI6MTAwMH0AAAAGAAAAY29uZmlnAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAIgCAABBUlJPVzE= diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt index e63cc8828b8..fe85719448f 100644 --- a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt @@ -6,23 +6,17 @@ Frame[0] { } } Name: go_goroutines{job="prometheus"} -Dimensions: 2 Fields by 9 Rows +Dimensions: 2 Fields by 3 Rows +-------------------------------+------------------------------------------------+ | Name: Time | Name: Value | | Labels: | Labels: __name__=go_goroutines, job=prometheus | | Type: []time.Time | Type: []*float64 | +-------------------------------+------------------------------------------------+ -| 2022-01-11 08:25:30 +0000 UTC | null | -| 2022-01-11 08:25:31 +0000 UTC | null | -| 2022-01-11 08:25:32 +0000 UTC | null | | 2022-01-11 08:25:33 +0000 UTC | 21 | | 2022-01-11 08:25:34 +0000 UTC | 32 | -| 2022-01-11 08:25:35 +0000 UTC | null | -| 2022-01-11 08:25:36 +0000 UTC | null | | 2022-01-11 08:25:37 +0000 UTC | 43 | -| 2022-01-11 08:25:38 +0000 UTC | null | +-------------------------------+------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt index f4d34e2a377..78de205a99e 100644 --- a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt index 7866187f0e6..bd1e0d265da 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt @@ -38,5 +38,5 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////QAMAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAApAAAACgAAAAEAAAAUP3//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABw/f//CAAAAGQAAABbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjIwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAEAAAAbmFtZQAAAADo/f//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAALQBAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAGgBAABoAQAAAAADAWgBAAADAAAAvAAAACwAAAAEAAAAcP7//wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAlP7//wgAAAB4AAAAbQAAAHsiX19uYW1lX18iOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWwiLCJjb2RlIjoiMjAwIiwiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAGAAAAbGFiZWxzAAAg////CAAAAIQAAAB5AAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPVwiMjAwXCIsIGhhbmRsZXI9XCIvYXBpL3YxL3F1ZXJ5X3JhbmdlXCIsIGpvYj1cInByb21ldGhldXNcIn0ifQAAAAYAAABjb25maWcAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAARBUU1CnJFgAOsE/UKckWANhKi9QpyRYAAAAAAAA1QAAAAAAAAEBAAAAAAACARUAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAAAwABAAAAUAMAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAADsAAAAAwAAAKQAAAAoAAAABAAAAFD9//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAcP3//wgAAABkAAAAWwAAAHByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPSIyMDAiLCBoYW5kbGVyPSIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwgam9iPSJwcm9tZXRoZXVzIn0ABAAAAG5hbWUAAAAA6P3//wgAAAAsAAAAIgAAAHsiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJtYXRyaXgifX0AAAQAAABtZXRhAAAAAAIAAAC0AQAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABoAQAAaAEAAAAAAwFoAQAAAwAAALwAAAAsAAAABAAAAHD+//8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAJT+//8IAAAAeAAAAG0AAAB7Il9fbmFtZV9fIjoicHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFsIiwiY29kZSI6IjIwMCIsImhhbmRsZXIiOiIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAABgAAAGxhYmVscwAAIP///wgAAACEAAAAeQAAAHsiZGlzcGxheU5hbWVGcm9tRFMiOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT1cIjIwMFwiLCBoYW5kbGVyPVwiL2FwaS92MS9xdWVyeV9yYW5nZVwiLCBqb2I9XCJwcm9tZXRoZXVzXCJ9In0AAAAGAAAAY29uZmlnAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAcAMAAEFSUk9XMQ== -FRAME=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 +FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/time_series_query.go b/pkg/tsdb/prometheus/time_series_query.go index f86638771b8..ae7abf12406 100644 --- a/pkg/tsdb/prometheus/time_series_query.go +++ b/pkg/tsdb/prometheus/time_series_query.go @@ -47,7 +47,7 @@ const ( ExemplarQueryType TimeSeriesQueryType = "exemplar" ) -func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery, fillNulls bool) (*backend.QueryDataResponse, error) { +func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery) (*backend.QueryDataResponse, error) { result := backend.QueryDataResponse{ Responses: backend.Responses{}, } @@ -101,7 +101,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P } } - frames, err := parseTimeSeriesResponse(response, query, fillNulls) + frames, err := parseTimeSeriesResponse(response, query) if err != nil { return &result, err } @@ -128,12 +128,7 @@ func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.Query return &result, err } - fillNulls := true - if req.Headers["FromAlert"] == "true" { - fillNulls = false - } - - return s.runQueries(ctx, client, queries, fillNulls) + return s.runQueries(ctx, client, queries) } func formatLegend(metric model.Metric, query *PrometheusQuery) string { @@ -207,7 +202,7 @@ func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, d return qs, nil } -func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery, fillNulls bool) (data.Frames, error) { +func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery) (data.Frames, error) { var ( frames = data.Frames{} nextFrames = data.Frames{} @@ -219,11 +214,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P switch v := value.(type) { case model.Matrix: - if fillNulls { - nextFrames = matrixToDataFramesWithNullFill(v, query, nextFrames) - } else { - nextFrames = matrixToDataFrames(v, query, nextFrames) - } + nextFrames = matrixToDataFrames(v, query, nextFrames) case model.Vector: nextFrames = vectorToDataFrames(v, query, nextFrames) case *model.Scalar: @@ -317,56 +308,6 @@ func interpolateVariables(model *QueryModel, interval time.Duration, timeRange t return expr } -func matrixToDataFramesWithNullFill(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { - for _, v := range matrix { - tags := make(map[string]string, len(v.Metric)) - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - - baseTimestamp := alignTimeRange(query.Start, query.Step, query.UtcOffsetSec).UnixMilli() - endTimestamp := alignTimeRange(query.End, query.Step, query.UtcOffsetSec).UnixMilli() - // For each step we create 1 data point. This results in range / step + 1 data points. - datapointsCount := int((endTimestamp-baseTimestamp)/query.Step.Milliseconds()) + 1 - - timeField := data.NewFieldFromFieldType(data.FieldTypeTime, datapointsCount) - valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, datapointsCount) - idx := 0 - - for _, pair := range v.Values { - timestamp := int64(pair.Timestamp) - value := float64(pair.Value) - - for t := baseTimestamp; t < timestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - timeField.Set(idx, time.Unix(pair.Timestamp.Unix(), 0).UTC()) - if !math.IsNaN(value) { - valueField.Set(idx, &value) - } - baseTimestamp = timestamp + query.Step.Milliseconds() - idx++ - } - - for t := baseTimestamp; t <= endTimestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - name := formatLegend(v.Metric, query) - timeField.Name = data.TimeSeriesTimeFieldName - valueField.Name = data.TimeSeriesValueFieldName - valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} - valueField.Labels = tags - - frames = append(frames, newDataFrame(name, "matrix", timeField, valueField)) - } - - return frames -} - func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { for _, v := range matrix { tags := make(map[string]string, len(v.Metric)) @@ -387,6 +328,7 @@ func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data name := formatLegend(v.Metric, query) timeField.Name = data.TimeSeriesTimeFieldName + timeField.Config = &data.FieldConfig{Interval: float64(query.Step.Milliseconds())} valueField.Name = data.TimeSeriesValueFieldName valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} valueField.Labels = tags diff --git a/pkg/tsdb/prometheus/time_series_query_test.go b/pkg/tsdb/prometheus/time_series_query_test.go index 2f5ec14f160..fc50c3fb78f 100644 --- a/pkg/tsdb/prometheus/time_series_query_test.go +++ b/pkg/tsdb/prometheus/time_series_query_test.go @@ -556,7 +556,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) // Test fields @@ -594,7 +594,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(5, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -631,16 +631,16 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) - require.Equal(t, res[0].Fields[0].Len(), 4) - require.Equal(t, res[0].Fields[0].At(1), time.Unix(2, 0).UTC()) - require.Equal(t, res[0].Fields[0].At(2), time.Unix(3, 0).UTC()) - require.Equal(t, res[0].Fields[1].Len(), 4) - require.Nil(t, res[0].Fields[1].At(1)) - require.Nil(t, res[0].Fields[1].At(2)) + require.Equal(t, res[0].Fields[0].Len(), 2) + require.Equal(t, time.Unix(1, 0).UTC(), res[0].Fields[0].At(0)) + require.Equal(t, time.Unix(4, 0).UTC(), res[0].Fields[0].At(1)) + require.Equal(t, res[0].Fields[1].Len(), 2) + require.Equal(t, float64(1), *res[0].Fields[1].At(0).(*float64)) + require.Equal(t, float64(4), *res[0].Fields[1].At(1).(*float64)) }) t.Run("matrix response with from alerting missed data points should be parsed correctly", func(t *testing.T) { @@ -662,7 +662,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, false) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -693,7 +693,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) var nilPointer *float64 @@ -713,7 +713,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -740,7 +740,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } query := &PrometheusQuery{} - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) diff --git a/pkg/util/maputil/maputil.go b/pkg/util/maputil/maputil.go new file mode 100644 index 00000000000..becd6d81e0c --- /dev/null +++ b/pkg/util/maputil/maputil.go @@ -0,0 +1,73 @@ +package maputil + +import "fmt" + +func GetMap(obj map[string]interface{}, key string) (map[string]interface{}, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(map[string]interface{}); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be an object", key) + return nil, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return nil, err + } +} + +func GetBool(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return false, err + } +} + +func GetBoolOptional(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + // Value optional, not error + return false, nil + } +} + +func GetString(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return "", err + } +} + +func GetStringOptional(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + // Value optional, not error + return "", nil + } +} diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 67bc2ced531..ce4d3ebc8aa 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "8.4.0-pre", + "version": "8.5.0-pre", "description": "Input Datasource", "private": true, "repository": { @@ -24,9 +24,9 @@ "webpack": "5.58.1" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/toolkit": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.5.0-pre", + "@grafana/toolkit": "8.5.0-pre", + "@grafana/ui": "8.5.0-pre", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index b2da564b1fe..aef87a20de6 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; -import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; +import { Alert, Button, Form, HorizontalGroup, Input, Select } from '@grafana/ui'; import { OrgRole } from 'app/types/acl'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { Assignments, PermissionTarget, SetPermission } from './types'; @@ -54,10 +54,22 @@ export const AddPermission = ({ (target === PermissionTarget.User && userId > 0) || (PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); + const renderMissingListUserRights = () => { + return ( + + You are missing the permission to list users (org.users:read). Please contact your administrator to get this + resolved. + + ); + }; + return (
{title}
+ + {target === PermissionTarget.User && !canListUsers && renderMissingListUserRights()} +
setPermissionTarget(v.value!)} + disabled={targetOptions.length === 0} menuShouldPortal /> - {target === PermissionTarget.User && ( + {target === PermissionTarget.User && canListUsers && ( setUserId(u.value || 0)} className={'width-20'} /> )} + {target === PermissionTarget.User && !canListUsers && } {target === PermissionTarget.Team && ( setTeamId(t.value?.id || 0)} className={'width-20'} /> diff --git a/public/app/core/components/Select/UserPicker.test.tsx b/public/app/core/components/Select/UserPicker.test.tsx index cbc1ea6236a..d63fbda5868 100644 --- a/public/app/core/components/Select/UserPicker.test.tsx +++ b/public/app/core/components/Select/UserPicker.test.tsx @@ -7,8 +7,9 @@ jest.mock('@grafana/runtime', () => ({ })); describe('UserPicker', () => { - it('renders correctly', () => { + it('renders correctly', async () => { render( {}} />); - expect(screen.getByTestId('userPicker')).toBeInTheDocument(); + + expect(await screen.findByTestId('userPicker')).toBeInTheDocument(); }); }); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 262ac14abd4..bf719821487 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,8 +1,8 @@ import config from '../../core/config'; import { extend } from 'lodash'; import { rangeUtil, WithAccessControlMetadata } from '@grafana/data'; -import { featureEnabled } from '@grafana/runtime'; import { AccessControlAction, UserPermission } from 'app/types'; +import { featureEnabled } from '@grafana/runtime'; export class User { id: number; @@ -83,13 +83,17 @@ export class ContextSrv { } accessControlEnabled(): boolean { + return Boolean(config.featureToggles['accesscontrol']); + } + + licensedAccessControlEnabled(): boolean { return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); } // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -99,7 +103,7 @@ export class ContextSrv { // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -126,22 +130,29 @@ export class ContextSrv { } hasAccessToExplore() { - if (config.featureToggles['accesscontrol']) { + if (this.accessControlEnabled()) { return this.hasPermission(AccessControlAction.DataSourcesExplore); } return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } hasAccess(action: string, fallBack: boolean) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallBack; } return this.hasPermission(action); } + hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) { + if (!config.featureToggles['accesscontrol']) { + return fallBack; + } + return this.hasPermissionInMetadata(action, object); + } + // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallback(); } if (actions.some((action) => this.hasPermission(action))) { diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a25e8f75a54..97fde9b8820 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -11,6 +11,7 @@ import { getExploreUrl, GetExploreUrlArguments, getTimeRangeFromUrl, + getTimeRange, } from './explore'; import store from 'app/core/store'; import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data'; @@ -361,6 +362,19 @@ describe('getTimeRangeFromUrl', () => { }); }); +describe('getTimeRange', () => { + describe('should flip from and to when from is after to', () => { + const rawRange = { + from: 'now', + to: 'now-6h', + }; + + const range = getTimeRange('utc', rawRange, 0); + + expect(range.from.isBefore(range.to)).toBe(true); + }); +}); + describe('getRefIds', () => { describe('when called with a null value', () => { it('then it should return empty array', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 77a29040e18..f10af2037f1 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -358,11 +358,13 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSour }; export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange, fiscalYearStartMonth: number): TimeRange => { - return { - from: dateMath.parse(rawRange.from, false, timeZone as any, fiscalYearStartMonth)!, - to: dateMath.parse(rawRange.to, true, timeZone as any, fiscalYearStartMonth)!, - raw: rawRange, - }; + let range = rangeUtil.convertRawToRange(rawRange, timeZone, fiscalYearStartMonth); + + if (range.to.isBefore(range.from)) { + range = rangeUtil.convertRawToRange({ from: range.raw.to, to: range.raw.from }, timeZone, fiscalYearStartMonth); + } + + return range; }; const parseRawTime = (value: string | DateTime): TimeFragment | null => { diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 8cff0b30c8c..5af09928063 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -176,7 +176,7 @@ class UnThemedOrgRow extends PureComponent { - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? (
diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index 9beee9941c1..5ff067820a2 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -91,7 +91,7 @@ export const DynamicTable = ({ {items.map((item, index) => { const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); return ( -
+
{renderPrefixCell && renderPrefixCell(item, index, items)} {isExpandable && (
diff --git a/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx b/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx index ae595e84372..b43b69847f3 100644 --- a/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx @@ -31,6 +31,7 @@ export const GroupBy = ({ className, groups, groupBy, onGroupingChange }: Props) onGroupingChange(items.map(({ value }) => value as string)); }} options={labelKeyOptions} + menuShouldPortal />
); diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx index 567a9488759..76ec7d4a250 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx @@ -82,6 +82,7 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, onChange={(value) => onChange(value?.value)} options={matcherFieldOptions} aria-label="Operator" + menuShouldPortal /> )} defaultValue={field.operator} diff --git a/public/app/features/alerting/unified/components/rules/ActionIcon.tsx b/public/app/features/alerting/unified/components/rules/ActionIcon.tsx index 0de605ff02b..90c066b3259 100644 --- a/public/app/features/alerting/unified/components/rules/ActionIcon.tsx +++ b/public/app/features/alerting/unified/components/rules/ActionIcon.tsx @@ -56,21 +56,24 @@ interface GoToProps { url: string; label?: string; target?: string; + children?: React.ReactNode; } -const GoTo: FC = ({ url, label, target, children }) => { +const GoTo = React.forwardRef(({ url, label, target, children }, ref) => { const absoluteUrl = url?.startsWith('http'); return absoluteUrl ? ( - + {children} ) : ( - + {children} ); -}; +}); + +GoTo.displayName = 'GoTo'; export const getStyle = () => css` cursor: pointer; diff --git a/public/app/features/alerting/unified/utils/redux.ts b/public/app/features/alerting/unified/utils/redux.ts index 4bc6cb1819f..345164d3c9c 100644 --- a/public/app/features/alerting/unified/utils/redux.ts +++ b/public/app/features/alerting/unified/utils/redux.ts @@ -161,3 +161,19 @@ export function messageFromError(e: Error | FetchError | SerializedError): strin } return (e as Error)?.message || String(e); } + +export function isAsyncRequestMapSliceFulfilled(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).every(isAsyncRequestStateFulfilled); +} + +export function isAsyncRequestStateFulfilled(state: AsyncRequestState): boolean { + return state.dispatched && !state.loading && !state.error; +} + +export function isAsyncRequestMapSlicePending(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).some(isAsyncRequestStatePending); +} + +export function isAsyncRequestStatePending(state: AsyncRequestState): boolean { + return state.dispatched && state.loading; +} diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 1c04cfc2473..8c903e3fbfa 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -41,7 +41,7 @@ export const PanelEditorTabs: FC = React.memo(({ panel, da {tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert) { - renderAlertTab(tab, panel, dashboard, onChangeTab); + return renderAlertTab(tab, panel, dashboard, onChangeTab); } return ( void ) { - if (!config.alertingEnabled || !config.unifiedAlertingEnabled) { + const alertingDisabled = !config.alertingEnabled && !config.unifiedAlertingEnabled; + + if (alertingDisabled) { return null; - } else if (config.unifiedAlertingEnabled) { + } + + if (config.unifiedAlertingEnabled) { return ( ); - } else if (config.alertingEnabled) { + } + + if (config.alertingEnabled) { return ( { store.dispatch(splitOpen({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any); await waitFor(() => expect(document.title).toEqual('Explore - loki | elastic - Grafana')); }); + + it('removes `from` and `to` parameters from url when first mounted', () => { + setup({ searchParams: 'from=1&to=2&orgId=1' }); + + expect(locationService.getSearchObject()).toEqual(expect.not.objectContaining({ from: '1', to: '2' })); + expect(locationService.getSearchObject()).toEqual(expect.objectContaining({ orgId: '1' })); + }); }); type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type SetupOptions = { datasources?: DatasourceSetup[]; query?: any; + searchParams?: string; }; function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi }; store: EnhancedStore } { @@ -368,7 +376,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou }, }; - locationService.push({ pathname: '/explore' }); + locationService.push({ pathname: '/explore', search: options?.searchParams }); if (options?.query) { locationService.partial(options.query); diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 1f2633a08f2..fdd2afde72c 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -10,6 +10,7 @@ import { Branding } from '../../core/components/Branding/Branding'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from 'app/types'; +import { locationService } from '@grafana/runtime'; interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {} interface OwnProps {} @@ -38,6 +39,22 @@ class WrapperUnconnected extends PureComponent { lastSavedUrl.left = undefined; lastSavedUrl.right = undefined; + // timeSrv (which is used internally) on init reads `from` and `to` param from the URL and updates itself + // using those value regardless of what is passed to the init method. + // The updated value is then used by Explore to get the range for each pane. + // This means that if `from` and `to` parameters are present in the URL, + // it would be impossible to change the time range in Explore. + // We are only doing this on mount for 2 reasons: + // 1: Doing it on update means we'll enter a render loop. + // 2: when parsing time in Explore (before feeding it to timeSrv) we make sure `from` is before `to` inside + // each pane state in order to not trigger un URL update from timeSrv. + const searchParams = locationService.getSearchObject(); + if (searchParams.from || searchParams.to) { + locationService.partial({ from: undefined, to: undefined }, true); + } + + const richHistory = getRichHistory(); + this.props.richHistoryUpdatedAction({ richHistory }); getRichHistory().then((richHistory) => { this.props.richHistoryUpdatedAction({ richHistory }); }); diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index f3deb62c77b..6ba6f050278 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -6,6 +6,7 @@ import { getMockTeamGroups } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { + isReadOnly: false, groups: [] as TeamGroup[], loadTeamGroups: jest.fn(), addTeamGroup: jest.fn(), diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index aeedfb12f16..a550b0cd9df 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -23,13 +23,17 @@ const mapDispatchToProps = { removeTeamGroup, }; +interface OwnProps { + isReadOnly: boolean; +} + interface State { isAdding: boolean; newGroupId: string; } const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; +export type Props = OwnProps & ConnectedProps; const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; @@ -70,11 +74,12 @@ export class TeamGroupSync extends PureComponent { } renderGroup(group: TeamGroup) { + const { isReadOnly } = this.props; return ( {group.groupId} - @@ -84,7 +89,7 @@ export class TeamGroupSync extends PureComponent { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.groups; + const { groups, isReadOnly } = this.props; return (
@@ -95,7 +100,7 @@ export class TeamGroupSync extends PureComponent {
{groups.length > 0 && ( - )} @@ -113,11 +118,12 @@ export class TeamGroupSync extends PureComponent { value={newGroupId} onChange={this.onNewGroupIdChanged} placeholder="cn=ops,ou=groups,dc=grafana,dc=org" + disabled={isReadOnly} />
-
@@ -135,6 +141,7 @@ export class TeamGroupSync extends PureComponent { proTipLinkTitle="Learn more" proTipLink="http://docs.grafana.org/auth/enhanced_ldap/" proTipTarget="_blank" + buttonDisabled={isReadOnly} /> )} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 27c6af5be0b..83894dc5e46 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,7 +43,7 @@ export class TeamList extends PureComponent { componentDidMount() { this.fetchTeams(); - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { this.fetchRoleOptions(); } } @@ -69,7 +69,11 @@ export class TeamList extends PureComponent { const { editorsCanAdmin, signedInUser } = this.props; const permission = team.permission; const teamUrl = `org/teams/edit/${team.id}`; - const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); + const canDelete = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsDelete, + team, + isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }) + ); return ( @@ -89,7 +93,7 @@ export class TeamList extends PureComponent { {team.memberCount} - {contextSrv.accessControlEnabled() && ( + {contextSrv.licensedAccessControlEnabled() && ( this.state.roleOptions} /> @@ -155,7 +159,7 @@ export class TeamList extends PureComponent { Name Email Members - {contextSrv.accessControlEnabled() && Roles} + {contextSrv.licensedAccessControlEnabled() && Roles} diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 8f5fa7ec666..c4f852b2bf6 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime/src/config', () => ({ licenseInfo: { enabledFeatures: { teamsync: true }, }, + featureToggles: { accesscontrol: false }, }, })); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 400dc0079c5..65430e5e898 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -4,9 +4,10 @@ import { includes } from 'lodash'; import config from 'app/core/config'; import Page from 'app/core/components/Page/Page'; import TeamMembers from './TeamMembers'; +import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { StoreState } from 'app/types'; +import { AccessControlAction, StoreState } from 'app/types'; import { loadTeam, loadTeamMembers } from './state/actions'; import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeamLoadingNav } from './state/navModel'; @@ -37,10 +38,17 @@ enum PageTypes { function mapStateToProps(state: StoreState, props: OwnProps) { const teamId = parseInt(props.match.params.id, 10); - const pageName = props.match.params.page ?? 'members'; + const team = getTeam(state.team, teamId); + let defaultPage = 'members'; + if (contextSrv.accessControlEnabled()) { + // With FGAC the settings page will always be available + if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { + defaultPage = 'settings'; + } + } + const pageName = props.match.params.page ?? defaultPage; const teamLoadingNav = getTeamLoadingNav(pageName as string); const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); - const team = getTeam(state.team, teamId); const members = getTeamMembers(state.team); return { @@ -81,7 +89,10 @@ export class TeamPages extends PureComponent { const { loadTeam, teamId } = this.props; this.setState({ isLoading: true }); const team = await loadTeam(teamId); - await this.props.loadTeamMembers(); + // With accesscontrol, the TeamPermissions will fetch team members + if (!contextSrv.accessControlEnabled()) { + await this.props.loadTeamMembers(); + } this.setState({ isLoading: false }); return team; } @@ -105,6 +116,10 @@ export class TeamPages extends PureComponent { }; hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + if (contextSrv.accessControlEnabled()) { + return navModel; + } + if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { navModel.main.children .filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members)) @@ -121,16 +136,35 @@ export class TeamPages extends PureComponent { const { members, team } = this.props; const currentPage = this.getCurrentPage(); + const canReadTeam = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsRead, + team!, + isSignedInUserTeamAdmin + ); + const canReadTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsRead, + team!, + isSignedInUserTeamAdmin + ); + const canWriteTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + team!, + isSignedInUserTeamAdmin + ); + switch (currentPage) { case PageTypes.Members: - return ; - + if (contextSrv.accessControlEnabled()) { + return ; + } else { + return ; + } case PageTypes.Settings: - return isSignedInUserTeamAdmin && ; + return canReadTeam && ; case PageTypes.GroupSync: - if (isSignedInUserTeamAdmin && isSyncEnabled) { - return ; - } else if (config.featureHighlights.enabled) { + if (canReadTeamPermissions && isSyncEnabled) { + return ; + } else if (config.featureToggles.featureHighlights) { return ( { + const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + props.team + ); + + return ( + + ); +}; + +export default TeamPermissions; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index b261116718e..4d4cfc0e83c 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -3,6 +3,12 @@ import { shallow } from 'enzyme'; import { Props, TeamSettings } from './TeamSettings'; import { getMockTeam } from './__mocks__/teamMocks'; +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermissionInMetadata: () => true, + }, +})); + const setup = (propOverrides?: object) => { const props: Props = { team: getMockTeam(), diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 8ef7dd288f5..1d34aee41e9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -4,7 +4,8 @@ import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; -import { Team } from 'app/types'; +import { AccessControlAction, Team } from 'app/types'; +import { contextSrv } from 'app/core/core'; const mapDispatchToProps = { updateTeam, @@ -18,6 +19,8 @@ interface OwnProps { export type Props = ConnectedProps & OwnProps; export const TeamSettings: FC = ({ team, updateTeam }) => { + const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); + return (
@@ -26,6 +29,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { onSubmit={(formTeam: Team) => { updateTeam(formTeam.name, formTeam.email); }} + disabled={!canWriteTeamSettings} > {({ register }) => ( <> @@ -44,7 +48,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { )}
- +
); }; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 35719d0908d..3abd26f7778 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -44,6 +44,7 @@ exports[`Render should render component 1`] = ` >