From 1dff00a7b3da9925d5ee4ee84863049f5650282f Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 24 Feb 2022 00:38:49 -0800 Subject: [PATCH 001/125] Fixes state timeline and status history colors when pallet is used (#45765) Co-authored-by: Victor Marin Co-authored-by: Victor Marin --- .../panel/state-timeline/TimelineChart.tsx | 16 +++++++++++++++- public/app/plugins/panel/state-timeline/types.ts | 1 + public/app/plugins/panel/state-timeline/utils.ts | 16 +++++++++------- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/state-timeline/TimelineChart.tsx b/public/app/plugins/panel/state-timeline/TimelineChart.tsx index e4d6b9850c3..72ee7e27640 100755 --- a/public/app/plugins/panel/state-timeline/TimelineChart.tsx +++ b/public/app/plugins/panel/state-timeline/TimelineChart.tsx @@ -10,7 +10,7 @@ import { VizLegend, VizLegendItem, } from '@grafana/ui'; -import { DataFrame, FieldType, TimeRange } from '@grafana/data'; +import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; import { preparePlotConfigBuilder } from './utils'; import { TimelineMode, TimelineOptions, TimelineValueAlignment } from './types'; @@ -34,6 +34,19 @@ export class TimelineChart extends React.Component { static contextType = PanelContextRoot; panelContext: PanelContext = {} as PanelContext; + getValueColor = (frameIdx: number, fieldIdx: number, value: any) => { + const field = this.props.frames[frameIdx].fields[fieldIdx]; + + if (field.display) { + const disp = field.display(value); // will apply color modes + if (disp.color) { + return disp.color; + } + } + + return FALLBACK_COLOR; + }; + prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { this.panelContext = this.context as PanelContext; const { eventBus, sync } = this.panelContext; @@ -48,6 +61,7 @@ export class TimelineChart extends React.Component { // When there is only one row, use the full space rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, + getValueColor: this.getValueColor, }); }; diff --git a/public/app/plugins/panel/state-timeline/types.ts b/public/app/plugins/panel/state-timeline/types.ts index 1c3a298827e..ecd1f5f56e8 100644 --- a/public/app/plugins/panel/state-timeline/types.ts +++ b/public/app/plugins/panel/state-timeline/types.ts @@ -18,6 +18,7 @@ export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip { alignValue?: TimelineValueAlignment; sync?: () => DashboardCursorSync; + getValueColor?: (frameIdx: number, fieldIdx: number, value: any) => string; } export type TimelineValueAlignment = 'center' | 'left' | 'right'; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 1b5a7ff3036..07b4d336c2d 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -70,6 +70,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ showValue, alignValue, mergeValues, + getValueColor, }) => { const builder = new UPlotConfigBuilder(timeZone); @@ -81,14 +82,15 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ return !(mode && field.display && mode.startsWith('continuous-')); }; - const getValueColor = (seriesIdx: number, value: any) => { + const getValueColorFn = (seriesIdx: number, value: any) => { const field = frame.fields[seriesIdx]; - if (field.display) { - const disp = field.display(value); // will apply color modes - if (disp.color) { - return disp.color; - } + if ( + field.state?.origin?.fieldIndex !== undefined && + field.state?.origin?.frameIndex !== undefined && + getValueColor + ) { + return getValueColor(field.state?.origin?.frameIndex, field.state?.origin?.fieldIndex, value); } return FALLBACK_COLOR; @@ -107,7 +109,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ theme, label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame), getFieldConfig: (seriesIdx) => frame.fields[seriesIdx].config.custom, - getValueColor, + getValueColor: getValueColorFn, getTimeRange, // hardcoded formatter for state values formatValue: (seriesIdx, value) => formattedValueToString(frame.fields[seriesIdx].display!(value)), From b798520ba757727b78705ac9f6fa452653172a4d Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 24 Feb 2022 09:29:39 +0000 Subject: [PATCH 002/125] Chore: Add github action to manage stale PRs (#45766) * Add github action to manage stale PRs * Update days before close --- .github/stale.yml | 47 ------------------------------------- .github/workflows/stale.yml | 32 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 47 deletions(-) delete mode 100644 .github/stale.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 701b0b260d0..00000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# General configuration -# Label to use when marking as stale -staleLabel: stale - -# Pull request specific configuration -pulls: - # Number of days of inactivity before an Issue or Pull Request becomes stale - daysUntilStale: 14 - # Number of days of inactivity before a stale Issue or Pull Request is closed. - # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. - daysUntilClose: 30 - # Comment to post when marking as stale. Set to `false` to disable - markComment: > - This pull request has been automatically marked as stale because it has not had - activity in the last 2 weeks. It will be closed in 30 days if no further activity occurs. Please - feel free to give a status update now, ping for review, or re-open when it's ready. - Thank you for your contributions! - # Comment to post when closing a stale Issue or Pull Request. - closeComment: > - This pull request has been automatically closed because it has not had - activity in the last 30 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. - Thank you for your contributions! - # Limit the number of actions per hour, from 1-30. Default is 30 - limitPerRun: 1 - -exemptLabels: - - help wanted - - type/bug - - type/feature-request - - Epic - - no stalebot - -# Issue specific configuration -issues: - limitPerRun: 1 - daysUntilStale: 100000 - daysUntilClose: 100000 - markComment: > - This issue has been automatically marked as stale because it has not had activity in the - last 100 days. It will be closed in the next 100 days if no activity occurs. - Thank you for your contributions. - closeComment: > - This issue has been automatically closed because it has not had activity in the - last month and a half. If this issue is still valid, please ping a maintainer and ask them to check this again. - Thank you for your contributions. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..85335b924d8 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,32 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + # Number of days of inactivity before a stale Issue or Pull Request is closed. + # Set to -1 to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. + days-before-close: 14 + # Number of days of inactivity before an Issue or Pull Request becomes stale + days-before-stale: 30 + # We don't want any Issues to be marked as stale for now. + days-before-issue-stale: -1 + exempt-issue-label: no stalebot + exempt-pr-label: no stalebot + stale-issue-label: stale + stale-pr-label: stale + stale-pr-message: > + This pull request has been automatically marked as stale because it has not had + activity in the last 30 days. It will be closed in 2 weeks if no further activity occurs. Please + feel free to give a status update now, ping for review, or re-open when it's ready. + Thank you for your contributions! + close-pr-message: > + This pull request has been automatically closed because it has not had + activity in the last 2 weeks. Please feel free to give a status update now, ping for review, or re-open when it's ready. + Thank you for your contributions! From f4d9de00e91534818c72e33e28475b1ef0c62a54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:32:55 +0100 Subject: [PATCH 003/125] Update dependency cypress to v9.5.0 (#44678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency cypress to v9.5.0 * drone: update cypress version * Update drone.yml Co-authored-by: Renovate Bot Co-authored-by: Gábor Farkas Co-authored-by: Zoltán Bedi --- .drone.yml | 50 +++++++++++++++---------------- package.json | 2 +- packages/grafana-e2e/package.json | 2 +- scripts/drone/steps/lib.star | 2 +- yarn.lock | 14 ++++----- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.drone.yml b/.drone.yml index a7ce2b6a034..21d670c58b0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -208,7 +208,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -217,7 +217,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -226,7 +226,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -235,7 +235,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -745,7 +745,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -754,7 +754,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -763,7 +763,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -772,7 +772,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -1380,7 +1380,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -1389,7 +1389,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -1398,7 +1398,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -1407,7 +1407,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -1982,7 +1982,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -1991,7 +1991,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -2000,7 +2000,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -2009,7 +2009,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -3134,7 +3134,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -3143,7 +3143,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -3152,7 +3152,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -3161,7 +3161,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -3661,7 +3661,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-dashboards-suite - commands: - apt-get install -y netcat @@ -3670,7 +3670,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-smoke-tests-suite - commands: - apt-get install -y netcat @@ -3679,7 +3679,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-panels-suite - commands: - apt-get install -y netcat @@ -3688,7 +3688,7 @@ steps: - grafana-server environment: HOST: grafana-server - image: cypress/included:9.3.1 + image: cypress/included:9.5.0 name: end-to-end-tests-various-suite - commands: - apt-get update @@ -4305,6 +4305,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: d9de3f45c31338ed1936f253e8a1f3390822468daeb3395bc368dced68068519 +hmac: b92ebbf48ca675f25c96f4b182a72c08b0e79f9c50d29caaacab124e97f32b4d ... diff --git a/package.json b/package.json index ba20522e9b9..9e53a1b81cf 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "copy-webpack-plugin": "9.0.1", "css-loader": "6.6.0", "css-minimizer-webpack-plugin": "3.4.1", - "cypress": "9.3.1", + "cypress": "9.5.0", "enzyme": "3.11.0", "enzyme-to-json": "3.6.2", "eslint": "8.9.0", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 18f138f6b27..11d719ca564 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -55,7 +55,7 @@ "blink-diff": "1.0.13", "chrome-remote-interface": "0.31.2", "commander": "8.3.0", - "cypress": "9.3.1", + "cypress": "9.5.0", "cypress-file-upload": "5.0.8", "devtools-protocol": "0.0.967529", "execa": "5.1.1", diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 26b03acdba3..837a27d9253 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -695,7 +695,7 @@ def e2e_tests_step(suite, edition, port=3001, tries=None): cmd += ' --tries {}'.format(tries) return { 'name': 'end-to-end-tests-{}'.format(suite) + enterprise2_suffix(edition), - 'image': 'cypress/included:9.3.1', + 'image': 'cypress/included:9.5.0', 'depends_on': [ 'grafana-server', ], diff --git a/yarn.lock b/yarn.lock index 3a5ae2f855f..a33f6e7fff9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4092,7 +4092,7 @@ __metadata: blink-diff: 1.0.13 chrome-remote-interface: 0.31.2 commander: 8.3.0 - cypress: 9.3.1 + cypress: 9.5.0 cypress-file-upload: 5.0.8 devtools-protocol: 0.0.967529 execa: 5.1.1 @@ -16623,9 +16623,9 @@ __metadata: languageName: node linkType: hard -"cypress@npm:9.3.1": - version: 9.3.1 - resolution: "cypress@npm:9.3.1" +"cypress@npm:9.5.0": + version: 9.5.0 + resolution: "cypress@npm:9.5.0" dependencies: "@cypress/request": ^2.88.10 "@cypress/xvfb": ^1.2.4 @@ -16664,14 +16664,14 @@ __metadata: pretty-bytes: ^5.6.0 proxy-from-env: 1.0.0 request-progress: ^3.0.0 + semver: ^7.3.2 supports-color: ^8.1.1 tmp: ~0.2.1 untildify: ^4.0.0 - url: ^0.11.0 yauzl: ^2.10.0 bin: cypress: bin/cypress - checksum: 6992e0f293618d1dec2cbd278fae08c9c3375ce462a1f767f735b06faa9a52ad375d174f2371103e443f3dc1d5cbb2c93d2ca73204d99bbd845db033d9891270 + checksum: 0a4ef1413676e37c19526e86dba7bc82420b9afa840b2a68b94e749b878e6b394f4e64bb099bca12c44f4520e7a1cb07ab17d6c9ccf87746bc1bfc3c9555f334 languageName: node linkType: hard @@ -20779,7 +20779,7 @@ __metadata: core-js: 3.21.0 css-loader: 6.6.0 css-minimizer-webpack-plugin: 3.4.1 - cypress: 9.3.1 + cypress: 9.5.0 d3: 5.15.0 d3-force: ^2.1.1 d3-scale-chromatic: 1.5.0 From ede31b1602da1b53da2cdfdc8b5a31e0ea55b8a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:33:59 +0100 Subject: [PATCH 004/125] Update dependency react-dropzone to v12 (#45089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency react-dropzone to v12 * Don't use FS Access api * kick drone Co-authored-by: Renovate Bot Co-authored-by: Zoltán Bedi Co-authored-by: Ashley Harrison --- packages/grafana-ui/package.json | 2 +- .../components/FileDropzone/FileDropzone.tsx | 3 +- yarn.lock | 28 +++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 321fadb1443..44f5bb78e0e 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -72,7 +72,7 @@ "react-colorful": "5.5.1", "react-custom-scrollbars-2": "4.4.0", "react-dom": "17.0.2", - "react-dropzone": "11.5.1", + "react-dropzone": "12.0.4", "react-highlight-words": "0.17.0", "react-hook-form": "7.5.3", "react-inlinesvg": "2.3.0", diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 3c49ffbc176..5f3f5bf120f 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -18,6 +18,7 @@ export interface FileDropzoneProps { * maxSize: Infinity, * minSize: 0, * multiple: true, + * useFsAccessApi: false, * maxFiles: 0, * } */ @@ -135,7 +136,7 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender setFiles(newFiles); }; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ ...options, onDrop }); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ ...options, useFsAccessApi: false, onDrop }); const theme = useTheme2(); const styles = getStyles(theme, isDragActive); const fileList = files.map((file) => { diff --git a/yarn.lock b/yarn.lock index a33f6e7fff9..0b8757376c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4447,7 +4447,7 @@ __metadata: react-custom-scrollbars-2: 4.4.0 react-docgen-typescript-loader: 3.7.2 react-dom: 17.0.2 - react-dropzone: 11.5.1 + react-dropzone: 12.0.4 react-highlight-words: 0.17.0 react-hook-form: 7.5.3 react-inlinesvg: 2.3.0 @@ -13377,7 +13377,7 @@ __metadata: languageName: node linkType: hard -"attr-accept@npm:^2.2.1": +"attr-accept@npm:^2.2.2": version: 2.2.2 resolution: "attr-accept@npm:2.2.2" checksum: 496f7249354ab53e522510c1dc8f67a1887382187adde4dc205507d2f014836a247073b05e9d9ea51e2e9c7f71b0d2aa21730af80efa9af2d68303e5f0565c4d @@ -19453,12 +19453,12 @@ __metadata: languageName: node linkType: hard -"file-selector@npm:^0.2.2": - version: 0.2.4 - resolution: "file-selector@npm:0.2.4" +"file-selector@npm:^0.4.0": + version: 0.4.0 + resolution: "file-selector@npm:0.4.0" dependencies: tslib: ^2.0.3 - checksum: 83341e7416352c7de0caf433b33c8d007d3c298c17d6ba0e70168af82bb045d905ddd6f4bc3ca764a2035c6f18eae7f52223dc6a784056f61b8391c351f88323 + checksum: 1c9986e94bd033442cb4299d10d73d98fbc0b5f1af5b47d7a27b39f1e6bbd8d677f66c2628167da387166c652ad3c2429875042b453ba62f14945d903adf88d5 languageName: node linkType: hard @@ -29815,7 +29815,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:15.8.1": +"prop-types@npm:15.8.1, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -30780,16 +30780,16 @@ __metadata: languageName: node linkType: hard -"react-dropzone@npm:11.5.1": - version: 11.5.1 - resolution: "react-dropzone@npm:11.5.1" +"react-dropzone@npm:12.0.4": + version: 12.0.4 + resolution: "react-dropzone@npm:12.0.4" dependencies: - attr-accept: ^2.2.1 - file-selector: ^0.2.2 - prop-types: ^15.7.2 + attr-accept: ^2.2.2 + file-selector: ^0.4.0 + prop-types: ^15.8.1 peerDependencies: react: ">= 16.8" - checksum: 54d801ca5b952d9e00913c516bcb3b961d34058116d0444ac3ce911e98e777fbd936fb60e81c0739664f68bf1dd1bdc6a1477d0b50b46ffe986140671562110d + checksum: cc8c2036c72cbe02ddea1e141de45aeee7b399321b5791174d04fb8c1f52eb2ee3ae2d3fb1240fc23008a435c4ceb437e85831f0388a6b34be2bc39417701096 languageName: node linkType: hard From bb5a39faefd6d800c5eb8fb552159bccc70641f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 24 Feb 2022 10:55:00 +0100 Subject: [PATCH 005/125] Chore: Enables flakey e2e test (#45816) --- .../templating-dashboard-links-and-variables.spec.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 39c87b73448..a2b6a252631 100644 --- a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -5,7 +5,7 @@ e2e.scenario({ itName: 'Tests dashboard links and variables in links', addScenarioDataSource: false, addScenarioDashBoard: false, - skipScenario: true, // Skipped because it was causing many failures in main. + skipScenario: false, scenario: () => { e2e.flows.openDashboard({ uid: 'yBCC3aKGk' }); e2e() @@ -21,7 +21,9 @@ e2e.scenario({ }) .as('tagsDemoSearch'); - // waiting for links to render, couldn't find a better way using routes for instance + // waiting for network requests first + e2e().wait(['@tagsTemplatingSearch', '@tagsDemoSearch']); + // and then waiting for links to render e2e().wait(1000); const verifyLinks = (variableValue: string) => { @@ -36,11 +38,7 @@ e2e.scenario({ }); }; - e2e.components.DashboardLinks.dropDown() - .should('be.visible') - .click() - .wait('@tagsTemplatingSearch') - .wait('@tagsDemoSearch'); + e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch'); // verify all links, should have All value verifyLinks('All'); From d6c580e3387d70a4d0708ba2227e1244b3fde9c8 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 24 Feb 2022 11:31:36 +0100 Subject: [PATCH 006/125] Alerting: Lotex alert groups removal (#45150) * Add lotex group removal UI * Connect UI to delete group action * Add rules' refreshing after deletion of a group * Improve confirmation message * Add tests for RulesGroup * Remove redundant check --- .../components/rules/RulesGroup.test.tsx | 111 ++++++++++++++++++ .../unified/components/rules/RulesGroup.tsx | 57 +++++++-- .../alerting/unified/state/actions.ts | 28 ++++- 3 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx new file mode 100644 index 00000000000..a9eafa81ed3 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -0,0 +1,111 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { configureStore } from 'app/store/configureStore'; +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { byTestId, byText } from 'testing-library-selector'; +import { mockCombinedRule, mockDataSource } from '../../mocks'; +import { RulesGroup } from './RulesGroup'; + +const hasRulerMock = jest.fn(); +jest.mock('../../hooks/useHasRuler', () => ({ + useHasRuler: () => hasRulerMock, +})); + +beforeEach(() => hasRulerMock.mockReset()); + +const ui = { + editGroupButton: byTestId('edit-group'), + deleteGroupButton: byTestId('delete-group'), + confirmDeleteModal: { + header: byText('Delete group'), + confirmButton: byText('Delete'), + }, +}; + +describe('Rules group tests', () => { + const store = configureStore(); + + function renderRulesGroup(namespace: CombinedRuleNamespace, group: CombinedRuleGroup) { + return render( + + + + ); + } + + describe('When the datasource is grafana', () => { + const group: CombinedRuleGroup = { + name: 'TestGroup', + rules: [mockCombinedRule()], + }; + + const namespace: CombinedRuleNamespace = { + name: 'TestNamespace', + rulesSource: 'grafana', + groups: [group], + }; + + it('Should hide delete and edit group buttons', () => { + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(ui.deleteGroupButton.query()).not.toBeInTheDocument(); + expect(ui.editGroupButton.query()).not.toBeInTheDocument(); + }); + }); + + describe('When the datasource is not grafana', () => { + const group: CombinedRuleGroup = { + name: 'TestGroup', + rules: [mockCombinedRule()], + }; + + const namespace: CombinedRuleNamespace = { + name: 'TestNamespace', + rulesSource: mockDataSource(), + groups: [group], + }; + + it('When ruler enabled should display delete and edit group buttons', () => { + // Arrange + hasRulerMock.mockReturnValue(true); + + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(hasRulerMock).toHaveBeenCalled(); + expect(ui.deleteGroupButton.get()).toBeInTheDocument(); + expect(ui.editGroupButton.get()).toBeInTheDocument(); + }); + + it('When ruler disabled should hide delete and edit group buttons', () => { + // Arrange + hasRulerMock.mockReturnValue(false); + + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(hasRulerMock).toHaveBeenCalled(); + expect(ui.deleteGroupButton.query()).not.toBeInTheDocument(); + expect(ui.editGroupButton.query()).not.toBeInTheDocument(); + }); + + it('Delete button click should display confirmation modal', () => { + // Arrange + hasRulerMock.mockReturnValue(true); + + // Act + renderRulesGroup(namespace, group); + userEvent.click(ui.deleteGroupButton.get()); + + // Assert + expect(ui.confirmDeleteModal.header.get()).toBeInTheDocument(); + expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index ab267eeb172..04c61de0f57 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,18 +1,21 @@ -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; -import React, { FC, useState, useEffect } from 'react'; -import { HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; -import { GrafanaTheme2 } from '@grafana/data'; import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; +import kbn from 'app/core/utils/kbn'; +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import pluralize from 'pluralize'; +import React, { FC, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useFolder } from '../../hooks/useFolder'; +import { useHasRuler } from '../../hooks/useHasRuler'; +import { deleteRulesGroupAction } from '../../state/actions'; +import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { isGrafanaRulerRule } from '../../utils/rules'; import { CollapseToggle } from '../CollapseToggle'; -import { RulesTable } from './RulesTable'; -import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { ActionIcon } from './ActionIcon'; -import { useHasRuler } from '../../hooks/useHasRuler'; -import kbn from 'app/core/utils/kbn'; -import { useFolder } from '../../hooks/useFolder'; -import { RuleStats } from './RuleStats'; import { EditCloudGroupModal } from './EditCloudGroupModal'; +import { RulesTable } from './RulesTable'; +import { RuleStats } from './RuleStats'; interface Props { namespace: CombinedRuleNamespace; @@ -22,9 +25,11 @@ interface Props { export const RulesGroup: FC = React.memo(({ group, namespace, expandAll }) => { const { rulesSource } = namespace; + const dispatch = useDispatch(); const styles = useStyles2(getStyles); const [isEditingGroup, setIsEditingGroup] = useState(false); + const [isDeletingGroup, setIsDeletingGroup] = useState(false); const [isCollapsed, setIsCollapsed] = useState(!expandAll); useEffect(() => { @@ -39,6 +44,11 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } // group "is deleting" if rules source has ruler, but this group has no rules that are in ruler const isDeleting = hasRuler(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule); + const deleteGroup = () => { + dispatch(deleteRulesGroupAction(namespace, group)); + setIsDeletingGroup(false); + }; + const actionIcons: React.ReactNode[] = []; // for grafana, link to folder views @@ -88,6 +98,17 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } onClick={() => setIsEditingGroup(true)} /> ); + + actionIcons.push( + setIsDeletingGroup(true)} + /> + ); } const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name; @@ -129,6 +150,22 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } {isEditingGroup && ( setIsEditingGroup(false)} /> )} + + Deleting this group will permanently remove the group +
+ and {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it. +
+ Are you sure you want to delete this group? + + } + onConfirm={deleteGroup} + onDismiss={() => setIsDeletingGroup(false)} + confirmText="Delete" + /> ); }); diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 38e4a4fdb40..c8c5113ef4a 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -12,7 +12,14 @@ import { TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types'; -import { RuleIdentifier, RuleNamespace, RuleWithLocation, StateHistoryItem } from 'app/types/unified-alerting'; +import { + CombinedRuleGroup, + CombinedRuleNamespace, + RuleIdentifier, + RuleNamespace, + RuleWithLocation, + StateHistoryItem, +} from 'app/types/unified-alerting'; import { PostableRulerRuleGroupDTO, RulerGrafanaRuleDTO, @@ -49,6 +56,7 @@ import { import { RuleFormType, RuleFormValues } from '../types/rule-form'; import { getAllRulesSourceNames, + getRulesSourceName, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource, isVanillaPrometheusAlertManagerDataSource, @@ -263,6 +271,24 @@ async function deleteRule(ruleWithLocation: RuleWithLocation): Promise { }); } +export function deleteRulesGroupAction( + namespace: CombinedRuleNamespace, + ruleGroup: CombinedRuleGroup +): ThunkResult { + return async (dispatch) => { + withAppEvents( + (async () => { + const sourceName = getRulesSourceName(namespace.rulesSource); + + await deleteRulerRulesGroup(sourceName, namespace.name, ruleGroup.name); + dispatch(fetchRulerRulesAction({ rulesSourceName: sourceName })); + dispatch(fetchPromRulesAction({ rulesSourceName: sourceName })); + })(), + { successMessage: 'Group deleted' } + ); + }; +} + export function deleteRuleAction( ruleIdentifier: RuleIdentifier, options: { navigateTo?: string } = {} From 0036233fa62487e2593248024c181c1b10b71906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 24 Feb 2022 11:34:49 +0100 Subject: [PATCH 007/125] Chore: fix flaky e2e (#45822) --- .../templating-dashboard-links-and-variables.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index a2b6a252631..c2eb79f780c 100644 --- a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -7,7 +7,6 @@ e2e.scenario({ addScenarioDashBoard: false, skipScenario: false, scenario: () => { - e2e.flows.openDashboard({ uid: 'yBCC3aKGk' }); e2e() .intercept({ method: 'GET', @@ -21,6 +20,8 @@ e2e.scenario({ }) .as('tagsDemoSearch'); + e2e.flows.openDashboard({ uid: 'yBCC3aKGk' }); + // waiting for network requests first e2e().wait(['@tagsTemplatingSearch', '@tagsDemoSearch']); // and then waiting for links to render From 5c6061acd2d42d24f9243465d4f9b6699ab78d53 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Thu, 24 Feb 2022 11:37:25 +0100 Subject: [PATCH 008/125] Prometheus/QueryBuilder: Fix parsing of functions without args (#45508) --- .../prometheus/querybuilder/parsing.test.ts | 11 +++++++++ .../prometheus/querybuilder/parsing.ts | 23 ++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts index 08e735d2f97..2ce889d3a4a 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts @@ -365,6 +365,17 @@ describe('buildVisualQueryFromString', () => { }, }); }); + + it('lone aggregation without params', () => { + expect(buildVisualQueryFromString('sum()')).toEqual({ + errors: [], + query: { + metric: '', + labels: [], + operations: [{ id: 'sum', params: [] }], + }, + }); + }); }); function noErrors(query: PromVisualQuery) { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts index 4953acc0dd8..cf0cd342adf 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts @@ -72,19 +72,27 @@ export function buildVisualQueryFromString(expr: string): Context { labels: [], operations: [], }; - const context = { + const context: Context = { query: visQuery, errors: [], }; - handleExpression(replacedExpr, node, context); + try { + handleExpression(replacedExpr, node, context); + } catch (err) { + // Not ideal to log it here, but otherwise we would lose the stack trace. + console.error(err); + context.errors.push({ + text: err.message, + }); + } return context; } interface ParsingError { text: string; - from: number; - to: number; + from?: number; + to?: number; parentType?: string; } @@ -262,7 +270,7 @@ function handleAggregation(expr: string, node: SyntaxNode, context: Context) { const op: QueryBuilderOperation = { id: funcName, params: [] }; visQuery.operations.unshift(op); - updateFunctionArgs(expr, callArgs!, context, op); + updateFunctionArgs(expr, callArgs, context, op); // We add labels after params in the visual query editor. op.params.push(...labels); } @@ -279,7 +287,10 @@ function handleAggregation(expr: string, node: SyntaxNode, context: Context) { * @param context * @param op - We need the operation to add the params to as an additional context. */ -function updateFunctionArgs(expr: string, node: SyntaxNode, context: Context, op: QueryBuilderOperation) { +function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Context, op: QueryBuilderOperation) { + if (!node) { + return; + } switch (node.name) { // In case we have an expression we don't know what kind so we have to look at the child as it can be anything. case 'Expr': From d3700c40326302947547e464d57de76717e01cd9 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 24 Feb 2022 10:49:48 +0000 Subject: [PATCH 009/125] Chore: Remove console output from some jest tests (#45792) * Chore: Remove console output from some jest tests * Skip manual performance test --- .../src/config/webpack/loaders.test.ts | 10 ++-- .../GraphNG/nullInsertThreshold.test.ts | 3 +- public/app/core/utils/explore.test.ts | 53 +------------------ .../DataSourceVariableEditor.test.tsx | 2 +- .../datasource/cloudwatch/utils/logsRetry.ts | 1 - .../prometheus/language_provider.test.ts | 4 ++ 6 files changed, 14 insertions(+), 59 deletions(-) diff --git a/packages/grafana-toolkit/src/config/webpack/loaders.test.ts b/packages/grafana-toolkit/src/config/webpack/loaders.test.ts index 7f5b47e5755..0c5e3f02804 100644 --- a/packages/grafana-toolkit/src/config/webpack/loaders.test.ts +++ b/packages/grafana-toolkit/src/config/webpack/loaders.test.ts @@ -2,9 +2,11 @@ import { getStylesheetEntries, hasThemeStylesheets } from './loaders'; describe('Loaders', () => { describe('stylesheet helpers', () => { - jest.spyOn(console, 'log').mockImplementation(); + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(); + }); - afterAll(() => { + afterEach(() => { jest.restoreAllMocks(); }); @@ -23,12 +25,12 @@ describe('Loaders', () => { describe('hasThemeStylesheets', () => { it('throws when only one theme file is defined', () => { - jest.spyOn(console, 'error').mockImplementation(); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(); const result = () => { hasThemeStylesheets(`${__dirname}/../mocks/stylesheetsSupport/missing-theme-file`); }; expect(result).toThrow(); - jest.restoreAllMocks(); + errorSpy.mockRestore(); }); it('returns false when no theme files present', () => { diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts index c74fb32dbf5..01ec591f9ca 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts @@ -202,7 +202,8 @@ describe('nullInsertThreshold Transformer', () => { expect(result).toBe(df); }); - test('perf stress test should be <= 10ms', () => { + // Leave this test skipped - it should be run manually + test.skip('perf stress test should be <= 10ms', () => { // 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values) let bigFrameA = genFrame(); diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 42e85d45fcc..7b1a4d2b999 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -113,31 +113,6 @@ describe('state functions', () => { '{"expr":"super{foo=\\"x/z\\"}","refId":"B"}],"range":{"from":"now-5h","to":"now"}}' ); }); - - // TODO: remove in 9.0 - it('returns url parameter value for a state object', () => { - const state = { - ...DEFAULT_EXPLORE_STATE, - datasource: 'foo', - queries: [ - { - expr: 'metric{test="a/b"}', - refId: 'A', - }, - { - expr: 'super{foo="x/z"}', - refId: 'B', - }, - ], - range: { - from: 'now-5h', - to: 'now', - }, - }; - expect(serializeStateToUrlParam(state, true)).toBe( - '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}","refId":"A"},{"expr":"super{foo=\\"x/z\\"}","refId":"B"}],"range":{"from":"now-5h","to":"now"}}' - ); - }); }); describe('interplay', () => { @@ -165,32 +140,6 @@ describe('state functions', () => { expect(state).toMatchObject(parsed); }); - // TODO: remove in 9.0 - it('can parse the compact serialized state into the original state', () => { - const state = { - ...DEFAULT_EXPLORE_STATE, - datasource: 'foo', - queries: [ - { - expr: 'metric{test="a/b"}', - refId: 'A', - }, - { - expr: 'super{foo="x/z"}', - refId: 'B', - }, - ], - range: { - from: 'now - 5h', - to: 'now', - }, - panelsState: undefined, - }; - const serialized = serializeStateToUrlParam(state, true); - const parsed = parseUrlState(serialized); - expect(state).toMatchObject(parsed); - }); - it('can parse serialized panelsState into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, @@ -215,7 +164,7 @@ describe('state functions', () => { }, }, }; - const serialized = serializeStateToUrlParam(state, true); + const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); expect(state).toMatchObject(parsed); }); diff --git a/public/app/features/variables/datasource/DataSourceVariableEditor.test.tsx b/public/app/features/variables/datasource/DataSourceVariableEditor.test.tsx index 9bf77dec0c2..42190e73bb4 100644 --- a/public/app/features/variables/datasource/DataSourceVariableEditor.test.tsx +++ b/public/app/features/variables/datasource/DataSourceVariableEditor.test.tsx @@ -13,7 +13,7 @@ const props = { { text: 'Loki', value: 'ds-loki' }, ], }, - variable: { ...initialDataSourceVariableModelState }, + variable: { ...initialDataSourceVariableModelState, rootStateKey: 'foo' }, onPropChange: jest.fn(), // connected actions diff --git a/public/app/plugins/datasource/cloudwatch/utils/logsRetry.ts b/public/app/plugins/datasource/cloudwatch/utils/logsRetry.ts index 40eb33fb0e9..06db908daa2 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/logsRetry.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/logsRetry.ts @@ -106,7 +106,6 @@ export function runWithRetry( timerID = setTimeout( () => { retries++; - console.log(`Attempt ${retries}`); run(errorData!.errors); }, // We want to know how long to wait for the next retry. First time this will be 0. diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index 950021b60fc..a9adfcd8950 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -382,6 +382,7 @@ describe('Language completion provider', () => { }); it('returns a refresher on label context and unavailable metric', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); const instance = new LanguageProvider(datasource); const value = Plain.deserialize('metric{}'); const ed = new SlateEditor({ value }); @@ -394,6 +395,7 @@ describe('Language completion provider', () => { }); expect(result.context).toBeUndefined(); expect(result.suggestions).toEqual([]); + expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {__name__="metric"}'); }); it('returns label values on label context when given a metric and a label key', async () => { @@ -598,6 +600,7 @@ describe('Language completion provider', () => { }); describe('disabled metrics lookup', () => { it('does not issue any metadata requests when lookup is disabled', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); const datasource: PrometheusDatasource = { metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })), getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })), @@ -619,6 +622,7 @@ describe('Language completion provider', () => { expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0); await instance.provideCompletionItems(args); expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0); + expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {}'); }); it('issues metadata requests when lookup is not disabled', async () => { const datasource: PrometheusDatasource = { From 8d57318941a72d09d9056168731895b2aaa73819 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 24 Feb 2022 10:58:54 +0000 Subject: [PATCH 010/125] Alerting: Use expanded labels in dashboard annotations (#45726) --- pkg/services/ngalert/state/manager.go | 34 ++++++++++----- pkg/services/ngalert/state/manager_test.go | 48 ++++++++++++++++++++++ pkg/services/ngalert/tests/util.go | 5 +++ 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 6695cf16a94..1bf0119e63a 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -5,18 +5,19 @@ import ( "fmt" "net/url" "strconv" + "strings" "time" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" - + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/sqlstore" ) var ResendDelay = 30 * time.Second @@ -188,7 +189,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu st.set(currentState) if oldState != currentState.State { - go st.createAlertAnnotation(ctx, currentState.State, alertRule, result, oldState) + go st.createAlertAnnotation(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) } return currentState } @@ -236,18 +237,19 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State { } } -func (st *Manager) createAlertAnnotation(ctx context.Context, new eval.State, alertRule *ngModels.AlertRule, result eval.Result, oldState eval.State) { - st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", new.String(), "oldState", oldState.String()) +func (st *Manager) createAlertAnnotation(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { + st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", state.String(), "oldState", previousState.String()) - annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, result.Instance.String(), new.String()) + labels = removePrivateLabels(labels) + annotationText := fmt.Sprintf("%s {%s} - %s", alertRule.Title, labels.String(), state.String()) item := &annotations.Item{ AlertId: alertRule.ID, OrgId: alertRule.OrgID, - PrevState: oldState.String(), - NewState: new.String(), + PrevState: previousState.String(), + NewState: state.String(), Text: annotationText, - Epoch: result.EvaluatedAt.UnixNano() / int64(time.Millisecond), + Epoch: evaluatedAt.UnixNano() / int64(time.Millisecond), } dashUid, ok := alertRule.Annotations[ngModels.DashboardUIDAnnotation] @@ -305,3 +307,13 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels. func isItStale(lastEval time.Time, intervalSeconds int64) bool { return lastEval.Add(2 * time.Duration(intervalSeconds) * time.Second).Before(time.Now()) } + +func removePrivateLabels(labels data.Labels) data.Labels { + result := make(data.Labels) + for k, v := range labels { + if !strings.HasPrefix(k, "__") && !strings.HasSuffix(k, "__") { + result[k] = v + } + } + return result +} diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 02374646775..59151839976 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "testing" "time" @@ -28,6 +29,53 @@ import ( var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry()) +func TestDashboardAnnotations(t *testing.T) { + evaluationTime, err := time.Parse("2006-01-02", "2022-01-01") + require.NoError(t, err) + + ctx := context.Background() + _, dbstore := tests.SetupTestEnv(t, 1) + + sqlStore := mockstore.NewSQLStoreMock() + st := state.NewManager(log.New("test_stale_results_handler"), testMetrics.GetStateMetrics(), nil, dbstore, dbstore, sqlStore) + + fakeAnnoRepo := store.NewFakeAnnotationsRepo() + annotations.SetRepository(fakeAnnoRepo) + + const mainOrgID int64 = 1 + + rule := tests.CreateTestAlertRuleWithLabels(t, ctx, dbstore, 600, mainOrgID, map[string]string{ + "test1": "testValue1", + "test2": "{{ $labels.instance_label }}", + }) + + st.Warm(ctx) + _ = st.ProcessEvalResults(ctx, rule, eval.Results{{ + Instance: data.Labels{"instance_label": "testValue2"}, + State: eval.Alerting, + EvaluatedAt: evaluationTime, + }}) + + expected := []string{rule.Title + " {alertname=" + rule.Title + ", instance_label=testValue2, test1=testValue1, test2=testValue2} - Alerting"} + sort.Strings(expected) + require.Eventuallyf(t, func() bool { + var actual []string + for _, next := range fakeAnnoRepo.Items { + actual = append(actual, next.Text) + } + sort.Strings(actual) + if len(expected) != len(actual) { + return false + } + for i := 0; i < len(expected); i++ { + if expected[i] != actual[i] { + return false + } + } + return true + }, time.Second, 100*time.Millisecond, "unexpected annotations") +} + func TestProcessEvalResults(t *testing.T) { evaluationTime, err := time.Parse("2006-01-02", "2021-03-25") if err != nil { diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index d8ab0e11a85..5a9dfaf1e02 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -56,6 +56,10 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * // CreateTestAlertRule creates a dummy alert definition to be used by the tests. func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64) *models.AlertRule { + return CreateTestAlertRuleWithLabels(t, ctx, dbstore, intervalSeconds, orgID, nil) +} + +func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule { ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID()) err := dbstore.UpsertAlertRules(ctx, []store.UpsertRule{ { @@ -78,6 +82,7 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto RefID: "A", }, }, + Labels: labels, Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, IntervalSeconds: intervalSeconds, NamespaceUID: "namespace", From 60db64398356061e28c10889a7abbc9b48728b48 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Thu, 24 Feb 2022 06:03:07 -0500 Subject: [PATCH 011/125] grafana-cli: Diff generated ts directly instead of relying on git (#45815) * Add diffing support to grafana-cli cue gen-ts * Rely on diff comparison in cuetsify pipeline step * Ignore *.gen.ts files with eslint * Chore: Fix lint `sdboyer/cuetsify-compare` (#45818) * Sync drone (cherry picked from commit 40645ab19e39ff9b0a12b7ebb13a4dc4c5e1d472) * Fix lint (cherry picked from commit c95ece983984432fea029335b2b729b09d76c7eb) * Sign drone Co-authored-by: Dimitris Sotirakis --- .drone.yml | 122 ++++++------------ .eslintignore | 3 + pkg/cmd/grafana-cli/commands/commands.go | 5 + .../grafana-cli/commands/cuetsify_command.go | 51 +++++++- public/app/plugins/panel/news/models.gen.ts | 2 + scripts/drone/steps/lib.star | 15 +-- 6 files changed, 97 insertions(+), 101 deletions(-) diff --git a/.drone.yml b/.drone.yml index 21d670c58b0..90cdb8819c8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -159,22 +159,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -686,22 +678,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -1290,22 +1274,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -1880,22 +1856,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -3044,22 +3012,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -3561,22 +3521,14 @@ steps: image: grafana/build-container:1.5.1 name: validate-scuemata - commands: - - '# Make sure the git tree is clean.' - - '# Stashing changes, since packages that were produced in build-backend step are - needed.' - - git stash - - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . - - '# The above command generates Typescript files (*.gen.ts) from all appropriate - .cue files.' - '# It is required that the generated Typescript be in sync with the input CUE files.' - - '# ...Modulo eslint auto-fixes...:' - - yarn run eslint . --ext .gen.ts --fix - - '# If any filenames are emitted by the below script, run the generator command - `grafana-cli cue gen-ts` locally and commit the result.' - - ./scripts/clean-git-or-error.sh - - '# Un-stash changes.' - - git stash pop + - '# To enforce this, the following command will attempt to generate Typescript + from all' + - '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file + the generated' + - '# code would have been written to. It exits 1 if any diffs are found.' + - ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff depends_on: - validate-scuemata image: grafana/build-container:1.5.1 @@ -4305,6 +4257,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: b92ebbf48ca675f25c96f4b182a72c08b0e79f9c50d29caaacab124e97f32b4d +hmac: fb2a26bf088c9ff2b7cc63cee4fa1f1da06baf560265df4703414f4db9c90708 ... diff --git a/.eslintignore b/.eslintignore index 1fc48c822a9..30d54f55eba 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,9 @@ scripts/grafana-server/tmp public/lib/monaco deployment_tools_config.json +# TS generate from cue by cuetsy +**/*.gen.ts + # Auto-generated localisation files public/locales/_build/ public/locales/**/*.js diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 809dd35adf3..82bcb8ffac3 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -257,6 +257,11 @@ so must be recompiled to validate newly-added CUE files.`, Name: "grafana-root", Usage: "path to the root of a Grafana repository in which to generate TypeScript from CUE files", }, + &cli.BoolFlag{ + Name: "diff", + Usage: "diff results of codegen against files already on disk. Exits 1 if diff is non-empty", + Value: false, + }, }, }, } diff --git a/pkg/cmd/grafana-cli/commands/cuetsify_command.go b/pkg/cmd/grafana-cli/commands/cuetsify_command.go index d71335f2431..8ce9853bb68 100644 --- a/pkg/cmd/grafana-cli/commands/cuetsify_command.go +++ b/pkg/cmd/grafana-cli/commands/cuetsify_command.go @@ -17,6 +17,7 @@ import ( "cuelang.org/go/cue/errors" cload "cuelang.org/go/cue/load" "cuelang.org/go/cue/parser" + "github.com/google/go-cmp/cmp" "github.com/grafana/cuetsy" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/schema/load" @@ -63,6 +64,7 @@ var skipPaths = []string{ const prefix = "/" +//nolint: gocyclo func (cmd Command) generateTypescript(c utils.CommandLine) error { root := c.String("grafana-root") if root == "" { @@ -237,13 +239,46 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { return gerrors.New(errors.Details(err, nil)) } + diff := c.Bool("diff") + var derr bool for of, b := range outfiles { - err := os.WriteFile(filepath.Join(root, of), b, 0644) - if err != nil { - return err + p := filepath.Join(root, of) + if diff { + if _, err := os.Stat(p); err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Printf("%s: no generated code file to compare against\n", p) + derr = true + continue + } + return fmt.Errorf("%s: %w", p, err) + } + + f, err := os.Open(filepath.Clean(p)) + if err != nil { + return fmt.Errorf("%s: %w", p, err) + } + + ob, err := io.ReadAll(f) + if err != nil { + return err + } + dstr := cmp.Diff(string(ob), string(b)) + if dstr != "" { + derr = true + fmt.Printf("%s would have changed:\n%s\n", p, dstr) + } + } else { + err := os.WriteFile(p, b, 0644) + if err != nil { + return err + } } } + if derr { + return errors.New("some files changed") + } + return nil } @@ -283,7 +318,7 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error if !filepath.IsAbs(prefix) { return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix) } - err := fs.WalkDir(vfs, ".", (func(path string, d fs.DirEntry, err error) error { + err := fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -296,6 +331,12 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error if err != nil { return err } + defer func(f fs.File) { + err := f.Close() + if err != nil { + return + } + }(f) b, err := io.ReadAll(f) if err != nil { @@ -304,7 +345,7 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error overlay[filepath.Join(prefix, path)] = cload.FromBytes(b) return nil - })) + }) if err != nil { return err diff --git a/public/app/plugins/panel/news/models.gen.ts b/public/app/plugins/panel/news/models.gen.ts index 82db65040ac..61a31557145 100644 --- a/public/app/plugins/panel/news/models.gen.ts +++ b/public/app/plugins/panel/news/models.gen.ts @@ -2,8 +2,10 @@ // This file was autogenerated by cuetsy. DO NOT EDIT! //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + export const modelVersion = Object.freeze([0, 0]); + export interface PanelOptions { feedUrl?: string; showImage?: boolean; diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 837a27d9253..5ca6fb1ae63 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1134,18 +1134,11 @@ def ensure_cuetsified_step(): 'validate-scuemata', ], 'commands': [ - '# Make sure the git tree is clean.', - '# Stashing changes, since packages that were produced in build-backend step are needed.', - 'git stash', - './bin/linux-amd64/grafana-cli cue gen-ts --grafana-root .', - '# The above command generates Typescript files (*.gen.ts) from all appropriate .cue files.', '# It is required that the generated Typescript be in sync with the input CUE files.', - '# ...Modulo eslint auto-fixes...:', - 'yarn run eslint . --ext .gen.ts --fix', - '# If any filenames are emitted by the below script, run the generator command `grafana-cli cue gen-ts` locally and commit the result.', - './scripts/clean-git-or-error.sh', - '# Un-stash changes.', - 'git stash pop', + '# To enforce this, the following command will attempt to generate Typescript from all', + '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file the generated', + '# code would have been written to. It exits 1 if any diffs are found.', + './bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff', ], } From 85af6d271848ee7af8f6deeb9a2fce59a40fab2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 24 Feb 2022 12:06:53 +0100 Subject: [PATCH 012/125] renovate: do not update monaco-editor (#45762) * renovate: do not update monaco-editor * renovate: alphabetically sort the entries --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ef70d6fa713..c878865fa93 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -17,6 +17,7 @@ "d3-scale-chromatic", // we should bump this once we move to esm modules "execa", // we should bump this once we move to esm modules "history", // we should bump this together with react-router-dom + "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins "react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins "react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead "react-router-dom", // we should bump this together with history From 0a572cae4ba1b1ab225f86bcc8159bffdbd7d3f8 Mon Sep 17 00:00:00 2001 From: Tharun Rajendran Date: Thu, 24 Feb 2022 17:15:51 +0530 Subject: [PATCH 013/125] Explore: fix object value parsing for downloading traces as csv (#44492) * Explore: fix object value parsing for downloading traces as csv Signed-off-by: tharun * add replacer function to fix circular objects Signed-off-by: tharun --- .../src/field/displayProcessor.test.ts | 2 +- .../grafana-data/src/field/displayProcessor.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/field/displayProcessor.test.ts b/packages/grafana-data/src/field/displayProcessor.test.ts index 3bae9138192..7cb49137e7a 100644 --- a/packages/grafana-data/src/field/displayProcessor.test.ts +++ b/packages/grafana-data/src/field/displayProcessor.test.ts @@ -460,7 +460,7 @@ describe('getRawDisplayProcessor', () => { ${'a string'} | ${'a string'} ${null} | ${'null'} ${undefined} | ${'undefined'} - ${{ value: 0, label: 'a label' }} | ${'[object Object]'} + ${{ value: 0, label: 'a label' }} | ${'{"value":0,"label":"a label"}'} `('when called with value:{$value}', ({ value, expected }) => { const result = processor(value); diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index 7e09960c4a7..a8bab9f7ac6 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -11,6 +11,7 @@ import { KeyValue, TimeZone } from '../types'; import { getScaleCalculator } from './scale'; import { GrafanaTheme2 } from '../themes/types'; import { anyToNumber } from '../utils/anyToNumber'; +import { getFieldTypeFromValue } from '../dataframe/processDataFrame'; interface DisplayProcessorOptions { field: Partial; @@ -168,7 +169,20 @@ function toStringProcessor(value: any): DisplayValue { export function getRawDisplayProcessor(): DisplayProcessor { return (value: any) => ({ - text: `${value}`, + text: getFieldTypeFromValue(value) === 'other' ? `${JSON.stringify(value, getCircularReplacer())}` : `${value}`, numeric: null as unknown as number, }); } + +const getCircularReplacer = () => { + const seen = new WeakSet(); + return (_key: any, value: object | null) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; From ba469be3226c6e8535cca5af5dc394d72582c51f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 12:50:34 +0100 Subject: [PATCH 014/125] Update dependency @babel/plugin-transform-react-constant-elements to v7.17.6 (#45730) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9e53a1b81cf..f9d50ba8f6d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@babel/plugin-proposal-object-rest-spread": "7.17.3", "@babel/plugin-proposal-optional-chaining": "7.16.7", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/plugin-transform-react-constant-elements": "7.16.7", + "@babel/plugin-transform-react-constant-elements": "7.17.6", "@babel/plugin-transform-runtime": "7.17.0", "@babel/plugin-transform-typescript": "7.16.8", "@babel/preset-env": "7.16.11", diff --git a/yarn.lock b/yarn.lock index 0b8757376c3..4aa34f92e80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2364,14 +2364,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-constant-elements@npm:7.16.7": - version: 7.16.7 - resolution: "@babel/plugin-transform-react-constant-elements@npm:7.16.7" +"@babel/plugin-transform-react-constant-elements@npm:7.17.6": + version: 7.17.6 + resolution: "@babel/plugin-transform-react-constant-elements@npm:7.17.6" dependencies: "@babel/helper-plugin-utils": ^7.16.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b2c586deba5ca86ebb4e26d01a26d056e48fd6f64760676a4b6a0be6f81b8afdd91189e976b57ba18ff966971f92f5ce97fd08798c9bf1a687e6e71bf4198b87 + checksum: 4c111328511c4dcb8d1d7970d7cccff463c0c34dfe3380f5bb80b692fa191b441fa8c6ef4229c5140dc0b8c69cc3f9aeb6dc05455c4367be93e784658ce11cc5 languageName: node linkType: hard @@ -20635,7 +20635,7 @@ __metadata: "@babel/plugin-proposal-object-rest-spread": 7.17.3 "@babel/plugin-proposal-optional-chaining": 7.16.7 "@babel/plugin-syntax-dynamic-import": 7.8.3 - "@babel/plugin-transform-react-constant-elements": 7.16.7 + "@babel/plugin-transform-react-constant-elements": 7.17.6 "@babel/plugin-transform-runtime": 7.17.0 "@babel/plugin-transform-typescript": 7.16.8 "@babel/preset-env": 7.16.11 From 0e7b0f16b8fa14055fdfcc4cc07c945da3b4f7a0 Mon Sep 17 00:00:00 2001 From: Yaelle Chaudy <42030685+yaelleC@users.noreply.github.com> Date: Thu, 24 Feb 2022 14:00:24 +0100 Subject: [PATCH 015/125] Adding ap-southeast-3 to cloudwatch regions (#45821) --- pkg/tsdb/cloudwatch/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metrics.go b/pkg/tsdb/cloudwatch/metrics.go index 6234f013913..cf2779ab8e7 100644 --- a/pkg/tsdb/cloudwatch/metrics.go +++ b/pkg/tsdb/cloudwatch/metrics.go @@ -514,7 +514,7 @@ var dimensionsMap = map[string][]string{ // Known AWS regions. var knownRegions = []string{ "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", - "ap-southeast-2", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1", + "ap-southeast-2", "ap-southeast-3", "ca-central-1", "cn-north-1", "cn-northwest-1", "eu-central-1", "eu-north-1", "eu-south-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-gov-east-1", "us-gov-west-1", "us-iso-east-1", "us-isob-east-1", "us-west-1", "us-west-2", } From 64ad33f31a94b2aca9e705d3f2b6c6c0f6885d0e Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 24 Feb 2022 13:58:56 +0000 Subject: [PATCH 016/125] Add a fallback for the clipboard API (#45831) --- .../ClipboardButton/ClipboardButton.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index d979d8b6aa2..63c1aba06c9 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -21,24 +21,46 @@ export interface Props extends ButtonProps { const dummyClearFunc = () => {}; export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) { - // Can be removed in 9.x const buttonRef = useRef(null); - const copyText = useCallback(() => { - const copiedText = getText(); + const copyTextCallback = useCallback(async () => { + const textToCopy = getText(); + // Can be removed in 9.x const dummyEvent: ClipboardEvent = { action: 'copy', clearSelection: dummyClearFunc, - text: copiedText, + text: textToCopy, trigger: buttonRef.current!, }; - navigator.clipboard - .writeText(copiedText) - .then(() => (onClipboardCopy?.(dummyEvent), () => onClipboardError?.(dummyEvent))); + try { + await copyText(textToCopy, buttonRef); + onClipboardCopy?.(dummyEvent); + } catch { + onClipboardError?.(dummyEvent); + } }, [getText, onClipboardCopy, onClipboardError]); return ( - ); } + +const copyText = async (text: string, buttonRef: React.MutableRefObject) => { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } else { + // Use a fallback method for browsers/contexts that don't support the Clipboard API. + // See https://web.dev/async-clipboard/#feature-detection. + const input = document.createElement('input'); + // Normally we'd append this to the body. However if we're inside a focus manager + // from react-aria, we can't focus anything outside of the managed area. + // Instead, let's append it to the button. Then we're guaranteed to be able to focus + copy. + buttonRef.current?.appendChild(input); + input.value = text; + input.focus(); + input.select(); + document.execCommand('copy'); + input.remove(); + } +}; From b7a2fda2aeb495a92a24df70df00df9adada7d0b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 24 Feb 2022 15:29:18 +0100 Subject: [PATCH 017/125] Azure Monitor: Fixes broken log queries that use workspace (#45820) * allow log queries to be executed also without a resource * add unit tests --- .../azure_log_analytics_datasource.test.ts | 27 ++++++++++---- .../azure_log_analytics_datasource.ts | 35 +++++++++++-------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 080cee7d06a..d7031081f88 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -1,11 +1,12 @@ -import AzureMonitorDatasource from '../datasource'; -import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; -import FakeSchemaData from './__mocks__/schema'; -import { TemplateSrv } from 'app/features/templating/template_srv'; -import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types'; import { toUtc } from '@grafana/data'; +import { TemplateSrv } from 'app/features/templating/template_srv'; + import createMockQuery from '../__mocks__/query'; import { singleVariable } from '../__mocks__/variables'; +import AzureMonitorDatasource from '../datasource'; +import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types'; +import FakeSchemaData from './__mocks__/schema'; +import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; const templateSrv = new TemplateSrv(); @@ -273,7 +274,7 @@ describe('AzureLogAnalyticsDatasource', () => { laDatasource = new AzureLogAnalyticsDatasource(ctx.instanceSettings); }); - it('should run complete queries', () => { + it('should run queries with a resource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { @@ -285,6 +286,18 @@ describe('AzureLogAnalyticsDatasource', () => { expect(laDatasource.filterQuery(query)).toBeTruthy(); }); + it('should run queries with a workspace', () => { + const query: AzureMonitorQuery = { + refId: 'A', + azureLogAnalytics: { + query: 'perf | take 100', + workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67', + }, + }; + + expect(laDatasource.filterQuery(query)).toBeTruthy(); + }); + it('should not run empty queries', () => { const query: AzureMonitorQuery = { refId: 'A', @@ -317,7 +330,7 @@ describe('AzureLogAnalyticsDatasource', () => { expect(laDatasource.filterQuery(query)).toBeFalsy(); }); - it('should not run queries missing a resource', () => { + it('should not run queries missing a resource and a missing workspace', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 1bedebfc2b7..b4458f8d640 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,26 +1,27 @@ -import { map } from 'lodash'; -import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder'; -import ResponseParser, { transformMetadataToKustoSchema } from './response_parser'; -import { - AzureMonitorQuery, - AzureDataSourceJsonData, - AzureLogsVariable, - AzureQueryType, - DatasourceValidationResult, -} from '../types'; import { DataQueryRequest, DataQueryResponse, - ScopedVars, DataSourceInstanceSettings, DataSourceRef, + ScopedVars, } from '@grafana/data'; -import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; -import { Observable, from } from 'rxjs'; +import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; +import { map } from 'lodash'; +import { from, Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials'; + import { isGUIDish } from '../components/ResourcePicker/utils'; +import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials'; +import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder'; +import { + AzureDataSourceJsonData, + AzureLogsVariable, + AzureMonitorQuery, + AzureQueryType, + DatasourceValidationResult, +} from '../types'; import { interpolateVariable, routeNames } from '../utils/common'; +import ResponseParser, { transformMetadataToKustoSchema } from './response_parser'; interface AdhocQuery { datasource: DataSourceRef; @@ -60,7 +61,11 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< } filterQuery(item: AzureMonitorQuery): boolean { - return item.hide !== true && !!item.azureLogAnalytics?.query && !!item.azureLogAnalytics.resource; + return ( + item.hide !== true && + !!item.azureLogAnalytics?.query && + (!!item.azureLogAnalytics.resource || !!item.azureLogAnalytics.workspace) + ); } async getSubscriptions(): Promise> { From 14decdb58cb70e4eb94efd60ea9bdb62004c5753 Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Thu, 24 Feb 2022 09:39:31 -0500 Subject: [PATCH 018/125] Chore: Remove bus from plugin context service (#45633) * Chore: Remove bus from plugin context * fix the plugincontext Co-authored-by: Ying WANG --- pkg/plugins/plugincontext/plugincontext.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/plugins/plugincontext/plugincontext.go index 7dc16d586fe..cd540888a4d 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/plugins/plugincontext/plugincontext.go @@ -114,7 +114,7 @@ func (p *Provider) getCachedPluginSettings(ctx context.Context, pluginID string, } query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: user.OrgId} - if err := p.Bus.Dispatch(ctx, &query); err != nil { + if err := p.PluginSettingsService.GetPluginSettingById(ctx, &query); err != nil { return nil, err } From 9e452c7166d9121d42616f5dd74d07def08ea81c Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 24 Feb 2022 16:48:18 +0100 Subject: [PATCH 019/125] Devenv: Update Elasticsearch v7 image so it is usable on ARM (M1) (#45612) * Devenv: Update Elasticsearch v7 images so they are usable on ARM (M1) * Remove changes in 77 * Update data sources --- devenv/datasources.yaml | 8 ++++---- devenv/docker/blocks/elastic7/docker-compose.yaml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index 94b419e3cca..2bf076b4ec4 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -195,7 +195,7 @@ datasources: timeInterval: 10s interval: Daily timeField: "@timestamp" - esVersion: 70 + esVersion: 7.10.0 - name: gdev-elasticsearch-v7-logs type: elasticsearch @@ -205,7 +205,7 @@ datasources: jsonData: interval: Daily timeField: "@timestamp" - esVersion: 70 + esVersion: 7.10.0 - name: gdev-elasticsearch-v7-filebeat type: elasticsearch @@ -215,7 +215,7 @@ datasources: jsonData: interval: Daily timeField: "@timestamp" - esVersion: 70 + esVersion: 7.10.0 timeInterval: "10s" logMessageField: message logLevelField: fields.level @@ -228,7 +228,7 @@ datasources: jsonData: interval: Daily timeField: "@timestamp" - esVersion: 70 + esVersion: 7.10.0 timeInterval: "10s" - name: gdev-mysql diff --git a/devenv/docker/blocks/elastic7/docker-compose.yaml b/devenv/docker/blocks/elastic7/docker-compose.yaml index d4d981c6b55..b4e144c2b5b 100644 --- a/devenv/docker/blocks/elastic7/docker-compose.yaml +++ b/devenv/docker/blocks/elastic7/docker-compose.yaml @@ -1,7 +1,7 @@ # You need to run 'sysctl -w vm.max_map_count=262144' on the host machine elasticsearch7: - image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0 + image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 command: elasticsearch -E "discovery.type=single-node" ports: - "12200:9200" @@ -17,7 +17,7 @@ FD_PORT: 9200 filebeat7: - image: docker.elastic.co/beats/filebeat-oss:7.0.0 + image: docker.elastic.co/beats/filebeat-oss:7.17.0 command: filebeat -e -strict.perms=false volumes: - ./docker/blocks/elastic7/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro @@ -25,7 +25,7 @@ - ../data/log:/var/log/grafana:ro metricbeat7: - image: docker.elastic.co/beats/metricbeat-oss:7.0.0 + image: docker.elastic.co/beats/metricbeat-oss:7.17.0 command: metricbeat -e -strict.perms=false user: root volumes: From 9a87755c3ede4bb70ee8412e10acedf321c752e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 24 Feb 2022 16:59:48 +0100 Subject: [PATCH 020/125] renovate: do not update the "commander" package (#45839) --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c878865fa93..7b7616a4068 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -11,6 +11,7 @@ "@types/d3-scale-chromatic", // we should bump this once we move to esm modules "@types/grafana__slate-react", // should be updated when the `slate` package is updated "@types/react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead + "commander", // we are planning to remove this, so no need to update it "d3", "d3-force", // we should bump this once we move to esm modules "d3-interpolate", // we should bump this once we move to esm modules From 9e32357e69017dc89193474b4ebe72e0334d501f Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Thu, 24 Feb 2022 20:17:05 +0400 Subject: [PATCH 021/125] Previews: increase crawler timeout (#45848) --- pkg/services/thumbs/crawler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/thumbs/crawler.go b/pkg/services/thumbs/crawler.go index a17b48c4ae0..38a106d9b9b 100644 --- a/pkg/services/thumbs/crawler.go +++ b/pkg/services/thumbs/crawler.go @@ -120,7 +120,7 @@ func (r *simpleCrawler) Run(ctx context.Context, authOpts rendering.AuthOpts, mo r.opts = rendering.Opts{ AuthOpts: authOpts, TimeoutOpts: rendering.TimeoutOpts{ - Timeout: 10 * time.Second, + Timeout: 20 * time.Second, RequestTimeoutMultiplier: 3, }, Theme: theme, From feae959c9da12885fc79786d57d7c415595c260a Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 24 Feb 2022 16:25:28 +0000 Subject: [PATCH 022/125] Alerting: Create annotation if Firing alert is removed (#45703) This commit changes staleResultsHandler to create an annotation if the current state is Alerting and the result is being removed from the state cache as it has not been updated since 2x the evaluation interval. --- pkg/services/ngalert/state/manager.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 1bf0119e63a..edc26ce2399 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -189,7 +189,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu st.set(currentState) if oldState != currentState.State { - go st.createAlertAnnotation(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) + go st.annotateState(ctx, alertRule, currentState.Labels, result.EvaluatedAt, currentState.State, oldState) } return currentState } @@ -237,7 +237,7 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State { } } -func (st *Manager) createAlertAnnotation(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { +func (st *Manager) annotateState(ctx context.Context, alertRule *ngModels.AlertRule, labels data.Labels, evaluatedAt time.Time, state eval.State, previousState eval.State) { st.log.Debug("alert state changed creating annotation", "alertRuleUID", alertRule.UID, "newState", state.String(), "oldState", previousState.String()) labels = removePrivateLabels(labels) @@ -300,6 +300,10 @@ func (st *Manager) staleResultsHandler(ctx context.Context, alertRule *ngModels. if err = st.instanceStore.DeleteAlertInstance(ctx, s.OrgID, s.AlertRuleUID, labelsHash); err != nil { st.log.Error("unable to delete stale instance from database", "error", err.Error(), "orgID", s.OrgID, "alertRuleUID", s.AlertRuleUID, "cacheID", s.CacheId) } + + if s.State == eval.Alerting { + st.annotateState(ctx, alertRule, s.Labels, time.Now(), eval.Normal, s.State) + } } } } From dcd98f7819f3af14ef5233e484ffbc10e2b0364c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Thu, 24 Feb 2022 17:37:49 +0100 Subject: [PATCH 023/125] Escape windows newline. (#45771) Fixes #45746 --- packages/grafana-data/src/utils/logs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/utils/logs.ts b/packages/grafana-data/src/utils/logs.ts index 96af4c4a602..a95d14fc42e 100644 --- a/packages/grafana-data/src/utils/logs.ts +++ b/packages/grafana-data/src/utils/logs.ts @@ -229,4 +229,4 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM }; export const escapeUnescapedString = (string: string) => - string.replace(/\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); + string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); From 91af956eb72b52dbe529f93468fb89e8ffa3d844 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:56:37 -0600 Subject: [PATCH 024/125] ReleaseNotes: Updated changelog and release notes for 8.4.2 (#45850) --- CHANGELOG.md | 14 ++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes/release-notes-8-4-2.md | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-4-2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bd5c155a3..046201ce80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 8.4.2 (2022-02-23) + +### Features and enhancements + +- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90) +- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost) + + # 8.4.1 (2022-02-18) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index cb08b39c58e..04b066d9b8c 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.2]({{< relref "release-notes-8-4-2" >}}) - [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}}) - [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) - [Release notes for 8.3.6]({{< relref "release-notes-8-3-6" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-2.md b/docs/sources/release-notes/release-notes-8-4-2.md new file mode 100644 index 00000000000..760462b592f --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-2.md @@ -0,0 +1,17 @@ ++++ +title = "Release notes for Grafana 8.4.2" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.2 + +### Features and enhancements + +- **OAuth:** Add setting to skip org assignment for external users. [#34834](https://github.com/grafana/grafana/pull/34834), [@baez90](https://github.com/baez90) +- **Tracing:** Add option to map tag names to log label names in trace to logs settings. [#45178](https://github.com/grafana/grafana/pull/45178), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Explore:** Fix closing split pane when logs panel is used. [#45602](https://github.com/grafana/grafana/pull/45602), [@ifrost](https://github.com/ifrost) From 359ef074fabfc65ab7647f6310e8b117de6d903b Mon Sep 17 00:00:00 2001 From: Borja Garrido Date: Thu, 24 Feb 2022 18:01:04 +0100 Subject: [PATCH 025/125] Transformations: Add new grouping to matrix transformer (#28739) * Add new transformer grouping to matrix * Add new transformer grouping to matrix tests * Add new transformer grouping to matrix UI * Fix tests for grouping to matrix transformer * Update transformer to latest interfaces * Add field selector to form * Make linter happier * Replace Fields with InlineSnapshot as it was to taking units properly * Rearrange for new transformers structure * Expose GroupingToMatrix options as part of data package * Increase labelWidth as suggested * Add uniqueValues helper function and use it to extract Column and Row Values --- packages/grafana-data/src/index.ts | 1 + .../src/transformations/transformers.ts | 2 + .../transformers/groupingToMatrix.test.ts | 170 ++++++++++++++++++ .../transformers/groupingToMatrix.ts | 115 ++++++++++++ .../src/transformations/transformers/ids.ts | 1 + .../GroupingToMatrixTransformerEditor.tsx | 85 +++++++++ .../transformers/standardTransformers.ts | 2 + 7 files changed, 376 insertions(+) create mode 100644 packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts create mode 100644 packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts create mode 100644 public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 46477608f9b..2cefae622b7 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -25,6 +25,7 @@ export { LayoutModes, LayoutMode } from './types/layout'; export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin'; export { createFieldConfigRegistry } from './panel/registryFactories'; export { QueryRunner, QueryRunnerOptions } from './types/queryRunner'; +export { GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix'; // Moved to `@grafana/schema`, in Grafana 9, this will be removed export * from './schema'; diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index 835be0d0228..063e9f8ddf4 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -19,6 +19,7 @@ import { renameByRegexTransformer } from './transformers/renameByRegex'; import { filterByValueTransformer } from './transformers/filterByValue'; import { histogramTransformer } from './transformers/histogram'; import { convertFieldTypeTransformer } from './transformers/convertFieldType'; +import { groupingToMatrixTransformer } from './transformers/groupingToMatrix'; export const standardTransformers = { noopTransformer, @@ -43,4 +44,5 @@ export const standardTransformers = { renameByRegexTransformer, histogramTransformer, convertFieldTypeTransformer, + groupingToMatrixTransformer, }; diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts new file mode 100644 index 00000000000..103cf335a67 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts @@ -0,0 +1,170 @@ +import { + ArrayVector, + DataTransformerConfig, + DataTransformerID, + Field, + FieldType, + toDataFrame, + transformDataFrame, +} from '@grafana/data'; +import { GroupingToMatrixTransformerOptions, groupingToMatrixTransformer } from './groupingToMatrix'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; + +describe('Grouping to Matrix', () => { + beforeAll(() => { + mockTransformationsRegistry([groupingToMatrixTransformer]); + }); + + it('generates Matrix with default fields', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.groupingToMatrix, + options: {}, + }; + + const seriesA = toDataFrame({ + name: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1000, 1001, 1002] }, + { name: 'Value', type: FieldType.number, values: [1, 2, 3] }, + ], + }); + + await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => { + const processed = received[0]; + const expected: Field[] = [ + { + name: 'Time\\Time', + type: FieldType.string, + values: new ArrayVector([1000, 1001, 1002]), + config: {}, + }, + { + name: '1000', + type: FieldType.number, + values: new ArrayVector([1, '', '']), + config: {}, + }, + { + name: '1001', + type: FieldType.number, + values: new ArrayVector(['', 2, '']), + config: {}, + }, + { + name: '1002', + type: FieldType.number, + values: new ArrayVector(['', '', 3]), + config: {}, + }, + ]; + + expect(processed[0].fields).toEqual(expected); + }); + }); + + it('generates Matrix with multiple fields', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.groupingToMatrix, + options: { + columnField: 'Column', + rowField: 'Row', + valueField: 'Temp', + }, + }; + + const seriesA = toDataFrame({ + name: 'A', + fields: [ + { name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] }, + { name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] }, + { name: 'Temp', type: FieldType.number, values: [1, 4, 5] }, + ], + }); + + await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => { + const processed = received[0]; + const expected: Field[] = [ + { + name: 'Row\\Column', + type: FieldType.string, + values: new ArrayVector(['R1', 'R2']), + config: {}, + }, + { + name: 'C1', + type: FieldType.number, + values: new ArrayVector([1, 4]), + config: {}, + }, + { + name: 'C2', + type: FieldType.number, + values: new ArrayVector([5, '']), + config: {}, + }, + ]; + + expect(processed[0].fields).toEqual(expected); + }); + }); + + it('generates Matrix with multiple fields and value type', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.groupingToMatrix, + options: { + columnField: 'Column', + rowField: 'Row', + valueField: 'Temp', + }, + }; + + const seriesA = toDataFrame({ + name: 'C', + fields: [ + { name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] }, + { name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] }, + { name: 'Temp', type: FieldType.number, values: [1, 4, 5], config: { units: 'celsius' } }, + ], + }); + + await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => { + const processed = received[0]; + + expect(processed[0].fields).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object {}, + "name": "Row\\\\Column", + "type": "string", + "values": Array [ + "R1", + "R2", + ], + }, + Object { + "config": Object { + "units": "celsius", + }, + "name": "C1", + "type": "number", + "values": Array [ + 1, + 4, + ], + }, + Object { + "config": Object { + "units": "celsius", + }, + "name": "C2", + "type": "number", + "values": Array [ + 5, + "", + ], + }, + ] + `); + }); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts new file mode 100644 index 00000000000..4d8a8729ff7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -0,0 +1,115 @@ +import { map } from 'rxjs/operators'; + +import { DataFrame, DataTransformerInfo, Field, FieldType, Vector } from '../../types'; +import { DataTransformerID } from './ids'; +import { MutableDataFrame } from '../../dataframe'; +import { getFieldDisplayName } from '../../field/fieldState'; + +export interface GroupingToMatrixTransformerOptions { + columnField?: string; + rowField?: string; + valueField?: string; +} + +const DEFAULT_COLUMN_FIELD = 'Time'; +const DEFAULT_ROW_FIELD = 'Time'; +const DEFAULT_VALUE_FIELD = 'Value'; + +export const groupingToMatrixTransformer: DataTransformerInfo = { + id: DataTransformerID.groupingToMatrix, + name: 'Grouping to Matrix', + description: 'Groups series by field and return a matrix visualisation', + defaultOptions: { + columnField: DEFAULT_COLUMN_FIELD, + rowField: DEFAULT_ROW_FIELD, + valueField: DEFAULT_VALUE_FIELD, + }, + + operator: (options) => (source) => + source.pipe( + map((data) => { + const columnFieldMatch = options.columnField || DEFAULT_COLUMN_FIELD; + const rowFieldMatch = options.rowField || DEFAULT_ROW_FIELD; + const valueFieldMatch = options.valueField || DEFAULT_VALUE_FIELD; + + // Accept only single queries + if (data.length !== 1) { + return data; + } + + const frame = data[0]; + const keyColumnField = findKeyField(frame, columnFieldMatch); + const keyRowField = findKeyField(frame, rowFieldMatch); + const valueField = findKeyField(frame, valueFieldMatch); + const rowColumnField = `${rowFieldMatch}\\${columnFieldMatch}`; + + if (!keyColumnField || !keyRowField || !valueField) { + return data; + } + + const columnValues = uniqueValues(keyColumnField.values); + const rowValues = uniqueValues(keyRowField.values); + + const matrixValues: { [key: string]: { [key: string]: any } } = {}; + + for (let index = 0; index < valueField.values.length; index++) { + const columnName = keyColumnField.values.get(index); + const rowName = keyRowField.values.get(index); + const value = valueField.values.get(index); + + if (!matrixValues[columnName]) { + matrixValues[columnName] = {}; + } + + matrixValues[columnName][rowName] = value; + } + + const resultFrame = new MutableDataFrame(); + + resultFrame.addField({ + name: rowColumnField, + values: rowValues, + type: FieldType.string, + }); + + for (const columnName of columnValues) { + let values = []; + for (const rowName of rowValues) { + const value = matrixValues[columnName][rowName] ?? ''; + values.push(value); + } + + resultFrame.addField({ + name: columnName.toString(), + values: values, + config: valueField.config, + type: valueField.type, + }); + } + + return [resultFrame]; + }) + ), +}; + +function uniqueValues(values: Vector): any[] { + const unique = new Set(); + + for (let index = 0; index < values.length; index++) { + unique.add(values.get(index)); + } + + return Array.from(unique); +} + +function findKeyField(frame: DataFrame, matchTitle: string): Field | null { + for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) { + const field = frame.fields[fieldIndex]; + + if (matchTitle === getFieldDisplayName(field)) { + return field; + } + } + + return null; +} diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index e8254b1949a..1032e9c7d68 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -31,4 +31,5 @@ export enum DataTransformerID { heatmap = 'heatmap', spatial = 'spatial', extractFields = 'extractFields', + groupingToMatrix = 'groupingToMatrix', } diff --git a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx new file mode 100644 index 00000000000..32cc311f61b --- /dev/null +++ b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; +import { + DataTransformerID, + SelectableValue, + standardTransformers, + TransformerRegistryItem, + TransformerUIProps, + GroupingToMatrixTransformerOptions, +} from '@grafana/data'; +import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; +import { useAllFieldNamesFromDataFrames } from '../utils'; + +export const GroupingToMatrixTransformerEditor: React.FC> = ({ + input, + options, + onChange, +}) => { + const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item })); + + const onSelectColumn = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + columnField: value.value, + }); + }, + [onChange, options] + ); + + const onSelectRow = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + rowField: value.value, + }); + }, + [onChange, options] + ); + + const onSelectValue = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + valueField: value.value, + }); + }, + [onChange, options] + ); + + return ( + <> + + + + + + Date: Fri, 25 Feb 2022 11:18:08 +0100 Subject: [PATCH 034/125] Prometheus: Remove the auto range vector option (#45715) * Prometheus: Remove auto range option * Prometheus: Remove auto range option * Overhaul of range vector operations and default param * Make sure label is string --- .../loki/querybuilder/operations.ts | 15 +-- .../querybuilder/PromQueryModeller.test.ts | 14 +- .../prometheus/querybuilder/aggregations.ts | 21 +-- .../prometheus/querybuilder/operations.ts | 125 +++++++++--------- .../querybuilder/shared/OperationName.tsx | 7 +- .../shared/OperationParamEditor.tsx | 18 ++- .../querybuilder/shared/operationUtils.ts | 43 +++++- .../prometheus/querybuilder/shared/types.ts | 1 + 8 files changed, 132 insertions(+), 112 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index 06e6c7cd499..8efb0c4868a 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -134,7 +134,7 @@ function createRangeOperation(name: string): QueryBuilderOperationDef { id: name, name: getPromAndLokiOperationDisplayName(name), params: [getRangeVectorParamDef()], - defaultParams: ['auto'], + defaultParams: ['$__interval'], alternativesKey: 'range function', category: LokiVisualQueryOperationCategory.RangeFunctions, renderer: operationWithRangeVectorRenderer, @@ -142,7 +142,7 @@ function createRangeOperation(name: string): QueryBuilderOperationDef { explainHandler: (op, def) => { let opDocs = FUNCTIONS.find((x) => x.insertText === op.id)?.documentation ?? ''; - if (op.params[0] === 'auto' || op.params[0] === '$__interval') { + if (op.params[0] === '$__interval') { return `${opDocs} \`$__interval\` is variable that will be replaced with a calculated interval based on **Max data points**, **Min interval** and query time range. You find these options you find under **Query options** at the right of the data source select dropdown.`; } else { return `${opDocs} The [range vector](https://grafana.com/docs/loki/latest/logql/metric_queries/#range-vector-aggregation) is set to \`${op.params[0]}\`.`; @@ -170,9 +170,9 @@ function createAggregationOperation(name: string): QueryBuilderOperationDef { function getRangeVectorParamDef(): QueryBuilderOperationParamDef { return { - name: 'Range vector', + name: 'Range', type: 'string', - options: ['auto', '$__interval', '$__range', '1m', '5m', '10m', '1h', '24h'], + options: ['$__interval', '$__range', '1m', '5m', '10m', '1h', '24h'], }; } @@ -181,12 +181,7 @@ function operationWithRangeVectorRenderer( def: QueryBuilderOperationDef, innerExpr: string ) { - let rangeVector = (model.params ?? [])[0] ?? 'auto'; - - if (rangeVector === 'auto') { - rangeVector = '$__interval'; - } - + let rangeVector = (model.params ?? [])[0] ?? '$__interval'; return `${def.id}(${innerExpr} [${rangeVector}])`; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts index 1c2e18f8600..cdf8c5e4a50 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts @@ -105,7 +105,7 @@ describe('PromQueryModeller', () => { modeller.renderQuery({ metric: 'metric', labels: [{ label: 'pod', op: '=', value: 'A' }], - operations: [{ id: PromOperationId.Rate, params: ['auto'] }], + operations: [{ id: PromOperationId.Rate, params: ['$__rate_interval'] }], }) ).toBe('rate(metric{pod="A"}[$__rate_interval])'); }); @@ -115,9 +115,9 @@ describe('PromQueryModeller', () => { modeller.renderQuery({ metric: 'metric', labels: [{ label: 'pod', op: '=', value: 'A' }], - operations: [{ id: PromOperationId.Increase, params: ['auto'] }], + operations: [{ id: PromOperationId.Increase, params: ['$__interval'] }], }) - ).toBe('increase(metric{pod="A"}[$__rate_interval])'); + ).toBe('increase(metric{pod="A"}[$__interval])'); }); it('Can render rate with custom range-vector', () => { @@ -283,18 +283,18 @@ describe('PromQueryModeller', () => { modeller.renderQuery({ metric: 'metric_a', labels: [], - operations: [{ id: 'holt_winters', params: ['auto', 0.5, 0.5] }], + operations: [{ id: 'holt_winters', params: ['5m', 0.5, 0.5] }], }) - ).toBe('holt_winters(metric_a[$__rate_interval], 0.5, 0.5)'); + ).toBe('holt_winters(metric_a[5m], 0.5, 0.5)'); }); it('Can render functions that require parameters left of a range', () => { expect( modeller.renderQuery({ metric: 'metric_a', labels: [], - operations: [{ id: 'quantile_over_time', params: ['auto', 1] }], + operations: [{ id: 'quantile_over_time', params: ['5m', 1] }], }) - ).toBe('quantile_over_time(1, metric_a[$__rate_interval])'); + ).toBe('quantile_over_time(1, metric_a[5m])'); }); it('Can render the label_join function', () => { expect( diff --git a/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts b/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts index 4f0efddeeb9..6fc94bbc85e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts @@ -5,6 +5,7 @@ import { defaultAddOperationHandler, functionRendererLeft, getPromAndLokiOperationDisplayName, + getRangeVectorParamDef, } from './shared/operationUtils'; import { QueryBuilderOperation, QueryBuilderOperationDef, QueryBuilderOperationParamDef } from './shared/types'; import { PromVisualQueryOperationCategory, PromOperationId } from './types'; @@ -25,6 +26,7 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] { createAggregationOverTime(PromOperationId.CountOverTime), createAggregationOverTime(PromOperationId.LastOverTime), createAggregationOverTime(PromOperationId.PresentOverTime), + createAggregationOverTime(PromOperationId.AbsentOverTime), createAggregationOverTime(PromOperationId.StddevOverTime), ]; } @@ -175,8 +177,8 @@ function createAggregationOverTime(name: string): QueryBuilderOperationDef { return { id: name, name: getPromAndLokiOperationDisplayName(name), - params: [getAggregationOverTimeRangeVector()], - defaultParams: ['auto'], + params: [getRangeVectorParamDef()], + defaultParams: ['$__interval'], alternativesKey: 'overtime function', category: PromVisualQueryOperationCategory.RangeFunctions, renderer: operationWithRangeVectorRenderer, @@ -184,24 +186,11 @@ function createAggregationOverTime(name: string): QueryBuilderOperationDef { }; } -function getAggregationOverTimeRangeVector(): QueryBuilderOperationParamDef { - return { - name: 'Range vector', - type: 'string', - options: ['auto', '$__interval', '$__range', '1m', '5m', '10m', '1h', '24h'], - }; -} - function operationWithRangeVectorRenderer( model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string ) { - let rangeVector = (model.params ?? [])[0] ?? 'auto'; - - if (rangeVector === 'auto') { - rangeVector = '$__interval'; - } - + let rangeVector = (model.params ?? [])[0] ?? '$__interval'; return `${def.id}(${innerExpr}[${rangeVector}])`; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts index d1f6ecf22cf..59a037e4f22 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts @@ -4,13 +4,13 @@ import { functionRendererLeft, functionRendererRight, getPromAndLokiOperationDisplayName, + getRangeVectorParamDef, rangeRendererLeftWithParams, rangeRendererRightWithParams, } from './shared/operationUtils'; import { QueryBuilderOperation, QueryBuilderOperationDef, - QueryBuilderOperationParamDef, QueryWithOperations, VisualQueryModeller, } from './shared/types'; @@ -51,10 +51,45 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { addOperationHandler: defaultAddOperationHandler, }, createRangeFunction(PromOperationId.Changes), - createRangeFunction(PromOperationId.Rate), + createRangeFunction(PromOperationId.Rate, true), createRangeFunction(PromOperationId.Irate), - createRangeFunction(PromOperationId.Increase), + createRangeFunction(PromOperationId.Increase, true), + createRangeFunction(PromOperationId.Idelta), createRangeFunction(PromOperationId.Delta), + createFunction({ + id: PromOperationId.HoltWinters, + params: [ + getRangeVectorParamDef(), + { name: 'Smoothing Factor', type: 'number' }, + { name: 'Trend Factor', type: 'number' }, + ], + defaultParams: ['$__interval', 0.5, 0.5], + alternativesKey: 'range function', + category: PromVisualQueryOperationCategory.RangeFunctions, + renderer: rangeRendererRightWithParams, + addOperationHandler: addOperationWithRangeVector, + changeTypeHandler: operationTypeChangedHandlerForRangeFunction, + }), + createFunction({ + id: PromOperationId.PredictLinear, + params: [getRangeVectorParamDef(), { name: 'Seconds from now', type: 'number' }], + defaultParams: ['$__interval', 60], + alternativesKey: 'range function', + category: PromVisualQueryOperationCategory.RangeFunctions, + renderer: rangeRendererRightWithParams, + addOperationHandler: addOperationWithRangeVector, + changeTypeHandler: operationTypeChangedHandlerForRangeFunction, + }), + createFunction({ + id: PromOperationId.QuantileOverTime, + params: [getRangeVectorParamDef(), { name: 'Quantile', type: 'number' }], + defaultParams: ['$__interval', 0.5], + alternativesKey: 'overtime function', + category: PromVisualQueryOperationCategory.RangeFunctions, + renderer: rangeRendererLeftWithParams, + addOperationHandler: addOperationWithRangeVector, + changeTypeHandler: operationTypeChangedHandlerForRangeFunction, + }), // Not sure about this one. It could also be a more generic 'Simple math operation' where user specifies // both the operator and the operand in a single input { @@ -85,7 +120,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { addOperationHandler: addNestedQueryHandler, }, createFunction({ id: PromOperationId.Absent }), - createRangeFunction(PromOperationId.AbsentOverTime), createFunction({ id: PromOperationId.Acos, category: PromVisualQueryOperationCategory.Trigonometric, @@ -163,20 +197,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { createFunction({ id: PromOperationId.Exp }), createFunction({ id: PromOperationId.Floor }), createFunction({ id: PromOperationId.Group }), - createFunction({ - id: PromOperationId.HoltWinters, - params: [ - getRangeVectorParamDef(), - { name: 'Smoothing Factor', type: 'number' }, - { name: 'Trend Factor', type: 'number' }, - ], - defaultParams: ['auto', 0.5, 0.5], - alternativesKey: 'range function', - category: PromVisualQueryOperationCategory.RangeFunctions, - renderer: rangeRendererRightWithParams, - }), createFunction({ id: PromOperationId.Hour }), - createRangeFunction(PromOperationId.Idelta), createFunction({ id: PromOperationId.LabelJoin, params: [ @@ -209,28 +230,12 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { id: PromOperationId.Pi, renderer: (model) => `${model.id}()`, }), - createFunction({ - id: PromOperationId.PredictLinear, - params: [getRangeVectorParamDef(), { name: 'Seconds from now', type: 'number' }], - defaultParams: ['auto', 60], - alternativesKey: 'range function', - category: PromVisualQueryOperationCategory.RangeFunctions, - renderer: rangeRendererRightWithParams, - }), createFunction({ id: PromOperationId.Quantile, params: [{ name: 'Value', type: 'number' }], defaultParams: [1], renderer: functionRendererLeft, }), - createFunction({ - id: PromOperationId.QuantileOverTime, - params: [getRangeVectorParamDef(), { name: 'Quantile', type: 'number' }], - defaultParams: ['auto', 0.5], - alternativesKey: 'range function', - category: PromVisualQueryOperationCategory.RangeFunctions, - renderer: rangeRendererLeftWithParams, - }), createFunction({ id: PromOperationId.Rad }), createRangeFunction(PromOperationId.Resets), createFunction({ @@ -288,30 +293,40 @@ export function createFunction(definition: Partial): Q }; } -export function createRangeFunction(name: string): QueryBuilderOperationDef { +export function createRangeFunction(name: string, withRateInterval = false): QueryBuilderOperationDef { return { id: name, name: getPromAndLokiOperationDisplayName(name), - params: [getRangeVectorParamDef()], - defaultParams: ['auto'], + params: [getRangeVectorParamDef(withRateInterval)], + defaultParams: [withRateInterval ? '$__rate_interval' : '$__interval'], alternativesKey: 'range function', category: PromVisualQueryOperationCategory.RangeFunctions, renderer: operationWithRangeVectorRenderer, addOperationHandler: addOperationWithRangeVector, + changeTypeHandler: operationTypeChangedHandlerForRangeFunction, }; } +function operationTypeChangedHandlerForRangeFunction( + operation: QueryBuilderOperation, + newDef: QueryBuilderOperationDef +) { + // validate current parameter + if (operation.params[0] === '$__rate_interval' && newDef.defaultParams[0] !== '$__rate_interval') { + operation.params = newDef.defaultParams; + } else if (operation.params[0] === '$__interval' && newDef.defaultParams[0] !== '$__interval') { + operation.params = newDef.defaultParams; + } + + return operation; +} + export function operationWithRangeVectorRenderer( model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string ) { - let rangeVector = (model.params ?? [])[0] ?? 'auto'; - - if (rangeVector === 'auto') { - rangeVector = '$__rate_interval'; - } - + let rangeVector = (model.params ?? [])[0] ?? '5m'; return `${def.id}(${innerExpr}[${rangeVector}])`; } @@ -321,14 +336,6 @@ function getSimpleBinaryRenderer(operator: string) { }; } -function getRangeVectorParamDef(): QueryBuilderOperationParamDef { - return { - name: 'Range vector', - type: 'string', - options: ['auto', '$__rate_interval', '$__interval', '$__range', '1m', '5m', '10m', '1h', '24h'], - }; -} - /** * Since there can only be one operation with range vector this will replace the current one (if one was added ) */ @@ -337,28 +344,22 @@ export function addOperationWithRangeVector( query: PromVisualQuery, modeller: VisualQueryModeller ) { + const newOperation: QueryBuilderOperation = { + id: def.id, + params: def.defaultParams, + }; + if (query.operations.length > 0) { const firstOp = modeller.getOperationDef(query.operations[0].id); if (firstOp.addOperationHandler === addOperationWithRangeVector) { return { ...query, - operations: [ - { - ...query.operations[0], - id: def.id, - }, - ...query.operations.slice(1), - ], + operations: [newOperation, ...query.operations.slice(1)], }; } } - const newOperation: QueryBuilderOperation = { - id: def.id, - params: def.defaultParams, - }; - return { ...query, operations: [newOperation, ...query.operations], diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx index c8f234575ab..1abef104fb6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx @@ -60,10 +60,9 @@ export const OperationName = React.memo(({ operation, def, index, onChang onCloseMenu={onToggleSwitcher} onChange={(value) => { if (value.value) { - onChange(index, { - ...operation, - id: value.value.id, - }); + const newDef = queryModeller.getOperationDef(value.value.id); + let changedOp = { ...operation, id: value.value.id }; + onChange(index, def.changeTypeHandler ? def.changeTypeHandler(changedOp, newDef) : changedOp); } }} /> diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx index 168d880342c..3549b82a2ac 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx @@ -1,4 +1,4 @@ -import { toOption } from '@grafana/data'; +import { SelectableValue, toOption } from '@grafana/data'; import { Input, Select } from '@grafana/ui'; import React, { ComponentType } from 'react'; import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types'; @@ -45,16 +45,22 @@ function SelectInputParamEditor({ operationIndex, onChange, }: QueryBuilderOperationParamEditorProps) { - const selectOptions = paramDef.options!.map((option) => ({ - label: option as string, - value: option as string, - })); + let selectOptions = paramDef.options as Array>; + + if (!selectOptions[0]?.label) { + selectOptions = paramDef.options!.map((option) => ({ + label: option.toString(), + value: option as string, + })); + } + + let valueOption = selectOptions.find((x) => x.value === value) ?? toOption(value as string); return ( (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), })} placeholder={option.placeholder} @@ -157,10 +157,27 @@ const OptionInput: FC = ({ option, invalid, id, pathPref const styles = { checkbox: css` - height: auto; // native chekbox has fixed height which does not take into account description + height: auto; // native checkbox has fixed height which does not take into account description `, }; const validateOption = (value: string, validationRule: string) => { return RegExp(validationRule).test(value) ? true : 'Invalid format'; }; + +const determineRequired = (option: NotificationChannelOption, getValues: any) => { + if (!option.dependsOn) { + return option.required ? 'Required' : false; + } + + const dependentOn = getValues(`items[0].${option.dependsOn}`); + return !dependentOn && option.required ? 'Required' : false; +}; + +const determineReadOnly = (option: NotificationChannelOption, getValues: any) => { + if (!option.dependsOn) { + return false; + } + + return getValues(`items[0].${option.dependsOn}`); +}; diff --git a/public/app/features/alerting/unified/mocks/grafana-notifiers.ts b/public/app/features/alerting/unified/mocks/grafana-notifiers.ts index 3cd3063abe5..eec2843582a 100644 --- a/public/app/features/alerting/unified/mocks/grafana-notifiers.ts +++ b/public/app/features/alerting/unified/mocks/grafana-notifiers.ts @@ -20,79 +20,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, - }, - ], - }, - { - type: 'dingding', - name: 'DingDing', - heading: 'DingDing settings', - description: 'Sends HTTP POST request to DingDing', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Url', - description: '', - placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'Message Type', - description: '', - placeholder: '', - propertyName: 'msgType', - selectOptions: [ - { value: 'link', label: 'Link' }, - { value: 'actionCard', label: 'ActionCard' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - ], - }, - { - type: 'googlechat', - name: 'Google Hangouts Chat', - heading: 'Google Hangouts Chat settings', - description: 'Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message format', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Url', - description: '', - placeholder: 'Google Hangouts Chat incoming webhook url', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'textarea', - inputType: '', - label: 'Message', - description: '', - placeholder: '{{ template "default.message" . }}', - propertyName: 'message', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, + dependsOn: '', }, ], }, @@ -115,6 +43,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -128,6 +57,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -141,6 +71,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, ], }, @@ -163,6 +94,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, + dependsOn: '', }, { element: 'select', @@ -179,6 +111,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -192,6 +125,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -205,537 +139,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: true, - }, - ], - }, - { - type: 'kafka', - name: 'Kafka REST Proxy', - heading: 'Kafka settings', - description: 'Sends notifications to Kafka Rest Proxy', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Kafka REST Proxy', - description: '', - placeholder: 'http://localhost:8082', - propertyName: 'kafkaRestProxy', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Topic', - description: '', - placeholder: 'topic1', - propertyName: 'kafkaTopic', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - ], - }, - { - type: 'sensu', - name: 'Sensu', - heading: 'Sensu settings', - description: 'Sends HTTP POST request to a Sensu API', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Url', - description: '', - placeholder: 'http://sensu-api.local:4567/results', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Source', - description: 'If empty rule id will be used', - placeholder: '', - propertyName: 'source', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Handler', - description: '', - placeholder: 'default', - propertyName: 'handler', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Username', - description: '', - placeholder: '', - propertyName: 'username', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'password', - label: 'Password', - description: '', - placeholder: '', - propertyName: 'passsword ', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: true, - }, - ], - }, - { - type: 'threema', - name: 'Threema Gateway', - heading: 'Threema Gateway settings', - description: 'Sends notifications to Threema using Threema Gateway (Basic IDs)', - info: 'Notifications can be configured for any Threema Gateway ID of type "Basic". End-to-End IDs are not currently supported.The Threema Gateway ID can be set up at https://gateway.threema.ch/.', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Gateway ID', - description: 'Your 8 character Threema Gateway Basic ID (starting with a *).', - placeholder: '*3MAGWID', - propertyName: 'gateway_id', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '\\*[0-9A-Z]{7}', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Recipient ID', - description: 'The 8 character Threema ID that should receive the alerts.', - placeholder: 'YOUR3MID', - propertyName: 'recipient_id', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '[0-9A-Z]{8}', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'API Secret', - description: 'Your Threema Gateway API secret.', - placeholder: '', - propertyName: 'api_secret', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - ], - }, - { - type: 'LINE', - name: 'LINE', - heading: 'LINE notify settings', - description: 'Send notifications to LINE notify', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Token', - description: '', - placeholder: 'LINE notify token key', - propertyName: 'token', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - ], - }, - { - type: 'opsgenie', - name: 'OpsGenie', - heading: 'OpsGenie settings', - description: 'Sends notifications to OpsGenie', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'API Key', - description: '', - placeholder: 'OpsGenie API Key', - propertyName: 'apiKey', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - { - element: 'input', - inputType: 'text', - label: 'Alert API Url', - description: '', - placeholder: 'https://api.opsgenie.com/v2/alerts', - propertyName: 'apiUrl', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'checkbox', - inputType: '', - label: 'Auto close incidents', - description: 'Automatically close alerts in OpsGenie once the alert goes back to ok.', - placeholder: '', - propertyName: 'autoClose', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'checkbox', - inputType: '', - label: 'Override priority', - description: 'Allow the alert priority to be set using the og_priority tag', - placeholder: '', - propertyName: 'overridePriority', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'Send notification tags as', - description: 'Send the notification tags to Opsgenie as either Extra Properties, Tags or both', - placeholder: '', - propertyName: 'sendTagsAs', - selectOptions: [ - { value: 'tags', label: 'Tags' }, - { value: 'details', label: 'Extra Properties' }, - { value: 'both', label: 'Tags \u0026 Extra Properties' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - ], - }, - { - type: 'pushover', - name: 'Pushover', - heading: 'Pushover settings', - description: 'Sends HTTP POST request to the Pushover API', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'API Token', - description: '', - placeholder: 'Application token', - propertyName: 'apiToken', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - { - element: 'input', - inputType: 'text', - label: 'User key(s)', - description: '', - placeholder: 'comma-separated list', - propertyName: 'userKey', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - { - element: 'input', - inputType: 'text', - label: 'Device(s) (optional)', - description: '', - placeholder: 'comma-separated list; leave empty to send to all devices', - propertyName: 'device', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'Alerting priority', - description: '', - placeholder: '', - propertyName: 'priority', - selectOptions: [ - { value: '2', label: 'Emergency' }, - { value: '1', label: 'High' }, - { value: '0', label: 'Normal' }, - { value: '-1', label: 'Low' }, - { value: '-2', label: 'Lowest' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'OK priority', - description: '', - placeholder: '', - propertyName: 'okPriority', - selectOptions: [ - { value: '2', label: 'Emergency' }, - { value: '1', label: 'High' }, - { value: '0', label: 'Normal' }, - { value: '-1', label: 'Low' }, - { value: '-2', label: 'Lowest' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Retry (Only used for Emergency Priority)', - description: - 'How often (in seconds) the Pushover servers will send the same alerting or OK notification to the user.', - placeholder: 'minimum 30 seconds', - propertyName: 'retry', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Expire (Only used for Emergency Priority)', - description: 'How many seconds the alerting or OK notification will continue to be retried.', - placeholder: 'maximum 86400 seconds', - propertyName: 'expire', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'Alerting sound', - description: '', - placeholder: '', - propertyName: 'sound', - selectOptions: [ - { value: 'default', label: 'Default' }, - { value: 'pushover', label: 'Pushover' }, - { value: 'bike', label: 'Bike' }, - { value: 'bugle', label: 'Bugle' }, - { value: 'cashregister', label: 'Cashregister' }, - { value: 'classical', label: 'Classical' }, - { value: 'cosmic', label: 'Cosmic' }, - { value: 'falling', label: 'Falling' }, - { value: 'gamelan', label: 'Gamelan' }, - { value: 'incoming', label: 'Incoming' }, - { value: 'intermission', label: 'Intermission' }, - { value: 'magic', label: 'Magic' }, - { value: 'mechanical', label: 'Mechanical' }, - { value: 'pianobar', label: 'Pianobar' }, - { value: 'siren', label: 'Siren' }, - { value: 'spacealarm', label: 'Spacealarm' }, - { value: 'tugboat', label: 'Tugboat' }, - { value: 'alien', label: 'Alien' }, - { value: 'climb', label: 'Climb' }, - { value: 'persistent', label: 'Persistent' }, - { value: 'echo', label: 'Echo' }, - { value: 'updown', label: 'Updown' }, - { value: 'none', label: 'None' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'OK sound', - description: '', - placeholder: '', - propertyName: 'okSound', - selectOptions: [ - { value: 'default', label: 'Default' }, - { value: 'pushover', label: 'Pushover' }, - { value: 'bike', label: 'Bike' }, - { value: 'bugle', label: 'Bugle' }, - { value: 'cashregister', label: 'Cashregister' }, - { value: 'classical', label: 'Classical' }, - { value: 'cosmic', label: 'Cosmic' }, - { value: 'falling', label: 'Falling' }, - { value: 'gamelan', label: 'Gamelan' }, - { value: 'incoming', label: 'Incoming' }, - { value: 'intermission', label: 'Intermission' }, - { value: 'magic', label: 'Magic' }, - { value: 'mechanical', label: 'Mechanical' }, - { value: 'pianobar', label: 'Pianobar' }, - { value: 'siren', label: 'Siren' }, - { value: 'spacealarm', label: 'Spacealarm' }, - { value: 'tugboat', label: 'Tugboat' }, - { value: 'alien', label: 'Alien' }, - { value: 'climb', label: 'Climb' }, - { value: 'persistent', label: 'Persistent' }, - { value: 'echo', label: 'Echo' }, - { value: 'updown', label: 'Updown' }, - { value: 'none', label: 'None' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - ], - }, - { - type: 'sensugo', - name: 'Sensu Go', - heading: 'Sensu Go Settings', - description: 'Sends HTTP POST request to a Sensu Go API', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Backend URL', - description: '', - placeholder: 'http://sensu-api.local:8080', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'password', - label: 'API Key', - description: 'API Key to auth to Sensu Go backend', - placeholder: '', - propertyName: 'apikey', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - { - element: 'input', - inputType: 'text', - label: 'Proxy entity name', - description: 'If empty, rule name will be used', - placeholder: '', - propertyName: 'entity', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Check name', - description: 'If empty, rule id will be used', - placeholder: '', - propertyName: 'check', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Handler', - description: '', - placeholder: '', - propertyName: 'handler', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Namespace', - description: '', - placeholder: 'default', - propertyName: 'namespace', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, + dependsOn: '', }, ], }, @@ -759,6 +163,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -772,6 +177,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -785,41 +191,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: true, - }, - ], - }, - { - type: 'discord', - name: 'Discord', - heading: 'Discord settings', - description: 'Sends notifications to Discord', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Message Content', - description: 'Mention a group using @ or a user using \u003c@ID\u003e when notifying in a channel', - placeholder: '', - propertyName: 'content', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Webhook URL', - description: '', - placeholder: 'Discord webhook URL', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, + dependsOn: '', }, ], }, @@ -842,6 +214,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'textarea', @@ -855,108 +228,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: true, validationRule: '', secure: false, - }, - ], - }, - { - type: 'victorops', - name: 'VictorOps', - heading: 'VictorOps settings', - description: 'Sends notifications to VictorOps', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Url', - description: '', - placeholder: 'VictorOps url', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'checkbox', - inputType: '', - label: 'Auto resolve incidents', - description: 'Resolve incidents in VictorOps once the alert goes back to ok.', - placeholder: '', - propertyName: 'autoResolve', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - ], - }, - { - type: 'pagerduty', - name: 'PagerDuty', - heading: 'PagerDuty settings', - description: 'Sends notifications to PagerDuty', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Integration Key', - description: '', - placeholder: 'Pagerduty Integration Key', - propertyName: 'integrationKey', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: true, - }, - { - element: 'select', - inputType: '', - label: 'Severity', - description: '', - placeholder: '', - propertyName: 'severity', - selectOptions: [ - { value: 'critical', label: 'Critical' }, - { value: 'error', label: 'Error' }, - { value: 'warning', label: 'Warning' }, - { value: 'info', label: 'Info' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'checkbox', - inputType: '', - label: 'Auto resolve incidents', - description: 'Resolve incidents in pagerduty once the alert goes back to ok.', - placeholder: '', - propertyName: 'autoResolve', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'checkbox', - inputType: '', - label: 'Include message in details', - description: - 'Move the alert message from the PD summary into the custom details. This changes the custom details object and may break event rules you have configured', - placeholder: '', - propertyName: 'messageInDetails', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, + dependsOn: '', }, ], }, @@ -977,9 +249,10 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ propertyName: 'recipient', selectOptions: null, showWhen: { field: '', is: '' }, - required: false, + required: true, validationRule: '', secure: false, + dependsOn: 'secureSettings.url', }, { element: 'input', @@ -990,9 +263,10 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ propertyName: 'token', selectOptions: null, showWhen: { field: '', is: '' }, - required: false, + required: true, validationRule: '', secure: true, + dependsOn: 'secureSettings.url', }, { element: 'input', @@ -1006,6 +280,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -1019,6 +294,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -1032,6 +308,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -1046,6 +323,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -1060,6 +338,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'select', @@ -1077,6 +356,7 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ required: false, validationRule: '', secure: false, + dependsOn: '', }, { element: 'input', @@ -1088,44 +368,10 @@ export const grafanaNotifiersMock: NotifierDTO[] = [ propertyName: 'url', selectOptions: null, showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: true, - }, - ], - }, - { - type: 'telegram', - name: 'Telegram', - heading: 'Telegram API settings', - description: 'Sends notifications to Telegram', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'BOT API Token', - description: '', - placeholder: 'Telegram BOT API Token', - propertyName: 'bottoken', - selectOptions: null, - showWhen: { field: '', is: '' }, required: true, validationRule: '', secure: true, - }, - { - element: 'input', - inputType: 'text', - label: 'Chat ID', - description: 'Integer Telegram Chat Identifier', - placeholder: '', - propertyName: 'chatid', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, + dependsOn: 'token', }, ], }, diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index d1e3bfcfbc6..4dcf2e44fdd 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -17,6 +17,7 @@ function option( placeholder: '', validationRule: '', showWhen: { field: '', is: '' }, + dependsOn: '', ...rest, }; } diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts index aff14234045..4501687e77b 100644 --- a/public/app/types/alerting.ts +++ b/public/app/types/alerting.ts @@ -133,6 +133,7 @@ export interface NotificationChannelOption { showWhen: { field: string; is: string }; validationRule: string; subformOptions?: NotificationChannelOption[]; + dependsOn: string; } export interface NotificationChannelState { From 525ecab3eed9769b9eac5618cb14e51d10ad694e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 25 Feb 2022 15:30:23 +0100 Subject: [PATCH 045/125] renovate: stop updating "@mdx-js/react" (#45909) it is a peer-dependency of "@storybook/addon-docs" with version 1.x, the new version is 2.x. we should update them together. --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 7b7616a4068..047b72811cf 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -18,6 +18,7 @@ "d3-scale-chromatic", // we should bump this once we move to esm modules "execa", // we should bump this once we move to esm modules "history", // we should bump this together with react-router-dom + "@mdx-js/react", // storybook peer-depends on it's 1.x version, we should upgrade this when we upgrade storybook "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins "react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins "react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead From f87bfdf2ffd3e795a9385f475ba957c7ff4ca16b Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 25 Feb 2022 14:43:08 +0000 Subject: [PATCH 046/125] Update comment for scheduler_behind_seconds metric (#45918) --- pkg/services/ngalert/schedule/schedule.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 8e8c5fd220c..7d607778021 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -369,9 +369,9 @@ func (sch *schedule) schedulePeriodic(ctx context.Context) error { select { case tick := <-sch.ticker.C: // We use Round(0) on the start time to remove the monotonic clock. - // This is required as late ticks from the ticker have current monotonic - // timestamps such that start.Sub(tick) does not return the expected - // delta. + // This is required as ticks from the ticker and time.Now() can have + // a monotonic clock that when subtracted do not represent the delta + // in wall clock time. start := time.Now().Round(0) sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds()) From c656a6bf1e3929f25980bb6005c978c4a6b799e2 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Sat, 26 Feb 2022 00:20:16 +0900 Subject: [PATCH 047/125] Live: fix typo in channel.ts (#45915) recieved -> received --- public/app/features/live/centrifuge/channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/live/centrifuge/channel.ts b/public/app/features/live/centrifuge/channel.ts index 92bae084747..0ad529a6598 100644 --- a/public/app/features/live/centrifuge/channel.ts +++ b/public/app/features/live/centrifuge/channel.ts @@ -61,7 +61,7 @@ export class CentrifugeLiveChannel { this.initalized = true; const events: SubscriptionEvents = { - // Called when a message is recieved from the socket + // Called when a message is received from the socket publish: (ctx: PublicationContext) => { try { if (ctx.data) { From 893f9e8ee46323bc949ab7005875aee3a649fa62 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 25 Feb 2022 16:56:44 +0100 Subject: [PATCH 048/125] use datasource service instead of store in provisioning (#45835) --- pkg/services/provisioning/provisioning.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 2621fac3eb8..303ab11c17c 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" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards" + datasourceservice "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" @@ -24,6 +25,7 @@ import ( func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store, encryptionService encryption.Internal, notificatonService *notifications.NotificationService, dashboardService dashboardservice.DashboardProvisioningService, + datasourceService datasourceservice.DataSourceService, ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, @@ -37,6 +39,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p provisionDatasources: datasources.Provision, provisionPlugins: plugins.Provision, dashboardService: dashboardService, + datasourceService: datasourceService, } return s, nil } @@ -94,6 +97,7 @@ type ProvisioningServiceImpl struct { provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store) error mutex sync.Mutex dashboardService dashboardservice.DashboardProvisioningService + datasourceService datasourceservice.DataSourceService } func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { @@ -146,7 +150,7 @@ func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) error { datasourcePath := filepath.Join(ps.Cfg.ProvisioningPath, "datasources") - if err := ps.provisionDatasources(ctx, datasourcePath, ps.SQLStore, ps.SQLStore); err != nil { + if err := ps.provisionDatasources(ctx, datasourcePath, ps.datasourceService, ps.SQLStore); err != nil { err = errutil.Wrap("Datasource provisioning error", err) ps.log.Error("Failed to provision data sources", "error", err) return err From 190757b3c602ba4667d402664966daa409bab735 Mon Sep 17 00:00:00 2001 From: Shachi Solanki Date: Fri, 25 Feb 2022 12:14:13 -0600 Subject: [PATCH 049/125] Tempo / Trace Viewer: Support Span Links in Trace Viewer (#45632) * Support Span Links in Trace Viewer * Update ReferencesButton styles * Remove datasource prop Co-authored-by: Connor Lindsey --- .betterer.results | 4 +- packages/grafana-data/src/types/data.ts | 3 +- packages/grafana-data/src/types/trace.ts | 8 +- .../TraceTimelineViewer/ReferencesButton.tsx | 37 ++-- .../TraceTimelineViewer/SpanBarRow.test.js | 37 ++-- .../src/TraceTimelineViewer/SpanBarRow.tsx | 6 +- .../SpanDetail/AccordianReferences.test.js | 2 +- .../SpanDetail/AccordianReferences.tsx | 204 ++++++++++++------ .../SpanDetail/DetailState.tsx | 20 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 10 +- .../src/TraceTimelineViewer/SpanDetailRow.tsx | 5 +- .../VirtualizedTraceView.tsx | 5 +- .../src/TraceTimelineViewer/index.tsx | 3 +- .../jaeger-ui-components/src/types/trace.ts | 1 + .../src/url/ReferenceLink.tsx | 1 + pkg/tsdb/tempo/trace_transform.go | 45 ++++ pkg/tsdb/tempo/trace_transform_test.go | 1 + .../features/explore/TraceView/TraceView.tsx | 18 +- .../explore/TraceView/useDetailState.test.ts | 2 +- .../explore/TraceView/useDetailState.ts | 17 +- .../datasource/tempo/resultTransformer.ts | 23 +- .../plugins/datasource/tempo/testResponse.ts | 10 + 22 files changed, 335 insertions(+), 127 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9bbf07ee673..f228dec2e0f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -104,7 +104,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2127169675": [ [15, 17, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:2454947085": [ + "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:814916029": [ [15, 26, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.js:2200354834": [ @@ -113,7 +113,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.test.js:3242453659": [ [15, 19, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:3043344541": [ + "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:1301875390": [ [15, 19, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.test.js:2881451220": [ diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index f6941ede4f7..642c3cfee5e 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -20,7 +20,8 @@ export enum LoadingState { } // Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go -export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace' | 'nodeGraph'; +export const preferredVisualizationTypes = ['graph', 'table', 'logs', 'trace', 'nodeGraph'] as const; +export type PreferredVisualisationType = typeof preferredVisualizationTypes[number]; /** * @public diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index 363584adab7..db73ff9eebe 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -15,6 +15,12 @@ export type TraceLog = { fields: TraceKeyValuePair[]; }; +export type TraceSpanReference = { + traceID: string; + spanID: string; + tags?: TraceKeyValuePair[]; +}; + /** * This describes the structure of the dataframe that should be returned from a tracing data source to show trace * in a TraceView component. @@ -31,7 +37,7 @@ export interface TraceSpanRow { // Milliseconds duration: number; logs?: TraceLog[]; - + references?: TraceSpanReference[]; // Note: To mark spen as having error add tag error: true tags?: TraceKeyValuePair[]; warnings?: string[]; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx index 34fc50f4750..f48b86a243b 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx @@ -14,16 +14,15 @@ import React from 'react'; import { css } from '@emotion/css'; -import { stylesFactory, Tooltip } from '@grafana/ui'; +import { Tooltip, useStyles2 } from '@grafana/ui'; import { TraceSpanReference } from '../types/trace'; import ReferenceLink from '../url/ReferenceLink'; -export const getStyles = stylesFactory(() => { +export const getStyles = () => { return { MultiParent: css` padding: 0 5px; - color: #000; & ~ & { margin-left: 5px; } @@ -39,7 +38,7 @@ export const getStyles = stylesFactory(() => { max-width: none; `, }; -}); +}; type TReferencesButtonProps = { references: TraceSpanReference[]; @@ -48,19 +47,19 @@ type TReferencesButtonProps = { focusSpan: (spanID: string) => void; }; -export default class ReferencesButton extends React.PureComponent { - render() { - const { references, children, tooltipText, focusSpan } = this.props; - const styles = getStyles(); +const ReferencesButton = (props: TReferencesButtonProps) => { + const { references, children, tooltipText, focusSpan } = props; + const styles = useStyles2(getStyles); - // TODO: handle multiple items with some dropdown - const ref = references[0]; - return ( - - - {children} - - - ); - } -} + // TODO: handle multiple items with some dropdown + const ref = references[0]; + return ( + + + {children} + + + ); +}; + +export default ReferencesButton; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js index 5cbc0735b95..6c6c2ff78c3 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js @@ -54,6 +54,7 @@ describe('', () => { }, spanID, logs: [], + references: [], }, }; @@ -84,29 +85,27 @@ describe('', () => { }); it('render references button', () => { - const span = Object.assign( - { - references: [ - { - refType: 'CHILD_OF', - traceID: 'trace1', + const newSpan = Object.assign({}, props.span); + const span = Object.assign(newSpan, { + references: [ + { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span0', + span: { spanID: 'span0', - span: { - spanID: 'span0', - }, }, - { - refType: 'CHILD_OF', - traceID: 'otherTrace', + }, + { + refType: 'CHILD_OF', + traceID: 'otherTrace', + spanID: 'span1', + span: { spanID: 'span1', - span: { - spanID: 'span1', - }, }, - ], - }, - props.span - ); + }, + ], + }); const spanRow = shallow() .dive() diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx index 0a5caf1dce4..0244e410252 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx @@ -13,13 +13,13 @@ // limitations under the License. import * as React from 'react'; + import IoAlert from 'react-icons/lib/io/alert'; import IoArrowRightA from 'react-icons/lib/io/arrow-right-a'; -import IoNetwork from 'react-icons/lib/io/network'; import MdFileUpload from 'react-icons/lib/md/file-upload'; import { css, keyframes } from '@emotion/css'; import cx from 'classnames'; -import { stylesFactory, withTheme2 } from '@grafana/ui'; +import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import ReferencesButton from './ReferencesButton'; @@ -510,7 +510,7 @@ export class UnthemedSpanBarRow extends React.PureComponent { tooltipText="Contains multiple references" focusSpan={focusSpan} > - + )} {span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js index 774b0d57a0d..b69fda5e9e2 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js @@ -104,7 +104,7 @@ describe('', () => { expect(serviceName).toBe(span.process.serviceName); expect(endpointName).toBe(span.operationName); } else { - expect(serviceName).toBe('< span in another trace >'); + expect(serviceName).toBe('View Linked Span '); } }); }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx index 5adf26e802e..8796d0d60b5 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx @@ -14,17 +14,51 @@ import * as React from 'react'; import { css } from '@emotion/css'; -import cx from 'classnames'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; +import AccordianKeyValues from './AccordianKeyValues'; import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; import { TraceSpanReference } from '../../types/trace'; import ReferenceLink from '../../url/ReferenceLink'; -import { uAlignIcon } from '../../uberUtilityStyles'; +import { uAlignIcon, ubMb1 } from '../../uberUtilityStyles'; +import { GrafanaTheme2 } from '@grafana/data'; +import { autoColor } from '../../Theme'; -const getStyles = () => { +const getStyles = (theme: GrafanaTheme2) => { return { + AccordianReferenceItem: css` + border-bottom: 1px solid ${autoColor(theme, '#d8d8d8')}; + `, + AccordianKeyValues: css` + margin-left: 10px; + `, + AccordianReferences: css` + label: AccordianReferences; + border: 1px solid ${autoColor(theme, '#d8d8d8')}; + position: relative; + margin-bottom: 0.25rem; + `, + AccordianReferencesHeader: css` + label: AccordianReferencesHeader; + background: ${autoColor(theme, '#e4e4e4')}; + color: inherit; + display: block; + padding: 0.25rem 0.5rem; + &:hover { + background: ${autoColor(theme, '#dadada')}; + } + `, + AccordianReferencesContent: css` + label: AccordianReferencesContent; + background: ${autoColor(theme, '#f0f0f0')}; + border-top: 1px solid ${autoColor(theme, '#d8d8d8')}; + padding: 0.5rem 0.5rem 0.25rem 0.5rem; + `, + AccordianReferencesFooter: css` + label: AccordianReferencesFooter; + color: ${autoColor(theme, '#999')}; + `, ReferencesList: css` background: #fff; border: 1px solid #ddd; @@ -53,6 +87,9 @@ const getStyles = () => { debugInfo: css` letter-spacing: 0.25px; margin: 0.5em 0 0; + flex-wrap: wrap; + display: flex; + justify-content: flex-end; `, debugLabel: css` margin: 0 5px 0 5px; @@ -69,86 +106,117 @@ type AccordianReferencesProps = { highContrast?: boolean; interactive?: boolean; isOpen: boolean; + openedItems?: Set; + onItemToggle?: (reference: TraceSpanReference) => void; onToggle?: null | (() => void); focusSpan: (uiFind: string) => void; }; type ReferenceItemProps = { data: TraceSpanReference[]; + interactive?: boolean; + openedItems?: Set; + onItemToggle?: (reference: TraceSpanReference) => void; focusSpan: (uiFind: string) => void; }; // export for test export function References(props: ReferenceItemProps) { - const { data, focusSpan } = props; + const { data, focusSpan, openedItems, onItemToggle, interactive } = props; const styles = useStyles2(getStyles); return ( -
-
    - {data.map((reference) => { - return ( -
  • - - - {reference.span ? ( - - {reference.span.process.serviceName} - {reference.span.operationName} - - ) : ( - < span in another trace > - )} - - - {reference.refType} - - - {reference.spanID} - - - - -
  • - ); - })} -
+
+ {data.map((reference, i) => ( +
+
+ + + {reference.span ? ( + + {reference.span.process.serviceName} + {reference.span.operationName} + + ) : ( + + View Linked Span + + )} + + + {reference.traceID} + + + {reference.spanID} + + + + +
+ {!!reference.tags?.length && ( +
+ onItemToggle(reference) : null} + /> +
+ )} +
+ ))}
); } -export default class AccordianReferences extends React.PureComponent { - static defaultProps: Partial = { - highContrast: false, - interactive: true, - onToggle: null, - }; - - render() { - const { data, interactive, isOpen, onToggle, focusSpan } = this.props; - const isEmpty = !Array.isArray(data) || !data.length; - const iconCls = uAlignIcon; - let arrow: React.ReactNode | null = null; - let headerProps: {} | null = null; - if (interactive) { - arrow = isOpen ? : ; - headerProps = { - 'aria-checked': isOpen, - onClick: isEmpty ? null : onToggle, - role: 'switch', - }; - } - return ( -
-
- {arrow} - - References - {' '} - ({data.length}) -
- {isOpen && } -
- ); +const AccordianReferences: React.FC = ({ + data, + interactive = true, + isOpen, + onToggle, + onItemToggle, + openedItems, + focusSpan, +}) => { + const isEmpty = !Array.isArray(data) || !data.length; + let arrow: React.ReactNode | null = null; + let HeaderComponent: 'span' | 'a' = 'span'; + let headerProps: {} | null = null; + if (interactive) { + arrow = isOpen ? : ; + HeaderComponent = 'a'; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; } -} + + const styles = useStyles2(getStyles); + return ( +
+ + {arrow} + + References + {' '} + ({data.length}) + + {isOpen && ( + + )} +
+ ); +}; + +export default React.memo(AccordianReferences); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx index 22459037132..da040f4d2dc 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceLog } from '../../types/trace'; +import { TraceLog, TraceSpanReference } from '../../types/trace'; /** * Which items of a {@link SpanDetail} component are expanded. @@ -21,6 +21,7 @@ export default class DetailState { isTagsOpen: boolean; isProcessOpen: boolean; logs: { isOpen: boolean; openedItems: Set }; + references: { isOpen: boolean; openedItems: Set }; isWarningsOpen: boolean; isStackTracesOpen: boolean; isReferencesOpen: boolean; @@ -33,6 +34,7 @@ export default class DetailState { isWarningsOpen, isStackTracesOpen, logs, + references, }: DetailState | Record = oldState || {}; this.isTagsOpen = Boolean(isTagsOpen); this.isProcessOpen = Boolean(isProcessOpen); @@ -43,6 +45,10 @@ export default class DetailState { isOpen: Boolean(logs && logs.isOpen), openedItems: logs && logs.openedItems ? new Set(logs.openedItems) : new Set(), }; + this.references = { + isOpen: Boolean(references && references.isOpen), + openedItems: references && references.openedItems ? new Set(references.openedItems) : new Set(), + }; } toggleTags() { @@ -59,7 +65,17 @@ export default class DetailState { toggleReferences() { const next = new DetailState(this); - next.isReferencesOpen = !this.isReferencesOpen; + next.references.isOpen = !this.references.isOpen; + return next; + } + + toggleReferenceItem(reference: TraceSpanReference) { + const next = new DetailState(this); + if (next.references.openedItems.has(reference)) { + next.references.openedItems.delete(reference); + } else { + next.references.openedItems.add(reference); + } return next; } diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index 6555b51bc84..f423349d98b 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -26,7 +26,7 @@ import DetailState from './DetailState'; import { formatDuration } from '../utils'; import LabeledList from '../../common/LabeledList'; import { SpanLinkFunc, TNil } from '../../types'; -import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace'; +import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import AccordianReferences from './AccordianReferences'; import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; @@ -110,6 +110,7 @@ type SpanDetailProps = { traceStartTime: number; warningsToggle: (spanID: string) => void; stackTracesToggle: (spanID: string) => void; + referenceItemToggle: (spanID: string, reference: TraceSpanReference) => void; referencesToggle: (spanID: string) => void; focusSpan: (uiFind: string) => void; createSpanLink?: SpanLinkFunc; @@ -130,6 +131,7 @@ export default function SpanDetail(props: SpanDetailProps) { warningsToggle, stackTracesToggle, referencesToggle, + referenceItemToggle, focusSpan, createSpanLink, createFocusSpanLink, @@ -139,7 +141,7 @@ export default function SpanDetail(props: SpanDetailProps) { isProcessOpen, logs: logsState, isWarningsOpen, - isReferencesOpen, + references: referencesState, isStackTracesOpen, } = detailState; const { @@ -258,8 +260,10 @@ export default function SpanDetail(props: SpanDetailProps) { {references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && ( referencesToggle(spanID)} + onItemToggle={(reference) => referenceItemToggle(spanID, reference)} focusSpan={focusSpan} /> )} diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx index d254f34d554..c82ca42087f 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx @@ -23,7 +23,7 @@ import { autoColor } from '../Theme'; import { stylesFactory, withTheme2 } from '@grafana/ui'; import { GrafanaTheme2, LinkModel } from '@grafana/data'; -import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace'; +import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace'; import { SpanLinkFunc } from '../types'; const getStyles = stylesFactory((theme: GrafanaTheme2) => { @@ -77,6 +77,7 @@ type SpanDetailRowProps = { logItemToggle: (spanID: string, log: TraceLog) => void; logsToggle: (spanID: string) => void; processToggle: (spanID: string) => void; + referenceItemToggle: (spanID: string, reference: TraceSpanReference) => void; referencesToggle: (spanID: string) => void; warningsToggle: (spanID: string) => void; stackTracesToggle: (spanID: string) => void; @@ -111,6 +112,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent void; detailStackTracesToggle: (spanID: string) => void; detailReferencesToggle: (spanID: string) => void; + detailReferenceItemToggle: (spanID: string, reference: TraceSpanReference) => void; detailProcessToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void; detailToggle: (spanID: string) => void; @@ -440,6 +441,7 @@ export class UnthemedVirtualizedTraceView extends React.Component void; detailStackTracesToggle: (spanID: string) => void; detailReferencesToggle: (spanID: string) => void; + detailReferenceItemToggle: (spanID: string, reference: TraceSpanReference) => void; detailProcessToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void; detailToggle: (spanID: string) => void; diff --git a/packages/jaeger-ui-components/src/types/trace.ts b/packages/jaeger-ui-components/src/types/trace.ts index 0b54d267cff..62e0a899827 100644 --- a/packages/jaeger-ui-components/src/types/trace.ts +++ b/packages/jaeger-ui-components/src/types/trace.ts @@ -44,6 +44,7 @@ export type TraceSpanReference = { span?: TraceSpan | null | undefined; spanID: string; traceID: string; + tags?: TraceKeyValuePair[]; }; export type TraceSpanData = { diff --git a/packages/jaeger-ui-components/src/url/ReferenceLink.tsx b/packages/jaeger-ui-components/src/url/ReferenceLink.tsx index b6149269ff7..631cd86e2b1 100644 --- a/packages/jaeger-ui-components/src/url/ReferenceLink.tsx +++ b/packages/jaeger-ui-components/src/url/ReferenceLink.tsx @@ -41,6 +41,7 @@ export default function ReferenceLink(props: ReferenceLinkProps) { if (!createLinkToExternalSpan) { throw new Error("ExternalLinkContext does not have a value, you probably forgot to setup it's provider"); } + return ( { + const link = createFocusSpanLink(traceId, spanId); + return link.href; + }; + const traceTimeline: TTraceTimeline = useMemo( () => ({ childrenHiddenIDs, @@ -144,7 +150,7 @@ export function TraceView(props: Props) { updateViewRangeTime={updateViewRangeTime} viewRange={viewRange} focusSpan={noop} - createLinkToExternalSpan={noop as any} + createLinkToExternalSpan={createLinkToExternalSpan} setSpanNameColumnWidth={setSpanNameColumnWidth} collapseAll={collapseAll} collapseOne={collapseOne} @@ -157,6 +163,7 @@ export function TraceView(props: Props) { detailWarningsToggle={detailWarningsToggle} detailStackTracesToggle={detailStackTracesToggle} detailReferencesToggle={detailReferencesToggle} + detailReferenceItemToggle={detailReferenceItemToggle} detailProcessToggle={detailProcessToggle} detailTagsToggle={detailTagsToggle} detailToggle={toggleDetail} @@ -203,13 +210,20 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse { traceID: view.get(0).traceID, processes, spans: view.toArray().map((s, index) => { + const references = []; + if (s.parentSpanID) { + references.push({ refType: 'CHILD_OF' as const, spanID: s.parentSpanID, traceID: s.traceID }); + } + if (s.references) { + references.push(...s.references.map((reference) => ({ refType: 'FOLLOWS_FROM' as const, ...reference }))); + } return { ...s, duration: s.duration * 1000, startTime: s.startTime * 1000, processID: s.spanID, flags: 0, - references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined, + references, logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [], dataFrameRowIndex: index, }; diff --git a/public/app/features/explore/TraceView/useDetailState.test.ts b/public/app/features/explore/TraceView/useDetailState.test.ts index 6433dbd5f0d..0a641f461c8 100644 --- a/public/app/features/explore/TraceView/useDetailState.test.ts +++ b/public/app/features/explore/TraceView/useDetailState.test.ts @@ -44,7 +44,7 @@ describe('useDetailState', () => { const { result } = renderHook(() => useDetailState(sampleFrame)); act(() => result.current.toggleDetail('span1')); act(() => result.current.detailReferencesToggle('span1')); - expect(result.current.detailStates.get('span1')?.isReferencesOpen).toBe(true); + expect(result.current.detailStates.get('span1')?.references.isOpen).toBe(true); }); it('toggles processes', async () => { diff --git a/public/app/features/explore/TraceView/useDetailState.ts b/public/app/features/explore/TraceView/useDetailState.ts index 802ec4a74c3..c58e91f6d0d 100644 --- a/public/app/features/explore/TraceView/useDetailState.ts +++ b/public/app/features/explore/TraceView/useDetailState.ts @@ -1,7 +1,7 @@ import { useCallback, useState, useEffect } from 'react'; import { DataFrame } from '@grafana/data'; import { DetailState } from '@jaegertracing/jaeger-ui-components'; -import { TraceLog } from '@jaegertracing/jaeger-ui-components/src/types/trace'; +import { TraceLog, TraceSpanReference } from '@jaegertracing/jaeger-ui-components/src/types/trace'; /** * Keeps state of the span detail. This means whether span details are open but also state of each detail subitem @@ -42,6 +42,20 @@ export function useDetailState(frame: DataFrame) { [detailStates] ); + const detailReferenceItemToggle = useCallback( + function detailReferenceItemToggle(spanID: string, reference: TraceSpanReference) { + const old = detailStates.get(spanID); + if (!old) { + return; + } + const detailState = old.toggleReferenceItem(reference); + const newDetailStates = new Map(detailStates); + newDetailStates.set(spanID, detailState); + return setDetailStates(newDetailStates); + }, + [detailStates] + ); + return { detailStates, toggleDetail, @@ -58,6 +72,7 @@ export function useDetailState(frame: DataFrame) { (spanID: string) => makeDetailSubsectionToggle('stackTraces', detailStates, setDetailStates)(spanID), [detailStates] ), + detailReferenceItemToggle, detailReferencesToggle: useCallback( (spanID: string) => makeDetailSubsectionToggle('references', detailStates, setDetailStates)(spanID), [detailStates] diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index 89aa62437f1..8902cc1025d 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -8,6 +8,7 @@ import { MutableDataFrame, TraceKeyValuePair, TraceLog, + TraceSpanReference, TraceSpanRow, dateTimeFormat, } from '@grafana/data'; @@ -230,6 +231,24 @@ function getSpanTags( return spanTags; } +function getReferences(span: collectorTypes.opentelemetryProto.trace.v1.Span) { + const references: TraceSpanReference[] = []; + if (span.links) { + for (const link of span.links) { + const { traceId, spanId } = link; + const tags: TraceKeyValuePair[] = []; + if (link.attributes) { + for (const attribute of link.attributes) { + tags.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); + } + } + references.push({ traceID: traceId, spanID: spanId, tags }); + } + } + + return references; +} + function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) { const logs: TraceLog[] = []; if (span.events) { @@ -262,6 +281,7 @@ export function transformFromOTLP( { name: 'startTime', type: FieldType.number }, { name: 'duration', type: FieldType.number }, { name: 'logs', type: FieldType.other }, + { name: 'references', type: FieldType.other }, { name: 'tags', type: FieldType.other }, ], meta: { @@ -287,6 +307,7 @@ export function transformFromOTLP( duration: (span.endTimeUnixNano! - span.startTimeUnixNano!) / 1000000, tags: getSpanTags(span, librarySpan.instrumentationLibrary), logs: getLogs(span), + references: getReferences(span), } as TraceSpanRow); } } @@ -513,7 +534,7 @@ export function transformTrace(response: DataQueryResponse, nodeGraph = false): * Change fields which are json string into JS objects. Modifies the frame in place. */ function parseJsonFields(frame: DataFrame) { - for (const fieldName of ['serviceTags', 'logs', 'tags']) { + for (const fieldName of ['serviceTags', 'logs', 'tags', 'references']) { const field = frame.fields.find((f) => f.name === fieldName); if (field) { const fieldIndex = frame.fields.indexOf(field); diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/testResponse.ts index d099abd7b20..eb922a36c8c 100644 --- a/public/app/plugins/datasource/tempo/testResponse.ts +++ b/public/app/plugins/datasource/tempo/testResponse.ts @@ -1923,6 +1923,16 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({ displayName: 'logs', }, }, + { + name: 'references', + type: 'other', + config: {}, + labels: undefined, + values: [[]], + state: { + displayName: 'references', + }, + }, { name: 'tags', type: 'other', From 970dee419911e8794660b4f957b4b9cddf85f653 Mon Sep 17 00:00:00 2001 From: Tharun Rajendran Date: Sat, 26 Feb 2022 00:52:01 +0530 Subject: [PATCH 050/125] Trace View: Show number of child spans in span details view (#44393) * package(jaegar): show no of child spans in view Signed-off-by: tharun Co-authored-by: Connor Lindsey --- .../TraceTimelineViewer/SpanDetail/index.tsx | 38 +++++++++++------- .../src/TraceTimelineViewer/utils.test.js | 1 + .../src/common/LabeledList.tsx | 39 +++++++++++-------- .../src/model/transform-trace-data.tsx | 1 + .../jaeger-ui-components/src/types/trace.ts | 1 + .../src/uberUtilityStyles.ts | 4 ++ 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index f423349d98b..729b645e095 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -29,20 +29,21 @@ import { SpanLinkFunc, TNil } from '../../types'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import AccordianReferences from './AccordianReferences'; import { autoColor } from '../../Theme'; +import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { Divider } from '../../common/Divider'; -import { - uAlignIcon, - ubFlex, - ubFlexAuto, - ubItemsCenter, - ubM0, - ubMb1, - ubMy1, - ubTxRightAlign, -} from '../../uberUtilityStyles'; const getStyles = (theme: GrafanaTheme2) => { return { + header: css` + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0 1rem; + margin-bottom: 0.25rem; + `, + listWrapper: css` + overflow: hidden; + `, debugInfo: css` label: debugInfo; display: block; @@ -173,6 +174,15 @@ export default function SpanDetail(props: SpanDetailProps) { label: 'Start Time:', value: formatDuration(relativeStartTime), }, + ...(span.childSpanCount > 0 + ? [ + { + key: 'child_count', + label: 'Child Count:', + value: span.childSpanCount, + }, + ] + : []), ]; const styles = useStyles2(getStyles); const link = createSpanLink?.(span); @@ -180,9 +190,11 @@ export default function SpanDetail(props: SpanDetailProps) { return (
-
-

{operationName}

- +
+

{operationName}

+
+ +
{link ? ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.js index c74238fcb16..a8ad75f111f 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.js @@ -20,6 +20,7 @@ import { isServerSpan, spanContainsErredSpan, spanHasTag, + formatNumber, } from './utils'; import traceGenerator from '../demo/trace-generators'; diff --git a/packages/jaeger-ui-components/src/common/LabeledList.tsx b/packages/jaeger-ui-components/src/common/LabeledList.tsx index 2adb283d0f7..c03cb9482d3 100644 --- a/packages/jaeger-ui-components/src/common/LabeledList.tsx +++ b/packages/jaeger-ui-components/src/common/LabeledList.tsx @@ -17,20 +17,31 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import { useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; +import { autoColor } from '../Theme'; -import { Divider } from './Divider'; - -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { return { LabeledList: css` label: LabeledList; list-style: none; margin: 0; padding: 0; + ${divider === true && + ` + margin-right: -8px; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + `} `, LabeledListItem: css` label: LabeledListItem; display: inline-block; + ${divider === true && + ` + border-right: 1px solid ${autoColor(theme, '#ddd')}; + padding: 0 8px; + `} `, LabeledListLabel: css` label: LabeledListLabel; @@ -42,27 +53,23 @@ const getStyles = (theme: GrafanaTheme2) => { type LabeledListProps = { className?: string; + divider?: boolean; items: Array<{ key: string; label: React.ReactNode; value: React.ReactNode }>; }; export default function LabeledList(props: LabeledListProps) { - const { className, items } = props; - const styles = useStyles2(getStyles); + const { className, divider = false, items } = props; + const styles = useStyles2(getStyles(divider)); + return (
    - {items.map(({ key, label, value }, i) => { - const divider = i < items.length - 1 && ( -
  • - -
  • - ); - return [ -
  • + {items.map(({ key, label, value }) => { + return ( +
  • {label} {value} -
  • , - divider, - ]; + + ); })}
); diff --git a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx b/packages/jaeger-ui-components/src/model/transform-trace-data.tsx index 6652239bba1..1174a2e260b 100644 --- a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx +++ b/packages/jaeger-ui-components/src/model/transform-trace-data.tsx @@ -138,6 +138,7 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra span.relativeStartTime = span.startTime - traceStartTime; span.depth = depth - 1; span.hasChildren = node.children.length > 0; + span.childSpanCount = node.children.length; span.warnings = span.warnings || []; span.tags = span.tags || []; span.references = span.references || []; diff --git a/packages/jaeger-ui-components/src/types/trace.ts b/packages/jaeger-ui-components/src/types/trace.ts index 62e0a899827..4e180189a4d 100644 --- a/packages/jaeger-ui-components/src/types/trace.ts +++ b/packages/jaeger-ui-components/src/types/trace.ts @@ -68,6 +68,7 @@ export type TraceSpanData = { export type TraceSpan = TraceSpanData & { depth: number; hasChildren: boolean; + childSpanCount: number; process: TraceProcess; relativeStartTime: number; tags: NonNullable; diff --git a/packages/jaeger-ui-components/src/uberUtilityStyles.ts b/packages/jaeger-ui-components/src/uberUtilityStyles.ts index f2c7457f2d5..d95db6e9c8c 100644 --- a/packages/jaeger-ui-components/src/uberUtilityStyles.ts +++ b/packages/jaeger-ui-components/src/uberUtilityStyles.ts @@ -34,6 +34,10 @@ export const ubItemsCenter = css` align-items: center; `; +export const ubItemsStart = css` + align-items: start; +`; + export const ubFlexAuto = css` flex: 1 1 auto; min-width: 0; /* 1 */ From 1b2c4dca61648fb65345df329d51b1deaac7a41d Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Sat, 26 Feb 2022 10:45:05 +0100 Subject: [PATCH 051/125] Capitalize Webhook contact point type (#45942) --- pkg/services/ngalert/notifier/available_channels.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 7fb228b1583..65c52ff48e1 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -603,7 +603,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { }, { Type: "webhook", - Name: "webhook", + Name: "Webhook", Description: "Sends HTTP POST request to a URL", Heading: "Webhook settings", Options: []alerting.NotifierOption{ From 1df040eb23e4ee3daa541d048ccd9ed75f718d83 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 28 Feb 2022 09:28:27 +0100 Subject: [PATCH 052/125] CloudWatch: Remove error message when using multi-valued template vars in region field (#45886) * don't show error message when using multi-valued template variables in region field * add unit tests * remove commented code --- .../__mocks__/CloudWatchDataSource.ts | 7 +- .../cloudwatch/components/QueryHeader.tsx | 9 ++- .../datasource/cloudwatch/datasource.test.ts | 65 +++++++++++++++++-- .../datasource/cloudwatch/datasource.ts | 42 ++++++------ 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 8070eeb8688..5479778b689 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -1,11 +1,12 @@ import { dateTime } from '@grafana/data'; import { setBackendSrv } from '@grafana/runtime'; -import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock'; +import { TemplateSrv } from 'app/features/templating/template_srv'; import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer'; import { CustomVariableModel } from 'app/features/variables/types'; import { of } from 'rxjs'; + +import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock'; import { CloudWatchDatasource } from '../datasource'; -import { TemplateSrv } from 'app/features/templating/template_srv'; export function setupMockedDataSource({ data = [], variables }: { data?: any; variables?: any } = {}) { let templateService = new TemplateSrvMock({ @@ -16,6 +17,8 @@ export function setupMockedDataSource({ data = [], variables }: { data?: any; va if (variables) { templateService = new TemplateSrv(); templateService.init(variables); + templateService.getVariables = jest.fn().mockReturnValue(variables); + templateService.getVariableName = (name: string) => name; } const datasource = new CloudWatchDatasource( diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index def9d666832..268abb355e1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import { pick } from 'lodash'; - import { ExploreMode, SelectableValue } from '@grafana/data'; import { EditorHeader, InlineSelect } from '@grafana/experimental'; +import { pick } from 'lodash'; +import React from 'react'; import { CloudWatchDatasource } from '../datasource'; -import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; import { useRegions } from '../hooks'; +import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; import MetricsQueryHeader from './MetricsQueryHeader'; interface QueryHeaderProps { @@ -59,7 +58,7 @@ const QueryHeader: React.FC = ({ v.value === region)} + value={region} placeholder="Select region" allowCustomValue onChange={({ value: region }) => region && onRegion({ value: region })} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 591e95dcee6..840e90ffbad 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -1,17 +1,17 @@ -import { lastValueFrom, of } from 'rxjs'; -import { setDataSourceSrv } from '@grafana/runtime'; import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFrame } from '@grafana/data'; - +import { setDataSourceSrv } from '@grafana/runtime'; +import { lastValueFrom, of } from 'rxjs'; import { toArray } from 'rxjs/operators'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType, CloudWatchLogsQueryStatus } from './types'; + import { - setupMockedDataSource, - namespaceVariable, - metricVariable, labelsVariable, limitVariable, + metricVariable, + namespaceVariable, + setupMockedDataSource, } from './__mocks__/CloudWatchDataSource'; import { CloudWatchDatasource } from './datasource'; +import { CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from './types'; describe('datasource', () => { describe('query', () => { @@ -91,6 +91,57 @@ describe('datasource', () => { }, ]); }); + + describe('debouncedCustomAlert', () => { + const debouncedAlert = jest.fn(); + beforeEach(() => { + const { datasource } = setupMockedDataSource({ + variables: [ + { ...namespaceVariable, multi: true }, + { ...metricVariable, multi: true }, + ], + }); + datasource.debouncedCustomAlert = debouncedAlert; + datasource.performTimeSeriesQuery = jest.fn().mockResolvedValue([]); + datasource.query({ + targets: [ + { + queryMode: 'Metrics', + id: '', + region: 'us-east-2', + namespace: namespaceVariable.id, + metricName: metricVariable.id, + period: '', + alias: '', + dimensions: {}, + matchExact: true, + statistic: '', + refId: '', + expression: 'x * 2', + metricQueryType: MetricQueryType.Search, + metricEditorMode: MetricEditorMode.Code, + }, + ], + } as any); + }); + it('should show debounced alert for namespace and metric name', async () => { + expect(debouncedAlert).toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for namespace' + ); + expect(debouncedAlert).toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for metric name' + ); + }); + + it('should not show debounced alert for region', async () => { + expect(debouncedAlert).not.toHaveBeenCalledWith( + 'CloudWatch templating error', + 'Multi template variables are not supported for region' + ); + }); + }); }); describe('filterMetricQuery', () => { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 91881025ddc..32f1f374671 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -1,9 +1,3 @@ -import React from 'react'; -import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash'; -import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs'; -import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators'; -import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; -import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { DataFrame, DataQueryError, @@ -22,44 +16,50 @@ import { TimeRange, toLegacyResponseData, } from '@grafana/data'; - +import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; +import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; +import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; -import { AppNotificationTimeout } from 'app/types'; -import { store } from 'app/store/store'; -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; +import { VariableWithMultiSupport } from 'app/features/variables/types'; +import { store } from 'app/store/store'; +import { AppNotificationTimeout } from 'app/types'; +import { cloneDeep, find, findLast, isEmpty, isString, set } from 'lodash'; +import React from 'react'; +import { from, lastValueFrom, merge, Observable, of, throwError, zip } from 'rxjs'; +import { catchError, concatMap, finalize, map, mergeMap, repeat, scan, share, takeWhile, tap } from 'rxjs/operators'; + +import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; +import { CloudWatchLanguageProvider } from './language_provider'; import memoizedDebounce from './memoizedDebounce'; +import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider'; import { - MetricEditorMode, CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchLogsQueryStatus, + CloudWatchLogsRequest, CloudWatchMetricsQuery, CloudWatchQuery, DescribeLogGroupsRequest, + Dimensions, GetLogEventsRequest, GetLogGroupFieldsRequest, GetLogGroupFieldsResponse, isCloudWatchLogsQuery, LogAction, - MetricQueryType, + MetricEditorMode, MetricQuery, + MetricQueryType, MetricRequest, StartQueryRequest, TSDBResponse, - Dimensions, - CloudWatchLogsRequest, } from './types'; -import { CloudWatchLanguageProvider } from './language_provider'; -import { VariableWithMultiSupport } from 'app/features/variables/types'; -import { increasingInterval } from './utils/rxjs/increasingInterval'; -import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; import { addDataLinksToLogsResponse } from './utils/datalinks'; import { runWithRetry } from './utils/logsRetry'; -import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; -import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider'; +import { increasingInterval } from './utils/rxjs/increasingInterval'; const DS_QUERY_ENDPOINT = '/api/ds/query'; @@ -268,7 +268,7 @@ export class CloudWatchDatasource const validMetricsQueries = metricQueries .filter(this.filterMetricQuery) .map((item: CloudWatchMetricsQuery): MetricQuery => { - item.region = this.replace(this.getActualRegion(item.region), options.scopedVars, true, 'region'); + item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars); item.namespace = this.replace(item.namespace, options.scopedVars, true, 'namespace'); item.metricName = this.replace(item.metricName, options.scopedVars, true, 'metric name'); item.dimensions = this.convertDimensionFormat(item.dimensions ?? {}, options.scopedVars); From 2c90dcf3c033431bd7d67dc56abec5fad8394cd1 Mon Sep 17 00:00:00 2001 From: Selene Date: Mon, 28 Feb 2022 09:54:56 +0100 Subject: [PATCH 053/125] Dashboard Alert Extractor: Create service for dashboard extractor and remove bus (#45518) * Create DashAlertService service * Remove no used dashboard service from plugin's manager that generates dependency cycle in Enterprise * Remove bus for dashboard permissions * Remove bus from dashboard extractor service * Add missing argument * Fix wire * Fix lint * More goimports * Use datasource service instead sql calls * Fix integration test --- pkg/api/common_test.go | 2 +- pkg/api/dashboard_permission_test.go | 2 +- pkg/api/dashboard_test.go | 4 +- pkg/api/datasources_test.go | 13 +- pkg/api/folder_permission_test.go | 2 +- pkg/api/http_server.go | 5 +- pkg/plugins/manager/dashboards_test.go | 6 +- pkg/plugins/manager/manager.go | 36 ++--- .../manager/manager_integration_test.go | 3 +- pkg/plugins/manager/manager_test.go | 9 +- pkg/server/wire.go | 2 + pkg/server/wireexts_oss.go | 6 +- pkg/services/alerting/engine.go | 41 ++--- .../alerting/engine_integration_test.go | 2 +- pkg/services/alerting/engine_test.go | 2 +- pkg/services/alerting/extractor.go | 86 +++++----- pkg/services/alerting/extractor_test.go | 150 +++++++++++------- pkg/services/alerting/models.go | 7 + pkg/services/alerting/test_rule.go | 9 +- .../dashboards/manager/dashboard_service.go | 75 +++++---- .../dashboard_service_integration_test.go | 28 ++-- .../manager/dashboard_service_test.go | 5 +- .../dashboards/manager/folder_service_test.go | 2 +- .../permissions}/datasource_permissions.go | 2 +- .../datasource_permissions_mocks.go | 8 +- .../libraryelements/libraryelements_test.go | 8 +- .../librarypanels/librarypanels_test.go | 8 +- pkg/services/ngalert/tests/util.go | 2 +- pkg/services/sqlstore/mockstore/mockstore.go | 1 + 29 files changed, 294 insertions(+), 232 deletions(-) rename pkg/{api => services/datasources/permissions}/datasource_permissions.go (96%) rename pkg/{api => services/datasources/permissions}/datasource_permissions_mocks.go (71%) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index f03eb0eb304..993b5aa8e0f 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -364,7 +364,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont RouteRegister: routeRegister, SQLStore: db, searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()), - dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore), + dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore, nil), } // Defining the accesscontrol service has to be done before registering routes diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index a64ef0b8da3..9d40c5529a6 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -29,7 +29,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { hs := &HTTPServer{ Cfg: settings, - dashboardService: dashboardservice.ProvideDashboardService(dashboardStore), + dashboardService: dashboardservice.ProvideDashboardService(dashboardStore, nil), SQLStore: mockSQLStore, } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 36fa2f632bc..ea37a7da2ba 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -219,7 +219,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { Live: newTestLive(t), LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, - dashboardService: service.ProvideDashboardService(dashboardStore), + dashboardService: service.ProvideDashboardService(dashboardStore, nil), SQLStore: mockSQLStore, } hs.SQLStore = mockSQLStore @@ -939,7 +939,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr LibraryPanelService: &libraryPanelsService, LibraryElementService: &libraryElementsService, ProvisioningService: provisioningService, - dashboardProvisioningService: service.ProvideDashboardService(dashboardStore), + dashboardProvisioningService: service.ProvideDashboardService(dashboardStore, nil), SQLStore: sc.sqlStore, } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 0005e7d1aff..7f7191e3428 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" @@ -29,7 +30,7 @@ const ( func TestDataSourcesProxy_userLoggedIn(t *testing.T) { mockSQLStore := mockstore.NewSQLStoreMock() - mockDatasourcePermissionService := newMockDatasourcePermissionService() + mockDatasourcePermissionService := permissions.NewMockDatasourcePermissionService() loggedInUserScenario(t, "When calling GET on", "/api/datasources/", "/api/datasources/", func(sc *scenarioContext) { // Stubs the database query ds := []*models.DataSource{ @@ -38,7 +39,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) { {Name: "BBB"}, {Name: "aaa"}, } - mockDatasourcePermissionService.dsResult = ds + mockDatasourcePermissionService.DsResult = ds // handler func being tested hs := &HTTPServer{ @@ -209,8 +210,8 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { dsServiceMock := &dataSourcesServiceMock{ expectedDatasource: &testDatasource, } - dsPermissionService := newMockDatasourcePermissionService() - dsPermissionService.dsResult = []*models.DataSource{ + dsPermissionService := permissions.NewMockDatasourcePermissionService() + dsPermissionService.DsResult = []*models.DataSource{ &testDatasource, } @@ -505,9 +506,9 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { // mock sqlStore and datasource permission service dsServiceMock.expectedError = test.expectedSQLError dsServiceMock.expectedDatasource = test.expectedDS - dsPermissionService.dsResult = []*models.DataSource{test.expectedDS} + dsPermissionService.DsResult = []*models.DataSource{test.expectedDS} if test.expectedDS == nil { - dsPermissionService.dsResult = nil + dsPermissionService.DsResult = nil } hs.DataSourcesService = dsServiceMock hs.DatasourcePermissionsService = dsPermissionService diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 46df1f74192..1405b75ba2a 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -30,7 +30,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { dashboardStore := &database.FakeDashboardStore{} defer dashboardStore.AssertExpectations(t) - hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore)} + hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore, nil)} t.Run("Given folder not exists", func(t *testing.T) { folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 34770a3bf5d..25040a02729 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -36,6 +36,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" @@ -135,7 +136,7 @@ type HTTPServer struct { dashboardService dashboards.DashboardService dashboardProvisioningService dashboards.DashboardProvisioningService folderService dashboards.FolderService - DatasourcePermissionsService DatasourcePermissionsService + DatasourcePermissionsService permissions.DatasourcePermissionsService commentsService *comments.Service AlertNotificationService *alerting.AlertNotificationService DashboardsnapshotsService *dashboardsnapshots.Service @@ -169,7 +170,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authInfoService login.AuthInfoService, permissionsServices accesscontrol.PermissionsServices, notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService, - datasourcePermissionsService DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService, + datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService, dashboardsnapshotsService *dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings *pluginsettings.ServiceImpl, ) (*HTTPServer, error) { web.Env = cfg.Env diff --git a/pkg/plugins/manager/dashboards_test.go b/pkg/plugins/manager/dashboards_test.go index d801c65e78b..55397f0bff2 100644 --- a/pkg/plugins/manager/dashboards_test.go +++ b/pkg/plugins/manager/dashboards_test.go @@ -11,9 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/services/dashboards/database" - service "github.com/grafana/grafana/pkg/services/dashboards/manager" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -27,9 +24,8 @@ func TestGetPluginDashboards(t *testing.T) { }, } pmCfg := plugins.FromGrafanaCfg(cfg) - dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) pm, err := ProvideService(cfg, loader.New(pmCfg, nil, - signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}), dashboardService) + signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{})) require.NoError(t, err) bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error { diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index d74bad4f744..5e2a4ef1cf6 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation" "github.com/grafana/grafana/pkg/plugins/manager/installer" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -30,38 +29,35 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil) var _ plugins.RendererManager = (*PluginManager)(nil) type PluginManager struct { - cfg *plugins.Cfg - store map[string]*plugins.Plugin - pluginInstaller plugins.Installer - pluginLoader plugins.Loader - pluginsMu sync.RWMutex - pluginPaths map[plugins.Class][]string - dashboardService dashboards.DashboardService - log log.Logger + cfg *plugins.Cfg + store map[string]*plugins.Plugin + pluginInstaller plugins.Installer + pluginLoader plugins.Loader + pluginsMu sync.RWMutex + pluginPaths map[plugins.Class][]string + log log.Logger } -func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader, dashboardService dashboards.DashboardService) (*PluginManager, error) { +func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader) (*PluginManager, error) { pm := New(plugins.FromGrafanaCfg(grafanaCfg), map[plugins.Class][]string{ plugins.Core: corePluginPaths(grafanaCfg), plugins.Bundled: {grafanaCfg.BundledPluginsPath}, plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...), - }, pluginLoader, dashboardService) + }, pluginLoader) if err := pm.Init(); err != nil { return nil, err } return pm, nil } -func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader, - dashboardService dashboards.DashboardService) *PluginManager { +func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader) *PluginManager { return &PluginManager{ - cfg: cfg, - pluginLoader: pluginLoader, - pluginPaths: pluginPaths, - store: make(map[string]*plugins.Plugin), - log: log.New("plugin.manager"), - pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)), - dashboardService: dashboardService, + cfg: cfg, + pluginLoader: pluginLoader, + pluginPaths: pluginPaths, + store: make(map[string]*plugins.Plugin), + log: log.New("plugin.manager"), + pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)), } } diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index e0c871f0428..1bb2651f8d2 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/signature" - service "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/searchV2" @@ -94,7 +93,7 @@ func TestPluginManager_int_init(t *testing.T) { pmCfg := plugins.FromGrafanaCfg(cfg) pm, err := ProvideService(cfg, loader.New(pmCfg, license, signature.NewUnsignedAuthorizer(pmCfg), - provider.ProvideService(coreRegistry)), &service.DashboardServiceImpl{}) + provider.ProvideService(coreRegistry))) require.NoError(t, err) verifyCorePluginCatalogue(t, pm) diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 6a35bd463d0..d0b5d9dc813 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -12,9 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/grafana/grafana/pkg/services/dashboards/database" - service "github.com/grafana/grafana/pkg/services/dashboards/manager" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -469,8 +466,7 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) { func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager { t.Helper() - dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) - pm := New(&plugins.Cfg{}, nil, &fakeLoader{}, dashboardService) + pm := New(&plugins.Cfg{}, nil, &fakeLoader{}) for _, cb := range cbs { cb(pm) @@ -524,8 +520,7 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS cfg.Azure.ManagedIdentityClientId = "client-id" loader := &fakeLoader{} - dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) - manager := New(cfg, nil, loader, dashboardService) + manager := New(cfg, nil, loader) manager.pluginLoader = loader ctx := &managerScenarioCtx{ manager: manager, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index e5b2d32beb3..ab8daa4d39a 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -213,6 +213,8 @@ var wireBasicSet = wire.NewSet( dashboardimportservice.ProvideService, wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)), plugindashboards.ProvideService, + alerting.ProvideDashAlertExtractorService, + wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), comments.ProvideService, ) diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 292b666c3f5..37c66778ded 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -5,7 +5,6 @@ package server import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" @@ -18,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" @@ -75,8 +75,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), ldap.ProvideGroupsService, wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), - api.ProvideDatasourcePermissionsService, - wire.Bind(new(api.DatasourcePermissionsService), new(*api.OSSDatasourcePermissionsService)), + permissions.ProvideDatasourcePermissionsService, + wire.Bind(new(permissions.DatasourcePermissionsService), new(*permissions.OSSDatasourcePermissionsService)), ossaccesscontrol.ProvidePermissionsServices, wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsService)), ) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 54b5e50010d..cb39ff67448 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -45,16 +45,17 @@ type AlertEngine struct { DataService legacydata.RequestHandler Cfg *setting.Cfg - execQueue chan *Job - ticker *Ticker - scheduler scheduler - evalHandler evalHandler - ruleReader ruleReader - log log.Logger - resultHandler resultHandler - usageStatsService usagestats.Service - tracer tracing.Tracer - sqlStore AlertStore + execQueue chan *Job + ticker *Ticker + scheduler scheduler + evalHandler evalHandler + ruleReader ruleReader + log log.Logger + resultHandler resultHandler + usageStatsService usagestats.Service + tracer tracing.Tracer + sqlStore AlertStore + dashAlertExtractor DashAlertExtractor } // IsDisabled returns true if the alerting service is disabled for this instance. @@ -65,16 +66,18 @@ 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, - notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg) *AlertEngine { + notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg, + dashAlertExtractor DashAlertExtractor) *AlertEngine { e := &AlertEngine{ - Cfg: cfg, - RenderService: renderer, - Bus: bus, - RequestValidator: requestValidator, - DataService: dataService, - usageStatsService: usageStatsService, - tracer: tracer, - sqlStore: sqlStore, + Cfg: cfg, + RenderService: renderer, + Bus: bus, + RequestValidator: requestValidator, + DataService: dataService, + usageStatsService: usageStatsService, + tracer: tracer, + sqlStore: sqlStore, + dashAlertExtractor: dashAlertExtractor, } e.ticker = NewTicker(time.Now(), time.Second*0, clock.New(), 1) e.execQueue = make(chan *Job, 1000) diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index f8a308ac5f2..05725068215 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(), nil, tracer, nil, setting.NewCfg()) + engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg(), nil) 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 771970efd95..6e7fb674d68 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -102,7 +102,7 @@ func TestEngineProcessJob(t *testing.T) { require.NoError(t, err) store := &AlertStoreMock{} - engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg()) + engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg(), nil) setting.AlertingEvaluationTimeout = 30 * time.Second setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index c3273ed6ca6..f8af2b8f858 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -6,31 +6,34 @@ import ( "errors" "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/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" ) -// DashAlertExtractor extracts alerts from the dashboard json. -type DashAlertExtractor struct { - User *models.SignedInUser - Dash *models.Dashboard - OrgID int64 - log log.Logger +type DashAlertExtractor interface { + GetAlerts(ctx context.Context, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) + ValidateAlerts(ctx context.Context, dashAlertInfo DashAlertInfo) error } -// NewDashAlertExtractor returns a new DashAlertExtractor. -func NewDashAlertExtractor(dash *models.Dashboard, orgID int64, user *models.SignedInUser) *DashAlertExtractor { - return &DashAlertExtractor{ - User: user, - Dash: dash, - OrgID: orgID, - log: log.New("alerting.extractor"), +// DashAlertExtractorService extracts alerts from the dashboard json. +type DashAlertExtractorService struct { + datasourcePermissionsService permissions.DatasourcePermissionsService + datasourceService datasources.DataSourceService + log log.Logger +} + +func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService) *DashAlertExtractorService { + return &DashAlertExtractorService{ + datasourcePermissionsService: datasourcePermissionsService, + datasourceService: datasourceService, + log: log.New("alerting.extractor"), } } -func (e *DashAlertExtractor) lookupQueryDataSource(ctx context.Context, panel *simplejson.Json, panelQuery *simplejson.Json) (*models.DataSource, error) { +func (e *DashAlertExtractorService) lookupQueryDataSource(ctx context.Context, panel *simplejson.Json, panelQuery *simplejson.Json, orgID int64) (*models.DataSource, error) { dsName := "" dsUid := "" @@ -47,15 +50,15 @@ func (e *DashAlertExtractor) lookupQueryDataSource(ctx context.Context, panel *s } if dsName == "" && dsUid == "" { - query := &models.GetDefaultDataSourceQuery{OrgId: e.OrgID} - if err := bus.Dispatch(ctx, query); err != nil { + query := &models.GetDefaultDataSourceQuery{OrgId: orgID} + if err := e.datasourceService.GetDefaultDataSource(ctx, query); err != nil { return nil, err } return query.Result, nil } - query := &models.GetDataSourceQuery{Name: dsName, Uid: dsUid, OrgId: e.OrgID} - if err := bus.Dispatch(ctx, query); err != nil { + query := &models.GetDataSourceQuery{Name: dsName, Uid: dsUid, OrgId: orgID} + if err := e.datasourceService.GetDataSource(ctx, query); err != nil { return nil, err } @@ -101,7 +104,7 @@ func UAEnabled(ctx context.Context) bool { return enabled } -func (e *DashAlertExtractor) getAlertFromPanels(ctx context.Context, jsonWithPanels *simplejson.Json, validateAlertFunc func(*models.Alert) bool, logTranslationFailures bool) ([]*models.Alert, error) { +func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, jsonWithPanels *simplejson.Json, validateAlertFunc func(*models.Alert) bool, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) { alerts := make([]*models.Alert, 0) for _, panelObj := range jsonWithPanels.Get("panels").MustArray() { @@ -111,7 +114,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(ctx context.Context, jsonWithPan // check if the panel is collapsed if collapsed && collapsedJSON.MustBool() { // extract alerts from sub panels for collapsed panels - alertSlice, err := e.getAlertFromPanels(ctx, panel, validateAlertFunc, logTranslationFailures) + alertSlice, err := e.getAlertFromPanels(ctx, panel, validateAlertFunc, logTranslationFailures, dashAlertInfo) if err != nil { return nil, err } @@ -143,8 +146,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(ctx context.Context, jsonWithPan Err: validationErr.Err, PanelID: panelID, } - if e.Dash != nil { - ve.DashboardID = e.Dash.Id + if dashAlertInfo.Dash != nil { + ve.DashboardID = dashAlertInfo.Dash.Id } return ve } @@ -170,8 +173,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(ctx context.Context, jsonWithPan } alert := &models.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgID, + DashboardId: dashAlertInfo.Dash.Id, + OrgId: dashAlertInfo.OrgID, PanelId: panelID, Id: jsonAlert.Get("id").MustInt64(), Name: jsonAlert.Get("name").MustString(), @@ -198,24 +201,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(ctx context.Context, jsonWithPan return nil, ValidationError{Reason: reason} } - datasource, err := e.lookupQueryDataSource(ctx, panel, panelQuery) + datasource, err := e.lookupQueryDataSource(ctx, panel, panelQuery, dashAlertInfo.OrgID) if err != nil { return nil, err } dsFilterQuery := models.DatasourcesPermissionFilterQuery{ - User: e.User, + User: dashAlertInfo.User, Datasources: []*models.DataSource{datasource}, } - if err := bus.Dispatch(ctx, &dsFilterQuery); err != nil { - if !errors.Is(err, bus.ErrHandlerNotFound) { - return nil, err - } - } else { - if len(dsFilterQuery.Result) == 0 { - return nil, models.ErrDataSourceAccessDenied - } + if err := e.datasourcePermissionsService.FilterDatasourcesBasedOnQueryPermissions(ctx, &dsFilterQuery); err != nil { + return nil, err + } + if len(dsFilterQuery.Result) == 0 { + return nil, models.ErrDataSourceAccessDenied } jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) @@ -250,12 +250,12 @@ func validateAlertRule(alert *models.Alert) bool { } // GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data. -func (e *DashAlertExtractor) GetAlerts(ctx context.Context) ([]*models.Alert, error) { - return e.extractAlerts(ctx, validateAlertRule, true) +func (e *DashAlertExtractorService) GetAlerts(ctx context.Context, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) { + return e.extractAlerts(ctx, validateAlertRule, true, dashAlertInfo) } -func (e *DashAlertExtractor) extractAlerts(ctx context.Context, validateFunc func(alert *models.Alert) bool, logTranslationFailures bool) ([]*models.Alert, error) { - dashboardJSON, err := copyJSON(e.Dash.Data) +func (e *DashAlertExtractorService) extractAlerts(ctx context.Context, validateFunc func(alert *models.Alert) bool, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) { + dashboardJSON, err := copyJSON(dashAlertInfo.Dash.Data) if err != nil { return nil, err } @@ -268,7 +268,7 @@ func (e *DashAlertExtractor) extractAlerts(ctx context.Context, validateFunc fun if len(rows) > 0 { for _, rowObj := range rows { row := simplejson.NewFromAny(rowObj) - a, err := e.getAlertFromPanels(ctx, row, validateFunc, logTranslationFailures) + a, err := e.getAlertFromPanels(ctx, row, validateFunc, logTranslationFailures, dashAlertInfo) if err != nil { return nil, err } @@ -276,7 +276,7 @@ func (e *DashAlertExtractor) extractAlerts(ctx context.Context, validateFunc fun alerts = append(alerts, a...) } } else { - a, err := e.getAlertFromPanels(ctx, dashboardJSON, validateFunc, logTranslationFailures) + a, err := e.getAlertFromPanels(ctx, dashboardJSON, validateFunc, logTranslationFailures, dashAlertInfo) if err != nil { return nil, err } @@ -290,9 +290,9 @@ func (e *DashAlertExtractor) extractAlerts(ctx context.Context, validateFunc fun // ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id // in the first validation pass. -func (e *DashAlertExtractor) ValidateAlerts(ctx context.Context) error { +func (e *DashAlertExtractorService) ValidateAlerts(ctx context.Context, dashAlertInfo DashAlertInfo) error { _, err := e.extractAlerts(ctx, func(alert *models.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 - }, false) + }, false, dashAlertInfo) return err } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 68ee6cc3567..6b8ea7861a6 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -6,9 +6,10 @@ import ( "testing" "time" - "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/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/require" ) @@ -21,40 +22,24 @@ func TestAlertRuleExtraction(t *testing.T) { // mock data defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true, Uid: "def-uid"} graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2", Uid: "graphite2-uid"} - influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB", Uid: "InfluxDB-uid"} - prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus", Uid: "Prometheus-uid"} - - bus.AddHandler("test", func(ctx context.Context, query *models.GetDefaultDataSourceQuery) error { - query.Result = defaultDs - return nil - }) - - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error { - if query.Name == defaultDs.Name || query.Uid == defaultDs.Uid { - query.Result = defaultDs - } - if query.Name == graphite2Ds.Name || query.Uid == graphite2Ds.Uid { - query.Result = graphite2Ds - } - if query.Name == influxDBDs.Name || query.Uid == influxDBDs.Uid { - query.Result = influxDBDs - } - if query.Name == prom.Name || query.Uid == prom.Uid { - query.Result = prom - } - - return nil - }) json, err := ioutil.ReadFile("./testdata/graphite-alert.json") require.Nil(t, err) + dsPermissions := permissions.NewMockDatasourcePermissionService() + dsPermissions.DsResult = []*models.DataSource{ + { + Id: 1, + }, + } + + dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs} + extractor := ProvideDashAlertExtractorService(dsPermissions, dsService) + t.Run("Parsing alert rules from dashboard json", func(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - getTarget := func(j *simplejson.Json) string { rowObj := j.Get("rows").MustArray()[0] row := simplejson.NewFromAny(rowObj) @@ -67,8 +52,11 @@ func TestAlertRuleExtraction(t *testing.T) { require.Equal(t, getTarget(dashJSON), "") - extractor := NewDashAlertExtractor(dash, 1, nil) - _, _ = extractor.GetAlerts(context.Background()) + _, _ = extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Equal(t, getTarget(dashJSON), "") }) @@ -77,10 +65,12 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - - alerts, err := extractor.GetAlerts(context.Background()) + dsService.ExpectedDatasource = &models.DataSource{Id: 12} + alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Nil(t, err) @@ -127,10 +117,12 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(panelWithoutID) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - _, err = extractor.GetAlerts(context.Background()) + _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.NotNil(t, err) }) @@ -141,10 +133,12 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(panelWithIDZero) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - _, err = extractor.GetAlerts(context.Background()) + _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.NotNil(t, err) }) @@ -154,10 +148,12 @@ func TestAlertRuleExtraction(t *testing.T) { require.Nil(t, err) dashJSON, err := simplejson.NewJson(panelWithQuery) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true)) + _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Equal(t, "alert validation error: Alert on PanelId: 2 refers to query(B) that cannot be found. Legacy alerting queries are not able to be removed at this time in order to preserve the ability to rollback to previous versions of Grafana", err.Error()) }) @@ -167,10 +163,13 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(panelWithoutSpecifiedDatasource) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - alerts, err := extractor.GetAlerts(context.Background()) + dsService.ExpectedDatasource = &models.DataSource{Id: 12} + alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Nil(t, err) condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0]) @@ -184,10 +183,12 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - alerts, err := extractor.GetAlerts(context.Background()) + alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Nil(t, err) require.Len(t, alerts, 2) @@ -209,10 +210,12 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - alerts, err := extractor.GetAlerts(context.Background()) + alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) require.Nil(t, err) require.Len(t, alerts, 1) @@ -235,9 +238,12 @@ func TestAlertRuleExtraction(t *testing.T) { require.Nil(t, err) dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - alerts, err := extractor.GetAlerts(context.Background()) + alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ + User: nil, + Dash: dash, + OrgID: 1, + }) require.Nil(t, err) require.Len(t, alerts, 4) @@ -249,14 +255,17 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - err = extractor.ValidateAlerts(context.Background()) + dashAlertInfo := DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + } + err = extractor.ValidateAlerts(context.Background(), dashAlertInfo) require.Nil(t, err) - _, err = extractor.GetAlerts(context.Background()) + _, err = extractor.GetAlerts(context.Background(), dashAlertInfo) require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") }) @@ -266,14 +275,18 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1, nil) - err = extractor.ValidateAlerts(context.Background()) + dsService.ExpectedDatasource = graphite2Ds + dashAlertInfo := DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + } + err = extractor.ValidateAlerts(context.Background(), dashAlertInfo) require.Nil(t, err) - alerts, err := extractor.GetAlerts(context.Background()) + alerts, err := extractor.GetAlerts(context.Background(), dashAlertInfo) require.Nil(t, err) condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0]) @@ -281,3 +294,18 @@ func TestAlertRuleExtraction(t *testing.T) { require.EqualValues(t, 15, query.Get("datasourceId").MustInt64()) }) } + +type fakeDatasourceService struct { + ExpectedDatasource *models.DataSource + datasources.DataSourceService +} + +func (f *fakeDatasourceService) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error { + query.Result = f.ExpectedDatasource + return nil +} + +func (f *fakeDatasourceService) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { + query.Result = f.ExpectedDatasource + return nil +} diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 0085418ac74..d4cb0bf84b5 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/models" ) // Job holds state about when the alert rule should be evaluated. @@ -42,3 +43,9 @@ type EvalMatch struct { Metric string `json:"metric"` Tags map[string]string `json:"tags"` } + +type DashAlertInfo struct { + User *models.SignedInUser + Dash *models.Dashboard + OrgID int64 +} diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 3ccee726f6c..d04e052672f 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -11,9 +11,12 @@ import ( // AlertTest makes a test alert. func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID int64, user *models.SignedInUser) (*EvalContext, error) { dash := models.NewDashboardFromJson(dashboard) - - extractor := NewDashAlertExtractor(dash, orgID, user) - alerts, err := extractor.GetAlerts(context.Background()) + dashInfo := DashAlertInfo{ + User: user, + Dash: dash, + OrgID: orgID, + } + alerts, err := e.dashAlertExtractor.GetAlerts(context.Background(), dashInfo) if err != nil { return nil, err } diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index 93457ffa7bc..a48829ee8c8 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -19,14 +19,16 @@ import ( ) type DashboardServiceImpl struct { - dashboardStore m.Store - log log.Logger + dashboardStore m.Store + dashAlertExtractor alerting.DashAlertExtractor + log log.Logger } -func ProvideDashboardService(store m.Store) *DashboardServiceImpl { +func ProvideDashboardService(store m.Store, dashAlertExtractor alerting.DashAlertExtractor) *DashboardServiceImpl { return &DashboardServiceImpl{ - dashboardStore: store, - log: log.New("dashboard-service"), + dashboardStore: store, + dashAlertExtractor: dashAlertExtractor, + log: log.New("dashboard-service"), } } @@ -74,7 +76,8 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if shouldValidateAlerts { - if err := validateAlerts(ctx, dash, dto.User); err != nil { + dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgId} + if err := dr.dashAlertExtractor.ValidateAlerts(ctx, dashAlertInfo); err != nil { return nil, err } } @@ -139,11 +142,6 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) } -var validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - extractor := alerting.NewDashAlertExtractor(dash, dash.OrgId, user) - return extractor.ValidateAlerts(ctx) -} - func validateDashboardRefreshInterval(dash *models.Dashboard) error { if setting.MinRefreshInterval == "" { return nil @@ -171,19 +169,6 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { return nil } -// UpdateAlerting updates alerting. -// -// Stubbable by tests. -var UpdateAlerting = func(ctx context.Context, store m.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { - extractor := alerting.NewDashAlertExtractor(dashboard, orgID, user) - alerts, err := extractor.GetAlerts(ctx) - if err != nil { - return err - } - - return store.SaveAlerts(ctx, dashboard.Id, alerts) -} - func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *m.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { @@ -210,7 +195,19 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt } // alerts - if err := UpdateAlerting(ctx, dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { + dashAlertInfo := alerting.DashAlertInfo{ + User: dto.User, + Dash: dash, + OrgID: dto.OrgId, + } + + alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) + if err != nil { + return nil, err + } + + err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + if err != nil { return nil, err } @@ -232,7 +229,19 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return nil, err } - if err := UpdateAlerting(ctx, dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { + dashAlertInfo := alerting.DashAlertInfo{ + User: dto.User, + Dash: dash, + OrgID: dto.OrgId, + } + + alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) + if err != nil { + return nil, err + } + + err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + if err != nil { return nil, err } @@ -258,7 +267,19 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *m.SaveDa return nil, fmt.Errorf("saving dashboard failed: %w", err) } - if err := UpdateAlerting(ctx, dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { + dashAlertInfo := alerting.DashAlertInfo{ + User: dto.User, + Dash: dash, + OrgID: dto.OrgId, + } + + alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) + if err != nil { + return nil, err + } + + err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + if err != nil { return nil, err } diff --git a/pkg/services/dashboards/manager/dashboard_service_integration_test.go b/pkg/services/dashboards/manager/dashboard_service_integration_test.go index 00bc7ca305b..713b56aedd5 100644 --- a/pkg/services/dashboards/manager/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_integration_test.go @@ -5,6 +5,7 @@ package service import ( "context" + "github.com/grafana/grafana/pkg/services/alerting" "testing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -21,14 +22,6 @@ const testOrgID int64 = 1 func TestIntegratedDashboardService(t *testing.T) { t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { - origUpdateAlerting := UpdateAlerting - t.Cleanup(func() { - UpdateAlerting = origUpdateAlerting - }) - UpdateAlerting = func(ctx context.Context, store dashbboardservice.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { - return nil - } - // Basic validation tests permissionScenario(t, "When saving a dashboard with non-existing id", true, @@ -861,7 +854,7 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore dto := toSaveDashboardDto(cmd) dashboardStore := database.ProvideDashboardStore(sqlStore) - res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) + res, err := ProvideDashboardService(dashboardStore, &dummyDashAlertExtractor{}).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res @@ -870,7 +863,7 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) error { dto := toSaveDashboardDto(cmd) dashboardStore := database.ProvideDashboardStore(sqlStore) - _, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) + _, err := ProvideDashboardService(dashboardStore, &dummyDashAlertExtractor{}).SaveDashboard(context.Background(), &dto, false) return err } @@ -897,7 +890,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto } dashboardStore := database.ProvideDashboardStore(sqlStore) - res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) + res, err := ProvideDashboardService(dashboardStore, &dummyDashAlertExtractor{}).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res @@ -925,7 +918,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore. } dashboardStore := database.ProvideDashboardStore(sqlStore) - res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) + res, err := ProvideDashboardService(dashboardStore, &dummyDashAlertExtractor{}).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res @@ -942,3 +935,14 @@ func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashbboardservice.SaveD Overwrite: cmd.Overwrite, } } + +type dummyDashAlertExtractor struct { +} + +func (d *dummyDashAlertExtractor) GetAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) ([]*models.Alert, error) { + return nil, nil +} + +func (d *dummyDashAlertExtractor) ValidateAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) error { + return nil +} diff --git a/pkg/services/dashboards/manager/dashboard_service_test.go b/pkg/services/dashboards/manager/dashboard_service_test.go index 1fb721e2e9f..dba479c8160 100644 --- a/pkg/services/dashboards/manager/dashboard_service_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_test.go @@ -27,8 +27,9 @@ func TestDashboardService(t *testing.T) { fakeStore := database.FakeDashboardStore{} defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: &fakeStore, + log: log.New("test.logger"), + dashboardStore: &fakeStore, + dashAlertExtractor: &dummyDashAlertExtractor{}, } origNewDashboardGuardian := guardian.New diff --git a/pkg/services/dashboards/manager/folder_service_test.go b/pkg/services/dashboards/manager/folder_service_test.go index 962e0fe88de..9b199959694 100644 --- a/pkg/services/dashboards/manager/folder_service_test.go +++ b/pkg/services/dashboards/manager/folder_service_test.go @@ -25,7 +25,7 @@ func TestFolderService(t *testing.T) { store := &database.FakeDashboardStore{} defer store.AssertExpectations(t) service := ProvideFolderService( - &dashboards.FakeDashboardService{DashboardService: ProvideDashboardService(store)}, + &dashboards.FakeDashboardService{DashboardService: ProvideDashboardService(store, nil)}, store, nil, ) diff --git a/pkg/api/datasource_permissions.go b/pkg/services/datasources/permissions/datasource_permissions.go similarity index 96% rename from pkg/api/datasource_permissions.go rename to pkg/services/datasources/permissions/datasource_permissions.go index 5035b8da6c9..9dee1b2d0b5 100644 --- a/pkg/api/datasource_permissions.go +++ b/pkg/services/datasources/permissions/datasource_permissions.go @@ -1,4 +1,4 @@ -package api +package permissions import ( "context" diff --git a/pkg/api/datasource_permissions_mocks.go b/pkg/services/datasources/permissions/datasource_permissions_mocks.go similarity index 71% rename from pkg/api/datasource_permissions_mocks.go rename to pkg/services/datasources/permissions/datasource_permissions_mocks.go index 1fd921fa6ac..767fdda5e5d 100644 --- a/pkg/api/datasource_permissions_mocks.go +++ b/pkg/services/datasources/permissions/datasource_permissions_mocks.go @@ -1,4 +1,4 @@ -package api +package permissions import ( "context" @@ -7,14 +7,14 @@ import ( ) type mockDatasourcePermissionService struct { - dsResult []*models.DataSource + DsResult []*models.DataSource } func (m *mockDatasourcePermissionService) FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error { - cmd.Result = m.dsResult + cmd.Result = m.DsResult return nil } -func newMockDatasourcePermissionService() *mockDatasourcePermissionService { +func NewMockDatasourcePermissionService() *mockDatasourcePermissionService { return &mockDatasourcePermissionService{} } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 8c1df027581..273b90ad2d8 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "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/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" @@ -195,7 +196,8 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign } dashboardStore := database.ProvideDashboardStore(sqlStore) - dashboard, err := dashboardservice.ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), dashItem, true) + dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil) + dashboard, err := dashboardservice.ProvideDashboardService(dashboardStore, dashAlertExtractor).SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) return dashboard @@ -206,7 +208,7 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string t.Helper() dashboardStore := database.ProvideDashboardStore(sqlStore) - d := dashboardservice.ProvideDashboardService(dashboardStore) + d := dashboardservice.ProvideDashboardService(dashboardStore, nil) s := dashboardservice.ProvideFolderService(d, dashboardStore, nil) t.Logf("Creating folder with title and UID %q", title) folder, err := s.CreateFolder(context.Background(), &user, user.OrgId, title, title) @@ -292,7 +294,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) dashboardStore := database.ProvideDashboardStore(sqlStore) - dashboardService := dashboardservice.ProvideDashboardService(dashboardStore) + dashboardService := dashboardservice.ProvideDashboardService(dashboardStore, &alerting.DashAlertExtractorService{}) service := LibraryElementService{ Cfg: setting.NewCfg(), SQLStore: sqlStore, diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 7a101269709..51c5e7e957c 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "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/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" @@ -1416,7 +1417,8 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig } dashboadStore := database.ProvideDashboardStore(sqlStore) - dashboard, err := dashboardservice.ProvideDashboardService(dashboadStore).SaveDashboard(context.Background(), dashItem, true) + dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil) + dashboard, err := dashboardservice.ProvideDashboardService(dashboadStore, dashAlertService).SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) return dashboard @@ -1427,7 +1429,7 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string t.Helper() dashboardStore := database.ProvideDashboardStore(sqlStore) - d := dashboardservice.ProvideDashboardService(dashboardStore) + d := dashboardservice.ProvideDashboardService(dashboardStore, nil) s := dashboardservice.ProvideFolderService(d, dashboardStore, nil) t.Logf("Creating folder with title and UID %q", title) folder, err := s.CreateFolder(context.Background(), user, user.OrgId, title, title) @@ -1516,7 +1518,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) dashboardStore := database.ProvideDashboardStore(sqlStore) - folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore), dashboardStore, nil) + folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore, &alerting.DashAlertExtractorService{}), dashboardStore, nil) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService) service := LibraryPanelService{ diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 5a9dfaf1e02..826fe6e1f64 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -41,7 +41,7 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * sqlStore := sqlstore.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) dashboardStore := databasestore.ProvideDashboardStore(sqlStore) - folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore), dashboardStore, nil) + folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore, nil), dashboardStore, nil) ng, err := ngalert.ProvideService( cfg, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, nil, secretsService, nil, m, folderService, diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 61b19b0a53c..33d5c5f45bc 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -503,6 +503,7 @@ func (m *SQLStoreMock) GetDataSourcesByType(ctx context.Context, query *models.G } func (m *SQLStoreMock) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error { + query.Result = m.ExpectedDatasource return m.ExpectedError } From 9b6552c7b4c89c03bc077a55aef5984d7daded13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 28 Feb 2022 10:37:22 +0100 Subject: [PATCH 054/125] Prometheus: Fixes crash in the new options component when range was undefined (#45921) * Prometheus: Fixes crash in the new options component when range was undefined * Simplified conditions --- .../components/PromQueryBuilderOptions.test.tsx | 8 +++++++- .../datasource/prometheus/querybuilder/types.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx index ac6b5132951..153a72ce5ee 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx @@ -59,13 +59,19 @@ describe('PromQueryBuilderOptions', () => { legendFormat: '{{label_name}}', }); }); + + it('Handle defaults with undefined range', async () => { + setup(getQueryWithDefaults({ refId: 'A', expr: '', range: undefined, instant: true }, CoreApp.Dashboard)); + + expect(screen.getByText('Type: Instant')).toBeInTheDocument(); + }); }); function setup(queryOverrides: Partial = {}) { const props = { query: { ...getQueryWithDefaults({ refId: 'A' } as PromQuery, CoreApp.PanelEditor), - queryOverrides, + ...queryOverrides, }, onRunQuery: jest.fn(), onChange: jest.fn(), diff --git a/public/app/plugins/datasource/prometheus/querybuilder/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/types.ts index 7c639684958..e51ad1ff144 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/types.ts @@ -131,14 +131,14 @@ export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined) result = { ...result, expr: '', legendFormat: LegendFormatMode.Auto }; } - // Default to range query - if (query.range == null) { + if (query.range == null && query.instant == null) { + // Default to range query result = { ...result, range: true }; - } - // In explore we default to both instant & range - if (query.instant == null && app === CoreApp.Explore) { - result = { ...result, instant: true }; + // In explore we default to both instant & range + if (app === CoreApp.Explore) { + result.instant = true; + } } return result; From 4ab191b6121d9a7aed7cb0e35477b1d3ed53772a Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:52:58 +0100 Subject: [PATCH 055/125] Prometheus: Create AutoSizeInput with dynamic width (#45601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Autosize input for dynamic width * Update * Refactoring to use measureText util instead * removed react fragment tags * Add tests * Use AutoSize input in legend, step and nested queries vector matcher * Update * Remove unused imports Co-authored-by: Torkel Ödegaard --- .../grafana-ui/src/components/Input/Input.tsx | 2 +- .../querybuilder/components/NestedQuery.tsx | 9 +-- .../components/PromQueryBuilderOptions.tsx | 11 +-- .../components/PromQueryLegendEditor.tsx | 11 +-- .../shared/AutoSizeInput.test.tsx | 51 +++++++++++++ .../querybuilder/shared/AutoSizeInput.tsx | 71 +++++++++++++++++++ .../shared/OperationParamEditor.tsx | 17 ++--- 7 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 4f2eb4664b2..e24d961514c 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -43,7 +43,7 @@ export const Input = React.forwardRef((props, ref) => { const styles = getInputStyles({ theme, invalid: !!invalid, width }); return ( -
+
{!!addonBefore &&
{addonBefore}
}
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx index 46f0e3cd5d0..5a7b92dcfa1 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx @@ -1,9 +1,10 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, toOption } from '@grafana/data'; import { EditorRows, FlexItem } from '@grafana/experimental'; -import { IconButton, Input, Select, useStyles2 } from '@grafana/ui'; +import { IconButton, Select, useStyles2 } from '@grafana/ui'; import React from 'react'; import { PrometheusDatasource } from '../../datasource'; +import { AutoSizeInput } from '../shared/AutoSizeInput'; import { PromVisualQueryBinary } from '../types'; import { PromQueryBuilder } from './PromQueryBuilder'; @@ -36,10 +37,10 @@ export const NestedQuery = React.memo(({ nestedQuery, index, datasource, />
Vector matches
- { + onCommitChange={(evt) => { onChange(index, { ...nestedQuery, vectorMatches: evt.currentTarget.value, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index 33bcc9ae8e4..a9f63a62f57 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -1,12 +1,13 @@ import React, { SyntheticEvent } from 'react'; import { EditorRow, EditorField } from '@grafana/experimental'; import { CoreApp, SelectableValue } from '@grafana/data'; -import { Input, RadioButtonGroup, Select, Switch } from '@grafana/ui'; +import { RadioButtonGroup, Select, Switch } from '@grafana/ui'; import { QueryOptionGroup } from '../shared/QueryOptionGroup'; import { PromQuery } from '../../types'; import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from '../../components/PromQueryEditor'; import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField'; import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor'; +import { AutoSizeInput } from '../shared/AutoSizeInput'; export interface Props { query: PromQuery; @@ -21,7 +22,7 @@ export const PromQueryBuilderOptions = React.memo(({ query, app, onChange onRunQuery(); }; - const onChangeStep = (evt: React.FocusEvent) => { + const onChangeStep = (evt: React.FormEvent) => { onChange({ ...query, interval: evt.currentTarget.value }); onRunQuery(); }; @@ -57,12 +58,12 @@ export const PromQueryBuilderOptions = React.memo(({ query, app, onChange } > - diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryLegendEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryLegendEditor.tsx index 3a9ba54a9a4..f5c920a0a09 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryLegendEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryLegendEditor.tsx @@ -1,8 +1,9 @@ import React, { useRef } from 'react'; import { EditorField } from '@grafana/experimental'; import { SelectableValue } from '@grafana/data'; -import { Input, Select } from '@grafana/ui'; +import { Select } from '@grafana/ui'; import { LegendFormatMode, PromQuery } from '../../types'; +import { AutoSizeInput } from '../shared/AutoSizeInput'; export interface Props { query: PromQuery; @@ -27,7 +28,7 @@ export const PromQueryLegendEditor = React.memo(({ query, onChange, onRun const mode = getLegendMode(query.legendFormat); const inputRef = useRef(null); - const onLegendFormatChanged = (evt: React.FocusEvent) => { + const onLegendFormatChanged = (evt: React.FormEvent) => { let legendFormat = evt.currentTarget.value; if (legendFormat.length === 0) { legendFormat = LegendFormatMode.Auto; @@ -62,12 +63,12 @@ export const PromQueryLegendEditor = React.memo(({ query, onChange, onRun > <> {mode === LegendFormatMode.Custom && ( - )} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx new file mode 100644 index 00000000000..0e0ef42b541 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { screen, render, fireEvent } from '@testing-library/react'; +import { AutoSizeInput } from './AutoSizeInput'; + +jest.mock('@grafana/ui', () => { + const original = jest.requireActual('@grafana/ui'); + const mockedUi = { ...original }; + + // Mocking measureText + mockedUi.measureText = (text: string, fontSize: number) => { + return { width: text.length * fontSize }; + }; + + return mockedUi; +}); + +describe('AutoSizeInput', () => { + it('should have default minWidth when empty', () => { + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper'); + + fireEvent.change(input, { target: { value: '' } }); + + expect(input.value).toBe(''); + expect(getComputedStyle(inputWrapper).width).toBe('80px'); + }); + + it('should have default minWidth for short content', () => { + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper'); + + fireEvent.change(input, { target: { value: 'foo' } }); + + expect(input.value).toBe('foo'); + expect(getComputedStyle(inputWrapper).width).toBe('80px'); + }); + + it('should change width for long content', () => { + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper'); + + fireEvent.change(input, { target: { value: 'very very long value' } }); + expect(getComputedStyle(inputWrapper).width).toBe('304px'); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx new file mode 100644 index 00000000000..a0223f596c7 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx @@ -0,0 +1,71 @@ +import { Input, measureText } from '@grafana/ui'; +import { Props as InputProps } from '@grafana/ui/src/components/Input/Input'; +import React, { useEffect } from 'react'; +export interface Props extends InputProps { + /** Sets the min-width to a multiple of 8px. Default value is 10*/ + minWidth?: number; + /** Sets the max-width to a multiple of 8px.*/ + maxWidth?: number; + /** onChange function that will be run on onBlur and onKeyPress with enter*/ + onCommitChange?: (event: React.FormEvent) => void; +} + +export const AutoSizeInput = React.forwardRef((props, ref) => { + const { defaultValue = '', minWidth = 10, maxWidth, onCommitChange, onKeyDown, onBlur, ...restProps } = props; + const [value, setValue] = React.useState(defaultValue); + const [inputWidth, setInputWidth] = React.useState(minWidth); + + useEffect(() => { + setInputWidth(getWidthFor(value.toString(), minWidth, maxWidth)); + }, [value, maxWidth, minWidth]); + + return ( + { + setValue(event.currentTarget.value); + }} + width={inputWidth} + onBlur={(event) => { + if (onCommitChange) { + onCommitChange(event); + } + if (onBlur) { + onBlur(event); + } + }} + onKeyDown={(event) => { + if (event.key === 'Enter' && onCommitChange) { + onCommitChange(event); + } + if (onKeyDown) { + onKeyDown(event); + } + }} + data-testid={'autosize-input'} + /> + ); +}); + +function getWidthFor(value: string, minWidth: number, maxWidth: number | undefined): number { + if (!value) { + return minWidth; + } + + const extraSpace = 3; + const realWidth = measureText(value.toString(), 14).width / 8 + extraSpace; + + if (minWidth && realWidth < minWidth) { + return minWidth; + } + + if (maxWidth && realWidth > maxWidth) { + return realWidth; + } + + return realWidth; +} + +AutoSizeInput.displayName = 'AutoSizeInput'; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx index 3549b82a2ac..0befd0b5760 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx @@ -1,7 +1,8 @@ import { SelectableValue, toOption } from '@grafana/data'; -import { Input, Select } from '@grafana/ui'; +import { Select } from '@grafana/ui'; import React, { ComponentType } from 'react'; import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types'; +import { AutoSizeInput } from './AutoSizeInput'; import { getOperationParamId } from './operationUtils'; export function getOperationParamEditor( @@ -20,18 +21,10 @@ export function getOperationParamEditor( function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) { return ( - { - if (evt.key === 'Enter') { - if (evt.currentTarget.value !== props.value) { - props.onChange(props.index, evt.currentTarget.value); - } - props.onRunQuery(); - } - }} - onBlur={(evt) => { + defaultValue={props.value} + onCommitChange={(evt) => { props.onChange(props.index, evt.currentTarget.value); }} /> From f4658b72e1d3673af8cd7964b7d8d3199337c9dd Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:15:33 +0000 Subject: [PATCH 056/125] InfluxDB: variables migration to backend (#45512) * Remove check for explore * Remove core app import * applyVariables for select * applyVariables for tags * applyVariables for other query props * applyTemplateVariables for flux * applyTemplateVariables for influx * Backwards compatibility * interpolateVariablesInQueries * Update InfluxQL mode and alias in test * Should interpolate all variables with Flux mode * Variables should be interpolated correctly * Removed unused import * explore in if check * Removing backwards compat copy from classicQuery * Return expandedQuery * Const --- .../plugins/datasource/influxdb/datasource.ts | 130 +++++++++--------- .../influxdb/specs/datasource.test.ts | 122 +++++++++++----- 2 files changed, 151 insertions(+), 101 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index e9cff1ca03a..ae483f9b057 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -227,18 +227,21 @@ export default class InfluxDatasource extends DataSourceWithBackend { - // this only works in flux-mode, it should not be called in non-flux-mode - if (!this.isFlux) { - return query; - } - // We want to interpolate these variables on backend const { __interval, __interval_ms, ...rest } = scopedVars; - return { - ...query, - query: this.templateSrv.replace(query.query ?? '', rest), // The raw query text - }; + if (this.isFlux) { + return { + ...query, + query: this.templateSrv.replace(query.query ?? '', rest), // The raw query text + }; + } + + if (config.featureToggles.influxdbBackendMigration && this.access === 'proxy') { + query = this.applyVariables(query, scopedVars, rest); + } + + return query; } /** @@ -381,67 +384,68 @@ export default class InfluxDatasource extends DataSourceWithBackend 0) { - expandedQueries = queries.map((query) => { - if (this.isFlux) { - return { - ...query, - datasource: this.getRef(), - query: this.templateSrv.replace(query.query ?? '', scopedVars, 'regex'), - }; - } - - const expandedQuery = { + return queries.map((query) => { + if (this.isFlux) { + return { ...query, datasource: this.getRef(), - measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'), - policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'), - limit: this.templateSrv.replace(query.limit?.toString() ?? '', scopedVars, 'regex'), - slimit: this.templateSrv.replace(query.slimit?.toString() ?? '', scopedVars, 'regex'), - tz: this.templateSrv.replace(query.tz ?? '', scopedVars), + query: this.templateSrv.replace(query.query ?? '', scopedVars), // The raw query text }; + } - if (query.rawQuery) { - expandedQuery.query = this.templateSrv.replace(query.query ?? '', scopedVars, 'regex'); - } + return { + ...query, + datasource: this.getRef(), + ...this.applyVariables(query, scopedVars, scopedVars), + }; + }); + } - if (query.groupBy) { - expandedQuery.groupBy = query.groupBy.map((groupBy) => { - return { - ...groupBy, - params: groupBy.params?.map((param) => { - return this.templateSrv.replace(param.toString(), undefined, 'regex'); - }), - }; - }); - } - - if (query.select) { - expandedQuery.select = query.select.map((selects) => { - return selects.map((select: any) => { - return { - ...select, - params: select.params?.map((param: any) => { - return this.templateSrv.replace(param.toString(), undefined, 'regex'); - }), - }; - }); - }); - } - - if (query.tags) { - expandedQuery.tags = query.tags.map((tag) => { - return { - ...tag, - value: this.templateSrv.replace(tag.value, undefined, 'regex'), - }; - }); - } - return expandedQuery; + applyVariables(query: InfluxQuery, scopedVars: ScopedVars, rest: ScopedVars) { + const expandedQuery = { ...query }; + if (query.groupBy) { + expandedQuery.groupBy = query.groupBy.map((groupBy) => { + return { + ...groupBy, + params: groupBy.params?.map((param) => { + return this.templateSrv.replace(param.toString(), undefined, 'regex'); + }), + }; }); } - return expandedQueries; + + if (query.select) { + expandedQuery.select = query.select.map((selects) => { + return selects.map((select: any) => { + return { + ...select, + params: select.params?.map((param: any) => { + return this.templateSrv.replace(param.toString(), undefined, 'regex'); + }), + }; + }); + }); + } + + if (query.tags) { + expandedQuery.tags = query.tags.map((tag) => { + return { + ...tag, + value: this.templateSrv.replace(tag.value, undefined, 'regex'), + }; + }); + } + + return { + ...expandedQuery, + query: this.templateSrv.replace(query.query ?? '', rest), // The raw query text + alias: this.templateSrv.replace(query.alias ?? '', scopedVars), + limit: this.templateSrv.replace(query.limit?.toString() ?? '', scopedVars, 'regex'), + measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'), + policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'), + slimit: this.templateSrv.replace(query.slimit?.toString() ?? '', scopedVars, 'regex'), + tz: this.templateSrv.replace(query.tz ?? '', scopedVars), + }; } async metricFindQuery(query: string, options?: any): Promise { diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index c9042cd72ea..eb3fcc2e556 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -4,6 +4,7 @@ import { FetchResponse } from '@grafana/runtime'; import InfluxDatasource from '../datasource'; import { TemplateSrvStub } from 'test/specs/helpers'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ +import config from 'app/core/config'; //@ts-ignore const templateSrv = new TemplateSrvStub(); @@ -174,54 +175,99 @@ describe('InfluxDataSource', () => { }); }); - describe('Interpolating query variables for dashboard->explore', () => { + describe('Variables should be interpolated correctly', () => { const templateSrv: any = { replace: jest.fn() }; const instanceSettings: any = {}; const ds = new InfluxDatasource(instanceSettings, templateSrv); const text = 'interpolationText'; + templateSrv.replace.mockReturnValue(text); - it('Should interpolate all variables', () => { - const query = { - refId: 'x', - measurement: '$interpolationVar', - policy: '$interpolationVar', - limit: '$interpolationVar', - slimit: '$interpolationVar', - tz: '$interpolationVar', - tags: [ - { - key: 'cpu', - operator: '=~', - value: '/^$interpolationVar$/', - }, - ], - groupBy: [ + const fluxQuery = { + refId: 'x', + query: '$interpolationVar', + }; + + const influxQuery = { + refId: 'x', + alias: '$interpolationVar', + measurement: '$interpolationVar', + policy: '$interpolationVar', + limit: '$interpolationVar', + slimit: '$interpolationVar', + tz: '$interpolationVar', + tags: [ + { + key: 'cpu', + operator: '=~', + value: '/^$interpolationVar$/', + }, + ], + groupBy: [ + { + params: ['$interpolationVar'], + type: 'tag', + }, + ], + select: [ + [ { params: ['$interpolationVar'], - type: 'tag', + type: 'field', }, ], - select: [ - [ - { - params: ['$interpolationVar'], - type: 'field', - }, - ], - ], - }; - templateSrv.replace.mockReturnValue(text); + ], + }; - const queries = ds.interpolateVariablesInQueries([query], { interpolationVar: { text: text, value: text } }); - expect(templateSrv.replace).toBeCalledTimes(8); - expect(queries[0].measurement).toBe(text); - expect(queries[0].policy).toBe(text); - expect(queries[0].limit).toBe(text); - expect(queries[0].slimit).toBe(text); - expect(queries[0].tz).toBe(text); - expect(queries[0].tags![0].value).toBe(text); - expect(queries[0].groupBy![0].params![0]).toBe(text); - expect(queries[0].select![0][0].params![0]).toBe(text); + function fluxChecks(query: any) { + expect(templateSrv.replace).toBeCalledTimes(1); + expect(query).toBe(text); + } + + function influxChecks(query: any) { + expect(templateSrv.replace).toBeCalledTimes(10); + expect(query.alias).toBe(text); + expect(query.measurement).toBe(text); + expect(query.policy).toBe(text); + expect(query.limit).toBe(text); + expect(query.slimit).toBe(text); + expect(query.tz).toBe(text); + expect(query.tags![0].value).toBe(text); + expect(query.groupBy![0].params![0]).toBe(text); + expect(query.select![0][0].params![0]).toBe(text); + } + + describe('when interpolating query variables for dashboard->explore', () => { + it('should interpolate all variables with Flux mode', () => { + ds.isFlux = true; + const queries = ds.interpolateVariablesInQueries([fluxQuery], { + interpolationVar: { text: text, value: text }, + }); + fluxChecks(queries[0].query); + }); + + it('should interpolate all variables with InfluxQL mode', () => { + ds.isFlux = false; + const queries = ds.interpolateVariablesInQueries([influxQuery], { + interpolationVar: { text: text, value: text }, + }); + influxChecks(queries[0]); + }); + }); + + describe('when interpolating template variables', () => { + it('should apply all template variables with Flux mode', () => { + ds.isFlux = true; + const query = ds.applyTemplateVariables(fluxQuery, { interpolationVar: { text: text, value: text } }); + fluxChecks(query.query); + }); + + it('should apply all template variables with InfluxQL mode', () => { + ds.isFlux = false; + ds.access = 'proxy'; + config.featureToggles.influxdbBackendMigration = true; + const query = ds.applyTemplateVariables(influxQuery, { interpolationVar: { text: text, value: text } }); + influxChecks(query); + }); }); }); }); From 42e516523f03cabc559e268bb054b61bf040eb58 Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:22:10 +0000 Subject: [PATCH 057/125] InfluxDB: test datasource (#45636) * Test datasource * Remove core.explore check * Extract resused check to function * Update messages * Updated message --- .../plugins/datasource/influxdb/datasource.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index ae483f9b057..a1956b3d4db 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -22,7 +22,6 @@ import { TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, TimeSeries, - CoreApp, } from '@grafana/data'; import InfluxSeries from './influx_series'; import InfluxQueryModel from './influx_query_model'; @@ -165,7 +164,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { if (res.error) { @@ -449,7 +448,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { - if (this.isFlux || (config.featureToggles.influxdbBackendMigration && this.access === 'proxy')) { + if (this.isFlux || this.isMigrationToggleOnAndIsAccessProxy()) { const target: InfluxQuery = { refId: 'metricFindQuery', query, @@ -554,6 +553,33 @@ export default class InfluxDatasource extends DataSourceWithBackend { + if (!res || !res.data || res.state !== LoadingState.Done) { + return { + status: 'error', + message: 'Error reading InfluxDB.', + }; + } + if (res.data?.length) { + return { status: 'success', message: 'Data source is working.' }; + } + return { + status: 'error', + message: 'Successfully connected to InfluxDB, but no tags found.', + }; + }) + .catch((err: any) => { + return { status: 'error', message: err.message }; + }); + } + const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); const query = queryBuilder.buildExploreQuery('RETENTION POLICIES'); @@ -700,4 +726,8 @@ export default class InfluxDatasource extends DataSourceWithBackend Date: Mon, 28 Feb 2022 11:23:23 +0100 Subject: [PATCH 058/125] grafana/ui: Selectable Card (#45538) --- .../grafana-ui/src/components/Card/Card.mdx | 22 ++++ .../src/components/Card/Card.story.tsx | 24 ++++ .../src/components/Card/Card.test.tsx | 28 +++++ .../grafana-ui/src/components/Card/Card.tsx | 11 +- .../src/components/Card/CardContainer.tsx | 113 ++++++++++-------- 5 files changed, 146 insertions(+), 52 deletions(-) diff --git a/packages/grafana-ui/src/components/Card/Card.mdx b/packages/grafana-ui/src/components/Card/Card.mdx index e3364418c5a..a5efbed8383 100644 --- a/packages/grafana-ui/src/components/Card/Card.mdx +++ b/packages/grafana-ui/src/components/Card/Card.mdx @@ -384,6 +384,28 @@ Card can have a disabled state, effectively making it and its actions non-clicka +### Selectable + +```jsx + + Option #1 + This is a really great option, you won't regret it. + + Grafana Logo + + +``` + + + + Option #1 + This is a really great option, you won't regret it. + + Grafana Logo + + + + ### Props diff --git a/packages/grafana-ui/src/components/Card/Card.story.tsx b/packages/grafana-ui/src/components/Card/Card.story.tsx index 6c88efd080c..08893cf8d50 100644 --- a/packages/grafana-ui/src/components/Card/Card.story.tsx +++ b/packages/grafana-ui/src/components/Card/Card.story.tsx @@ -154,3 +154,27 @@ export const Full: Story = ({ disabled }) => { ); }; + +export const Selected: Story = () => { + return ( + + Spaces + Spaces are the superior form of indenting code. + + Grafana Logo + + + ); +}; + +export const NotSelected: Story = () => { + return ( + + Tabs + Tabs are the preferred way of indentation. + + Grafana Logo + + + ); +}; diff --git a/packages/grafana-ui/src/components/Card/Card.test.tsx b/packages/grafana-ui/src/components/Card/Card.test.tsx index 6dd2b587348..9b147a42b01 100644 --- a/packages/grafana-ui/src/components/Card/Card.test.tsx +++ b/packages/grafana-ui/src/components/Card/Card.test.tsx @@ -99,5 +99,33 @@ describe('Card', () => { expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled(); expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); }); + + it('Should allow selectable cards', () => { + const { rerender } = render( + + My Option + + ); + + expect(screen.getByRole('radio')).toBeInTheDocument(); + expect(screen.getByRole('radio')).toBeChecked(); + + rerender( + + My Option + + ); + + expect(screen.getByRole('radio')).toBeInTheDocument(); + expect(screen.getByRole('radio')).not.toBeChecked(); + + rerender( + + My Option + + ); + + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); }); }); diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 92466b9315e..2ba11fc31f5 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -19,6 +19,7 @@ export interface Props extends Omit { @@ -35,6 +36,7 @@ const CardContext = React.createContext<{ href?: string; onClick?: () => void; disabled?: boolean; + isSelected?: boolean; } | null>(null); /** @@ -49,6 +51,7 @@ export const Card: CardInterface = ({ children, heading: deprecatedHeading, description: deprecatedDescription, + isSelected, className, ...htmlProps }) => { @@ -63,16 +66,17 @@ export const Card: CardInterface = ({ const disableHover = disabled || (!onClick && !href); const onCardClick = onClick && !disabled ? onClick : undefined; const theme = useTheme2(); - const styles = getCardContainerStyles(theme, disabled, disableHover); + const styles = getCardContainerStyles(theme, disabled, disableHover, isSelected); return ( - + {!hasHeadingComponent && } {deprecatedHeading && {deprecatedHeading}} {deprecatedDescription && {deprecatedDescription}} @@ -96,7 +100,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & const context = useContext(CardContext); const styles = useStyles2(getHeadingStyles); - const { href, onClick } = context ?? { href: undefined, onClick: undefined }; + const { href, onClick, isSelected } = context ?? { href: undefined, onClick: undefined, isSelected: undefined }; return (

@@ -111,6 +115,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & ) : ( <>{children} )} + {isSelected !== undefined && }

); }; diff --git a/packages/grafana-ui/src/components/Card/CardContainer.tsx b/packages/grafana-ui/src/components/Card/CardContainer.tsx index 8cfa2e2ae2b..64bfe047e9c 100644 --- a/packages/grafana-ui/src/components/Card/CardContainer.tsx +++ b/packages/grafana-ui/src/components/Card/CardContainer.tsx @@ -39,6 +39,8 @@ export interface CardContainerProps extends HTMLAttributes, Ca disableEvents?: boolean; /** No style change on hover */ disableHover?: boolean; + /** Makes the card selectable, set to "true" to apply selected styles */ + isSelected?: boolean; /** Custom container styles */ className?: string; } @@ -48,12 +50,13 @@ export const CardContainer = ({ children, disableEvents, disableHover, + isSelected, className, href, ...props }: CardContainerProps) => { const theme = useTheme2(); - const { oldContainer } = getCardContainerStyles(theme, disableEvents, disableHover); + const { oldContainer } = getCardContainerStyles(theme, disableEvents, disableHover, isSelected); return (
{children} @@ -61,59 +64,71 @@ export const CardContainer = ({ ); }; -export const getCardContainerStyles = stylesFactory((theme: GrafanaTheme2, disabled = false, disableHover = false) => { - return { - container: css({ - display: 'grid', - position: 'relative', - gridTemplateColumns: 'auto 1fr auto', - gridTemplateRows: '1fr auto auto auto', - gridAutoColumns: '1fr', - gridAutoFlow: 'row', - gridTemplateAreas: ` +export const getCardContainerStyles = stylesFactory( + (theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected = false) => { + const isSelectable = isSelected !== undefined; + + return { + container: css({ + display: 'grid', + position: 'relative', + gridTemplateColumns: 'auto 1fr auto', + gridTemplateRows: '1fr auto auto auto', + gridAutoColumns: '1fr', + gridAutoFlow: 'row', + gridTemplateAreas: ` "Figure Heading Tags" "Figure Meta Tags" "Figure Description Tags" "Figure Actions Secondary"`, - width: '100%', - padding: theme.spacing(2), - background: theme.colors.background.secondary, - borderRadius: theme.shape.borderRadius(), - marginBottom: '8px', - pointerEvents: disabled ? 'none' : 'auto', - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + width: '100%', + padding: theme.spacing(2), + background: theme.colors.background.secondary, + borderRadius: theme.shape.borderRadius(), + marginBottom: '8px', + pointerEvents: disabled ? 'none' : 'auto', + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), - ...(!disableHover && { - '&:hover': { - background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), - cursor: 'pointer', - zIndex: 1, - }, - '&:focus': styleMixins.getFocusStyles(theme), - }), - }), - oldContainer: css({ - display: 'flex', - width: '100%', - background: theme.colors.background.secondary, - borderRadius: theme.shape.borderRadius(), - position: 'relative', - pointerEvents: disabled ? 'none' : 'auto', - marginBottom: theme.spacing(1), - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), + ...(!disableHover && { + '&:hover': { + background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + cursor: 'pointer', + zIndex: 1, + }, + '&:focus': styleMixins.getFocusStyles(theme), + }), - ...(!disableHover && { - '&:hover': { - background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + ...(isSelectable && { cursor: 'pointer', - zIndex: 1, - }, - '&:focus': styleMixins.getFocusStyles(theme), + }), + + ...(isSelected && { + outline: `solid 2px ${theme.colors.primary.border}`, + }), }), - }), - }; -}); + oldContainer: css({ + display: 'flex', + width: '100%', + background: theme.colors.background.secondary, + borderRadius: theme.shape.borderRadius(), + position: 'relative', + pointerEvents: disabled ? 'none' : 'auto', + marginBottom: theme.spacing(1), + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + + ...(!disableHover && { + '&:hover': { + background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + cursor: 'pointer', + zIndex: 1, + }, + '&:focus': styleMixins.getFocusStyles(theme), + }), + }), + }; + } +); From 5cb03d6e626d139446837a592eb0c48975f802df Mon Sep 17 00:00:00 2001 From: J Guerreiro Date: Mon, 28 Feb 2022 10:30:45 +0000 Subject: [PATCH 059/125] Separate API key store from SA token store (#45862) * ServiceAccounts: Fix token-apikey cross deletion * ServiceAccounts: separate API key store and service account token store * ServiceAccounts: hide service account tokens from API Keys page * ServiceAccounts: uppercase statement * ServiceAccounts: fix and add new tests for SAT store * ServiceAccounts: remove service account ID from add API key * ServiceAccounts: clear up errors --- pkg/api/apikey.go | 1 - pkg/models/apikey.go | 13 +- pkg/services/serviceaccounts/api/api.go | 10 -- pkg/services/serviceaccounts/api/api_test.go | 2 +- pkg/services/serviceaccounts/api/token.go | 28 +---- .../serviceaccounts/api/token_test.go | 27 ++-- .../serviceaccounts/database/errors.go | 41 +++++++ .../serviceaccounts/database/token_store.go | 63 ++++++++++ .../database/token_store_test.go | 115 ++++++++++++++++++ .../serviceaccounts/manager/service.go | 2 +- .../serviceaccounts/serviceaccounts.go | 2 + pkg/services/serviceaccounts/tests/common.go | 28 +++-- pkg/services/sqlstore/apikey.go | 6 +- pkg/services/sqlstore/apikey_test.go | 15 +-- 14 files changed, 272 insertions(+), 81 deletions(-) create mode 100644 pkg/services/serviceaccounts/database/errors.go create mode 100644 pkg/services/serviceaccounts/database/token_store.go create mode 100644 pkg/services/serviceaccounts/database/token_store_test.go diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 4f32a349d26..a282ee2f257 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -80,7 +80,6 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response { } } - cmd.ServiceAccountId = nil // Security: API keys can't be added to SAs through this endpoint since we do not implement access checks here cmd.OrgId = c.OrgId newKeyInfo, err := apikeygen.New(cmd.OrgId, cmd.Name) diff --git a/pkg/models/apikey.go b/pkg/models/apikey.go index 29f0f14b2bd..430dddf311f 100644 --- a/pkg/models/apikey.go +++ b/pkg/models/apikey.go @@ -27,13 +27,12 @@ type ApiKey struct { // --------------------- // COMMANDS type AddApiKeyCommand struct { - Name string `json:"name" binding:"Required"` - Role RoleType `json:"role" binding:"Required"` - OrgId int64 `json:"-"` - Key string `json:"-"` - SecondsToLive int64 `json:"secondsToLive"` - ServiceAccountId *int64 `json:"-"` - Result *ApiKey `json:"-"` + Name string `json:"name" binding:"Required"` + Role RoleType `json:"role" binding:"Required"` + OrgId int64 `json:"-"` + Key string `json:"-"` + SecondsToLive int64 `json:"secondsToLive"` + Result *ApiKey `json:"-"` } type DeleteApiKeyCommand struct { diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index c6742b5a945..bce3a4d8a6c 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -1,7 +1,6 @@ package api import ( - "context" "errors" "net/http" "strconv" @@ -20,19 +19,12 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type APIKeyStore interface { - AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error - GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error - DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCommand) error -} - type ServiceAccountsAPI struct { cfg *setting.Cfg service serviceaccounts.Service accesscontrol accesscontrol.AccessControl RouterRegister routing.RouteRegister store serviceaccounts.Store - apiKeyStore APIKeyStore log log.Logger } @@ -47,7 +39,6 @@ func NewServiceAccountsAPI( accesscontrol accesscontrol.AccessControl, routerRegister routing.RouteRegister, store serviceaccounts.Store, - apiKeyStore APIKeyStore, ) *ServiceAccountsAPI { return &ServiceAccountsAPI{ cfg: cfg, @@ -55,7 +46,6 @@ func NewServiceAccountsAPI( accesscontrol: accesscontrol, RouterRegister: routerRegister, store: store, - apiKeyStore: apiKeyStore, log: log.New("serviceaccounts.api"), } } diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index bff5d52cb61..37687b32147 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -102,7 +102,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) *web.Mux { - a := NewServiceAccountsAPI(setting.NewCfg(), svc, acmock, routerRegister, saStore, sqlStore) + a := NewServiceAccountsAPI(setting.NewCfg(), svc, acmock, routerRegister, saStore) a.RegisterAPIEndpoints(featuremgmt.WithFeatures(featuremgmt.FlagServiceAccounts)) a.cfg.ApiKeyMaxSecondsToLive = -1 // disable api key expiration diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index a66bcc226ff..7347084501a 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -97,7 +97,6 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon } // Force affected service account to be the one referenced in the URL - cmd.ServiceAccountId = &saID cmd.OrgId = c.OrgId if !cmd.Role.IsValid() { @@ -120,7 +119,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon cmd.Key = newKeyInfo.HashedKey - if err := api.apiKeyStore.AddAPIKey(c.Req.Context(), &cmd); err != nil { + if err := api.store.AddServiceAccountToken(c.Req.Context(), saID, &cmd); err != nil { if errors.Is(err, models.ErrInvalidApiKeyExpiration) { return response.Error(http.StatusBadRequest, err.Error(), nil) } @@ -143,7 +142,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon func (api *ServiceAccountsAPI) DeleteToken(c *models.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } // confirm service account exists @@ -158,29 +157,10 @@ func (api *ServiceAccountsAPI) DeleteToken(c *models.ReqContext) response.Respon tokenID, err := strconv.ParseInt(web.Params(c.Req)[":tokenId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Token ID is invalid", err) } - // confirm API key belongs to service account. TODO: refactor get & delete to single call - cmdGet := &models.GetApiKeyByIdQuery{ApiKeyId: tokenID} - if err = api.apiKeyStore.GetApiKeyById(c.Req.Context(), cmdGet); err != nil { - status := http.StatusNotFound - if err != nil && !errors.Is(err, models.ErrApiKeyNotFound) { - status = http.StatusInternalServerError - } else { - err = models.ErrApiKeyNotFound - } - - return response.Error(status, failedToDeleteMsg, err) - } - - // verify service account ID matches the URL - if *cmdGet.Result.ServiceAccountId != saID { - return response.Error(http.StatusNotFound, failedToDeleteMsg, err) - } - - cmdDel := &models.DeleteApiKeyCommand{Id: tokenID, OrgId: c.OrgId} - if err = api.apiKeyStore.DeleteApiKey(c.Req.Context(), cmdDel); err != nil { + if err = api.store.DeleteServiceAccountToken(c.Req.Context(), c.OrgId, saID, tokenID); err != nil { status := http.StatusNotFound if err != nil && !errors.Is(err, models.ErrApiKeyNotFound) { status = http.StatusInternalServerError diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index a90af8ec96f..959d7fc0e9a 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -31,19 +30,20 @@ const ( serviceaccountIDTokensDetailPath = "/api/serviceaccounts/%v/tokens/%v" // #nosec G101 ) -func createTokenforSA(t *testing.T, keyName string, orgID int64, saID int64, secondsToLive int64) *models.ApiKey { +func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string, orgID int64, saID int64, secondsToLive int64) *models.ApiKey { key, err := apikeygen.New(orgID, keyName) require.NoError(t, err) + cmd := models.AddApiKeyCommand{ - Name: keyName, - Role: "Viewer", - OrgId: orgID, - Key: key.HashedKey, - SecondsToLive: secondsToLive, - ServiceAccountId: &saID, - Result: &models.ApiKey{}, + Name: keyName, + Role: "Viewer", + OrgId: orgID, + Key: key.HashedKey, + SecondsToLive: secondsToLive, + Result: &models.ApiKey{}, } - err = bus.Dispatch(context.Background(), &cmd) + + err = store.AddServiceAccountToken(context.Background(), saID, &cmd) require.NoError(t, err) return cmd.Result } @@ -156,7 +156,8 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { func TestServiceAccountsAPI_DeleteToken(t *testing.T) { store := sqlstore.InitTestDB(t) - svcmock := tests.ServiceAccountMock{} + svcMock := &tests.ServiceAccountMock{} + saStore := database.NewServiceAccountsStore(store) sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) type testCreateSAToken struct { @@ -216,11 +217,11 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - token := createTokenforSA(t, tc.keyName, sa.OrgId, sa.Id, 1) + token := createTokenforSA(t, saStore, tc.keyName, sa.OrgId, sa.Id, 1) endpoint := fmt.Sprintf(serviceaccountIDTokensDetailPath, sa.Id, token.Id) bodyString := "" - server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store)) + server := setupTestServer(t, svcMock, routing.NewRouteRegister(), tc.acmock, store, saStore) actual := requestResponse(server, http.MethodDelete, endpoint, strings.NewReader(bodyString)) actualCode := actual.Code diff --git a/pkg/services/serviceaccounts/database/errors.go b/pkg/services/serviceaccounts/database/errors.go new file mode 100644 index 00000000000..09eb550c4a3 --- /dev/null +++ b/pkg/services/serviceaccounts/database/errors.go @@ -0,0 +1,41 @@ +package database + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/models" +) + +type ErrMisingSAToken struct { +} + +func (e *ErrMisingSAToken) Error() string { + return "service account token not found" +} + +func (e *ErrMisingSAToken) Unwrap() error { + return models.ErrApiKeyNotFound +} + +type ErrInvalidExpirationSAToken struct { +} + +func (e *ErrInvalidExpirationSAToken) Error() string { + return "service account token not found" +} + +func (e *ErrInvalidExpirationSAToken) Unwrap() error { + return models.ErrInvalidApiKeyExpiration +} + +type ErrDuplicateSAToken struct { + name string +} + +func (e *ErrDuplicateSAToken) Error() string { + return fmt.Sprintf("service account token %s already exists", e.name) +} + +func (e *ErrDuplicateSAToken) Unwrap() error { + return models.ErrDuplicateApiKey +} diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go new file mode 100644 index 00000000000..e36ab862e46 --- /dev/null +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -0,0 +1,63 @@ +package database + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, saID int64, cmd *models.AddApiKeyCommand) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + key := models.ApiKey{OrgId: cmd.OrgId, Name: cmd.Name} + exists, _ := sess.Get(&key) + if exists { + return &ErrDuplicateSAToken{cmd.Name} + } + + updated := time.Now() + var expires *int64 = nil + if cmd.SecondsToLive > 0 { + v := updated.Add(time.Second * time.Duration(cmd.SecondsToLive)).Unix() + expires = &v + } else if cmd.SecondsToLive < 0 { + return &ErrInvalidExpirationSAToken{} + } + + t := models.ApiKey{ + OrgId: cmd.OrgId, + Name: cmd.Name, + Role: cmd.Role, + Key: cmd.Key, + Created: updated, + Updated: updated, + Expires: expires, + ServiceAccountId: &saID, + } + + if _, err := sess.Insert(&t); err != nil { + return err + } + cmd.Result = &t + return nil + }) +} + +func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error { + rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id=?" + + return s.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + result, err := sess.Exec(rawSQL, tokenID, orgID, serviceAccountID) + if err != nil { + return err + } + n, err := result.RowsAffected() + if err != nil { + return err + } else if n == 0 { + return &ErrMisingSAToken{} + } + return nil + }) +} diff --git a/pkg/services/serviceaccounts/database/token_store_test.go b/pkg/services/serviceaccounts/database/token_store_test.go new file mode 100644 index 00000000000..2e24caa9c37 --- /dev/null +++ b/pkg/services/serviceaccounts/database/token_store_test.go @@ -0,0 +1,115 @@ +package database + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/components/apikeygen" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/stretchr/testify/require" +) + +func TestStore_AddServiceAccountToken(t *testing.T) { + userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} + db, store := setupTestDatabase(t) + user := tests.SetupUserServiceAccount(t, db, userToCreate) + + type testCasesAdd struct { + secondsToLive int64 + desc string + } + + testCases := []testCasesAdd{{-10, "invalid"}, {0, "no expiry"}, {10, "valid"}} + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + keyName := t.Name() + key, err := apikeygen.New(user.OrgId, keyName) + require.NoError(t, err) + + cmd := models.AddApiKeyCommand{ + Name: keyName, + Role: "Viewer", + OrgId: user.OrgId, + Key: key.HashedKey, + SecondsToLive: tc.secondsToLive, + Result: &models.ApiKey{}, + } + + err = store.AddServiceAccountToken(context.Background(), user.Id, &cmd) + if tc.secondsToLive < 0 { + require.Error(t, err) + return + } + + require.NoError(t, err) + newKey := cmd.Result + require.Equal(t, t.Name(), newKey.Name) + + // Verify against DB + keys, errT := store.ListTokens(context.Background(), user.OrgId, user.Id) + + require.NoError(t, errT) + + found := false + for _, k := range keys { + if k.Name == keyName { + found = true + require.Equal(t, key.HashedKey, newKey.Key) + if tc.secondsToLive == 0 { + require.Nil(t, k.Expires) + } else { + require.NotNil(t, k.Expires) + } + } + } + + require.True(t, found, "Key not found") + }) + } +} + +func TestStore_DeleteServiceAccountToken(t *testing.T) { + userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} + db, store := setupTestDatabase(t) + user := tests.SetupUserServiceAccount(t, db, userToCreate) + + keyName := t.Name() + key, err := apikeygen.New(user.OrgId, keyName) + require.NoError(t, err) + + cmd := models.AddApiKeyCommand{ + Name: keyName, + Role: "Viewer", + OrgId: user.OrgId, + Key: key.HashedKey, + SecondsToLive: 0, + Result: &models.ApiKey{}, + } + + err = store.AddServiceAccountToken(context.Background(), user.Id, &cmd) + require.NoError(t, err) + newKey := cmd.Result + + // Delete key from wrong service account + err = store.DeleteServiceAccountToken(context.Background(), user.OrgId, user.Id+2, newKey.Id) + require.Error(t, err) + + // Delete key from wrong org + err = store.DeleteServiceAccountToken(context.Background(), user.OrgId+2, user.Id, newKey.Id) + require.Error(t, err) + + err = store.DeleteServiceAccountToken(context.Background(), user.OrgId, user.Id, newKey.Id) + require.NoError(t, err) + + // Verify against DB + keys, errT := store.ListTokens(context.Background(), user.OrgId, user.Id) + require.NoError(t, errT) + + for _, k := range keys { + if k.Name == keyName { + require.Fail(t, "Key not deleted") + } + } +} diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index e74aaa5a02c..f5b066cad2b 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -43,7 +43,7 @@ func ProvideServiceAccountsService( } } - serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store, store) + serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store) serviceaccountsAPI.RegisterAPIEndpoints(features) return s, nil diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 3aa6d895169..ab8c35ba90c 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -21,4 +21,6 @@ type Store interface { UpgradeServiceAccounts(ctx context.Context) error ConvertToServiceAccounts(ctx context.Context, keys []int64) error ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error) + DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error + AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 17e9f16d9b2..8a177012bb9 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -67,14 +67,16 @@ func SetupMockAccesscontrol(t *testing.T, var _ serviceaccounts.Store = new(ServiceAccountsStoreMock) type Calls struct { - CreateServiceAccount []interface{} - ListServiceAccounts []interface{} - RetrieveServiceAccount []interface{} - DeleteServiceAccount []interface{} - UpgradeServiceAccounts []interface{} - ConvertServiceAccounts []interface{} - ListTokens []interface{} - UpdateServiceAccount []interface{} + CreateServiceAccount []interface{} + ListServiceAccounts []interface{} + RetrieveServiceAccount []interface{} + DeleteServiceAccount []interface{} + UpgradeServiceAccounts []interface{} + ConvertServiceAccounts []interface{} + ListTokens []interface{} + DeleteServiceAccountToken []interface{} + UpdateServiceAccount []interface{} + AddServiceAccountToken []interface{} } type ServiceAccountsStoreMock struct { @@ -124,3 +126,13 @@ func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context, return nil, nil } + +func (s *ServiceAccountsStoreMock) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error { + s.Calls.DeleteServiceAccountToken = append(s.Calls.DeleteServiceAccountToken, []interface{}{ctx, orgID, serviceAccountID, tokenID}) + return nil +} + +func (s *ServiceAccountsStoreMock) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error { + s.Calls.AddServiceAccountToken = append(s.Calls.AddServiceAccountToken, []interface{}{ctx, cmd}) + return nil +} diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index 72e4474b7fc..61037c291d9 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -34,6 +34,8 @@ func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuer Asc("name") } + sess = sess.Where("service_account_id IS NULL") + query.Result = make([]*models.ApiKey, 0) return sess.Find(&query.Result) }) @@ -61,7 +63,7 @@ func (ss *SQLStore) DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCo } func deleteAPIKey(sess *DBSession, id, orgID int64) error { - rawSQL := "DELETE FROM api_key WHERE id=? and org_id=?" + rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL" result, err := sess.Exec(rawSQL, id, orgID) if err != nil { return err @@ -101,7 +103,7 @@ func (ss *SQLStore) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) Created: updated, Updated: updated, Expires: expires, - ServiceAccountId: cmd.ServiceAccountId, + ServiceAccountId: nil, } if _, err := sess.Insert(&t); err != nil { diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go index 3e69a3c6498..06a17378f2c 100644 --- a/pkg/services/sqlstore/apikey_test.go +++ b/pkg/services/sqlstore/apikey_test.go @@ -34,20 +34,7 @@ func TestApiKeyDataAccess(t *testing.T) { }) t.Run("Add non expiring key", func(t *testing.T) { - cmd := models.AddApiKeyCommand{OrgId: 1, Name: "non-expiring", Key: "asd1", SecondsToLive: 0, ServiceAccountId: nil} - err := ss.AddAPIKey(context.Background(), &cmd) - assert.Nil(t, err) - - query := models.GetApiKeyByNameQuery{KeyName: "non-expiring", OrgId: 1} - err = ss.GetApiKeyByName(context.Background(), &query) - assert.Nil(t, err) - - assert.Nil(t, query.Result.Expires) - }) - - t.Run("Add key for service account", func(t *testing.T) { - var one int64 = 1 - cmd := models.AddApiKeyCommand{OrgId: 1, Name: "non-expiring-SA", Key: "sa1-key", ServiceAccountId: &one} + cmd := models.AddApiKeyCommand{OrgId: 1, Name: "non-expiring", Key: "asd1", SecondsToLive: 0} err := ss.AddAPIKey(context.Background(), &cmd) assert.Nil(t, err) From e152ae4cf121827ac2ce9e22134912d2f049503d Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 28 Feb 2022 12:28:19 +0100 Subject: [PATCH 060/125] remove unused dashboard service dependency (#45920) From c014f8b806468f5943c30af82b60ac0b70e04e10 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Mon, 28 Feb 2022 11:30:11 +0000 Subject: [PATCH 061/125] DashboardSrv: add saveDashboard method (#45627) * DashboardSrv: add saveDashboard method * revert external changes * use DashboardModel instead of DashboardDataDTO * use fetch instead of request * use getSaveModelClone in saveDashboard --- .../dashboard/services/DashboardSrv.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 2341977103f..89e5e1ce937 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -5,6 +5,25 @@ import { DashboardMeta } from 'app/types'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { saveDashboard } from 'app/features/manage-dashboards/state/actions'; import { RemovePanelEvent } from '../../../types/events'; +import { BackendSrvRequest } from '@grafana/runtime'; +import { lastValueFrom } from 'rxjs'; + +export interface SaveDashboardOptions { + /** The complete dashboard model. If `dashboard.id` is not set a new dashboard will be created. */ + dashboard: DashboardModel; + /** Set a commit message for the version history. */ + message?: string; + /** The id of the folder to save the dashboard in. */ + folderId?: number; + /** The UID of the folder to save the dashboard in. Overrides `folderId`. */ + folderUid?: string; + /** Set to `true` if you want to overwrite existing dashboard with newer version, + * same dashboard title in folder or same dashboard uid. */ + overwrite?: boolean; + /** Set the dashboard refresh interval. + * If this is lower than the minimum refresh interval, Grafana will ignore it and will enforce the minimum refresh interval. */ + refresh?: string; +} export class DashboardSrv { dashboard?: DashboardModel; @@ -43,6 +62,23 @@ export class DashboardSrv { }); } + saveDashboard( + data: SaveDashboardOptions, + requestOptions?: Pick + ) { + return lastValueFrom( + getBackendSrv().fetch({ + url: '/api/dashboards/db/', + method: 'POST', + data: { + ...data, + dashboard: data.dashboard.getSaveModelClone(), + }, + ...requestOptions, + }) + ); + } + starDashboard(dashboardId: string, isStarred: any) { const backendSrv = getBackendSrv(); let promise; From eb537e2efdbddfe7500fa5ef941607ed60044bc9 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 28 Feb 2022 06:35:05 -0800 Subject: [PATCH 062/125] TablePanel: Add cell inspect option (#45620) * TablePanel: Add cell preview option * Review comments * Change modal title * Review * Review 2 * Docs --- docs/sources/visualizations/table/_index.md | 6 ++ .../grafana-schema/src/schema/graph.gen.ts | 2 + .../src/components/Table/CellActions.tsx | 70 ++++++++++++++++ .../src/components/Table/DefaultCell.tsx | 26 +++--- .../src/components/Table/FilterActions.tsx | 31 ------- .../src/components/Table/JSONViewCell.tsx | 48 ++--------- .../Table/TableCellInspectModal.tsx | 75 +++++++++++++++++ .../grafana-ui/src/components/Table/styles.ts | 81 ++++++++++++------- public/app/plugins/panel/table/models.gen.ts | 1 + public/app/plugins/panel/table/module.tsx | 15 ++++ 10 files changed, 247 insertions(+), 108 deletions(-) create mode 100644 packages/grafana-ui/src/components/Table/CellActions.tsx delete mode 100644 packages/grafana-ui/src/components/Table/FilterActions.tsx create mode 100644 packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx diff --git a/docs/sources/visualizations/table/_index.md b/docs/sources/visualizations/table/_index.md index 8a43c64df62..57d8589d59a 100644 --- a/docs/sources/visualizations/table/_index.md +++ b/docs/sources/visualizations/table/_index.md @@ -96,6 +96,12 @@ If you have a field value that is an image URL or a base64 encoded image you can {{< figure src="/static/img/docs/v73/table_hover.gif" max-width="900px" caption="Table hover" >}} +## Cell value inspect + +Enables value inspection from table cell. The raw value is presented in a modal window. + +> **Note:** Cell value inspection is only available when cell display mode is set to Auto, Color text, Color background or JSON View. + ## Column filter You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}). diff --git a/packages/grafana-schema/src/schema/graph.gen.ts b/packages/grafana-schema/src/schema/graph.gen.ts index 623d750a250..f055541e9ce 100644 --- a/packages/grafana-schema/src/schema/graph.gen.ts +++ b/packages/grafana-schema/src/schema/graph.gen.ts @@ -283,6 +283,7 @@ export enum BarGaugeDisplayMode { export interface TableFieldOptions { align: string; displayMode: TableCellDisplayMode; + inspect: boolean; hidden?: boolean; minWidth?: number; width?: number; @@ -292,6 +293,7 @@ export interface TableFieldOptions { export const defaultTableFieldOptions: TableFieldOptions = { align: 'auto', displayMode: TableCellDisplayMode.Auto, + inspect: false, }; export interface VizTooltipOptions { diff --git a/packages/grafana-ui/src/components/Table/CellActions.tsx b/packages/grafana-ui/src/components/Table/CellActions.tsx new file mode 100644 index 00000000000..bf140fca42d --- /dev/null +++ b/packages/grafana-ui/src/components/Table/CellActions.tsx @@ -0,0 +1,70 @@ +import React, { useCallback, useState } from 'react'; +import { IconSize } from '../../types/icon'; +import { IconButton } from '../IconButton/IconButton'; +import { HorizontalGroup } from '../Layout/Layout'; +import { TooltipPlacement } from '../Tooltip'; +import { TableCellInspectModal } from './TableCellInspectModal'; +import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps, TableFieldOptions } from './types'; +import { getTextAlign } from './utils'; + +interface CellActionProps extends TableCellProps { + previewMode: 'text' | 'code'; +} + +export function CellActions({ field, cell, previewMode, onCellFilterAdded }: CellActionProps) { + const [isInspecting, setIsInspecting] = useState(false); + + const isRightAligned = getTextAlign(field) === 'flex-end'; + const showFilters = Boolean(field.config.filterable) && cell.value !== undefined; + const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect); + const commonButtonProps = { + size: 'sm' as IconSize, + tooltipPlacement: 'top' as TooltipPlacement, + }; + + const onFilterFor = useCallback( + (event: React.MouseEvent) => + onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }), + [cell, field, onCellFilterAdded] + ); + const onFilterOut = useCallback( + (event: React.MouseEvent) => + onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }), + [cell, field, onCellFilterAdded] + ); + + return ( + <> +
+ + {inspectEnabled && ( + { + setIsInspecting(true); + }} + {...commonButtonProps} + /> + )} + {showFilters && ( + + )} + {showFilters && ( + + )} + +
+ + {isInspecting && ( + { + setIsInspecting(false); + }} + /> + )} + + ); +} diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 503dbcd4da3..476cd60b492 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -1,15 +1,16 @@ import React, { FC, ReactElement } from 'react'; import { DisplayValue, Field, formattedValueToString } from '@grafana/data'; -import { TableCellDisplayMode, TableCellProps } from './types'; +import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; import tinycolor from 'tinycolor2'; import { TableStyles } from './styles'; -import { FilterActions } from './FilterActions'; import { getTextColorForBackground, getCellLinks } from '../../utils'; +import { CellActions } from './CellActions'; export const DefaultCell: FC = (props) => { const { field, cell, tableStyles, row, cellProps } = props; + const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect); const displayValue = field.display!(cell.value); let value: string | ReactElement; @@ -19,8 +20,9 @@ export const DefaultCell: FC = (props) => { value = formattedValueToString(displayValue); } - const cellStyle = getCellStyle(tableStyles, field, displayValue); const showFilters = field.config.filterable; + const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; + const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled); const { link, onClick } = getCellLinks(field, row); @@ -32,20 +34,25 @@ export const DefaultCell: FC = (props) => { {value}
)} - {showFilters && cell.value !== undefined && } + {showActions && }
); }; -function getCellStyle(tableStyles: TableStyles, field: Field, displayValue: DisplayValue) { +function getCellStyle( + tableStyles: TableStyles, + field: Field, + displayValue: DisplayValue, + disableOverflowOnHover = false +) { if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) { - return tableStyles.buildCellContainerStyle(displayValue.color); + return tableStyles.buildCellContainerStyle(displayValue.color, undefined, !disableOverflowOnHover); } if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackgroundSolid) { const bgColor = tinycolor(displayValue.color); const textColor = getTextColorForBackground(displayValue.color!); - return tableStyles.buildCellContainerStyle(textColor, bgColor.toRgbString()); + return tableStyles.buildCellContainerStyle(textColor, bgColor.toRgbString(), !disableOverflowOnHover); } if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) { @@ -59,9 +66,10 @@ function getCellStyle(tableStyles: TableStyles, field: Field, displayValue: Disp return tableStyles.buildCellContainerStyle( textColor, - `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})` + `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`, + !disableOverflowOnHover ); } - return tableStyles.cellContainer; + return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer; } diff --git a/packages/grafana-ui/src/components/Table/FilterActions.tsx b/packages/grafana-ui/src/components/Table/FilterActions.tsx deleted file mode 100644 index 21dc07dab90..00000000000 --- a/packages/grafana-ui/src/components/Table/FilterActions.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { FC, useCallback } from 'react'; -import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps } from './types'; -import { Icon, Tooltip } from '..'; - -export const FilterActions: FC = ({ cell, field, tableStyles, onCellFilterAdded }) => { - const onFilterFor = useCallback( - (event: React.MouseEvent) => - onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }), - [cell, field, onCellFilterAdded] - ); - const onFilterOut = useCallback( - (event: React.MouseEvent) => - onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }), - [cell, field, onCellFilterAdded] - ); - - return ( -
-
- - - -
-
- - - -
-
- ); -}; diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index e2cfe1d87f1..57fda2b325c 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -1,15 +1,12 @@ import React from 'react'; import { css, cx } from '@emotion/css'; import { isString } from 'lodash'; -import { Tooltip } from '../Tooltip/Tooltip'; -import { JSONFormatter } from '../JSONFormatter/JSONFormatter'; -import { useStyles2 } from '../../themes'; -import { TableCellProps } from './types'; -import { GrafanaTheme2 } from '@grafana/data'; +import { TableCellProps, TableFieldOptions } from './types'; +import { CellActions } from './CellActions'; export function JSONViewCell(props: TableCellProps): JSX.Element { - const { cell, tableStyles, cellProps } = props; - + const { cell, tableStyles, cellProps, field } = props; + const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect); const txt = css` cursor: pointer; font-family: monospace; @@ -26,41 +23,10 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { displayValue = JSON.stringify(value, null, ' '); } - const content = ; - return ( - -
-
{displayValue}
-
-
- ); -} - -interface PopupProps { - value: any; -} - -function JSONTooltip(props: PopupProps): JSX.Element { - const styles = useStyles2(getStyles); - return ( -
-
- -
+
+
{displayValue}
+ {inspectEnabled && }
); } - -function getStyles(theme: GrafanaTheme2) { - return { - container: css` - padding: ${theme.spacing(0.5)}; - `, - json: css` - width: fit-content; - max-height: 70vh; - overflow-y: auto; - `, - }; -} diff --git a/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx b/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx new file mode 100644 index 00000000000..a5d204fe058 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx @@ -0,0 +1,75 @@ +import { isString } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; +import { Icon } from '../Icon/Icon'; +import { Modal } from '../Modal/Modal'; +import { CodeEditor } from '../Monaco/CodeEditor'; + +interface TableCellInspectModalProps { + value: any; + onDismiss: () => void; + mode: 'code' | 'text'; +} + +export function TableCellInspectModal({ value, onDismiss, mode }: TableCellInspectModalProps) { + const [isInClipboard, setIsInClipboard] = useState(false); + const timeoutRef = React.useRef(); + + useEffect(() => { + if (isInClipboard) { + timeoutRef.current = window.setTimeout(() => { + setIsInClipboard(false); + }, 2000); + } + + return () => { + if (timeoutRef.current) { + window.clearTimeout(timeoutRef.current); + } + }; + }, [isInClipboard]); + + let displayValue = value; + if (isString(value)) { + try { + value = JSON.parse(value); + } catch {} // ignore errors + } else { + displayValue = JSON.stringify(value, null, ' '); + } + let text = displayValue; + + if (mode === 'code') { + text = JSON.stringify(value, null, ' '); + } + + return ( + + {mode === 'code' ? ( + 100} + value={text} + readOnly={true} + /> + ) : ( +
{text}
+ )} + + text} onClipboardCopy={() => setIsInClipboard(true)}> + {!isInClipboard ? ( + 'Copy to Clipboard' + ) : ( + <> + + Copied to clipboard + + )} + + +
+ ); +} diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 76adbf40729..d11a186dd58 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css, CSSObject } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { getScrollbarWidth } from '../../utils'; @@ -14,8 +14,27 @@ export const getTableStyles = (theme: GrafanaTheme2) => { const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03); const lastChildExtraPadding = Math.max(getScrollbarWidth(), cellPadding); - const buildCellContainerStyle = (color?: string, background?: string) => { + const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => { + const cellActionsOverflow: CSSObject = { + margin: theme.spacing(0, -0.5, 0, 0.5), + }; + const cellActionsNoOverflow: CSSObject = { + position: 'absolute', + top: 0, + right: 0, + margin: 'auto', + }; + + const onHoverOverflow: CSSObject = { + overflow: 'visible', + width: 'auto !important', + boxShadow: `0 0 2px ${theme.colors.primary.main}`, + background: background ?? rowHoverBg, + zIndex: 1, + }; + return css` + label: ${overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow'}; padding: ${cellPadding}px; width: 100%; height: 100%; @@ -33,19 +52,42 @@ export const getTableStyles = (theme: GrafanaTheme2) => { } &:hover { - overflow: visible; - width: auto !important; - box-shadow: 0 0 2px ${theme.colors.primary.main}; - background: ${background ?? rowHoverBg}; - z-index: 1; - - .cell-filter-actions  { - display: inline-flex; + ${overflowOnHover && onHoverOverflow}; + .cellActions { + visibility: visible; + opacity: 1; + width: auto; } } + a { color: inherit; } + + .cellActions { + display: flex; + ${overflowOnHover ? cellActionsOverflow : cellActionsNoOverflow} + visibility: hidden; + opacity: 0; + width: 0; + align-items: center; + height: 100%; + padding: ${theme.spacing(1, 0.5, 1, 0.5)}; + background: ${background ? 'none' : theme.colors.emphasize(theme.colors.background.primary, 0.03)}; + + svg { + color: ${color}; + } + } + + .cellActionsLeft { + right: auto !important; + left: 0; + } + + .cellActionsTransparent { + background: none; + } `; }; @@ -102,7 +144,8 @@ export const getTableStyles = (theme: GrafanaTheme2) => { display: flex; margin-right: ${theme.spacing(0.5)}; `, - cellContainer: buildCellContainerStyle(), + cellContainer: buildCellContainerStyle(undefined, undefined, true), + cellContainerNoOverflow: buildCellContainerStyle(undefined, undefined, false), cellText: css` overflow: hidden; text-overflow: ellipsis; @@ -161,22 +204,6 @@ export const getTableStyles = (theme: GrafanaTheme2) => { opacity: 1; } `, - filterWrapper: cx( - css` - label: filterWrapper; - display: none; - justify-content: flex-end; - flex-grow: 1; - opacity: 0.6; - padding-left: ${theme.spacing(0.25)}; - `, - 'cell-filter-actions' - ), - filterItem: css` - label: filterItem; - cursor: pointer; - padding: 0 ${theme.spacing(0.025)}; - `, typeIcon: css` margin-right: ${theme.spacing(1)}; color: ${theme.colors.text.secondary}; diff --git a/public/app/plugins/panel/table/models.gen.ts b/public/app/plugins/panel/table/models.gen.ts index a1eee4e24f8..413ccdf20e6 100644 --- a/public/app/plugins/panel/table/models.gen.ts +++ b/public/app/plugins/panel/table/models.gen.ts @@ -39,4 +39,5 @@ export const defaultPanelOptions: PanelOptions = { export const defaultPanelFieldConfig: TableFieldOptions = { displayMode: TableCellDisplayMode.Auto, align: 'auto', + inspect: false, }; diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index dbf33ba6187..f6cbc78a82f 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -75,6 +75,21 @@ export const plugin = new PanelPlugin(TablePane }, defaultValue: defaultPanelFieldConfig.displayMode, }) + .addBooleanSwitch({ + path: 'inspect', + name: 'Cell value inspect', + description: 'Enable cell value inspection in a modal window', + defaultValue: false, + showIf: (cfg) => { + return ( + cfg.displayMode === TableCellDisplayMode.Auto || + cfg.displayMode === TableCellDisplayMode.JSONView || + cfg.displayMode === TableCellDisplayMode.ColorText || + cfg.displayMode === TableCellDisplayMode.ColorBackground || + cfg.displayMode === TableCellDisplayMode.ColorBackgroundSolid + ); + }, + }) .addBooleanSwitch({ path: 'filterable', name: 'Column filter', From 4e19d7df6352b0dcbb680aeee00cebc97a90d937 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Mon, 28 Feb 2022 11:13:53 -0500 Subject: [PATCH 063/125] Alerting: Calculate diff for two AlertRules (#45877) * add custom diff reporter DiffReporter that reports only paths that have a difference * create Diff method for AlertRule that returns DiffReport, which is an alias for []Diff Tests: * create copy method for AlertRule in testing * create GenerateAlertQuery method in testing --- pkg/services/ngalert/models/alert_rule.go | 24 ++ .../ngalert/models/alert_rule_test.go | 315 ++++++++++++++++++ pkg/services/ngalert/models/testing.go | 99 +++++- pkg/util/cmputil/reporter.go | 101 ++++++ 4 files changed, 521 insertions(+), 18 deletions(-) create mode 100644 pkg/util/cmputil/reporter.go diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 4e59f55c6d6..3216961469c 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -1,9 +1,15 @@ package models import ( + "encoding/json" "errors" "fmt" "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/grafana/grafana/pkg/util/cmputil" ) var ( @@ -104,6 +110,24 @@ type AlertRule struct { Labels map[string]string } +// Diff calculates diff between two alert rules. Returns nil if two rules are equal. Otherwise, returns cmputil.DiffReport +func (alertRule *AlertRule) Diff(rule *AlertRule, ignore ...string) cmputil.DiffReport { + var reporter cmputil.DiffReporter + ops := make([]cmp.Option, 0, 4) + + // json.RawMessage is a slice of bytes and therefore cmp's default behavior is to compare it by byte, which is not really useful + var jsonCmp = cmp.Transformer("", func(in json.RawMessage) string { + return string(in) + }) + ops = append(ops, cmp.Reporter(&reporter), cmpopts.IgnoreFields(AlertQuery{}, "modelProps"), jsonCmp) + + if len(ignore) > 0 { + ops = append(ops, cmpopts.IgnoreFields(AlertRule{}, ignore...)) + } + cmp.Equal(alertRule, rule, ops...) + return reporter.Diffs +} + // AlertRuleKey is the alert definition identifier type AlertRuleKey struct { OrgID int64 diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index a5106fb7063..aa9934abc1a 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -1,12 +1,14 @@ package models import ( + "encoding/json" "math/rand" "strings" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/util" @@ -225,3 +227,316 @@ func TestPatchPartialAlertRule(t *testing.T) { } }) } + +func TestDiff(t *testing.T) { + t.Run("should return nil if there is no diff", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + result := rule1.Diff(rule2) + require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) + }) + + t.Run("should respect fields to ignore", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + rule2.ID = rule1.ID/2 + 1 + rule2.Version = rule1.Version/2 + 1 + rule2.Updated = rule1.Updated.Add(1 * time.Second) + result := rule1.Diff(rule2, "ID", "Version", "Updated") + require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) + }) + + t.Run("should find diff in simple fields", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := AlertRuleGen()() + + diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels") // these fields will be tested separately + + difCnt := 0 + if rule1.ID != rule2.ID { + diff := diffs.GetDiffsForField("ID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.ID, diff[0].Left.Int()) + assert.Equal(t, rule2.ID, diff[0].Right.Int()) + difCnt++ + } + if rule1.OrgID != rule2.OrgID { + diff := diffs.GetDiffsForField("OrgID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.OrgID, diff[0].Left.Int()) + assert.Equal(t, rule2.OrgID, diff[0].Right.Int()) + difCnt++ + } + if rule1.Title != rule2.Title { + diff := diffs.GetDiffsForField("Title") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Title, diff[0].Left.String()) + assert.Equal(t, rule2.Title, diff[0].Right.String()) + difCnt++ + } + if rule1.Condition != rule2.Condition { + diff := diffs.GetDiffsForField("Condition") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Condition, diff[0].Left.String()) + assert.Equal(t, rule2.Condition, diff[0].Right.String()) + difCnt++ + } + if rule1.Updated != rule2.Updated { + diff := diffs.GetDiffsForField("Updated") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Updated, diff[0].Left.Interface()) + assert.Equal(t, rule2.Updated, diff[0].Right.Interface()) + difCnt++ + } + if rule1.IntervalSeconds != rule2.IntervalSeconds { + diff := diffs.GetDiffsForField("IntervalSeconds") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.IntervalSeconds, diff[0].Left.Int()) + assert.Equal(t, rule2.IntervalSeconds, diff[0].Right.Int()) + difCnt++ + } + if rule1.Version != rule2.Version { + diff := diffs.GetDiffsForField("Version") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Version, diff[0].Left.Int()) + assert.Equal(t, rule2.Version, diff[0].Right.Int()) + difCnt++ + } + if rule1.UID != rule2.UID { + diff := diffs.GetDiffsForField("UID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.UID, diff[0].Left.String()) + assert.Equal(t, rule2.UID, diff[0].Right.String()) + difCnt++ + } + if rule1.NamespaceUID != rule2.NamespaceUID { + diff := diffs.GetDiffsForField("NamespaceUID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.NamespaceUID, diff[0].Left.String()) + assert.Equal(t, rule2.NamespaceUID, diff[0].Right.String()) + difCnt++ + } + if rule1.DashboardUID != rule2.DashboardUID { + diff := diffs.GetDiffsForField("DashboardUID") + assert.Len(t, diff, 1) + + if rule1.DashboardUID == nil { + assert.True(t, diff[0].Left.IsNil()) + } else { + assert.Equal(t, *rule1.DashboardUID, diff[0].Left.Elem().String()) + } + if rule2.DashboardUID == nil { + assert.True(t, diff[0].Right.IsNil()) + } else { + assert.Equal(t, *rule2.DashboardUID, diff[0].Right.Elem().String()) + } + difCnt++ + } + if rule1.PanelID != rule2.PanelID { + diff := diffs.GetDiffsForField("PanelID") + assert.Len(t, diff, 1) + + if rule1.PanelID == nil { + assert.True(t, diff[0].Left.IsNil()) + } else { + assert.Equal(t, *rule1.PanelID, diff[0].Left.Elem().Int()) + } + if rule2.PanelID == nil { + assert.True(t, diff[0].Right.IsNil()) + } else { + assert.Equal(t, *rule2.PanelID, diff[0].Right.Elem().Int()) + } + difCnt++ + } + if rule1.RuleGroup != rule2.RuleGroup { + diff := diffs.GetDiffsForField("RuleGroup") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.RuleGroup, diff[0].Left.String()) + assert.Equal(t, rule2.RuleGroup, diff[0].Right.String()) + difCnt++ + } + if rule1.NoDataState != rule2.NoDataState { + diff := diffs.GetDiffsForField("NoDataState") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.NoDataState, diff[0].Left.Interface()) + assert.Equal(t, rule2.NoDataState, diff[0].Right.Interface()) + difCnt++ + } + if rule1.ExecErrState != rule2.ExecErrState { + diff := diffs.GetDiffsForField("ExecErrState") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.ExecErrState, diff[0].Left.Interface()) + assert.Equal(t, rule2.ExecErrState, diff[0].Right.Interface()) + difCnt++ + } + if rule1.For != rule2.For { + diff := diffs.GetDiffsForField("For") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.For, diff[0].Left.Interface()) + assert.Equal(t, rule2.For, diff[0].Right.Interface()) + difCnt++ + } + + require.Lenf(t, diffs, difCnt, "Got some unexpected diffs. Either add to ignore or add assert to it") + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, diffs) + } + }) + + t.Run("should detect changes in Annotations", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + rule1.Annotations = map[string]string{ + "key1": "value1", + "key2": "value2", + } + + rule2.Annotations = map[string]string{ + "key2": "value22", + "key3": "value3", + } + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 3) + + d := diff.GetDiffsForField("Annotations[key1]") + assert.Len(t, d, 1) + assert.Equal(t, "value1", d[0].Left.String()) + assert.False(t, d[0].Right.IsValid()) + + d = diff.GetDiffsForField("Annotations[key2]") + assert.Len(t, d, 1) + assert.Equal(t, "value2", d[0].Left.String()) + assert.Equal(t, "value22", d[0].Right.String()) + + d = diff.GetDiffsForField("Annotations[key3]") + assert.Len(t, d, 1) + assert.False(t, d[0].Left.IsValid()) + assert.Equal(t, "value3", d[0].Right.String()) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect changes in Labels", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + rule1.Labels = map[string]string{ + "key1": "value1", + "key2": "value2", + } + + rule2.Labels = map[string]string{ + "key2": "value22", + "key3": "value3", + } + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 3) + + d := diff.GetDiffsForField("Labels[key1]") + assert.Len(t, d, 1) + assert.Equal(t, "value1", d[0].Left.String()) + assert.False(t, d[0].Right.IsValid()) + + d = diff.GetDiffsForField("Labels[key2]") + assert.Len(t, d, 1) + assert.Equal(t, "value2", d[0].Left.String()) + assert.Equal(t, "value22", d[0].Right.String()) + + d = diff.GetDiffsForField("Labels[key3]") + assert.Len(t, d, 1) + assert.False(t, d[0].Left.IsValid()) + assert.Equal(t, "value3", d[0].Right.String()) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, d) + } + }) + + t.Run("should detect changes in Data", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + query1 := AlertQuery{ + RefID: "A", + QueryType: util.GenerateShortUID(), + RelativeTimeRange: RelativeTimeRange{ + From: Duration(5 * time.Hour), + To: 0, + }, + DatasourceUID: util.GenerateShortUID(), + Model: json.RawMessage(`{ "test": "data"}`), + modelProps: map[string]interface{}{ + "test": 1, + }, + } + + rule1.Data = []AlertQuery{query1} + + t.Run("should ignore modelProps", func(t *testing.T) { + query2 := query1 + query2.modelProps = map[string]interface{}{ + "some": "other value", + } + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Nil(t, diff) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect changes inside the query", func(t *testing.T) { + query2 := query1 + query2.QueryType = "test" + query2.RefID = "test" + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 2) + + d := diff.GetDiffsForField("Data[0].QueryType") + assert.Len(t, d, 1) + d = diff.GetDiffsForField("Data[0].RefID") + assert.Len(t, d, 1) + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect new changes in array if too many fields changed", func(t *testing.T) { + query2 := query1 + query2.QueryType = "test" + query2.RefID = "test" + query2.DatasourceUID = "test" + query2.Model = json.RawMessage(`{ "test": "da2ta"}`) + + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 2) + + for _, d := range diff { + assert.Equal(t, "Data", d.Path) + if d.Left.IsValid() { + assert.Equal(t, query1, d.Left.Interface()) + } else { + assert.Equal(t, query2, d.Right.Interface()) + } + } + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + }) +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 3428086fc6a..8c05872ba1d 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "fmt" "math/rand" "time" @@ -60,24 +61,11 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } rule := &AlertRule{ - ID: rand.Int63(), - OrgID: rand.Int63(), - Title: "TEST-ALERT-" + util.GenerateShortUID(), - Condition: "A", - Data: []AlertQuery{ - { - DatasourceUID: "-100", - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 1 < 1" - }`), - RelativeTimeRange: RelativeTimeRange{ - From: Duration(5 * time.Hour), - To: Duration(3 * time.Hour), - }, - RefID: "A", - }}, + ID: rand.Int63(), + OrgID: rand.Int63(), + Title: "TEST-ALERT-" + util.GenerateShortUID(), + Condition: "A", + Data: []AlertQuery{GenerateAlertQuery()}, Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)), IntervalSeconds: rand.Int63n(60) + 1, Version: rand.Int63(), @@ -100,6 +88,25 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } } +func GenerateAlertQuery() AlertQuery { + f := rand.Intn(10) + 5 + t := rand.Intn(f) + + return AlertQuery{ + DatasourceUID: util.GenerateShortUID(), + Model: json.RawMessage(fmt.Sprintf(`{ + "%s": "%s", + "%s":"%d" + }`, util.GenerateShortUID(), util.GenerateShortUID(), util.GenerateShortUID(), rand.Int())), + RelativeTimeRange: RelativeTimeRange{ + From: Duration(time.Duration(f) * time.Minute), + To: Duration(time.Duration(t) * time.Minute), + }, + RefID: util.GenerateShortUID(), + QueryType: util.GenerateShortUID(), + } +} + // GenerateUniqueAlertRules generates many random alert rules and makes sure that they have unique UID. // It returns a tuple where first element is a map where keys are UID of alert rule and the second element is a slice of the same rules func GenerateUniqueAlertRules(count int, f func() *AlertRule) (map[string]*AlertRule, []*AlertRule) { @@ -125,3 +132,59 @@ func GenerateAlertRules(count int, f func() *AlertRule) []*AlertRule { } return result } + +// CopyRule creates a deep copy of AlertRule +func CopyRule(r *AlertRule) *AlertRule { + result := AlertRule{ + ID: r.ID, + OrgID: r.OrgID, + Title: r.Title, + Condition: r.Condition, + Updated: r.Updated, + IntervalSeconds: r.IntervalSeconds, + Version: r.Version, + UID: r.UID, + NamespaceUID: r.NamespaceUID, + RuleGroup: r.RuleGroup, + NoDataState: r.NoDataState, + ExecErrState: r.ExecErrState, + For: r.For, + } + + if r.DashboardUID != nil { + dash := *r.DashboardUID + result.DashboardUID = &dash + } + if r.PanelID != nil { + p := *r.PanelID + result.PanelID = &p + } + + for _, d := range r.Data { + q := AlertQuery{ + RefID: d.RefID, + QueryType: d.QueryType, + RelativeTimeRange: d.RelativeTimeRange, + DatasourceUID: d.DatasourceUID, + } + q.Model = make([]byte, 0, cap(d.Model)) + q.Model = append(q.Model, d.Model...) + result.Data = append(result.Data, q) + } + + if r.Annotations != nil { + result.Annotations = make(map[string]string, len(r.Annotations)) + for s, s2 := range r.Annotations { + result.Annotations[s] = s2 + } + } + + if r.Labels != nil { + result.Labels = make(map[string]string, len(r.Labels)) + for s, s2 := range r.Labels { + result.Labels[s] = s2 + } + } + + return &result +} diff --git a/pkg/util/cmputil/reporter.go b/pkg/util/cmputil/reporter.go new file mode 100644 index 00000000000..c9532d3cc0f --- /dev/null +++ b/pkg/util/cmputil/reporter.go @@ -0,0 +1,101 @@ +package cmputil + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +type DiffReport []Diff + +// GetDiffsForField returns subset of the diffs which path starts with the provided path +func (r DiffReport) GetDiffsForField(path string) DiffReport { + var result []Diff + for _, diff := range r { + if strings.HasPrefix(path, diff.Path) { + result = append(result, diff) + } + } + return result +} + +// DiffReporter is a simple custom reporter that only records differences +// detected during comparison. Implements an interface required by cmp.Reporter option +type DiffReporter struct { + path cmp.Path + Diffs DiffReport +} + +func (r *DiffReporter) PushStep(ps cmp.PathStep) { + r.path = append(r.path, ps) +} + +func (r *DiffReporter) PopStep() { + r.path = r.path[:len(r.path)-1] +} + +func (r *DiffReporter) Report(rs cmp.Result) { + if !rs.Equal() { + vx, vy := r.path.Last().Values() + r.Diffs = append(r.Diffs, Diff{ + Path: printPath(r.path), + Left: vx, + Right: vy, + }) + } +} + +func printPath(p cmp.Path) string { + ss := strings.Builder{} + for _, s := range p { + toAdd := "" + switch v := s.(type) { + case cmp.StructField: + toAdd = v.String() + case cmp.MapIndex: + toAdd = fmt.Sprintf("[%s]", v.Key()) + case cmp.SliceIndex: + if v.Key() >= 0 { + toAdd = fmt.Sprintf("[%d]", v.Key()) + } + } + if toAdd == "" { + continue + } + ss.WriteString(toAdd) + } + return strings.TrimPrefix(ss.String(), ".") +} + +func (r DiffReport) String() string { + b := strings.Builder{} + for _, diff := range r { + b.WriteString(diff.String()) + b.WriteByte('\n') + } + return b.String() +} + +type Diff struct { + // Path to the field that has difference separated by period. Array index and key are designated by square brackets. + // For example, Annotations[12345].Data.Fields[0].ID + Path string + Left reflect.Value + Right reflect.Value +} + +func (d *Diff) String() string { + left := d.Left.String() + // invalid reflect.Value is produced when two collections (slices\maps) are compared and one misses value. + // This way go-cmp indicates that an element was added\removed from a list. + if !d.Left.IsValid() { + left = "" + } + right := d.Right.String() + if !d.Right.IsValid() { + right = "" + } + return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v", d.Path, left, right) +} From e814e7364bebe80c61c2439e9f54f908a7dcce87 Mon Sep 17 00:00:00 2001 From: Nathan Rodman Date: Mon, 28 Feb 2022 09:50:17 -0800 Subject: [PATCH 064/125] Alerting: fix alert groups grouping (#45012) * fix multiple non-grouped groupings * drop duplicate alerts * add test for multiple groups without labels Co-authored-by: gillesdemey --- .../alerting/unified/AlertGroups.test.tsx | 15 ++++++++++++ .../unified/hooks/useFilteredAmGroups.ts | 4 ++-- .../unified/hooks/useGroupedAlerts.ts | 23 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index 582b4a94a2b..890aa4aef50 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -147,4 +147,19 @@ describe('AlertGroups', () => { expect(groups[0]).toHaveTextContent('No grouping'); expect(groups[1]).toHaveTextContent('uniqueLabel=true'); }); + + it('should combine multiple ungrouped groups', async () => { + mocks.api.fetchAlertGroups.mockImplementation(() => { + const groups = [ + mockAlertGroup({ labels: {} }), + mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }), + ]; + return Promise.resolve(groups); + }); + renderAmNotifications(); + await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled()); + const groups = ui.group.getAll(); + + expect(groups).toHaveLength(1); + }); }); diff --git a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts index fb5f54ae78a..a47e9685979 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts @@ -10,7 +10,7 @@ export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => { const matchers = parseMatchers(filters.queryString || ''); return useMemo(() => { - return groups.reduce((filteredGroup, group) => { + return groups.reduce((filteredGroup: AlertmanagerGroup[], group) => { const alerts = group.alerts.filter(({ labels, status }) => { const labelsMatch = labelsMatchMatchers(labels, matchers); const filtersMatch = filters.alertState ? status.state === filters.alertState : true; @@ -25,6 +25,6 @@ export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => { } } return filteredGroup; - }, [] as AlertmanagerGroup[]); + }, []); }, [groups, filters, matchers]); }; diff --git a/public/app/features/alerting/unified/hooks/useGroupedAlerts.ts b/public/app/features/alerting/unified/hooks/useGroupedAlerts.ts index 278851fbfc5..72932587d2a 100644 --- a/public/app/features/alerting/unified/hooks/useGroupedAlerts.ts +++ b/public/app/features/alerting/unified/hooks/useGroupedAlerts.ts @@ -1,11 +1,30 @@ import { useMemo } from 'react'; import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; import { Labels } from '@grafana/data'; +import { uniqBy } from 'lodash'; -export const useGroupedAlerts = (groups: AlertmanagerGroup[], groupBy: string[]) => { +export const useGroupedAlerts = (groups: AlertmanagerGroup[], groupBy: string[]): AlertmanagerGroup[] => { return useMemo(() => { if (groupBy.length === 0) { - return groups; + const emptyGroupings = groups.filter((group) => Object.keys(group.labels).length === 0); + if (emptyGroupings.length > 1) { + // Merges multiple ungrouped grouping + return groups.reduce((combinedGroups, group) => { + if (Object.keys(group.labels).length === 0) { + const noGroupingGroup = combinedGroups.find(({ labels }) => Object.keys(labels)); + if (!noGroupingGroup) { + combinedGroups.push({ alerts: group.alerts, labels: {}, receiver: { name: 'NONE' } }); + } else { + noGroupingGroup.alerts = uniqBy([...noGroupingGroup.alerts, ...group.alerts], 'labels'); + } + } else { + combinedGroups.push(group); + } + return combinedGroups; + }, [] as AlertmanagerGroup[]); + } else { + return groups; + } } const alerts = groups.flatMap(({ alerts }) => alerts); return alerts.reduce((groupings, alert) => { From 06ed5efdf09efeaffa766f0009f5272f05e808c7 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:58:56 +0800 Subject: [PATCH 065/125] Middleware: Fix IPv6 host parsing in CSRF check (#45911) - Also create tests for this middleware Co-authored-by: Kyle Brandt --- pkg/api/http_server.go | 2 +- pkg/middleware/csrf.go | 19 ++++-- pkg/middleware/csrf_test.go | 124 ++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 pkg/middleware/csrf_test.go diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 25040a02729..7fddd31bfaa 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -467,7 +467,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { } m.Use(middleware.Recovery(hs.Cfg)) - m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName)) + m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName, hs.log)) hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build") hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public", "/public/views/swagger.html") diff --git a/pkg/middleware/csrf.go b/pkg/middleware/csrf.go index bc70d09779d..7bce53f5666 100644 --- a/pkg/middleware/csrf.go +++ b/pkg/middleware/csrf.go @@ -4,10 +4,12 @@ import ( "errors" "net/http" "net/url" - "strings" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/util" ) -func CSRF(loginCookieName string) func(http.Handler) http.Handler { +func CSRF(loginCookieName string, logger log.Logger) func(http.Handler) http.Handler { // As per RFC 7231/4.2.2 these methods are idempotent: // (GET is excluded because it may have side effects in some APIs) safeMethods := []string{"HEAD", "OPTIONS", "TRACE"} @@ -27,12 +29,21 @@ func CSRF(loginCookieName string) func(http.Handler) http.Handler { } } // Otherwise - verify that Origin matches the server origin - host := strings.Split(r.Host, ":")[0] + netAddr, err := util.SplitHostPortDefault(r.Host, "", "0") // we ignore the port + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + origin, err := url.Parse(r.Header.Get("Origin")) - if err != nil || (origin.String() != "" && origin.Hostname() != host) { + if err != nil { + logger.Error("error parsing Origin header", "err", err) + } + if err != nil || netAddr.Host == "" || (origin.String() != "" && origin.Hostname() != netAddr.Host) { http.Error(w, "origin not allowed", http.StatusForbidden) return } + next.ServeHTTP(w, r) }) } diff --git a/pkg/middleware/csrf_test.go b/pkg/middleware/csrf_test.go new file mode 100644 index 00000000000..312356cce3f --- /dev/null +++ b/pkg/middleware/csrf_test.go @@ -0,0 +1,124 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/stretchr/testify/require" +) + +func TestMiddlewareCSRF(t *testing.T) { + tests := []struct { + name string + cookieName string + method string + origin string + host string + code int + }{ + { + name: "mismatched origin and host is forbidden", + cookieName: "foo", + method: "GET", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusForbidden, + }, + { + name: "mismatched origin and host is NOT forbidden with a 'Safe Method'", + cookieName: "foo", + method: "TRACE", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusOK, + }, + { + name: "mismatched origin and host is NOT forbidden without a cookie", + cookieName: "", + method: "GET", + origin: "http://notLocalhost", + host: "localhost", + code: http.StatusOK, + }, + { + name: "malformed host is a bad request", + cookieName: "foo", + method: "GET", + host: "localhost:80:80", + code: http.StatusBadRequest, + }, + { + name: "host works without port", + cookieName: "foo", + method: "GET", + host: "localhost", + origin: "http://localhost", + code: http.StatusOK, + }, + { + name: "port does not have to match", + cookieName: "foo", + method: "GET", + host: "localhost:80", + origin: "http://localhost:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host works with port", + cookieName: "foo", + method: "GET", + host: "[::1]:3000", + origin: "http://[::1]:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host (with longer address) works with port", + cookieName: "foo", + method: "GET", + host: "[2001:db8::1]:3000", + origin: "http://[2001:db8::1]:3000", + code: http.StatusOK, + }, + { + name: "IPv6 host (with longer address) works without port", + cookieName: "foo", + method: "GET", + host: "[2001:db8::1]", + origin: "http://[2001:db8::1]", + code: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rr := csrfScenario(t, tt.cookieName, tt.method, tt.origin, tt.host) + require.Equal(t, tt.code, rr.Code) + }) + } +} + +func csrfScenario(t *testing.T, cookieName, method, origin, host string) *httptest.ResponseRecorder { + req, err := http.NewRequest(method, "/", nil) + if err != nil { + t.Fatal(err) + } + req.AddCookie(&http.Cookie{ + Name: cookieName, + }) + + // Note: Not sure where host header populates req.Host, or how that works. + req.Host = host + req.Header.Set("HOST", host) + + req.Header.Set("ORIGIN", origin) + + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + }) + + rr := httptest.NewRecorder() + handler := CSRF(cookieName, log.New())(testHandler) + handler.ServeHTTP(rr, req) + return rr +} From 83664121bc09399f76ee387c7e5cd0e85aa70020 Mon Sep 17 00:00:00 2001 From: Cat Perry <000.perry@gmail.com> Date: Mon, 28 Feb 2022 11:06:09 -0800 Subject: [PATCH 066/125] Jaeger: Show loader when search options havent yet loaded (#45936) * Swap out Select component for AsyncSelect to Jaeger search panel --- .../jaeger/components/SearchForm.test.tsx | 154 ++++++++++++++++++ .../jaeger/components/SearchForm.tsx | 111 ++++++++----- 2 files changed, 224 insertions(+), 41 deletions(-) create mode 100644 public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx new file mode 100644 index 00000000000..77d85ac8526 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -0,0 +1,154 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { createFetchResponse } from 'test/helpers/createFetchResponse'; +import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginType } from '@grafana/data'; +import { of } from 'rxjs'; +import { JaegerDatasource, JaegerJsonData } from '../datasource'; +import { JaegerQuery } from '../types'; +import React from 'react'; +import SearchForm from './SearchForm'; +import { testResponse } from '../testResponse'; +import userEvent from '@testing-library/user-event'; + +describe('SearchForm', () => { + it('should call the `onChange` function on click of the Input', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + }; + const ds = { + async metadataRequest(url: string, params?: Record): Promise { + if (url === '/api/services') { + return Promise.resolve(['jaeger-query', 'service2', 'service3']); + } + }, + } as JaegerDatasource; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = await waitFor(() => screen.getByRole('combobox', { name: 'select-service-name' })); + expect(asyncServiceSelect).toBeInTheDocument(); + + userEvent.click(asyncServiceSelect); + + const jaegerService = await screen.findByText('jaeger-query'); + expect(jaegerService).toBeInTheDocument(); + }); + + it('should be able to select operation name if query.service exists', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query2 = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncOperationSelect2 = await waitFor(() => screen.getByRole('combobox', { name: 'select-operation-name' })); + expect(asyncOperationSelect2).toBeInTheDocument(); + }); +}); + +describe('SearchForm', () => { + it('should show loader if there is a delay fetching options', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => { + setTimeout(() => { + return promise; + }, 3000); + }); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + const ds = new JaegerDatasource(defaultSettings); + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); + userEvent.click(asyncServiceSelect); + const loader = screen.getByText('Loading options...'); + + expect(loader).toBeInTheDocument(); + await act(() => promise); + }); +}); + +function setupFetchMock(response: any, mock?: any) { + const defaultMock = () => mock ?? of(createFetchResponse(response)); + + const fetchMock = jest.spyOn(backendSrv, 'fetch'); + fetchMock.mockImplementation(defaultMock); + return fetchMock; +} + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: '0', + type: 'tracing', + name: 'jaeger', + url: 'http://grafana.com', + access: 'proxy', + meta: { + id: 'jaeger', + name: 'jaeger', + type: PluginType.datasource, + info: {} as any, + module: '', + baseUrl: '', + }, + jsonData: { + nodeGraph: { + enabled: true, + }, + }, +}; + +const defaultQuery: DataQueryRequest = { + requestId: '1', + dashboardId: 0, + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 1c4e6de235b..c362314bbed 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -1,11 +1,14 @@ import { css } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; -import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; -import React, { useEffect, useState } from 'react'; +import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui'; +import React, { useCallback, useEffect, useState } from 'react'; import { JaegerDatasource } from '../datasource'; import { JaegerQuery } from '../types'; import { transformToLogfmt } from '../util'; import { AdvancedOptions } from './AdvancedOptions'; +import { dispatch } from 'app/store/store'; +import { notifyApp } from 'app/core/actions'; +import { createErrorNotification } from 'app/core/copy/appNotification'; type Props = { datasource: JaegerDatasource; @@ -22,69 +25,110 @@ const allOperationsOption: SelectableValue = { export function SearchForm({ datasource, query, onChange }: Props) { const [serviceOptions, setServiceOptions] = useState>>(); const [operationOptions, setOperationOptions] = useState>>(); + const [isLoading, setIsLoading] = useState<{ + services: boolean; + operations: boolean; + }>({ + services: false, + operations: false, + }); + + const loadServices = useCallback( + async (url: string, loaderOfType: string): Promise>> => { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: true })); + + try { + const values: string[] | null = await datasource.metadataRequest(url); + if (!values) { + return [{ label: `No ${loaderOfType} found`, value: `No ${loaderOfType} found` }]; + } + + const serviceOptions: SelectableValue[] = values.sort().map((service) => ({ + label: service, + value: service, + })); + return serviceOptions; + } catch (error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + return []; + } finally { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false })); + } + }, + [datasource] + ); useEffect(() => { const getServices = async () => { - const services = await loadServices({ - dataSource: datasource, - url: '/api/services', - notFoundLabel: 'No service found', - }); + const services = await loadServices('/api/services', 'services'); setServiceOptions(services); }; getServices(); - }, [datasource]); + }, [datasource, loadServices]); useEffect(() => { const getOperations = async () => { - const operations = await loadServices({ - dataSource: datasource, - url: `/api/services/${encodeURIComponent(query.service!)}/operations`, - notFoundLabel: 'No operation found', - }); + const operations = await loadServices( + `/api/services/${encodeURIComponent(query.service!)}/operations`, + 'operations' + ); setOperationOptions([allOperationsOption, ...operations]); }; if (query.service) { getOperations(); } - }, [datasource, query.service]); + }, [datasource, query.service, loadServices]); return (
- + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + onOpenMenu={() => + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + isLoading={isLoading.operations} value={operationOptions?.find((v) => v.value === query.operation) || null} onChange={(v) => onChange({ ...query, - operation: v.value!, + operation: v?.value! || undefined, }) } menuPlacement="bottom" isClearable + defaultOptions + aria-label={'select-operation-name'} /> @@ -108,19 +152,4 @@ export function SearchForm({ datasource, query, onChange }: Props) { ); } -type Options = { dataSource: JaegerDatasource; url: string; notFoundLabel: string }; - -const loadServices = async ({ dataSource, url, notFoundLabel }: Options): Promise>> => { - const services: string[] | null = await dataSource.metadataRequest(url); - - if (!services) { - return [{ label: notFoundLabel, value: notFoundLabel }]; - } - - const serviceOptions: SelectableValue[] = services.sort().map((service) => ({ - label: service, - value: service, - })); - - return serviceOptions; -}; +export default SearchForm; From 5aab0063c7335474582b9a7d17b2fc069270500e Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 28 Feb 2022 14:16:30 -0600 Subject: [PATCH 067/125] StateTimeline: fix duration in tooltip (#45955) - Fixes duration in StateTimeline appearing incorrectly when "merge consecutive values" is enabled. --- public/app/plugins/panel/state-timeline/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 07b4d336c2d..dd71932f9f8 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -555,16 +555,18 @@ export function findNextStateIndex(field: Field, datapointIdx: number) { return null; } + const startValue = field.values.get(datapointIdx); + while (end === undefined) { if (rightPointer >= field.values.length) { return null; } const rightValue = field.values.get(rightPointer); - if (rightValue !== undefined) { - end = rightPointer; - } else { + if (rightValue === undefined || rightValue === startValue) { rightPointer++; + } else { + end = rightPointer; } } From 1c4b20b2686b38dfa2312dd0c36ec7fce777f493 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 28 Feb 2022 15:21:09 -0600 Subject: [PATCH 068/125] BarChart: fix single group rendering (#45953) --- public/app/plugins/panel/barchart/bars.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index a8e79ca1942..098160b6e73 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -189,7 +189,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // this expands the distr: 2 scale so that the indicies of each data[0] land at the proper justified positions const xRange: Scale.Range = (u, min, max) => { min = 0; - max = u.data[0].length - 1; + max = Math.max(1, u.data[0].length - 1); let pctOffset = 0; @@ -199,13 +199,17 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { }); // expand scale range by equal amounts on both ends - let rn = max - min; // TODO: clamp to 1? + let rn = max - min; - let upScale = 1 / (1 - pctOffset * 2); - let offset = (upScale * rn - rn) / 2; + if (pctOffset === 0.5) { + min -= rn; + } else { + let upScale = 1 / (1 - pctOffset * 2); + let offset = (upScale * rn - rn) / 2; - min -= offset; - max += offset; + min -= offset; + max += offset; + } return [min, max]; }; From b491d6b4dc5c768fa5f24d9e83f5485e0395a6ce Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 1 Mar 2022 01:55:53 -0600 Subject: [PATCH 069/125] Histogram: auto-skip x tick labels to avoid overlap (#45996) --- .../app/plugins/panel/histogram/Histogram.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index a4b21980c2f..29115ecff96 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -15,7 +15,15 @@ import { getFieldSeriesColor, GrafanaTheme2, } from '@grafana/data'; -import { Themeable2, UPlotConfigBuilder, UPlotChart, VizLayout, PlotLegend } from '@grafana/ui'; +import { + Themeable2, + UPlotConfigBuilder, + UPlotChart, + VizLayout, + PlotLegend, + measureText, + UPLOT_AXIS_FONT_SIZE, +} from '@grafana/ui'; import { histogramBucketSizes, @@ -119,7 +127,20 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { placement: AxisPlacement.Bottom, incrs: histogramBucketSizes, splits: xSplits, - values: (u: uPlot, vals: any[]) => vals.map(xAxisFormatter), + values: (u: uPlot, splits: any[]) => { + const tickLabels = splits.map(xAxisFormatter); + + const maxWidth = tickLabels.reduce( + (curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax), + 0 + ); + + const labelSpacing = 10; + const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio); + const keepMod = Math.ceil(tickLabels.length / maxCount); + + return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null)); + }, //incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize), //splits: config.xSplits, //values: config.xValues, From bc5237e8409ad1385cc6db0bc3f9c170b5db127d Mon Sep 17 00:00:00 2001 From: J Guerreiro Date: Tue, 1 Mar 2022 08:21:55 +0000 Subject: [PATCH 070/125] Service Accounts: Link final components in service accounts detail page (#45929) * ServiceAccounts: Delete/Disable service account from details page * ServiceAccounts: capitalize viewable messages from UI * ServiceAccounts: Link new update endpoint to details page * ServiceAccounts: reimplement service account retrieve to include is_disabled and only target service accounts * Cleanup styles * Fix modal show * ServiceAccounts: simplify handler functions * Apply suggestions from code review Co-authored-by: Alex Khomenko Co-authored-by: Clarity-89 Co-authored-by: Alex Khomenko --- pkg/services/serviceaccounts/api/api.go | 14 +-- pkg/services/serviceaccounts/api/token.go | 6 +- .../serviceaccounts/database/database.go | 104 +++++++++++------- pkg/services/serviceaccounts/models.go | 27 ++--- .../serviceaccounts/serviceaccounts.go | 2 +- pkg/services/serviceaccounts/tests/common.go | 2 +- .../serviceaccounts/ServiceAccountPage.tsx | 28 ++--- .../serviceaccounts/ServiceAccountProfile.tsx | 84 +++++++------- .../serviceaccounts/ServiceAccountRoleRow.tsx | 6 +- .../features/serviceaccounts/state/actions.ts | 13 ++- 10 files changed, 160 insertions(+), 126 deletions(-) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index bce3a4d8a6c..628597878ff 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -109,12 +109,12 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp if err != nil { return response.Error(http.StatusInternalServerError, "Service account deletion error", err) } - return response.Success("service account deleted") + return response.Success("Service account deleted") } func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) response.Response { if err := api.store.UpgradeServiceAccounts(ctx.Req.Context()); err == nil { - return response.Success("service accounts upgraded") + return response.Success("Service accounts upgraded") } else { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -123,10 +123,10 @@ func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) re func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "keyId is invalid", err) + return response.Error(http.StatusBadRequest, "Key ID is invalid", err) } if err := api.store.ConvertToServiceAccounts(ctx.Req.Context(), []int64{keyId}); err == nil { - return response.Success("service accounts converted") + return response.Success("Service accounts converted") } else { return response.Error(500, "Internal server error", err) } @@ -174,7 +174,7 @@ func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, sa func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } serviceAccount, err := api.store.RetrieveServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID) @@ -197,12 +197,12 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } cmd := &serviceaccounts.UpdateServiceAccountForm{} if err := web.Bind(c.Req, &cmd); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) + return response.Error(http.StatusBadRequest, "Bad request data", err) } if cmd.Role != nil && !cmd.Role.IsValid() { diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 7347084501a..787d06fdadb 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -39,7 +39,7 @@ const sevenDaysAhead = 7 * 24 * time.Hour func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } if saTokens, err := api.store.ListTokens(ctx.Req.Context(), ctx.OrgId, saID); err == nil { @@ -78,7 +78,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { - return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err) + return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } // confirm service account exists @@ -93,7 +93,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon cmd := models.AddApiKeyCommand{} if err := web.Bind(c.Req, &cmd); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) + return response.Error(http.StatusBadRequest, "Bad request data", err) } // Force affected service account to be the one referenced in the URL diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index d9ec41dd7e1..c83abb94db9 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -4,6 +4,7 @@ package database import ( "context" "fmt" + "strings" "time" "github.com/google/uuid" @@ -169,14 +170,53 @@ func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgI // RetrieveServiceAccountByID returns a service account by its ID func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true} - err := s.sqlStore.GetOrgUsers(ctx, &query) + serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{} + + err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + sess := dbSession.Table("org_user") + sess.Join("INNER", s.sqlStore.Dialect.Quote("user"), + fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.Dialect.Quote("user"))) + + whereConditions := make([]string, 0, 3) + whereParams := make([]interface{}, 0) + + whereConditions = append(whereConditions, "org_user.org_id = ?") + whereParams = append(whereParams, orgID) + + whereConditions = append(whereConditions, "org_user.user_id = ?") + whereParams = append(whereParams, serviceAccountID) + + whereConditions = append(whereConditions, + fmt.Sprintf("%s.is_service_account = %s", + s.sqlStore.Dialect.Quote("user"), + s.sqlStore.Dialect.BooleanStr(true))) + + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + + sess.Cols( + "org_user.user_id", + "org_user.org_id", + "org_user.role", + "user.email", + "user.name", + "user.login", + "user.created", + "user.updated", + "user.is_disabled", + ) + + if ok, err := sess.Get(serviceAccount); err != nil { + return err + } else if !ok { + return serviceaccounts.ErrServiceAccountNotFound + } + + return nil + }) + if err != nil { return nil, err } - if len(query.Result) != 1 { - return nil, serviceaccounts.ErrServiceAccountNotFound - } // Get Teams of service account. Can be optimized by combining with the query above // in refactor @@ -190,36 +230,24 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o teams[i] = getTeamQuery.Result[i].Name } - saProfile := &serviceaccounts.ServiceAccountProfileDTO{ - Id: query.Result[0].UserId, - Name: query.Result[0].Name, - Login: query.Result[0].Login, - OrgId: query.Result[0].OrgId, - UpdatedAt: query.Result[0].Updated, - CreatedAt: query.Result[0].Created, - Role: query.Result[0].Role, - Teams: teams, - } - return saProfile, nil + serviceAccount.Teams = teams + + return serviceAccount, nil } func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - updatedUser := &models.OrgUserDTO{} + saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { + updatedUser := &serviceaccounts.ServiceAccountProfileDTO{} err := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true} - if err := s.sqlStore.GetOrgUsers(ctx, &query); err != nil { + var err error + updatedUser, err = s.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + if err != nil { return err } - if len(query.Result) != 1 { - return serviceaccounts.ErrServiceAccountNotFound - } - updatedUser = query.Result[0] - - if saForm.Name == nil && saForm.Role == nil { + if saForm.Name == nil && saForm.Role == nil && saForm.IsDisabled == nil { return nil } @@ -236,29 +264,31 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, updatedUser.Role = string(*saForm.Role) } - if saForm.Name != nil { + if saForm.Name != nil || saForm.IsDisabled != nil { user := models.User{ - Name: *saForm.Name, Updated: updateTime, } + if saForm.IsDisabled != nil { + user.IsDisabled = *saForm.IsDisabled + updatedUser.IsDisabled = *saForm.IsDisabled + sess.UseBool("is_disabled") + } + + if saForm.Name != nil { + user.Name = *saForm.Name + updatedUser.Name = *saForm.Name + } + if _, err := sess.ID(serviceAccountID).Update(&user); err != nil { return err } - - updatedUser.Name = *saForm.Name } return nil }) - return &serviceaccounts.ServiceAccountDTO{ - Id: updatedUser.UserId, - Name: updatedUser.Name, - Login: updatedUser.Login, - Role: updatedUser.Role, - OrgId: updatedUser.OrgId, - }, err + return updatedUser, err } func contains(s []int64, e int64) bool { diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index bd0d9e54108..7bd167dad25 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -24,8 +24,9 @@ type ServiceAccount struct { } type UpdateServiceAccountForm struct { - Name *string `json:"name"` - Role *models.RoleType `json:"role"` + Name *string `json:"name"` + Role *models.RoleType `json:"role"` + IsDisabled *bool `json:"isDisabled"` } type CreateServiceAccountForm struct { @@ -45,15 +46,15 @@ type ServiceAccountDTO struct { } type ServiceAccountProfileDTO struct { - Id int64 `json:"id"` - Name string `json:"name"` - Login string `json:"login"` - OrgId int64 `json:"orgId"` - IsDisabled bool `json:"isDisabled"` - UpdatedAt time.Time `json:"updatedAt"` - CreatedAt time.Time `json:"createdAt"` - AvatarUrl string `json:"avatarUrl"` - Role string `json:"role"` - Teams []string `json:"teams"` - AccessControl map[string]bool `json:"accessControl,omitempty"` + Id int64 `json:"id" xorm:"user_id"` + Name string `json:"name" xorm:"name"` + Login string `json:"login" xorm:"login"` + OrgId int64 `json:"orgId" xorm:"org_id"` + IsDisabled bool `json:"isDisabled" xorm:"is_disabled"` + Created time.Time `json:"createdAt" xorm:"created"` + Updated time.Time `json:"updatedAt" xorm:"updated"` + AvatarUrl string `json:"avatarUrl" xorm:"-"` + Role string `json:"role" xorm:"role"` + Teams []string `json:"teams" xorm:"-"` + AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"` } diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index ab8c35ba90c..7ba3b1b8fc3 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -15,7 +15,7 @@ type Service interface { type Store interface { CreateServiceAccount(ctx context.Context, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*ServiceAccountDTO, error) - UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountDTO, error) + UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error UpgradeServiceAccounts(ctx context.Context) error diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 8a177012bb9..0321741ce25 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -121,7 +121,7 @@ func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, o func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, - saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { s.Calls.UpdateServiceAccount = append(s.Calls.UpdateServiceAccount, []interface{}{ctx, orgID, serviceAccountID, saForm}) return nil, nil diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 6494f31d77e..918c54102e0 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -12,9 +12,10 @@ import { createServiceAccountToken, fetchACOptions, updateServiceAccount, + deleteServiceAccount, } from './state/actions'; import { ServiceAccountTokensTable } from './ServiceAccountTokensTable'; -import { getTimeZone, NavModel, OrgRole } from '@grafana/data'; +import { getTimeZone, NavModel } from '@grafana/data'; import { Button, VerticalGroup } from '@grafana/ui'; import { CreateTokenModal } from './CreateTokenModal'; import { contextSrv } from 'app/core/core'; @@ -44,6 +45,8 @@ const mapDispatchToProps = { loadServiceAccountTokens, createServiceAccountToken, deleteServiceAccountToken, + deleteServiceAccount, + updateServiceAccount, fetchACOptions, }; @@ -63,6 +66,8 @@ const ServiceAccountPageUnconnected = ({ loadServiceAccountTokens, createServiceAccountToken, deleteServiceAccountToken, + deleteServiceAccount, + updateServiceAccount, fetchACOptions, }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -90,12 +95,6 @@ const ServiceAccountPageUnconnected = ({ setNewToken(''); }; - const onRoleChange = (role: OrgRole, serviceAccount: ServiceAccountDTO) => { - const updatedServiceAccount = { ...serviceAccount, role: role }; - - updateServiceAccount(updatedServiceAccount); - }; - return ( @@ -104,21 +103,10 @@ const ServiceAccountPageUnconnected = ({ { - console.log(`not implemented`); - }} - onServiceAccountUpdate={() => { - console.log(`not implemented`); - }} - onServiceAccountDisable={() => { - console.log(`not implemented`); - }} - onServiceAccountEnable={() => { - console.log(`not implemented`); - }} - onRoleChange={onRoleChange} roleOptions={roleOptions} builtInRoles={builtInRoles} + updateServiceAccount={updateServiceAccount} + deleteServiceAccount={deleteServiceAccount} /> )} diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx index 86c7b8c552a..98df4a391a9 100644 --- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx @@ -1,35 +1,27 @@ import React, { PureComponent, useRef, useState } from 'react'; import { Role, ServiceAccountDTO } from 'app/types'; import { css, cx } from '@emotion/css'; -import { config } from 'app/core/config'; -import { dateTimeFormat, GrafanaTheme, OrgRole, TimeZone } from '@grafana/data'; -import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui'; +import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data'; +import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, useStyles2 } from '@grafana/ui'; import { ServiceAccountRoleRow } from './ServiceAccountRoleRow'; interface Props { serviceAccount: ServiceAccountDTO; timeZone: TimeZone; - onServiceAccountUpdate: (serviceAccount: ServiceAccountDTO) => void; - onServiceAccountDelete: (serviceAccountId: number) => void; - onServiceAccountDisable: (serviceAccountId: number) => void; - onServiceAccountEnable: (serviceAccountId: number) => void; - - onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void; roleOptions: Role[]; builtInRoles: Record; + deleteServiceAccount: (serviceAccountId: number) => void; + updateServiceAccount: (serviceAccount: ServiceAccountDTO) => void; } export function ServiceAccountProfile({ serviceAccount, timeZone, - onServiceAccountUpdate, - onServiceAccountDelete, - onServiceAccountDisable, - onServiceAccountEnable, - onRoleChange, roleOptions, builtInRoles, + deleteServiceAccount, + updateServiceAccount, }: Props) { const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false); @@ -50,20 +42,27 @@ export function ServiceAccountProfile({ } }; - const handleServiceAccountDelete = () => onServiceAccountDelete(serviceAccount.id); - - const handleServiceAccountDisable = () => onServiceAccountDisable(serviceAccount.id); - - const handleServiceAccountEnable = () => onServiceAccountEnable(serviceAccount.id); - - const onServiceAccountNameChange = (newValue: string) => { - onServiceAccountUpdate({ - ...serviceAccount, - name: newValue, - }); + const handleServiceAccountDelete = () => { + deleteServiceAccount(serviceAccount.id); + }; + const handleServiceAccountDisable = () => { + updateServiceAccount({ ...serviceAccount, isDisabled: true }); + setShowDisableModal(false); }; - const styles = getStyles(config.theme); + const handleServiceAccountEnable = () => { + updateServiceAccount({ ...serviceAccount, isDisabled: false }); + }; + + const handleServiceAccountRoleChange = (role: OrgRole) => { + updateServiceAccount({ ...serviceAccount, role: role }); + }; + + const onServiceAccountNameChange = (newValue: string) => { + updateServiceAccount({ ...serviceAccount, name: newValue }); + }; + + const styles = useStyles2(getStyles); return ( <> @@ -84,7 +83,7 @@ export function ServiceAccountProfile({ @@ -98,26 +97,35 @@ export function ServiceAccountProfile({
<> - - {serviceAccount.isDisabled && ( - - )} - {!serviceAccount.isDisabled && ( + ) : ( <> - { +const getStyles = (theme: GrafanaTheme2) => { return { buttonRow: css` - margin-top: 0.8rem; + margin-top: ${theme.spacing(1.5)}; > * { - margin-right: 16px; + margin-right: ${theme.spacing(2)}; } `, }; -}); +}; interface ServiceAccountProfileRowProps { label: string; diff --git a/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx b/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx index 4fce6d47e59..851d4b1298d 100644 --- a/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountRoleRow.tsx @@ -8,7 +8,7 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; interface Props { label: string; serviceAccount: ServiceAccountDTO; - onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void; + onRoleChange: (role: OrgRole) => void; roleOptions: Role[]; builtInRoles: Record; } @@ -37,7 +37,7 @@ export class ServiceAccountRoleRow extends PureComponent { userId={serviceAccount.id} orgId={serviceAccount.orgId} builtInRole={serviceAccount.role} - onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)} + onBuiltinRoleChange={onRoleChange} roleOptions={roleOptions} builtInRoles={builtInRoles} disabled={rolePickerDisabled} @@ -47,7 +47,7 @@ export class ServiceAccountRoleRow extends PureComponent { aria-label="Role" value={serviceAccount.role} disabled={!canUpdateRole} - onChange={(newRole) => onRoleChange(newRole, serviceAccount)} + onChange={onRoleChange} /> )} diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index e372e9c1272..f8dccf7d1d1 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -1,5 +1,5 @@ import { ApiKey, ServiceAccountDTO, ThunkResult } from '../../../types'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, locationService } from '@grafana/runtime'; import { acOptionsLoaded, builtInRolesLoaded, @@ -90,8 +90,8 @@ export function loadServiceAccounts(): ThunkResult { export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult { return async (dispatch) => { - await getBackendSrv().patch(`/api/org/users/${serviceAccount.id}`, { role: serviceAccount.role }); - dispatch(loadServiceAccounts()); + const response = await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.id}`, { ...serviceAccount }); + dispatch(serviceAccountLoaded(response)); }; } @@ -101,3 +101,10 @@ export function removeServiceAccount(serviceAccountId: number): ThunkResult { + return async (dispatch) => { + await getBackendSrv().delete(`${BASE_URL}/${serviceAccountId}`); + locationService.push('/org/serviceaccounts'); + }; +} From 07dda8a299c3dc7be94e1c6481d87d4ed2f7b574 Mon Sep 17 00:00:00 2001 From: matt abrams <37156449+zuchka@users.noreply.github.com> Date: Mon, 28 Feb 2022 23:27:01 -1000 Subject: [PATCH 071/125] Transformations: Use asterisk for First non-null label (#45940) --- packages/grafana-data/src/transformations/fieldReducer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index 8b2fea28f97..aaf400ba105 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -140,14 +140,14 @@ export const fieldReducers = new Registry(() => [ standard: true, reduce: calculateLast, }, - { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst }, { id: ReducerID.firstNotNull, - name: 'First', + name: 'First *', description: 'First non-null value', standard: true, reduce: calculateFirstNotNull, }, + { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst }, { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true }, { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true }, { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] }, From 18cbfba596623991da2cb9b2c7d2bbf6d455157f Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 1 Mar 2022 10:58:41 +0100 Subject: [PATCH 072/125] Access control: Filter users and teams by read permissions (#45968) * pass signed in user and filter based on permissions --- pkg/services/accesscontrol/accesscontrol.go | 2 +- .../database/resource_permissions.go | 24 ++++++++--- .../database/resource_permissions_test.go | 20 +++++++-- pkg/services/accesscontrol/filter.go | 2 + .../accesscontrol/mock/service_mock.go | 5 ++- .../ossaccesscontrol/permissions_services.go | 2 +- .../accesscontrol/resourcepermissions/api.go | 2 +- .../resourcepermissions/api_test.go | 42 +++++++++++++++---- .../resourcepermissions/service.go | 5 ++- .../resourcepermissions/types/models.go | 6 ++- 10 files changed, 83 insertions(+), 27 deletions(-) diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 03ae32a23a2..015dcd2ab90 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -44,7 +44,7 @@ type PermissionsServices interface { type PermissionsService interface { // GetPermissions returns all permissions for given resourceID - GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) + GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error) // SetUserPermission sets permission on resource for a user SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) // SetTeamPermission sets permission on resource for a team diff --git a/pkg/services/accesscontrol/database/resource_permissions.go b/pkg/services/accesscontrol/database/resource_permissions.go index 68c266db351..ff2c3b39b93 100644 --- a/pkg/services/accesscontrol/database/resource_permissions.go +++ b/pkg/services/accesscontrol/database/resource_permissions.go @@ -359,16 +359,28 @@ func (s *AccessControlStore) getResourcesPermissions(sess *sqlstore.DBSession, o args = append(args, a) } - // Need args x3 due to union initialLength := len(args) - args = append(args, args[:initialLength]...) - args = append(args, args[:initialLength]...) - user := userSelect + userFrom + where - team := teamSelect + teamFrom + where + userFilter, err := accesscontrol.Filter(context.Background(), "u.id", "users", accesscontrol.ActionOrgUsersRead, query.User) + if err != nil { + return nil, err + } + user := userSelect + userFrom + where + " AND " + userFilter.Where + args = append(args, userFilter.Args...) + + teamFilter, err := accesscontrol.Filter(context.Background(), "t.id", "teams", accesscontrol.ActionTeamsRead, query.User) + if err != nil { + return nil, err + } + + team := teamSelect + teamFrom + where + " AND " + teamFilter.Where + args = append(args, args[:initialLength]...) + args = append(args, teamFilter.Args...) + builtin := builtinSelect + builtinFrom + where - sql := user + "UNION" + team + "UNION" + builtin + args = append(args, args[:initialLength]...) + sql := user + " UNION " + team + " UNION " + builtin queryResults := make([]flatResourcePermission, 0) if err := sess.SQL(sql, args...).Find(&queryResults); err != nil { return nil, err diff --git a/pkg/services/accesscontrol/database/resource_permissions_test.go b/pkg/services/accesscontrol/database/resource_permissions_test.go index aa2ac0e35be..415a0b621d7 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_test.go @@ -313,6 +313,7 @@ func TestAccessControlStore_SetResourcePermissions(t *testing.T) { type getResourcesPermissionsTest struct { desc string + user *models.SignedInUser numUsers int actions []string resource string @@ -323,14 +324,24 @@ type getResourcesPermissionsTest struct { func TestAccessControlStore_GetResourcesPermissions(t *testing.T) { tests := []getResourcesPermissionsTest{ { - desc: "should return permissions for all resource ids", + desc: "should return permissions for all resource ids", + user: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}, + }}, numUsers: 3, actions: []string{"datasources:query"}, resource: "datasources", resourceIDs: []string{"1", "2"}, }, { - desc: "should return manage permissions for all resource ids", + desc: "should return manage permissions for all resource ids", + user: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}, + }}, numUsers: 3, actions: []string{"datasources:query"}, resource: "datasources", @@ -345,7 +356,7 @@ func TestAccessControlStore_GetResourcesPermissions(t *testing.T) { err := sql.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { role := &accesscontrol.Role{ - OrgID: 1, + OrgID: test.user.OrgId, UID: "seeded", Name: "seeded", Updated: time.Now(), @@ -382,7 +393,8 @@ func TestAccessControlStore_GetResourcesPermissions(t *testing.T) { seedResourcePermissions(t, store, sql, test.actions, test.resource, id, test.numUsers) } - permissions, err := store.GetResourcesPermissions(context.Background(), 1, types.GetResourcesPermissionsQuery{ + permissions, err := store.GetResourcesPermissions(context.Background(), test.user.OrgId, types.GetResourcesPermissionsQuery{ + User: test.user, Actions: test.actions, Resource: test.resource, ResourceIDs: test.resourceIDs, diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 26d18d731a5..01e2a83184a 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -12,7 +12,9 @@ import ( var sqlIDAcceptList = map[string]struct{}{ "org_user.user_id": {}, "role.id": {}, + "t.id": {}, "team.id": {}, + "u.id": {}, "\"user\".\"id\"": {}, // For Postgres "`user`.`id`": {}, // For MySQL and SQLite } diff --git a/pkg/services/accesscontrol/mock/service_mock.go b/pkg/services/accesscontrol/mock/service_mock.go index c5f57caf056..fae65edeb72 100644 --- a/pkg/services/accesscontrol/mock/service_mock.go +++ b/pkg/services/accesscontrol/mock/service_mock.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/mock" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -14,8 +15,8 @@ type MockPermissionsService struct { mock.Mock } -func (m *MockPermissionsService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { - mockedArgs := m.Called(ctx, orgID, resourceID) +func (m *MockPermissionsService) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, user, resourceID) return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1) } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 578819c5cfa..a0babb86346 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -113,7 +113,7 @@ var _ accesscontrol.PermissionsService = new(emptyPermissionsService) type emptyPermissionsService struct{} -func (e emptyPermissionsService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { +func (e emptyPermissionsService) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) { return nil, nil } diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 0f90577282d..78b6f6c0a34 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -82,7 +82,7 @@ type resourcePermissionDTO struct { func (a *api) getPermissions(c *models.ReqContext) response.Response { resourceID := web.Params(c.Req)[":resourceID"] - permissions, err := a.service.GetPermissions(c.Req.Context(), c.OrgId, resourceID) + permissions, err := a.service.GetPermissions(c.Req.Context(), c.SignedInUser, resourceID) if err != nil { return response.Error(http.StatusInternalServerError, "failed to get permissions", err) } diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 515b37f6e75..b64a9b0bcb5 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -136,9 +136,13 @@ type getPermissionsTestCase struct { func TestApi_getPermissions(t *testing.T) { tests := []getPermissionsTestCase{ { - desc: "expect permissions for resource with id 1", - resourceID: "1", - permissions: []*accesscontrol.Permission{{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}}, + desc: "expect permissions for resource with id 1", + resourceID: "1", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, + }, expectedStatus: 200, }, { @@ -152,7 +156,7 @@ func TestApi_getPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { service, sql := setupTestEnvironment(t, tt.permissions, testOptions) - server := setupTestServer(t, &models.SignedInUser{OrgId: 1}, service) + server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) seedPermissions(t, tt.resourceID, sql, service) @@ -195,6 +199,8 @@ func TestApi_setBuiltinRolePermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -206,6 +212,8 @@ func TestApi_setBuiltinRolePermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -234,7 +242,7 @@ func TestApi_setBuiltinRolePermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { service, _ := setupTestEnvironment(t, tt.permissions, testOptions) - server := setupTestServer(t, &models.SignedInUser{OrgId: 1}, service) + server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "builtInRoles", tt.builtInRole) assert.Equal(t, tt.expectedStatus, recorder.Code) @@ -269,6 +277,8 @@ func TestApi_setTeamPermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -280,6 +290,8 @@ func TestApi_setTeamPermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -308,7 +320,7 @@ func TestApi_setTeamPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { service, sql := setupTestEnvironment(t, tt.permissions, testOptions) - server := setupTestServer(t, &models.SignedInUser{OrgId: 1}, service) + server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) // seed team _, err := sql.CreateTeam("test", "test@test.com", 1) @@ -348,6 +360,8 @@ func TestApi_setUserPermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -359,6 +373,8 @@ func TestApi_setUserPermission(t *testing.T) { permissions: []*accesscontrol.Permission{ {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, }, }, { @@ -387,7 +403,7 @@ func TestApi_setUserPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { service, sql := setupTestEnvironment(t, tt.permissions, testOptions) - server := setupTestServer(t, &models.SignedInUser{OrgId: 1}, service) + server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) // seed user _, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) @@ -432,9 +448,17 @@ func TestApi_UidSolver(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - userPermissions := []*accesscontrol.Permission{{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}} + userPermissions := []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}, + {Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll}, + } + service, sql := setupTestEnvironment(t, userPermissions, withSolver(testOptions, testSolver)) - server := setupTestServer(t, &models.SignedInUser{OrgId: 1}, service) + server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{ + 1: accesscontrol.GroupScopesByAction(userPermissions), + }}, service) + seedPermissions(t, tt.resourceID, sql, service) permissions, recorder := getPermission(t, server, testOptions.Resource, tt.uid) diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 191799af0c5..f0acd494b67 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -98,8 +98,9 @@ type Service struct { sqlStore *sqlstore.SQLStore } -func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { - return s.store.GetResourcesPermissions(ctx, orgID, types.GetResourcesPermissionsQuery{ +func (s *Service) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) { + return s.store.GetResourcesPermissions(ctx, user.OrgId, types.GetResourcesPermissionsQuery{ + User: user, Actions: s.actions, Resource: s.options.Resource, ResourceIDs: []string{resourceID}, diff --git a/pkg/services/accesscontrol/resourcepermissions/types/models.go b/pkg/services/accesscontrol/resourcepermissions/types/models.go index 36f4072dde0..7f3b662d9ad 100644 --- a/pkg/services/accesscontrol/resourcepermissions/types/models.go +++ b/pkg/services/accesscontrol/resourcepermissions/types/models.go @@ -1,6 +1,9 @@ package types -import "github.com/grafana/grafana/pkg/services/accesscontrol" +import ( + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) type SetResourcePermissionCommand struct { Actions []string @@ -22,4 +25,5 @@ type GetResourcesPermissionsQuery struct { Resource string ResourceIDs []string OnlyManaged bool + User *models.SignedInUser } From fd644c48ac73114560503df2db31d85c3015a33a Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Tue, 1 Mar 2022 11:09:03 +0100 Subject: [PATCH 073/125] Switching to github.event.number as issue.number not always work (#46018) --- .github/workflows/pr-checks.yml | 2 +- .github/workflows/pr-commands-closed.yml | 2 +- .github/workflows/pr-commands.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index be455a168a2..16e00f6d094 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -13,7 +13,7 @@ on: - milestoned - demilestoned concurrency: - group: pr-checks-${{ github.event.issue.number }} + group: pr-checks-${{ github.event.number }} jobs: main: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-commands-closed.yml b/.github/workflows/pr-commands-closed.yml index 0f0067450be..76719c6c009 100644 --- a/.github/workflows/pr-commands-closed.yml +++ b/.github/workflows/pr-commands-closed.yml @@ -4,7 +4,7 @@ on: types: - closed concurrency: - group: pr-commands-closed-${{ github.event.issue.number }} + group: pr-commands-closed-${{ github.event.number }} jobs: close_job: # this job will only run if the PR has been closed without being merged diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml index 523628e545d..7f47cfd8e92 100644 --- a/.github/workflows/pr-commands.yml +++ b/.github/workflows/pr-commands.yml @@ -5,7 +5,7 @@ on: - opened - synchronize concurrency: - group: pr-commands-${{ github.event.issue.number }} + group: pr-commands-${{ github.event.number }} jobs: main: runs-on: ubuntu-latest From 6f14490c6b48d2a2a4cd66b5308f6a4c84d20fc0 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Tue, 1 Mar 2022 11:16:18 +0100 Subject: [PATCH 074/125] Making yarn.lock bump work (#46016) --- .github/workflows/bump-version.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 8d2f329022e..debee02c499 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -16,6 +16,8 @@ on: required: true metricsWriteAPIKey: required: true +env: + YARN_ENABLE_IMMUTABLE_INSTALLS: false jobs: main: runs-on: ubuntu-latest From 36b039a10ea7692d9a52fa2a18a0a0f0e6a65a11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:49:46 +0000 Subject: [PATCH 075/125] Update sentry-javascript monorepo to v6.18.1 (#45842) Co-authored-by: Renovate Bot --- package.json | 6 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 82 +++++++++++++-------------- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index f9d50ba8f6d..aa2c5b6213a 100644 --- a/package.json +++ b/package.json @@ -264,9 +264,9 @@ "@react-stately/menu": "3.2.3", "@react-stately/tree": "3.2.0", "@reduxjs/toolkit": "1.7.2", - "@sentry/browser": "6.17.4", - "@sentry/types": "6.17.4", - "@sentry/utils": "6.17.4", + "@sentry/browser": "6.18.1", + "@sentry/types": "6.18.1", + "@sentry/utils": "6.18.1", "@visx/event": "2.6.0", "@visx/gradient": "2.1.0", "@visx/group": "2.1.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 5d7b4315ec1..22f5c186087 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -25,7 +25,7 @@ "@grafana/data": "8.5.0-pre", "@grafana/e2e-selectors": "8.5.0-pre", "@grafana/ui": "8.5.0-pre", - "@sentry/browser": "6.17.4", + "@sentry/browser": "6.18.1", "history": "4.10.1", "lodash": "4.17.21", "react": "17.0.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 44f5bb78e0e..e2554323b9d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -45,7 +45,7 @@ "@react-aria/menu": "3.4.1", "@react-aria/overlays": "3.7.3", "@react-stately/menu": "3.2.3", - "@sentry/browser": "6.17.4", + "@sentry/browser": "6.18.1", "ansicolor": "1.1.100", "calculate-size": "1.1.1", "classnames": "2.3.1", diff --git a/yarn.lock b/yarn.lock index 4aa34f92e80..835e4ddac77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4165,7 +4165,7 @@ __metadata: "@grafana/ui": 8.5.0-pre "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-node-resolve": 13.1.3 - "@sentry/browser": 6.17.4 + "@sentry/browser": 6.18.1 "@testing-library/dom": 8.11.3 "@testing-library/react": ^12.1.2 "@testing-library/user-event": ^13.5.0 @@ -4356,7 +4356,7 @@ __metadata: "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-image": 2.1.1 "@rollup/plugin-node-resolve": 13.1.3 - "@sentry/browser": 6.17.4 + "@sentry/browser": 6.18.1 "@storybook/addon-a11y": 6.4.15 "@storybook/addon-actions": 6.4.15 "@storybook/addon-docs": 6.4.15 @@ -7736,67 +7736,67 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/browser@npm:6.17.4" +"@sentry/browser@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/browser@npm:6.18.1" dependencies: - "@sentry/core": 6.17.4 - "@sentry/types": 6.17.4 - "@sentry/utils": 6.17.4 + "@sentry/core": 6.18.1 + "@sentry/types": 6.18.1 + "@sentry/utils": 6.18.1 tslib: ^1.9.3 - checksum: cbc780efc5c897854f40911b8bd79e66f33ec604d08600def8100e621e2354f4786d7e18411a752ce58dcaa83345965f60f8776a364e5c086f12fda171618f07 + checksum: 29c4b852311512b96e938e73629007829933335b2cecb979f065f68af08db1adc6fa8f0763b6da3acafea0ce89d5c3719426b92449780512a58aac6a205c1a62 languageName: node linkType: hard -"@sentry/core@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/core@npm:6.17.4" +"@sentry/core@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/core@npm:6.18.1" dependencies: - "@sentry/hub": 6.17.4 - "@sentry/minimal": 6.17.4 - "@sentry/types": 6.17.4 - "@sentry/utils": 6.17.4 + "@sentry/hub": 6.18.1 + "@sentry/minimal": 6.18.1 + "@sentry/types": 6.18.1 + "@sentry/utils": 6.18.1 tslib: ^1.9.3 - checksum: daf80e9b2df5acaf8eeafabfddf24d30b2f9ab981843e1b01120f428803dc71c58e761005eec17d9004ffb483c210118bad50d774a4042929f0d55d3fb476104 + checksum: 03b8b56c094938b177642c7d801fe6d5d7a2a8fffad6fab38e1080b46165b850fe1ef827e115880e484ad871b9660d34aa781cd58360afbd154acbabd82c8c5e languageName: node linkType: hard -"@sentry/hub@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/hub@npm:6.17.4" +"@sentry/hub@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/hub@npm:6.18.1" dependencies: - "@sentry/types": 6.17.4 - "@sentry/utils": 6.17.4 + "@sentry/types": 6.18.1 + "@sentry/utils": 6.18.1 tslib: ^1.9.3 - checksum: 61c27230a87c071050730a0d8cbd9ae1f92c5007c9852900d288fed5bf35303bdba8acdcb6cad849540085ce81e072b89ba0e1ede249f6d940e81e06c6452af6 + checksum: 814a33e7a77e7c327c1bef29ea4ce0dea31502b174f3490b158a53af805ca070e1ccd3a62a54b979cbc478e261913a03f9628fbe3174e358dbe06c8b1cd95e9e languageName: node linkType: hard -"@sentry/minimal@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/minimal@npm:6.17.4" +"@sentry/minimal@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/minimal@npm:6.18.1" dependencies: - "@sentry/hub": 6.17.4 - "@sentry/types": 6.17.4 + "@sentry/hub": 6.18.1 + "@sentry/types": 6.18.1 tslib: ^1.9.3 - checksum: e67efeaf1be5eda8afc1a9254f485fdf861361cc007cf2da18aad09fd7bae98600c94f013e673e907551dda80bc2925353ace2c7709cac42ba2e1924b6da6f04 + checksum: ce4db8bae8e0fa46d1650e791499a0c0463d765868460484b09237fc542556e2b331280d2f291d60e0c2ba3dd90793a9be54cd2730a2e037300ff3d0d2ea2f9d languageName: node linkType: hard -"@sentry/types@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/types@npm:6.17.4" - checksum: e2c514b42cb27143150bcbea3438e65b96deebf5804ffbe6d889c5997cd448ec61ed486a4b903fd57d7297cfcc9cb33d2dd0b3a394830a66fe3b99c0fee05aab +"@sentry/types@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/types@npm:6.18.1" + checksum: dbf4abc28adbd734cd7fb353b547e9686293e734a8fbe4f88fd711c65fcf9905e2affdd35f1e422038168153b7b0b02a972e76dd5fb2edc74a62b1733820a1d0 languageName: node linkType: hard -"@sentry/utils@npm:6.17.4": - version: 6.17.4 - resolution: "@sentry/utils@npm:6.17.4" +"@sentry/utils@npm:6.18.1": + version: 6.18.1 + resolution: "@sentry/utils@npm:6.18.1" dependencies: - "@sentry/types": 6.17.4 + "@sentry/types": 6.18.1 tslib: ^1.9.3 - checksum: 9c82b947d20a5324573963517fbde006f4b2b9568a788bd24889451eb922e4c726ff239793380d5af1a255c915f86d0c22d118e83eeedb5e0ec61c1b742127e9 + checksum: 998e2e565e693e86c2e2bae830fe7df04e9cf588eb13cc98f03df35e2b5de78fff669b1a9dffc893a0d2c81e2c61e1107ad5f876e1203415325514849fb25336 languageName: node linkType: hard @@ -20688,9 +20688,9 @@ __metadata: "@react-types/shared": 3.11.1 "@reduxjs/toolkit": 1.7.2 "@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1 - "@sentry/browser": 6.17.4 - "@sentry/types": 6.17.4 - "@sentry/utils": 6.17.4 + "@sentry/browser": 6.18.1 + "@sentry/types": 6.18.1 + "@sentry/utils": 6.18.1 "@swc/core": 1.2.136 "@swc/helpers": 0.3.2 "@testing-library/dom": 8.11.3 From 251d6ed0e073563247d1d334884f4f3b3305c4cc Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Tue, 1 Mar 2022 07:42:32 -0400 Subject: [PATCH 076/125] Chore: Add function documentation for new functions (Prometheus visual query editor) (#45745) * add missing function documentation * added trigonometric functions --- .../plugins/datasource/prometheus/promql.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index ae817cca80c..48bc2361ff7 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -18,6 +18,81 @@ export const RATE_RANGES: CompletionItem[] = [ export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without']; export const LOGICAL_OPERATORS = ['or', 'and', 'unless']; +const TRIGONOMETRIC_FUNCTIONS: CompletionItem[] = [ + { + label: 'acos', + insertText: 'acos', + detail: 'acos(v instant-vector)', + documentation: 'calculates the arccosine of all elements in v', + }, + { + label: 'acosh', + insertText: 'acosh', + detail: 'acosh(v instant-vector)', + documentation: 'calculates the inverse hyperbolic cosine of all elements in v', + }, + { + label: 'asin', + insertText: 'asin', + detail: 'asin(v instant-vector)', + documentation: 'calculates the arcsine of all elements in v', + }, + { + label: 'asinh', + insertText: 'asinh', + detail: 'asinh(v instant-vector)', + documentation: 'calculates the inverse hyperbolic sine of all elements in v', + }, + { + label: 'atan', + insertText: 'atan', + detail: 'atan(v instant-vector)', + documentation: 'calculates the arctangent of all elements in v', + }, + { + label: 'atanh', + insertText: 'atanh', + detail: 'atanh(v instant-vector)', + documentation: 'calculates the inverse hyperbolic tangent of all elements in v', + }, + { + label: 'cos', + insertText: 'cos', + detail: 'cos(v instant-vector)', + documentation: 'calculates the cosine of all elements in v', + }, + { + label: 'cosh', + insertText: 'cosh', + detail: 'cosh(v instant-vector)', + documentation: 'calculates the hyperbolic cosine of all elements in v', + }, + { + label: 'sin', + insertText: 'sin', + detail: 'sin(v instant-vector)', + documentation: 'calculates the sine of all elements in v', + }, + { + label: 'sinh', + insertText: 'sinh', + detail: 'sinh(v instant-vector)', + documentation: 'calculates the hyperbolic sine of all elements in v', + }, + { + label: 'tan', + insertText: 'tan', + detail: 'tan(v instant-vector)', + documentation: 'calculates the tangent of all elements in v', + }, + { + label: 'tanh', + insertText: 'tanh', + detail: 'tanh(v instant-vector)', + documentation: 'calculates the hyperbolic tangent of all elements in v', + }, +]; + const AGGREGATION_OPERATORS: CompletionItem[] = [ { label: 'sum', @@ -83,6 +158,7 @@ const AGGREGATION_OPERATORS: CompletionItem[] = [ export const FUNCTIONS = [ ...AGGREGATION_OPERATORS, + ...TRIGONOMETRIC_FUNCTIONS, { insertText: 'abs', label: 'abs', @@ -142,6 +218,12 @@ export const FUNCTIONS = [ documentation: 'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.', }, + { + insertText: 'deg', + label: 'deg', + detail: 'deg(v instant-vector)', + documentation: 'Converts radians to degrees for all elements in v', + }, { insertText: 'day_of_month', label: 'day_of_month', @@ -286,6 +368,12 @@ export const FUNCTIONS = [ documentation: 'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.', }, + { + insertText: 'pi', + label: 'pi', + detail: 'pi()', + documentation: 'Returns pi', + }, { insertText: 'predict_linear', label: 'predict_linear', @@ -293,6 +381,12 @@ export const FUNCTIONS = [ documentation: 'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.', }, + { + insertText: 'rad', + label: 'rad', + detail: 'rad(v instant-vector)', + documentation: 'Converts degrees to radians for all elements in v', + }, { insertText: 'rate', label: 'rate', From 16b99cc0f5426edcc56610324ab60ec0bf8cbe58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 Mar 2022 13:11:47 +0100 Subject: [PATCH 077/125] Prometheus: Query builder ux tweaks (#46017) --- .../datasource/prometheus/querybuilder/shared/OperationName.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx index 1abef104fb6..f91d5b70a64 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx @@ -47,7 +47,7 @@ export const OperationName = React.memo(({ operation, def, index, onChang title={'Click to replace with alternative function'} > {nameElement} - + )} {state.isOpen && ( From 9bd62909684056b56f4d0e459ccad7d086c8b38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 Mar 2022 13:12:14 +0100 Subject: [PATCH 078/125] Prometheus: Query builder operation docs popover improvements (#46006) --- .../shared/OperationInfoButton.tsx | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationInfoButton.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationInfoButton.tsx index a0f9645d880..075dd63d5d6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationInfoButton.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationInfoButton.tsx @@ -3,8 +3,7 @@ import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; import { FlexItem } from '@grafana/experimental'; import { Button, Portal, useStyles2 } from '@grafana/ui'; import React, { useState } from 'react'; -import { usePopper } from 'react-popper'; -import { useToggle } from 'react-use'; +import { usePopperTooltip } from 'react-popper-tooltip'; import { QueryBuilderOperation, QueryBuilderOperationDef } from './types'; export interface Props { @@ -14,41 +13,39 @@ export interface Props { export const OperationInfoButton = React.memo(({ def, operation }) => { const styles = useStyles2(getStyles); - const [popperTrigger, setPopperTrigger] = useState(null); - const [popover, setPopover] = useState(null); - const [isOpen, toggleIsOpen] = useToggle(false); - - const popper = usePopper(popperTrigger, popover, { + const [show, setShow] = useState(false); + const { getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({ placement: 'top', - modifiers: [ - { name: 'arrow', enabled: true }, - { - name: 'preventOverflow', - enabled: true, - options: { - rootBoundary: 'viewport', - }, - }, - ], + visible: show, + offset: [0, 16], + onVisibleChange: setShow, + interactive: true, + trigger: ['click'], }); return ( <>
{ return { docBox: css({ overflow: 'hidden', - background: theme.colors.background.canvas, + background: theme.colors.background.primary, border: `1px solid ${theme.colors.border.strong}`, - boxShadow: theme.shadows.z2, + boxShadow: theme.shadows.z3, maxWidth: '600px', padding: theme.spacing(1), borderRadius: theme.shape.borderRadius(), From 82aa5acba6b857d4eb7c6b5faf485ae6d20f7328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 1 Mar 2022 13:40:47 +0100 Subject: [PATCH 079/125] Revert "Alerting: Calculate diff for two AlertRules (#45877)" (#46023) This reverts commit 4e19d7df6352b0dcbb680aeee00cebc97a90d937. --- pkg/services/ngalert/models/alert_rule.go | 24 -- .../ngalert/models/alert_rule_test.go | 315 ------------------ pkg/services/ngalert/models/testing.go | 99 +----- pkg/util/cmputil/reporter.go | 101 ------ 4 files changed, 18 insertions(+), 521 deletions(-) delete mode 100644 pkg/util/cmputil/reporter.go diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 3216961469c..4e59f55c6d6 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -1,15 +1,9 @@ package models import ( - "encoding/json" "errors" "fmt" "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - - "github.com/grafana/grafana/pkg/util/cmputil" ) var ( @@ -110,24 +104,6 @@ type AlertRule struct { Labels map[string]string } -// Diff calculates diff between two alert rules. Returns nil if two rules are equal. Otherwise, returns cmputil.DiffReport -func (alertRule *AlertRule) Diff(rule *AlertRule, ignore ...string) cmputil.DiffReport { - var reporter cmputil.DiffReporter - ops := make([]cmp.Option, 0, 4) - - // json.RawMessage is a slice of bytes and therefore cmp's default behavior is to compare it by byte, which is not really useful - var jsonCmp = cmp.Transformer("", func(in json.RawMessage) string { - return string(in) - }) - ops = append(ops, cmp.Reporter(&reporter), cmpopts.IgnoreFields(AlertQuery{}, "modelProps"), jsonCmp) - - if len(ignore) > 0 { - ops = append(ops, cmpopts.IgnoreFields(AlertRule{}, ignore...)) - } - cmp.Equal(alertRule, rule, ops...) - return reporter.Diffs -} - // AlertRuleKey is the alert definition identifier type AlertRuleKey struct { OrgID int64 diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index aa9934abc1a..a5106fb7063 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -1,14 +1,12 @@ package models import ( - "encoding/json" "math/rand" "strings" "testing" "time" "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/util" @@ -227,316 +225,3 @@ func TestPatchPartialAlertRule(t *testing.T) { } }) } - -func TestDiff(t *testing.T) { - t.Run("should return nil if there is no diff", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := CopyRule(rule1) - result := rule1.Diff(rule2) - require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) - }) - - t.Run("should respect fields to ignore", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := CopyRule(rule1) - rule2.ID = rule1.ID/2 + 1 - rule2.Version = rule1.Version/2 + 1 - rule2.Updated = rule1.Updated.Add(1 * time.Second) - result := rule1.Diff(rule2, "ID", "Version", "Updated") - require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) - }) - - t.Run("should find diff in simple fields", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := AlertRuleGen()() - - diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels") // these fields will be tested separately - - difCnt := 0 - if rule1.ID != rule2.ID { - diff := diffs.GetDiffsForField("ID") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.ID, diff[0].Left.Int()) - assert.Equal(t, rule2.ID, diff[0].Right.Int()) - difCnt++ - } - if rule1.OrgID != rule2.OrgID { - diff := diffs.GetDiffsForField("OrgID") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.OrgID, diff[0].Left.Int()) - assert.Equal(t, rule2.OrgID, diff[0].Right.Int()) - difCnt++ - } - if rule1.Title != rule2.Title { - diff := diffs.GetDiffsForField("Title") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.Title, diff[0].Left.String()) - assert.Equal(t, rule2.Title, diff[0].Right.String()) - difCnt++ - } - if rule1.Condition != rule2.Condition { - diff := diffs.GetDiffsForField("Condition") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.Condition, diff[0].Left.String()) - assert.Equal(t, rule2.Condition, diff[0].Right.String()) - difCnt++ - } - if rule1.Updated != rule2.Updated { - diff := diffs.GetDiffsForField("Updated") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.Updated, diff[0].Left.Interface()) - assert.Equal(t, rule2.Updated, diff[0].Right.Interface()) - difCnt++ - } - if rule1.IntervalSeconds != rule2.IntervalSeconds { - diff := diffs.GetDiffsForField("IntervalSeconds") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.IntervalSeconds, diff[0].Left.Int()) - assert.Equal(t, rule2.IntervalSeconds, diff[0].Right.Int()) - difCnt++ - } - if rule1.Version != rule2.Version { - diff := diffs.GetDiffsForField("Version") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.Version, diff[0].Left.Int()) - assert.Equal(t, rule2.Version, diff[0].Right.Int()) - difCnt++ - } - if rule1.UID != rule2.UID { - diff := diffs.GetDiffsForField("UID") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.UID, diff[0].Left.String()) - assert.Equal(t, rule2.UID, diff[0].Right.String()) - difCnt++ - } - if rule1.NamespaceUID != rule2.NamespaceUID { - diff := diffs.GetDiffsForField("NamespaceUID") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.NamespaceUID, diff[0].Left.String()) - assert.Equal(t, rule2.NamespaceUID, diff[0].Right.String()) - difCnt++ - } - if rule1.DashboardUID != rule2.DashboardUID { - diff := diffs.GetDiffsForField("DashboardUID") - assert.Len(t, diff, 1) - - if rule1.DashboardUID == nil { - assert.True(t, diff[0].Left.IsNil()) - } else { - assert.Equal(t, *rule1.DashboardUID, diff[0].Left.Elem().String()) - } - if rule2.DashboardUID == nil { - assert.True(t, diff[0].Right.IsNil()) - } else { - assert.Equal(t, *rule2.DashboardUID, diff[0].Right.Elem().String()) - } - difCnt++ - } - if rule1.PanelID != rule2.PanelID { - diff := diffs.GetDiffsForField("PanelID") - assert.Len(t, diff, 1) - - if rule1.PanelID == nil { - assert.True(t, diff[0].Left.IsNil()) - } else { - assert.Equal(t, *rule1.PanelID, diff[0].Left.Elem().Int()) - } - if rule2.PanelID == nil { - assert.True(t, diff[0].Right.IsNil()) - } else { - assert.Equal(t, *rule2.PanelID, diff[0].Right.Elem().Int()) - } - difCnt++ - } - if rule1.RuleGroup != rule2.RuleGroup { - diff := diffs.GetDiffsForField("RuleGroup") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.RuleGroup, diff[0].Left.String()) - assert.Equal(t, rule2.RuleGroup, diff[0].Right.String()) - difCnt++ - } - if rule1.NoDataState != rule2.NoDataState { - diff := diffs.GetDiffsForField("NoDataState") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.NoDataState, diff[0].Left.Interface()) - assert.Equal(t, rule2.NoDataState, diff[0].Right.Interface()) - difCnt++ - } - if rule1.ExecErrState != rule2.ExecErrState { - diff := diffs.GetDiffsForField("ExecErrState") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.ExecErrState, diff[0].Left.Interface()) - assert.Equal(t, rule2.ExecErrState, diff[0].Right.Interface()) - difCnt++ - } - if rule1.For != rule2.For { - diff := diffs.GetDiffsForField("For") - assert.Len(t, diff, 1) - assert.Equal(t, rule1.For, diff[0].Left.Interface()) - assert.Equal(t, rule2.For, diff[0].Right.Interface()) - difCnt++ - } - - require.Lenf(t, diffs, difCnt, "Got some unexpected diffs. Either add to ignore or add assert to it") - - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, diffs) - } - }) - - t.Run("should detect changes in Annotations", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := CopyRule(rule1) - - rule1.Annotations = map[string]string{ - "key1": "value1", - "key2": "value2", - } - - rule2.Annotations = map[string]string{ - "key2": "value22", - "key3": "value3", - } - diff := rule1.Diff(rule2) - - assert.Len(t, diff, 3) - - d := diff.GetDiffsForField("Annotations[key1]") - assert.Len(t, d, 1) - assert.Equal(t, "value1", d[0].Left.String()) - assert.False(t, d[0].Right.IsValid()) - - d = diff.GetDiffsForField("Annotations[key2]") - assert.Len(t, d, 1) - assert.Equal(t, "value2", d[0].Left.String()) - assert.Equal(t, "value22", d[0].Right.String()) - - d = diff.GetDiffsForField("Annotations[key3]") - assert.Len(t, d, 1) - assert.False(t, d[0].Left.IsValid()) - assert.Equal(t, "value3", d[0].Right.String()) - - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) - } - }) - - t.Run("should detect changes in Labels", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := CopyRule(rule1) - - rule1.Labels = map[string]string{ - "key1": "value1", - "key2": "value2", - } - - rule2.Labels = map[string]string{ - "key2": "value22", - "key3": "value3", - } - diff := rule1.Diff(rule2) - - assert.Len(t, diff, 3) - - d := diff.GetDiffsForField("Labels[key1]") - assert.Len(t, d, 1) - assert.Equal(t, "value1", d[0].Left.String()) - assert.False(t, d[0].Right.IsValid()) - - d = diff.GetDiffsForField("Labels[key2]") - assert.Len(t, d, 1) - assert.Equal(t, "value2", d[0].Left.String()) - assert.Equal(t, "value22", d[0].Right.String()) - - d = diff.GetDiffsForField("Labels[key3]") - assert.Len(t, d, 1) - assert.False(t, d[0].Left.IsValid()) - assert.Equal(t, "value3", d[0].Right.String()) - - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, d) - } - }) - - t.Run("should detect changes in Data", func(t *testing.T) { - rule1 := AlertRuleGen()() - rule2 := CopyRule(rule1) - - query1 := AlertQuery{ - RefID: "A", - QueryType: util.GenerateShortUID(), - RelativeTimeRange: RelativeTimeRange{ - From: Duration(5 * time.Hour), - To: 0, - }, - DatasourceUID: util.GenerateShortUID(), - Model: json.RawMessage(`{ "test": "data"}`), - modelProps: map[string]interface{}{ - "test": 1, - }, - } - - rule1.Data = []AlertQuery{query1} - - t.Run("should ignore modelProps", func(t *testing.T) { - query2 := query1 - query2.modelProps = map[string]interface{}{ - "some": "other value", - } - rule2.Data = []AlertQuery{query2} - - diff := rule1.Diff(rule2) - - assert.Nil(t, diff) - - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) - } - }) - - t.Run("should detect changes inside the query", func(t *testing.T) { - query2 := query1 - query2.QueryType = "test" - query2.RefID = "test" - rule2.Data = []AlertQuery{query2} - - diff := rule1.Diff(rule2) - - assert.Len(t, diff, 2) - - d := diff.GetDiffsForField("Data[0].QueryType") - assert.Len(t, d, 1) - d = diff.GetDiffsForField("Data[0].RefID") - assert.Len(t, d, 1) - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) - } - }) - - t.Run("should detect new changes in array if too many fields changed", func(t *testing.T) { - query2 := query1 - query2.QueryType = "test" - query2.RefID = "test" - query2.DatasourceUID = "test" - query2.Model = json.RawMessage(`{ "test": "da2ta"}`) - - rule2.Data = []AlertQuery{query2} - - diff := rule1.Diff(rule2) - - assert.Len(t, diff, 2) - - for _, d := range diff { - assert.Equal(t, "Data", d.Path) - if d.Left.IsValid() { - assert.Equal(t, query1, d.Left.Interface()) - } else { - assert.Equal(t, query2, d.Right.Interface()) - } - } - if t.Failed() { - t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) - } - }) - }) -} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 8c05872ba1d..3428086fc6a 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -2,7 +2,6 @@ package models import ( "encoding/json" - "fmt" "math/rand" "time" @@ -61,11 +60,24 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } rule := &AlertRule{ - ID: rand.Int63(), - OrgID: rand.Int63(), - Title: "TEST-ALERT-" + util.GenerateShortUID(), - Condition: "A", - Data: []AlertQuery{GenerateAlertQuery()}, + ID: rand.Int63(), + OrgID: rand.Int63(), + Title: "TEST-ALERT-" + util.GenerateShortUID(), + Condition: "A", + Data: []AlertQuery{ + { + DatasourceUID: "-100", + Model: json.RawMessage(`{ + "datasourceUid": "-100", + "type":"math", + "expression":"2 + 1 < 1" + }`), + RelativeTimeRange: RelativeTimeRange{ + From: Duration(5 * time.Hour), + To: Duration(3 * time.Hour), + }, + RefID: "A", + }}, Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)), IntervalSeconds: rand.Int63n(60) + 1, Version: rand.Int63(), @@ -88,25 +100,6 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } } -func GenerateAlertQuery() AlertQuery { - f := rand.Intn(10) + 5 - t := rand.Intn(f) - - return AlertQuery{ - DatasourceUID: util.GenerateShortUID(), - Model: json.RawMessage(fmt.Sprintf(`{ - "%s": "%s", - "%s":"%d" - }`, util.GenerateShortUID(), util.GenerateShortUID(), util.GenerateShortUID(), rand.Int())), - RelativeTimeRange: RelativeTimeRange{ - From: Duration(time.Duration(f) * time.Minute), - To: Duration(time.Duration(t) * time.Minute), - }, - RefID: util.GenerateShortUID(), - QueryType: util.GenerateShortUID(), - } -} - // GenerateUniqueAlertRules generates many random alert rules and makes sure that they have unique UID. // It returns a tuple where first element is a map where keys are UID of alert rule and the second element is a slice of the same rules func GenerateUniqueAlertRules(count int, f func() *AlertRule) (map[string]*AlertRule, []*AlertRule) { @@ -132,59 +125,3 @@ func GenerateAlertRules(count int, f func() *AlertRule) []*AlertRule { } return result } - -// CopyRule creates a deep copy of AlertRule -func CopyRule(r *AlertRule) *AlertRule { - result := AlertRule{ - ID: r.ID, - OrgID: r.OrgID, - Title: r.Title, - Condition: r.Condition, - Updated: r.Updated, - IntervalSeconds: r.IntervalSeconds, - Version: r.Version, - UID: r.UID, - NamespaceUID: r.NamespaceUID, - RuleGroup: r.RuleGroup, - NoDataState: r.NoDataState, - ExecErrState: r.ExecErrState, - For: r.For, - } - - if r.DashboardUID != nil { - dash := *r.DashboardUID - result.DashboardUID = &dash - } - if r.PanelID != nil { - p := *r.PanelID - result.PanelID = &p - } - - for _, d := range r.Data { - q := AlertQuery{ - RefID: d.RefID, - QueryType: d.QueryType, - RelativeTimeRange: d.RelativeTimeRange, - DatasourceUID: d.DatasourceUID, - } - q.Model = make([]byte, 0, cap(d.Model)) - q.Model = append(q.Model, d.Model...) - result.Data = append(result.Data, q) - } - - if r.Annotations != nil { - result.Annotations = make(map[string]string, len(r.Annotations)) - for s, s2 := range r.Annotations { - result.Annotations[s] = s2 - } - } - - if r.Labels != nil { - result.Labels = make(map[string]string, len(r.Labels)) - for s, s2 := range r.Labels { - result.Labels[s] = s2 - } - } - - return &result -} diff --git a/pkg/util/cmputil/reporter.go b/pkg/util/cmputil/reporter.go deleted file mode 100644 index c9532d3cc0f..00000000000 --- a/pkg/util/cmputil/reporter.go +++ /dev/null @@ -1,101 +0,0 @@ -package cmputil - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp" -) - -type DiffReport []Diff - -// GetDiffsForField returns subset of the diffs which path starts with the provided path -func (r DiffReport) GetDiffsForField(path string) DiffReport { - var result []Diff - for _, diff := range r { - if strings.HasPrefix(path, diff.Path) { - result = append(result, diff) - } - } - return result -} - -// DiffReporter is a simple custom reporter that only records differences -// detected during comparison. Implements an interface required by cmp.Reporter option -type DiffReporter struct { - path cmp.Path - Diffs DiffReport -} - -func (r *DiffReporter) PushStep(ps cmp.PathStep) { - r.path = append(r.path, ps) -} - -func (r *DiffReporter) PopStep() { - r.path = r.path[:len(r.path)-1] -} - -func (r *DiffReporter) Report(rs cmp.Result) { - if !rs.Equal() { - vx, vy := r.path.Last().Values() - r.Diffs = append(r.Diffs, Diff{ - Path: printPath(r.path), - Left: vx, - Right: vy, - }) - } -} - -func printPath(p cmp.Path) string { - ss := strings.Builder{} - for _, s := range p { - toAdd := "" - switch v := s.(type) { - case cmp.StructField: - toAdd = v.String() - case cmp.MapIndex: - toAdd = fmt.Sprintf("[%s]", v.Key()) - case cmp.SliceIndex: - if v.Key() >= 0 { - toAdd = fmt.Sprintf("[%d]", v.Key()) - } - } - if toAdd == "" { - continue - } - ss.WriteString(toAdd) - } - return strings.TrimPrefix(ss.String(), ".") -} - -func (r DiffReport) String() string { - b := strings.Builder{} - for _, diff := range r { - b.WriteString(diff.String()) - b.WriteByte('\n') - } - return b.String() -} - -type Diff struct { - // Path to the field that has difference separated by period. Array index and key are designated by square brackets. - // For example, Annotations[12345].Data.Fields[0].ID - Path string - Left reflect.Value - Right reflect.Value -} - -func (d *Diff) String() string { - left := d.Left.String() - // invalid reflect.Value is produced when two collections (slices\maps) are compared and one misses value. - // This way go-cmp indicates that an element was added\removed from a list. - if !d.Left.IsValid() { - left = "" - } - right := d.Right.String() - if !d.Right.IsValid() { - right = "" - } - return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v", d.Path, left, right) -} From 3ea6c8cf64c529772b4c4320bafab122e7c08ff1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 1 Mar 2022 09:00:27 -0500 Subject: [PATCH 080/125] ReleaseNotes: Updated changelog and release notes for 8.3.7 (#46028) --- CHANGELOG.md | 10 ++++++++++ docs/sources/release-notes/_index.md | 1 + docs/sources/release-notes/release-notes-8-3-7.md | 12 ++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-3-7.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 046201ce80b..b38a0224cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,16 @@ AngularJS plugin support is now in a deprecated state, meaning it will be remove - **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.7 (2022-03-01) + +### Bug fixes + +- **Provisioning:** Ensure that the default value for orgID is set when provisioning datasources to be deleted. [#44244](https://github.com/grafana/grafana/pull/44244), [@filewalkwithme](https://github.com/filewalkwithme) + + + # 8.3.6 (2022-02-09) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 04b066d9b8c..563ccef4c4f 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -11,6 +11,7 @@ about deprecations, breaking changes as well as changes that relate to plugin de - [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}}) - [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}}) - [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) +- [Release notes for 8.3.7]({{< relref "release-notes-8-3-7" >}}) - [Release notes for 8.3.6]({{< relref "release-notes-8-3-6" >}}) - [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}}) - [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) diff --git a/docs/sources/release-notes/release-notes-8-3-7.md b/docs/sources/release-notes/release-notes-8-3-7.md new file mode 100644 index 00000000000..cf47672019a --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-3-7.md @@ -0,0 +1,12 @@ ++++ +title = "Release notes for Grafana 8.3.7" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.3.7 + +### Bug fixes + +- **Provisioning:** Ensure that the default value for orgID is set when provisioning datasources to be deleted. [#44244](https://github.com/grafana/grafana/pull/44244), [@filewalkwithme](https://github.com/filewalkwithme) From 86b4c4a08a66d750beb62dae22b7893d3ba57e96 Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Tue, 1 Mar 2022 09:06:46 -0600 Subject: [PATCH 081/125] (GitHubActions) Add grafana-partners team label to team board (#45990) --- .github/commands.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/commands.json b/.github/commands.json index 095d22e271d..1401dc09bbd 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -209,5 +209,13 @@ "removeFromProject":{ "url":"https://github.com/grafana/grafana/projects/33" } + }, + { + "type": "label", + "name": "team/grafana-partners", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/87" + } } ] From 77dddf43bc76ec8d887d357569733e3ad2541624 Mon Sep 17 00:00:00 2001 From: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:30:46 -0500 Subject: [PATCH 082/125] Docs update default.ini file description (#46036) * remove confusing wording * fixed broken alerting links --- docs/sources/administration/configuration.md | 2 +- .../alerting-rules/alert-annotation-label.md | 2 +- .../alerting/unified-alerting/notifications/_index.md | 6 +++--- .../alerting/unified-alerting/notifications/mute-timings.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 9440a278a46..242fbc164a1 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -14,7 +14,7 @@ Grafana has default and custom configuration files. You can customize your Grafa ## Configuration file location -The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change the location in this file. +The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change this file. Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/defaults.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter. diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md index c51c957447c..e7b634905c5 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md +++ b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md @@ -20,7 +20,7 @@ Labels are key-value pairs that contain information about, and are used to uniqu ### How are labels used? - The complete set of labels for an alert is what uniquely identifies an alert within Grafana Alerts. -- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notification-policies/" >}}). +- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notifications/_index.md" >}}). - The alerting UI displays labels for every alert instance generated by the evaluation of that rule. - Contact points can access labels to dynamically generate notifications that contain information specific to the alert that is resulting in a notification. - Labels can be added to an [alerting rule]({{< relref "../alerting-rules/" >}}). These manually configured labels are able to use template functions and reference other labels. Labels added to an alerting rule here take precedence in the event of a collision between labels. diff --git a/docs/sources/alerting/unified-alerting/notifications/_index.md b/docs/sources/alerting/unified-alerting/notifications/_index.md index ba1cbf0b638..3a263fb8143 100644 --- a/docs/sources/alerting/unified-alerting/notifications/_index.md +++ b/docs/sources/alerting/unified-alerting/notifications/_index.md @@ -9,7 +9,7 @@ weight = 450 Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more child policies. Each policy, except for the root policy, can also match specific alert labels. Each alert is evaluated by the root policy and subsequently by each child policy. If you enable the `Continue matching subsequent sibling nodes` option is enabled for a specific policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the child policies. A root policy governs any alert that does not match a specific policy. -You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Grouping @@ -33,7 +33,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. Click **Notification policies**. 1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected. 1. In the Root policy section, click **Edit** (pen icon). -1. In **Default contact point**, update the [contact point]({{< relref "./contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. +1. In **Default contact point**, update the [contact point]({{< relref "../contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. 1. In **Group by**, choose labels to group alerts by. If multiple alerts are matched for this policy, then they are grouped by these labels. A notification is sent per group. If the field is empty (default), then all notifications are sent in a single group. Use a special label `...` to group alerts by all labels (which effectively disables grouping). 1. In **Timing options**, select from the following options: - **Group wait** Time to wait to buffer alerts of the same group before sending an initial notification. Default is 30 seconds. @@ -48,7 +48,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. From the **Alertmanager** dropdown, select an Alertmanager. By default, the Grafana Alertmanager is selected. 1. To add a top level specific policy, go to the **Specific routing** section and click **New specific policy**. 1. In **Matching labels** section, add one or more rules for matching alert labels. For more information, see ["How label matching works"](#how-label-matching-works). -1. In **Contact point**, add the [contact point]({{< relref "./contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. +1. In **Contact point**, add the [contact point]({{< relref "../contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. 1. Optionally, enable **Continue matching subsequent sibling nodes** to continue matching nested policies even after the alert matched the parent policy. When this option is enabled, you can get more than one notification. Use it to send notification to a catch-all contact point as well as to one of more specific contact points handled by nested policies. 1. Optionally, enable **Override grouping** to specify the same grouping as the root policy. If this option is not enabled, the root policy grouping is used. 1. Optionally, enable **Override general timings** to override the timing options configured in the group notification policy. diff --git a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md b/docs/sources/alerting/unified-alerting/notifications/mute-timings.md index 254a9a0d26b..411f9fdb479 100644 --- a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md +++ b/docs/sources/alerting/unified-alerting/notifications/mute-timings.md @@ -11,7 +11,7 @@ A mute timing is a recurring interval of time when no new notifications for a po Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created. -You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Mute timings vs silences From 4502e40ed8f5544f2dd451fb25822cf9c2fa0359 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Tue, 1 Mar 2022 11:10:29 -0500 Subject: [PATCH 083/125] Alerting: Revert Revert "Alerting: Calculate diff for two AlertRules" (#46034) * Revert "Revert "Alerting: Calculate diff for two AlertRules (#45877)" (#46023)" This reverts commit 82aa5acba6b857d4eb7c6b5faf485ae6d20f7328. * remove flakiness --- pkg/services/ngalert/models/alert_rule.go | 24 ++ .../ngalert/models/alert_rule_test.go | 293 ++++++++++++++++++ pkg/services/ngalert/models/testing.go | 99 ++++-- pkg/util/cmputil/reporter.go | 101 ++++++ 4 files changed, 499 insertions(+), 18 deletions(-) create mode 100644 pkg/util/cmputil/reporter.go diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 4e59f55c6d6..3216961469c 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -1,9 +1,15 @@ package models import ( + "encoding/json" "errors" "fmt" "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/grafana/grafana/pkg/util/cmputil" ) var ( @@ -104,6 +110,24 @@ type AlertRule struct { Labels map[string]string } +// Diff calculates diff between two alert rules. Returns nil if two rules are equal. Otherwise, returns cmputil.DiffReport +func (alertRule *AlertRule) Diff(rule *AlertRule, ignore ...string) cmputil.DiffReport { + var reporter cmputil.DiffReporter + ops := make([]cmp.Option, 0, 4) + + // json.RawMessage is a slice of bytes and therefore cmp's default behavior is to compare it by byte, which is not really useful + var jsonCmp = cmp.Transformer("", func(in json.RawMessage) string { + return string(in) + }) + ops = append(ops, cmp.Reporter(&reporter), cmpopts.IgnoreFields(AlertQuery{}, "modelProps"), jsonCmp) + + if len(ignore) > 0 { + ops = append(ops, cmpopts.IgnoreFields(AlertRule{}, ignore...)) + } + cmp.Equal(alertRule, rule, ops...) + return reporter.Diffs +} + // AlertRuleKey is the alert definition identifier type AlertRuleKey struct { OrgID int64 diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index a5106fb7063..37681c06bed 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -1,12 +1,14 @@ package models import ( + "encoding/json" "math/rand" "strings" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/util" @@ -225,3 +227,294 @@ func TestPatchPartialAlertRule(t *testing.T) { } }) } + +func TestDiff(t *testing.T) { + t.Run("should return nil if there is no diff", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + result := rule1.Diff(rule2) + require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) + }) + + t.Run("should respect fields to ignore", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + rule2.ID = rule1.ID/2 + 1 + rule2.Version = rule1.Version/2 + 1 + rule2.Updated = rule1.Updated.Add(1 * time.Second) + result := rule1.Diff(rule2, "ID", "Version", "Updated") + require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result) + }) + + t.Run("should find diff in simple fields", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := AlertRuleGen()() + + diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels") // these fields will be tested separately + + difCnt := 0 + if rule1.ID != rule2.ID { + diff := diffs.GetDiffsForField("ID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.ID, diff[0].Left.Int()) + assert.Equal(t, rule2.ID, diff[0].Right.Int()) + difCnt++ + } + if rule1.OrgID != rule2.OrgID { + diff := diffs.GetDiffsForField("OrgID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.OrgID, diff[0].Left.Int()) + assert.Equal(t, rule2.OrgID, diff[0].Right.Int()) + difCnt++ + } + if rule1.Title != rule2.Title { + diff := diffs.GetDiffsForField("Title") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Title, diff[0].Left.String()) + assert.Equal(t, rule2.Title, diff[0].Right.String()) + difCnt++ + } + if rule1.Condition != rule2.Condition { + diff := diffs.GetDiffsForField("Condition") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Condition, diff[0].Left.String()) + assert.Equal(t, rule2.Condition, diff[0].Right.String()) + difCnt++ + } + if rule1.Updated != rule2.Updated { + diff := diffs.GetDiffsForField("Updated") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Updated, diff[0].Left.Interface()) + assert.Equal(t, rule2.Updated, diff[0].Right.Interface()) + difCnt++ + } + if rule1.IntervalSeconds != rule2.IntervalSeconds { + diff := diffs.GetDiffsForField("IntervalSeconds") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.IntervalSeconds, diff[0].Left.Int()) + assert.Equal(t, rule2.IntervalSeconds, diff[0].Right.Int()) + difCnt++ + } + if rule1.Version != rule2.Version { + diff := diffs.GetDiffsForField("Version") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.Version, diff[0].Left.Int()) + assert.Equal(t, rule2.Version, diff[0].Right.Int()) + difCnt++ + } + if rule1.UID != rule2.UID { + diff := diffs.GetDiffsForField("UID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.UID, diff[0].Left.String()) + assert.Equal(t, rule2.UID, diff[0].Right.String()) + difCnt++ + } + if rule1.NamespaceUID != rule2.NamespaceUID { + diff := diffs.GetDiffsForField("NamespaceUID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.NamespaceUID, diff[0].Left.String()) + assert.Equal(t, rule2.NamespaceUID, diff[0].Right.String()) + difCnt++ + } + if rule1.DashboardUID != rule2.DashboardUID { + diff := diffs.GetDiffsForField("DashboardUID") + assert.Len(t, diff, 1) + difCnt++ + } + if rule1.PanelID != rule2.PanelID { + diff := diffs.GetDiffsForField("PanelID") + assert.Len(t, diff, 1) + difCnt++ + } + if rule1.RuleGroup != rule2.RuleGroup { + diff := diffs.GetDiffsForField("RuleGroup") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.RuleGroup, diff[0].Left.String()) + assert.Equal(t, rule2.RuleGroup, diff[0].Right.String()) + difCnt++ + } + if rule1.NoDataState != rule2.NoDataState { + diff := diffs.GetDiffsForField("NoDataState") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.NoDataState, diff[0].Left.Interface()) + assert.Equal(t, rule2.NoDataState, diff[0].Right.Interface()) + difCnt++ + } + if rule1.ExecErrState != rule2.ExecErrState { + diff := diffs.GetDiffsForField("ExecErrState") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.ExecErrState, diff[0].Left.Interface()) + assert.Equal(t, rule2.ExecErrState, diff[0].Right.Interface()) + difCnt++ + } + if rule1.For != rule2.For { + diff := diffs.GetDiffsForField("For") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.For, diff[0].Left.Interface()) + assert.Equal(t, rule2.For, diff[0].Right.Interface()) + difCnt++ + } + + require.Lenf(t, diffs, difCnt, "Got some unexpected diffs. Either add to ignore or add assert to it") + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, diffs) + } + }) + + t.Run("should detect changes in Annotations", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + rule1.Annotations = map[string]string{ + "key1": "value1", + "key2": "value2", + } + + rule2.Annotations = map[string]string{ + "key2": "value22", + "key3": "value3", + } + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 3) + + d := diff.GetDiffsForField("Annotations[key1]") + assert.Len(t, d, 1) + assert.Equal(t, "value1", d[0].Left.String()) + assert.False(t, d[0].Right.IsValid()) + + d = diff.GetDiffsForField("Annotations[key2]") + assert.Len(t, d, 1) + assert.Equal(t, "value2", d[0].Left.String()) + assert.Equal(t, "value22", d[0].Right.String()) + + d = diff.GetDiffsForField("Annotations[key3]") + assert.Len(t, d, 1) + assert.False(t, d[0].Left.IsValid()) + assert.Equal(t, "value3", d[0].Right.String()) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect changes in Labels", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + rule1.Labels = map[string]string{ + "key1": "value1", + "key2": "value2", + } + + rule2.Labels = map[string]string{ + "key2": "value22", + "key3": "value3", + } + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 3) + + d := diff.GetDiffsForField("Labels[key1]") + assert.Len(t, d, 1) + assert.Equal(t, "value1", d[0].Left.String()) + assert.False(t, d[0].Right.IsValid()) + + d = diff.GetDiffsForField("Labels[key2]") + assert.Len(t, d, 1) + assert.Equal(t, "value2", d[0].Left.String()) + assert.Equal(t, "value22", d[0].Right.String()) + + d = diff.GetDiffsForField("Labels[key3]") + assert.Len(t, d, 1) + assert.False(t, d[0].Left.IsValid()) + assert.Equal(t, "value3", d[0].Right.String()) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, d) + } + }) + + t.Run("should detect changes in Data", func(t *testing.T) { + rule1 := AlertRuleGen()() + rule2 := CopyRule(rule1) + + query1 := AlertQuery{ + RefID: "A", + QueryType: util.GenerateShortUID(), + RelativeTimeRange: RelativeTimeRange{ + From: Duration(5 * time.Hour), + To: 0, + }, + DatasourceUID: util.GenerateShortUID(), + Model: json.RawMessage(`{ "test": "data"}`), + modelProps: map[string]interface{}{ + "test": 1, + }, + } + + rule1.Data = []AlertQuery{query1} + + t.Run("should ignore modelProps", func(t *testing.T) { + query2 := query1 + query2.modelProps = map[string]interface{}{ + "some": "other value", + } + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Nil(t, diff) + + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect changes inside the query", func(t *testing.T) { + query2 := query1 + query2.QueryType = "test" + query2.RefID = "test" + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 2) + + d := diff.GetDiffsForField("Data[0].QueryType") + assert.Len(t, d, 1) + d = diff.GetDiffsForField("Data[0].RefID") + assert.Len(t, d, 1) + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + + t.Run("should detect new changes in array if too many fields changed", func(t *testing.T) { + query2 := query1 + query2.QueryType = "test" + query2.RefID = "test" + query2.DatasourceUID = "test" + query2.Model = json.RawMessage(`{ "test": "da2ta"}`) + + rule2.Data = []AlertQuery{query2} + + diff := rule1.Diff(rule2) + + assert.Len(t, diff, 2) + + for _, d := range diff { + assert.Equal(t, "Data", d.Path) + if d.Left.IsValid() { + assert.Equal(t, query1, d.Left.Interface()) + } else { + assert.Equal(t, query2, d.Right.Interface()) + } + } + if t.Failed() { + t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff) + } + }) + }) +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 3428086fc6a..8c05872ba1d 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "fmt" "math/rand" "time" @@ -60,24 +61,11 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } rule := &AlertRule{ - ID: rand.Int63(), - OrgID: rand.Int63(), - Title: "TEST-ALERT-" + util.GenerateShortUID(), - Condition: "A", - Data: []AlertQuery{ - { - DatasourceUID: "-100", - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 1 < 1" - }`), - RelativeTimeRange: RelativeTimeRange{ - From: Duration(5 * time.Hour), - To: Duration(3 * time.Hour), - }, - RefID: "A", - }}, + ID: rand.Int63(), + OrgID: rand.Int63(), + Title: "TEST-ALERT-" + util.GenerateShortUID(), + Condition: "A", + Data: []AlertQuery{GenerateAlertQuery()}, Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)), IntervalSeconds: rand.Int63n(60) + 1, Version: rand.Int63(), @@ -100,6 +88,25 @@ func AlertRuleGen(mutators ...func(*AlertRule)) func() *AlertRule { } } +func GenerateAlertQuery() AlertQuery { + f := rand.Intn(10) + 5 + t := rand.Intn(f) + + return AlertQuery{ + DatasourceUID: util.GenerateShortUID(), + Model: json.RawMessage(fmt.Sprintf(`{ + "%s": "%s", + "%s":"%d" + }`, util.GenerateShortUID(), util.GenerateShortUID(), util.GenerateShortUID(), rand.Int())), + RelativeTimeRange: RelativeTimeRange{ + From: Duration(time.Duration(f) * time.Minute), + To: Duration(time.Duration(t) * time.Minute), + }, + RefID: util.GenerateShortUID(), + QueryType: util.GenerateShortUID(), + } +} + // GenerateUniqueAlertRules generates many random alert rules and makes sure that they have unique UID. // It returns a tuple where first element is a map where keys are UID of alert rule and the second element is a slice of the same rules func GenerateUniqueAlertRules(count int, f func() *AlertRule) (map[string]*AlertRule, []*AlertRule) { @@ -125,3 +132,59 @@ func GenerateAlertRules(count int, f func() *AlertRule) []*AlertRule { } return result } + +// CopyRule creates a deep copy of AlertRule +func CopyRule(r *AlertRule) *AlertRule { + result := AlertRule{ + ID: r.ID, + OrgID: r.OrgID, + Title: r.Title, + Condition: r.Condition, + Updated: r.Updated, + IntervalSeconds: r.IntervalSeconds, + Version: r.Version, + UID: r.UID, + NamespaceUID: r.NamespaceUID, + RuleGroup: r.RuleGroup, + NoDataState: r.NoDataState, + ExecErrState: r.ExecErrState, + For: r.For, + } + + if r.DashboardUID != nil { + dash := *r.DashboardUID + result.DashboardUID = &dash + } + if r.PanelID != nil { + p := *r.PanelID + result.PanelID = &p + } + + for _, d := range r.Data { + q := AlertQuery{ + RefID: d.RefID, + QueryType: d.QueryType, + RelativeTimeRange: d.RelativeTimeRange, + DatasourceUID: d.DatasourceUID, + } + q.Model = make([]byte, 0, cap(d.Model)) + q.Model = append(q.Model, d.Model...) + result.Data = append(result.Data, q) + } + + if r.Annotations != nil { + result.Annotations = make(map[string]string, len(r.Annotations)) + for s, s2 := range r.Annotations { + result.Annotations[s] = s2 + } + } + + if r.Labels != nil { + result.Labels = make(map[string]string, len(r.Labels)) + for s, s2 := range r.Labels { + result.Labels[s] = s2 + } + } + + return &result +} diff --git a/pkg/util/cmputil/reporter.go b/pkg/util/cmputil/reporter.go new file mode 100644 index 00000000000..c9532d3cc0f --- /dev/null +++ b/pkg/util/cmputil/reporter.go @@ -0,0 +1,101 @@ +package cmputil + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +type DiffReport []Diff + +// GetDiffsForField returns subset of the diffs which path starts with the provided path +func (r DiffReport) GetDiffsForField(path string) DiffReport { + var result []Diff + for _, diff := range r { + if strings.HasPrefix(path, diff.Path) { + result = append(result, diff) + } + } + return result +} + +// DiffReporter is a simple custom reporter that only records differences +// detected during comparison. Implements an interface required by cmp.Reporter option +type DiffReporter struct { + path cmp.Path + Diffs DiffReport +} + +func (r *DiffReporter) PushStep(ps cmp.PathStep) { + r.path = append(r.path, ps) +} + +func (r *DiffReporter) PopStep() { + r.path = r.path[:len(r.path)-1] +} + +func (r *DiffReporter) Report(rs cmp.Result) { + if !rs.Equal() { + vx, vy := r.path.Last().Values() + r.Diffs = append(r.Diffs, Diff{ + Path: printPath(r.path), + Left: vx, + Right: vy, + }) + } +} + +func printPath(p cmp.Path) string { + ss := strings.Builder{} + for _, s := range p { + toAdd := "" + switch v := s.(type) { + case cmp.StructField: + toAdd = v.String() + case cmp.MapIndex: + toAdd = fmt.Sprintf("[%s]", v.Key()) + case cmp.SliceIndex: + if v.Key() >= 0 { + toAdd = fmt.Sprintf("[%d]", v.Key()) + } + } + if toAdd == "" { + continue + } + ss.WriteString(toAdd) + } + return strings.TrimPrefix(ss.String(), ".") +} + +func (r DiffReport) String() string { + b := strings.Builder{} + for _, diff := range r { + b.WriteString(diff.String()) + b.WriteByte('\n') + } + return b.String() +} + +type Diff struct { + // Path to the field that has difference separated by period. Array index and key are designated by square brackets. + // For example, Annotations[12345].Data.Fields[0].ID + Path string + Left reflect.Value + Right reflect.Value +} + +func (d *Diff) String() string { + left := d.Left.String() + // invalid reflect.Value is produced when two collections (slices\maps) are compared and one misses value. + // This way go-cmp indicates that an element was added\removed from a list. + if !d.Left.IsValid() { + left = "" + } + right := d.Right.String() + if !d.Right.IsValid() { + right = "" + } + return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v", d.Path, left, right) +} From 703d7deedab18a27dddf7a56ef6202877eadc161 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 1 Mar 2022 10:49:49 -0600 Subject: [PATCH 084/125] Alerting: Add integration tests for dual-stage email templating (#43484) * Resolve merge conflicts * Remove cruft from local exploration * Move integration tests to intercept using new abstraction layer instead of channel * Fix linter error after rebase --- .../ngalert/notifier/channels/email_test.go | 202 ++++++++++++++++++ pkg/services/notifications/notifications.go | 4 + 2 files changed, 206 insertions(+) diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 42a738ba196..7e6da5adb7e 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -10,7 +10,10 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/setting" ) func TestEmailNotifier(t *testing.T) { @@ -102,3 +105,202 @@ func TestEmailNotifier(t *testing.T) { }, expected) }) } + +func TestEmailNotifierIntegration(t *testing.T) { + ns := createCoreEmailService(t) + + emailTmpl := templateForTests(t) + externalURL, err := url.Parse("http://localhost/base") + require.NoError(t, err) + emailTmpl.ExternalURL = externalURL + + cases := []struct { + name string + alerts []*types.Alert + messageTmpl string + expSubject string + expSnippets []string + }{ + { + name: "single alert with templated message", + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + }, + messageTmpl: `Hi, this is a custom template. + {{ if gt (len .Alerts.Firing) 0 }} + You have {{ len .Alerts.Firing }} alerts firing. + {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }} + {{ end }}`, + expSubject: "[FIRING:1] (AlwaysFiring warning)", + expSnippets: []string{ + "Hi, this is a custom template.", + "You have 1 alerts firing.", + "Firing: AlwaysFiring at warning", + }, + }, + { + name: "multiple alerts with templated message", + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + }, + messageTmpl: `Hi, this is a custom template. + {{ if gt (len .Alerts.Firing) 0 }} + You have {{ len .Alerts.Firing }} alerts firing. + {{ range .Alerts.Firing }} Firing: {{ .Labels.alertname }} at {{ .Labels.severity }} {{ end }} + {{ end }}`, + expSubject: "[FIRING:2] ", + expSnippets: []string{ + "Hi, this is a custom template.", + "You have 2 alerts firing.", + "Firing: FiringOne at warning", + "Firing: FiringTwo at critical", + }, + }, + { + name: "empty message with alerts uses default template content", + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "FiringOne", "severity": "warning"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "FiringTwo", "severity": "critical"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + }, + messageTmpl: "", + expSubject: "[FIRING:2] ", + expSnippets: []string{ + "Firing: 2 alerts", + "
  • alertname: FiringOne
  • severity: warning
  • ", + "
  • alertname: FiringTwo
  • severity: critical
  • ", + "Hi, this is a custom template. + {{ if gt (len .Alerts.Firing) 0 }} +
      + {{range .Alerts.Firing }}
    1. Firing: {{ .Labels.alertname }} at {{ .Labels.severity }}
    2. {{ end }} +
    + {{ end }}`, + expSubject: "[FIRING:1] (AlwaysFiring warning)", + expSnippets: []string{ + "<marquee>Hi, this is a custom template.</marquee>", + "<li>Firing: AlwaysFiring at warning </li>", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + emailNotifier := createSut(t, c.messageTmpl, emailTmpl, ns) + + ok, err := emailNotifier.Notify(context.Background(), c.alerts...) + require.NoError(t, err) + require.True(t, ok) + + sentMsg := getSingleSentMessage(t, ns) + + require.NotNil(t, sentMsg) + + require.Equal(t, "\"Grafana Admin\" ", sentMsg.From) + require.Equal(t, sentMsg.To[0], "someops@example.com") + + require.Equal(t, c.expSubject, sentMsg.Subject) + + require.Contains(t, sentMsg.Body, "text/html") + html := sentMsg.Body["text/html"] + require.NotNil(t, html) + + for _, s := range c.expSnippets { + require.Contains(t, html, s) + } + }) + } +} + +func createCoreEmailService(t *testing.T) *notifications.NotificationService { + t.Helper() + + bus := bus.New() + cfg := setting.NewCfg() + cfg.StaticRootPath = "../../../../../public/" + cfg.BuildVersion = "4.0.0" + cfg.Smtp.Enabled = true + cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"} + cfg.Smtp.FromAddress = "from@address.com" + cfg.Smtp.FromName = "Grafana Admin" + cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"} + cfg.Smtp.Host = "localhost:1234" + mailer := notifications.NewFakeMailer() + + ns, err := notifications.ProvideService(bus, cfg, mailer) + require.NoError(t, err) + + return ns +} + +func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, ns notifications.EmailSender) *EmailNotifier { + t.Helper() + + json := `{ + "addresses": "someops@example.com;somedev@example.com", + "singleEmail": true + }` + settingsJSON, err := simplejson.NewJson([]byte(json)) + if messageTmpl != "" { + settingsJSON.Set("message", messageTmpl) + } + require.NoError(t, err) + + emailNotifier, err := NewEmailNotifier(&NotificationChannelConfig{ + Name: "ops", + Type: "email", + Settings: settingsJSON, + }, ns, emailTmpl) + require.NoError(t, err) + + return emailNotifier +} + +func getSingleSentMessage(t *testing.T, ns *notifications.NotificationService) *notifications.Message { + t.Helper() + + mailer := ns.GetMailer().(*notifications.FakeMailer) + require.Len(t, mailer.Sent, 1) + sent := mailer.Sent[0] + mailer.Sent = []*notifications.Message{} + return sent +} diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 3ed26249234..9d13c0404c3 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -118,6 +118,10 @@ func (ns *NotificationService) Run(ctx context.Context) error { } } +func (ns *NotificationService) GetMailer() Mailer { + return ns.mailer +} + func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { return ns.sendWebRequestSync(ctx, &Webhook{ Url: cmd.Url, From 789cfc31e385182379ec2814b41e9d1a870e068d Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 1 Mar 2022 17:06:42 +0000 Subject: [PATCH 085/125] Alerting: Fix use of > instead of >= when checking the For duration (#46011) --- pkg/services/ngalert/state/manager_test.go | 4 ++-- pkg/services/ngalert/state/state.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 59151839976..8fcc8cf943b 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -552,7 +552,7 @@ func TestProcessEvalResults(t *testing.T) { }, }, }, - expectedAnnotations: 2, + expectedAnnotations: 3, expectedStates: map[string]*state.State{ `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`: { AlertRuleUID: "test_alert_rule_uid_2", @@ -578,7 +578,7 @@ func TestProcessEvalResults(t *testing.T) { Values: make(map[string]*float64), }, }, - StartsAt: evaluationTime, + StartsAt: evaluationTime.Add(20 * time.Second), EndsAt: evaluationTime.Add(30 * time.Second).Add(state.ResendDelay * 3), LastEvaluationTime: evaluationTime.Add(30 * time.Second), EvaluationDuration: evaluationDuration, diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 2947f789761..6b06eb1a453 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -64,7 +64,7 @@ func (a *State) resultAlerting(alertRule *ngModels.AlertRule, result eval.Result case eval.Alerting: a.setEndsAt(alertRule, result) case eval.Pending: - if result.EvaluatedAt.Sub(a.StartsAt) > alertRule.For { + if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For { a.State = eval.Alerting a.StartsAt = result.EvaluatedAt a.setEndsAt(alertRule, result) From 26e5af4b2e2f6a6301b8f088de608e774a334e43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:21:56 +0000 Subject: [PATCH 086/125] Update dependency rollup to v2.68.0 (#45747) Co-authored-by: Renovate Bot --- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index c211247abac..f701ef6d9de 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -67,7 +67,7 @@ "@types/tinycolor2": "1.4.3", "react-test-renderer": "17.0.2", "rimraf": "3.0.2", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "sinon": "13.0.1", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 6a6bd64642e..c5ba384cce8 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -28,7 +28,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@types/node": "16.11.22", "rimraf": "3.0.2", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2" }, diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 11d719ca564..7d622aced9f 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -37,7 +37,7 @@ "@types/lodash": "4.14.178", "@types/node": "16.11.22", "@types/uuid": "8.3.4", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-copy": "3.4.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 22f5c186087..7891e1b0e63 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -50,7 +50,7 @@ "@types/systemjs": "^0.20.6", "lodash": "4.17.21", "rimraf": "3.0.2", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "typescript": "4.4.4" diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 178defa6c1f..a04897c510c 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -28,7 +28,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@swc/helpers": "0.3.2", "rimraf": "3.0.2", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "typescript": "4.4.4" diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e2554323b9d..b97e7f39544 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -166,7 +166,7 @@ "react-docgen-typescript-loader": "3.7.2", "react-test-renderer": "17.0.2", "rimraf": "3.0.2", - "rollup": "2.67.1", + "rollup": "2.68.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "sass-loader": "12.6.0", diff --git a/yarn.lock b/yarn.lock index 835e4ddac77..02b77bf6557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4042,7 +4042,7 @@ __metadata: react-test-renderer: 17.0.2 regenerator-runtime: 0.13.9 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -4064,7 +4064,7 @@ __metadata: "@rollup/plugin-node-resolve": 13.1.3 "@types/node": 16.11.22 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 tslib: 2.3.1 @@ -4100,7 +4100,7 @@ __metadata: mocha: 9.2.0 resolve-as-bin: 2.1.0 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-copy: 3.4.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 @@ -4181,7 +4181,7 @@ __metadata: react: 17.0.2 react-dom: 17.0.2 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -4201,7 +4201,7 @@ __metadata: "@rollup/plugin-node-resolve": 13.1.3 "@swc/helpers": 0.3.2 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 tslib: 2.3.1 @@ -4462,7 +4462,7 @@ __metadata: react-use: 17.3.2 react-window: 1.8.6 rimraf: 3.0.2 - rollup: 2.67.1 + rollup: 2.68.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -32418,9 +32418,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:2.67.1": - version: 2.67.1 - resolution: "rollup@npm:2.67.1" +"rollup@npm:2.68.0": + version: 2.68.0 + resolution: "rollup@npm:2.68.0" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -32428,7 +32428,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 4320927ae9d42abf0a72ccdfd14303f7b6d6908ec94be953900a41bd4c0bbba3131887c3925dec65361a3e394cb572a4fa8e1f1be4db587a775068ad71d42acd + checksum: c883f6fb2e10e1c79a32527da0c50ef47a7beb8ddacfdae4197ff2d1911fb8d10bb2704496cf878d3048fbf3524d613bc87f25c5be0afc667fe30b7d04fa8092 languageName: node linkType: hard From ce3943bf4bba085ee4e18683b1a3081413dcc223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:22:24 +0000 Subject: [PATCH 087/125] Update typescript-eslint monorepo to v5.13.0 (#45988) Co-authored-by: Renovate Bot --- package.json | 4 +-- yarn.lock | 98 ++++++++++++++++++++++++++-------------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index aa2c5b6213a..92bc5d9fdee 100644 --- a/package.json +++ b/package.json @@ -155,8 +155,8 @@ "@types/testing-library__react-hooks": "^3.2.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", - "@typescript-eslint/eslint-plugin": "5.12.1", - "@typescript-eslint/parser": "5.12.1", + "@typescript-eslint/eslint-plugin": "5.13.0", + "@typescript-eslint/parser": "5.13.0", "@wojtekmaj/enzyme-adapter-react-17": "0.6.6", "autoprefixer": "10.4.2", "axios": "0.26.0", diff --git a/yarn.lock b/yarn.lock index 02b77bf6557..56e83c78272 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11384,13 +11384,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/eslint-plugin@npm:5.12.1" +"@typescript-eslint/eslint-plugin@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.13.0" dependencies: - "@typescript-eslint/scope-manager": 5.12.1 - "@typescript-eslint/type-utils": 5.12.1 - "@typescript-eslint/utils": 5.12.1 + "@typescript-eslint/scope-manager": 5.13.0 + "@typescript-eslint/type-utils": 5.13.0 + "@typescript-eslint/utils": 5.13.0 debug: ^4.3.2 functional-red-black-tree: ^1.0.1 ignore: ^5.1.8 @@ -11403,7 +11403,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 007f89f6ce8744a15cbdb1953efd41fcb4af84357070d8e13ea5d0fbbca0c9c6cfdc0598fd00a793100a0c37104e68dd61d5b7cada7abbcc5dfec50ea480b8ba + checksum: ff8863b8c414eeed874c7ef4e5d540c918f9ee9be2e44fe30c6c22f2f59529a61e71afb3d7a90bff9a8f894098f11373989df91b11ef67a424c12f703021c174 languageName: node linkType: hard @@ -11457,20 +11457,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/parser@npm:5.12.1" +"@typescript-eslint/parser@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/parser@npm:5.13.0" dependencies: - "@typescript-eslint/scope-manager": 5.12.1 - "@typescript-eslint/types": 5.12.1 - "@typescript-eslint/typescript-estree": 5.12.1 + "@typescript-eslint/scope-manager": 5.13.0 + "@typescript-eslint/types": 5.13.0 + "@typescript-eslint/typescript-estree": 5.13.0 debug: ^4.3.2 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: d2995b00937de1d289d0a5742ad0744cfa8fbaed1a1a965eaa5dc8dca6e36e18f5c3802148c4bde0ab014a91330ebe186d01b0871ee30f36ac41361dc1d525d4 + checksum: 9ca74f891df82f4f93150f0b69fcd2d9fb138c75a4629a154256108fbaa1248a96f69627cb472423890ff291e7cec30c20da25a87a21ef53fc1149ac9c18bfac languageName: node linkType: hard @@ -11504,13 +11504,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/scope-manager@npm:5.12.1" +"@typescript-eslint/scope-manager@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/scope-manager@npm:5.13.0" dependencies: - "@typescript-eslint/types": 5.12.1 - "@typescript-eslint/visitor-keys": 5.12.1 - checksum: b6e7f45b4fe39397430149ad005f7d28aa75a063dacfc947514abd52ba5235fecf937784416dfb7e8c168025b1bc74611332ceb214045cc362922e4b311bcb11 + "@typescript-eslint/types": 5.13.0 + "@typescript-eslint/visitor-keys": 5.13.0 + checksum: 43fade6759e751387ee91f85033c036f122b5051f7ad7baf35fe5db68e2129afc1cc1c12c2b0b8a25eb206092ad1073d8e640b21f6b04824413f40751d8e0d42 languageName: node linkType: hard @@ -11530,11 +11530,11 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/type-utils@npm:5.12.1" +"@typescript-eslint/type-utils@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/type-utils@npm:5.13.0" dependencies: - "@typescript-eslint/utils": 5.12.1 + "@typescript-eslint/utils": 5.13.0 debug: ^4.3.2 tsutils: ^3.21.0 peerDependencies: @@ -11542,7 +11542,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3f25e650f3be2af93d9cb1b26120b306af11c7cf36b3bd13f1c97f5c875a4abd282f8a0ae0daff0b90b4734c0dae3828b9ec508d46f65fd6cf6389a5b037fe26 + checksum: 454a2fe6c5faa211fec9d7992b44f377b9d492c3a18b8ce6d6da0077f0ea92320c7ee430cc33dcce8f0ec7afab7f8db59f39f9433be5358715754e64d7fbdef2 languageName: node linkType: hard @@ -11567,10 +11567,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/types@npm:5.12.1" - checksum: 753cb4b0f266500298f07c0386d72e7d9570f26e2deb7017fd677d21bb8bca7f2ca01d3f4b43d86fbb7337a76f0c9da86657de96f107dba92632d726b4e7797e +"@typescript-eslint/types@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/types@npm:5.13.0" + checksum: 2228935a9f7e80264a554ffadc458ee184259b56cd987bf10f12754183e032953fb93b7b31f8261dd0a40dbac4f341d4904ae7aa1f1aba9f2a92b1062f05c8dc languageName: node linkType: hard @@ -11628,12 +11628,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/typescript-estree@npm:5.12.1" +"@typescript-eslint/typescript-estree@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.13.0" dependencies: - "@typescript-eslint/types": 5.12.1 - "@typescript-eslint/visitor-keys": 5.12.1 + "@typescript-eslint/types": 5.13.0 + "@typescript-eslint/visitor-keys": 5.13.0 debug: ^4.3.2 globby: ^11.0.4 is-glob: ^4.0.3 @@ -11642,7 +11642,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 5677f5550fdc24879b51b3bc4c7551a5b241f70cf8f4cfe0fa2dc270dcd858c6d5085bf784c0bd471bb71da5abbbcf1ec44dc84a688ce61107d5ddba21d235ae + checksum: bcf2f94eb4b8e0a5f47fa1e04478aa3f36c8d2b629300bf3d3a375f87e8046cd7f2364cd7df8fceb97855e7789721de5c66dafcf17cfd93552a93a7d7733dfdb languageName: node linkType: hard @@ -11662,19 +11662,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/utils@npm:5.12.1" +"@typescript-eslint/utils@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/utils@npm:5.13.0" dependencies: "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.12.1 - "@typescript-eslint/types": 5.12.1 - "@typescript-eslint/typescript-estree": 5.12.1 + "@typescript-eslint/scope-manager": 5.13.0 + "@typescript-eslint/types": 5.13.0 + "@typescript-eslint/typescript-estree": 5.13.0 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 0eb277ad4e60fa46b586d5df8e05be6e0d3963a59cf409a348e2c512d5e88d657a56026754ee10c31ee1333794e145f7ffe33e08d28aa1bed4b4ab4b02a95712 + checksum: cb93cddc83bd5f9cee7fc72ab64c509b285392a005fb1315522374991f18a1cb8f233ee0d1e828cc18570c3fe27e81cc28471c36142284bd39351b8a3f8a83bd languageName: node linkType: hard @@ -11724,13 +11724,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.12.1": - version: 5.12.1 - resolution: "@typescript-eslint/visitor-keys@npm:5.12.1" +"@typescript-eslint/visitor-keys@npm:5.13.0": + version: 5.13.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.13.0" dependencies: - "@typescript-eslint/types": 5.12.1 + "@typescript-eslint/types": 5.13.0 eslint-visitor-keys: ^3.0.0 - checksum: ada52c77dc42d055a6cefb294b9a893d680d125eb0fc5cc0daf2f85007c603ef688f4f5a865893758e00e89739850409bb748e26bb8c834372409d24ea820677 + checksum: 3987217053e22a86f9105efe6250ca028ef437483b79d0dad45850edacfc273835b82178e77e5012a3c045df18561fef3eb4417cc26c328c901fbaa0da09e922 languageName: node linkType: hard @@ -20747,8 +20747,8 @@ __metadata: "@types/testing-library__react-hooks": ^3.2.0 "@types/tinycolor2": 1.4.3 "@types/uuid": 8.3.4 - "@typescript-eslint/eslint-plugin": 5.12.1 - "@typescript-eslint/parser": 5.12.1 + "@typescript-eslint/eslint-plugin": 5.13.0 + "@typescript-eslint/parser": 5.13.0 "@visx/event": 2.6.0 "@visx/gradient": 2.1.0 "@visx/group": 2.1.0 From f5705522aa391639dca103af824993881a1a7bcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:24:40 +0000 Subject: [PATCH 088/125] Update dependency eslint-plugin-react to v7.29.2 (#45880) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 92bc5d9fdee..04b6deaabd5 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "eslint-plugin-jsdoc": "37.9.1", "eslint-plugin-lodash": "7.4.0", "eslint-plugin-prettier": "4.0.0", - "eslint-plugin-react": "7.28.0", + "eslint-plugin-react": "7.29.2", "eslint-plugin-react-hooks": "4.3.0", "eslint-webpack-plugin": "3.1.1", "expose-loader": "3.1.0", diff --git a/yarn.lock b/yarn.lock index 56e83c78272..da9127ee2a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18620,6 +18620,30 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react@npm:7.29.2": + version: 7.29.2 + resolution: "eslint-plugin-react@npm:7.29.2" + dependencies: + array-includes: ^3.1.4 + array.prototype.flatmap: ^1.2.5 + doctrine: ^2.1.0 + estraverse: ^5.3.0 + jsx-ast-utils: ^2.4.1 || ^3.0.0 + minimatch: ^3.1.2 + object.entries: ^1.1.5 + object.fromentries: ^2.0.5 + object.hasown: ^1.1.0 + object.values: ^1.1.5 + prop-types: ^15.8.1 + resolve: ^2.0.0-next.3 + semver: ^6.3.0 + string.prototype.matchall: ^4.0.6 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: fcd793c0fd8e4570dcf8f70dd4afae9882303f4a4b650e09dac2f85fdc0b28747cb97b79832f4b8f4eabdbfb9edf69dc0ff04e8b7f33e0bb8e3f024185f4b037 + languageName: node + linkType: hard + "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -20795,7 +20819,7 @@ __metadata: eslint-plugin-jsdoc: 37.9.1 eslint-plugin-lodash: 7.4.0 eslint-plugin-prettier: 4.0.0 - eslint-plugin-react: 7.28.0 + eslint-plugin-react: 7.29.2 eslint-plugin-react-hooks: 4.3.0 eslint-webpack-plugin: 3.1.1 eventemitter3: 4.0.7 @@ -26150,6 +26174,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: ^1.1.7 + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" From 843e587a05f4e80bc1e27fcbccc8e1afc5cfe3fc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 1 Mar 2022 18:49:58 +0100 Subject: [PATCH 089/125] Docs: Fix typo in Forward OAuth identity for the logged-in user section (#46043) Fixes #45938 --- ...dd-authentication-for-data-source-plugins.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index bb0b333e998..c4a8e4e0bda 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -291,16 +291,15 @@ When configured, Grafana will pass the user's token to the plugin in an Authoriz ```go func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - for _, q := range req.Queries { - token := strings.Fields(q.Headers.Get("Authorization")) + token := strings.Fields(req.Headers["Authorization"]) + var ( + tokenType = token[0] + accessToken = token[1] + ) - var ( - tokenType = token[0] - accessToken = token[1] - ) - - // ... - } + for _, q := range req.Queries { + // ... + } } ``` From 09cde9a7007307ad7458b5c59f3caed8e6bfc545 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 18:55:06 +0100 Subject: [PATCH 090/125] Update dependency ts-node to v10.6.0 (#46045) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 81 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 04b6deaabd5..5e5244e7861 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,7 @@ "testing-library-selector": "0.2.1", "ts-jest": "27.1.3", "ts-loader": "9.2.6", - "ts-node": "10.5.0", + "ts-node": "10.6.0", "typescript": "4.4.4", "wait-on": "6.0.0", "webpack": "5.69.1", diff --git a/yarn.lock b/yarn.lock index da9127ee2a6..51433d02d57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20940,7 +20940,7 @@ __metadata: tinycolor2: 1.4.2 ts-jest: 27.1.3 ts-loader: 9.2.6 - ts-node: 10.5.0 + ts-node: 10.6.0 tslib: 2.3.1 typescript: 4.4.4 uplot: 1.6.19 @@ -35376,7 +35376,64 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:10.5.0, ts-node@npm:^10.2.1": +"ts-node@npm:10.6.0": + version: 10.6.0 + resolution: "ts-node@npm:10.6.0" + dependencies: + "@cspotcode/source-map-support": 0.7.0 + "@tsconfig/node10": ^1.0.7 + "@tsconfig/node12": ^1.0.7 + "@tsconfig/node14": ^1.0.0 + "@tsconfig/node16": ^1.0.2 + acorn: ^8.4.1 + acorn-walk: ^8.1.1 + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + v8-compile-cache-lib: ^3.0.0 + yn: 3.1.1 + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: bc7589d8c38dc75a2a6f832ac43faaac7edd3d0ef4643f46a9deeaabcdd35722e8c89e729fcd39a16069b30d09e297c2fb3eec917a82dd3f1e7da8b352bbd447 + languageName: node + linkType: hard + +"ts-node@npm:9.0.0": + version: 9.0.0 + resolution: "ts-node@npm:9.0.0" + dependencies: + arg: ^4.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + source-map-support: ^0.5.17 + yn: 3.1.1 + peerDependencies: + typescript: ">=2.7" + bin: + ts-node: dist/bin.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 63105fa59b53a835dd7ca843e392fcbac699a19b861c3fae8a77d73b6205db14f84f45250f57309c782cd2f9fde566963c94b5499f7d6f7bbb1a367f4fb3099b + languageName: node + linkType: hard + +"ts-node@npm:^10.2.1": version: 10.5.0 resolution: "ts-node@npm:10.5.0" dependencies: @@ -35413,26 +35470,6 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:9.0.0": - version: 9.0.0 - resolution: "ts-node@npm:9.0.0" - dependencies: - arg: ^4.1.0 - diff: ^4.0.1 - make-error: ^1.1.1 - source-map-support: ^0.5.17 - yn: 3.1.1 - peerDependencies: - typescript: ">=2.7" - bin: - ts-node: dist/bin.js - ts-node-script: dist/bin-script.js - ts-node-transpile-only: dist/bin-transpile.js - ts-script: dist/bin-script-deprecated.js - checksum: 63105fa59b53a835dd7ca843e392fcbac699a19b861c3fae8a77d73b6205db14f84f45250f57309c782cd2f9fde566963c94b5499f7d6f7bbb1a367f4fb3099b - languageName: node - linkType: hard - "ts-node@npm:^9": version: 9.1.1 resolution: "ts-node@npm:9.1.1" From 796bc27f75d52148d5b15cc8c4901276c344df2d Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 1 Mar 2022 14:46:52 -0800 Subject: [PATCH 091/125] Loki: support live streaming (#42804) --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 5 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/tsdb/loki/loki.go | 18 ++ pkg/tsdb/loki/parse_query.go | 4 +- pkg/tsdb/loki/streaming.go | 200 ++++++++++++++++++ pkg/tsdb/loki/streaming_frame.go | 52 +++++ pkg/tsdb/loki/streaming_frame_test.go | 40 ++++ .../loki/components/LokiOptionFields.tsx | 14 +- .../app/plugins/datasource/loki/datasource.ts | 10 +- .../app/plugins/datasource/loki/streaming.ts | 80 +++++++ public/app/plugins/datasource/loki/types.ts | 2 +- 12 files changed, 419 insertions(+), 11 deletions(-) create mode 100644 pkg/tsdb/loki/streaming.go create mode 100644 pkg/tsdb/loki/streaming_frame.go create mode 100644 pkg/tsdb/loki/streaming_frame_test.go create mode 100644 public/app/plugins/datasource/loki/streaming.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ef0242c0389..c566a4b2998 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -40,6 +40,7 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + lokiLive?: boolean; swaggerUi?: boolean; featureHighlights?: boolean; dashboardComments?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8aba04eda6a..f0a542c248f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -132,6 +132,11 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, + { + Name: "lokiLive", + Description: "support websocket streaming for loki (early prototype)", + State: FeatureStateAlpha, + }, { Name: "swaggerUi", Description: "Serves swagger UI", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 70f7b58c3eb..bb15e26e660 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -99,6 +99,10 @@ const ( // only execute the query saved in a panel FlagValidatedQueries = "validatedQueries" + // FlagLokiLive + // support websocket streaming for loki (early prototype) + FlagLokiLive = "lokiLive" + // FlagSwaggerUi // Serves swagger UI FlagSwaggerUi = "swaggerUi" diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index c554386854f..59cc30eb7e1 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -2,9 +2,11 @@ package loki import ( "context" + "encoding/json" "fmt" "net/http" "regexp" + "sync" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" @@ -22,6 +24,11 @@ type Service struct { tracer tracing.Tracer } +var ( + _ backend.QueryDataHandler = (*Service)(nil) + _ backend.StreamHandler = (*Service)(nil) +) + func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Tracer) *Service { return &Service{ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)), @@ -37,6 +44,10 @@ var ( type datasourceInfo struct { HTTPClient *http.Client URL string + + // open streams + streams map[string]data.FrameJSONCache + streamsMu sync.RWMutex } type QueryJSONModel struct { @@ -50,6 +61,12 @@ type QueryJSONModel struct { VolumeQuery bool `json:"volumeQuery"` } +func parseQueryModel(raw json.RawMessage) (*QueryJSONModel, error) { + model := &QueryJSONModel{} + err := json.Unmarshal(raw, model) + return model, err +} + func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { opts, err := settings.HTTPClientOptions() @@ -65,6 +82,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst model := &datasourceInfo{ HTTPClient: client, URL: settings.URL, + streams: make(map[string]data.FrameJSONCache), } return model, nil } diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 2ebf64dee6f..d62802e5f06 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -1,7 +1,6 @@ package loki import ( - "encoding/json" "fmt" "math" "strconv" @@ -71,8 +70,7 @@ func parseQueryType(jsonValue string) (QueryType, error) { func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) { qs := []*lokiQuery{} for _, query := range queryContext.Queries { - model := &QueryJSONModel{} - err := json.Unmarshal(query.JSON, model) + model, err := parseQueryModel(query.JSON) if err != nil { return nil, err } diff --git a/pkg/tsdb/loki/streaming.go b/pkg/tsdb/loki/streaming.go new file mode 100644 index 00000000000..e2c6ff96180 --- /dev/null +++ b/pkg/tsdb/loki/streaming.go @@ -0,0 +1,200 @@ +package loki + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "os/signal" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { + dsInfo, err := s.getDSInfo(req.PluginContext) + if err != nil { + return &backend.SubscribeStreamResponse{ + Status: backend.SubscribeStreamStatusNotFound, + }, err + } + + // Expect tail/${key} + if !strings.HasPrefix(req.Path, "tail/") { + return &backend.SubscribeStreamResponse{ + Status: backend.SubscribeStreamStatusNotFound, + }, fmt.Errorf("expected tail in channel path") + } + + query, err := parseQueryModel(req.Data) + if err != nil { + return nil, err + } + if query.Expr == "" { + return &backend.SubscribeStreamResponse{ + Status: backend.SubscribeStreamStatusNotFound, + }, fmt.Errorf("missing expr in channel (subscribe)") + } + + dsInfo.streamsMu.RLock() + defer dsInfo.streamsMu.RUnlock() + + cache, ok := dsInfo.streams[req.Path] + if ok { + msg, err := backend.NewInitialData(cache.Bytes(data.IncludeAll)) + return &backend.SubscribeStreamResponse{ + Status: backend.SubscribeStreamStatusOK, + InitialData: msg, + }, err + } + + // nothing yet + return &backend.SubscribeStreamResponse{ + Status: backend.SubscribeStreamStatusOK, + }, err +} + +// Single instance for each channel (results are shared with all listeners) +func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { + dsInfo, err := s.getDSInfo(req.PluginContext) + if err != nil { + return err + } + + query, err := parseQueryModel(req.Data) + if err != nil { + return err + } + if query.Expr == "" { + return fmt.Errorf("missing expr in cuannel") + } + + count := int64(0) + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + params := url.Values{} + params.Add("query", query.Expr) + + isV1 := false + wsurl, _ := url.Parse(dsInfo.URL) + + // Check if the v2alpha endpoint exists + wsurl.Path = "/loki/api/v2alpha/tail" + if !is400(dsInfo.HTTPClient, wsurl) { + isV1 = true + wsurl.Path = "/loki/api/v1/tail" + } + + if wsurl.Scheme == "https" { + wsurl.Scheme = "wss" + } else { + wsurl.Scheme = "ws" + } + wsurl.RawQuery = params.Encode() + + s.plog.Info("connecting to websocket", "url", wsurl) + c, r, err := websocket.DefaultDialer.Dial(wsurl.String(), nil) + if err != nil { + s.plog.Error("error connecting to websocket", "err", err) + return fmt.Errorf("error connecting to websocket") + } + + defer func() { + dsInfo.streamsMu.Lock() + delete(dsInfo.streams, req.Path) + dsInfo.streamsMu.Unlock() + if r != nil { + _ = r.Body.Close() + } + err = c.Close() + s.plog.Error("closing loki websocket", "err", err) + }() + + prev := data.FrameJSONCache{} + + // Read all messages + done := make(chan struct{}) + go func() { + defer close(done) + for { + _, message, err := c.ReadMessage() + if err != nil { + s.plog.Error("websocket read:", "err", err) + return + } + + frame := &data.Frame{} + if isV1 { + frame, err = lokiBytesToLabeledFrame(message) + } else { + err = json.Unmarshal(message, &frame) + } + + if err == nil && frame != nil { + next, _ := data.FrameToJSONCache(frame) + if next.SameSchema(&prev) { + err = sender.SendBytes(next.Bytes(data.IncludeDataOnly)) + } else { + err = sender.SendFrame(frame, data.IncludeAll) + } + prev = next + + // Cache the initial data + dsInfo.streamsMu.Lock() + dsInfo.streams[req.Path] = prev + dsInfo.streamsMu.Unlock() + } + + if err != nil { + s.plog.Error("websocket write:", "err", err, "raw", message) + return + } + } + }() + + ticker := time.NewTicker(time.Second * 60) //.Step) + defer ticker.Stop() + + for { + select { + case <-done: + s.plog.Info("socket done") + return nil + case <-ctx.Done(): + s.plog.Info("stop streaming (context canceled)") + return nil + case t := <-ticker.C: + count++ + s.plog.Error("loki websocket ping?", "time", t, "count", count) + } + } +} + +func (s *Service) PublishStream(_ context.Context, _ *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { + return &backend.PublishStreamResponse{ + Status: backend.PublishStreamStatusPermissionDenied, + }, nil +} + +// if the v2 endpoint exists it will give a 400 rather than 404/500 +func is400(client *http.Client, url *url.URL) bool { + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return false + } + rsp, err := client.Do(req) + if err != nil { + return false + } + defer func() { + _ = rsp.Body.Close() + }() + return rsp.StatusCode == 400 // will be true +} diff --git a/pkg/tsdb/loki/streaming_frame.go b/pkg/tsdb/loki/streaming_frame.go new file mode 100644 index 00000000000..e03cd298b7d --- /dev/null +++ b/pkg/tsdb/loki/streaming_frame.go @@ -0,0 +1,52 @@ +package loki + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type lokiResponse struct { + Streams []lokiStream `json:"streams"` +} + +type lokiStream struct { + Stream data.Labels `json:"stream"` + Values [][2]string `json:"values"` +} + +func lokiBytesToLabeledFrame(msg []byte) (*data.Frame, error) { + rsp := &lokiResponse{} + err := json.Unmarshal(msg, rsp) + if err != nil { + return nil, err + } + + labelField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + lineField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + + labelField.Name = "__labels" // for now, avoid automatically spreading this by labels + timeField.Name = "Time" + lineField.Name = "Line" + + for _, stream := range rsp.Streams { + label := stream.Stream.String() // TODO -- make it match prom labels! + for _, value := range stream.Values { + n, err := strconv.ParseInt(value[0], 10, 64) + if err != nil { + continue + } + ts := time.Unix(0, n) + line := value[1] + + labelField.Append(label) + timeField.Append(ts) + lineField.Append(line) + } + } + + return data.NewFrame("", labelField, timeField, lineField), nil +} diff --git a/pkg/tsdb/loki/streaming_frame_test.go b/pkg/tsdb/loki/streaming_frame_test.go new file mode 100644 index 00000000000..659bc8320db --- /dev/null +++ b/pkg/tsdb/loki/streaming_frame_test.go @@ -0,0 +1,40 @@ +package loki + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLokiFramer(t *testing.T) { + t.Run("converting metric name", func(t *testing.T) { + msg := []byte(`{"streams":[ + {"stream": + {"job":"node-exporter","metric":"go_memstats_heap_inuse_bytes"}, + "values":[ + ["1642091525267322910","line1"] + ]}, + {"stream": + {"job":"node-exporter","metric":"go_memstats_heap_inuse_bytes"}, + "values":[ + ["1642091525770585774","line2"], + ["1642091525770585775","line3"] + ]}, + {"stream": + {"metric":"go_memstats_heap_inuse_bytes","job":"node-exporter"}, + "values":[ + ["1642091526263785281","line4"] + ]} + ]}`) + + frame, err := lokiBytesToLabeledFrame(msg) + require.NoError(t, err) + + lines := frame.Fields[2] + require.Equal(t, 4, lines.Len()) + require.Equal(t, "line1", lines.At(0)) + require.Equal(t, "line2", lines.At(1)) + require.Equal(t, "line3", lines.At(2)) + require.Equal(t, "line4", lines.At(3)) + }) +} diff --git a/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx b/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx index ae66ec73ab4..17854499e85 100644 --- a/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx +++ b/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx @@ -6,6 +6,7 @@ import { map } from 'lodash'; // Types import { InlineFormLabel, RadioButtonGroup, InlineField, Input, Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { LokiQuery, LokiQueryType } from '../types'; export interface LokiOptionFieldsProps { @@ -24,13 +25,16 @@ const queryTypeOptions: Array> = [ label: 'Instant', description: 'Run query against a single point in time. For this query, the "To" time is used.', }, - // { - // value: LokiQueryType.Stream, - // label: 'Stream', - // description: 'Run a query and keep sending results on an interval', - // }, ]; +if (config.featureToggles.lokiLive) { + queryTypeOptions.push({ + value: LokiQueryType.Stream, + label: 'Stream', + description: 'Run a query and keep sending results on an interval', + }); +} + export const DEFAULT_RESOLUTION: SelectableValue = { value: 1, label: '1/1', diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 4d4b8332a37..4136a4fc770 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -33,7 +33,7 @@ import { ScopedVars, TimeRange, } from '@grafana/data'; -import { BackendSrvRequest, FetchError, getBackendSrv, DataSourceWithBackend } from '@grafana/runtime'; +import { BackendSrvRequest, FetchError, getBackendSrv, config, DataSourceWithBackend } from '@grafana/runtime'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { addLabelToQuery } from './add_label_to_query'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -63,7 +63,7 @@ import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContext import syntax from './syntax'; import { DEFAULT_RESOLUTION } from './components/LokiOptionFields'; import { queryLogsVolume } from 'app/core/logs_model'; -import config from 'app/core/config'; +import { doLokiChannelStream } from './streaming'; import { renderLegendFormat } from '../prometheus/legend'; export type RangeQueryOptions = DataQueryRequest | AnnotationQueryRequest; @@ -178,6 +178,12 @@ export class LokiDatasource for (const target of filteredTargets) { if (target.instant || target.queryType === LokiQueryType.Instant) { subQueries.push(this.runInstantQuery(target, request, filteredTargets.length)); + } else if ( + config.featureToggles.lokiLive && + target.queryType === LokiQueryType.Stream && + request.rangeRaw?.to === 'now' + ) { + subQueries.push(doLokiChannelStream(target, this, request)); } else { subQueries.push(this.runRangeQuery(target, request, filteredTargets.length)); } diff --git a/public/app/plugins/datasource/loki/streaming.ts b/public/app/plugins/datasource/loki/streaming.ts new file mode 100644 index 00000000000..08655ef76fd --- /dev/null +++ b/public/app/plugins/datasource/loki/streaming.ts @@ -0,0 +1,80 @@ +import { DataFrameJSON, DataQueryRequest, DataQueryResponse, LiveChannelScope, LoadingState } from '@grafana/data'; +import { getGrafanaLiveSrv } from '@grafana/runtime'; +import { map, Observable, defer, mergeMap } from 'rxjs'; +import LokiDatasource from './datasource'; +import { LokiQuery } from './types'; +import { StreamingDataFrame } from 'app/features/live/data/StreamingDataFrame'; + +/** + * Calculate a unique key for the query. The key is used to pick a channel and should + * be unique for each distinct query execution plan. This key is not secure and is only picked to avoid + * possible collisions + */ +export async function getLiveStreamKey(query: LokiQuery): Promise { + const str = JSON.stringify({ expr: query.expr }); + + const msgUint8 = new TextEncoder().encode(str); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer.slice(0, 8))); // first 8 bytes + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +// This will get both v1 and v2 result formats +export function doLokiChannelStream( + query: LokiQuery, + ds: LokiDatasource, + options: DataQueryRequest +): Observable { + // maximum time to keep values + const range = options.range; + const maxDelta = range.to.valueOf() - range.from.valueOf() + 1000; + let maxLength = options.maxDataPoints ?? 1000; + if (maxLength > 100) { + // for small buffers, keep them small + maxLength *= 2; + } + + let frame: StreamingDataFrame | undefined = undefined; + const updateFrame = (msg: any) => { + if (msg?.message) { + const p = msg.message as DataFrameJSON; + if (!frame) { + frame = StreamingDataFrame.fromDataFrameJSON(p, { + maxLength, + maxDelta, + displayNameFormat: query.legendFormat, + }); + } else { + frame.push(p); + } + } + return frame; + }; + + return defer(() => getLiveStreamKey(query)).pipe( + mergeMap((key) => { + return getGrafanaLiveSrv() + .getStream({ + scope: LiveChannelScope.DataSource, + namespace: ds.uid, + path: `tail/${key}`, + data: { + ...query, + timeRange: { + from: range.from.valueOf().toString(), + to: range.to.valueOf().toString(), + }, + }, + }) + .pipe( + map((evt) => { + const frame = updateFrame(evt); + return { + data: frame ? [frame] : [], + state: LoadingState.Streaming, + }; + }) + ); + }) + ); +} diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index 35e37bdad8f..c72954e7c38 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -27,7 +27,7 @@ export enum LokiResultType { export enum LokiQueryType { Range = 'range', Instant = 'instant', - // Stream = 'stream', + Stream = 'stream', } export interface LokiQuery extends DataQuery { From fa99143eee67dab19289b039e881d52a296ce31e Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 1 Mar 2022 23:23:48 -0600 Subject: [PATCH 092/125] Graph (old): use timeField.config.interval to apply null insertion logic (#46069) --- public/app/plugins/panel/graph/data_processor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index bc5b7a7aae0..37c19b9423e 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -12,6 +12,7 @@ import { } from '@grafana/data'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; +import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; type Options = { dataList: DataFrame[]; @@ -30,13 +31,15 @@ export class DataProcessor { } for (let i = 0; i < dataList.length; i++) { - const series = dataList[i]; + let series = dataList[i]; const { timeField } = getTimeField(series); if (!timeField) { continue; } + series = applyNullInsertThreshold(series, timeField.name); + for (let j = 0; j < series.fields.length; j++) { const field = series.fields[j]; From b4634afe33d657940efac448c2d34d07e45a8c89 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:40:53 +0800 Subject: [PATCH 093/125] use alerting service instead of store in provisioning (#45926) * fix notification * Fix confir reader notifiers test * Move UpdateAlertNotificationWithUid to alertingService * Rename Store to Manager * Rename Store to Manager in provisioning Co-authored-by: Ida Furjesova --- .../notifiers/alert_notifications.go | 50 ++++--- .../notifiers/config_reader_test.go | 126 +++++++----------- pkg/services/provisioning/provisioning.go | 10 +- 3 files changed, 86 insertions(+), 100 deletions(-) diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go index 3195837c72b..aac68649cda 100644 --- a/pkg/services/provisioning/notifiers/alert_notifications.go +++ b/pkg/services/provisioning/notifiers/alert_notifications.go @@ -8,38 +8,50 @@ import ( "golang.org/x/net/context" ) -type Store interface { - GetOrgById(c context.Context, cmd *models.GetOrgByIdQuery) error - GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error +type Manager interface { + GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error + CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error + UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error + DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error + GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error + GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error + SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error + SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error - CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error + GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error } +type SQLStore interface { + GetOrgById(c context.Context, cmd *models.GetOrgByIdQuery) error + GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error +} // Provision alert notifiers -func Provision(ctx context.Context, configDirectory string, store Store, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { - dc := newNotificationProvisioner(store, encryptionService, notificationService, log.New("provisioning.notifiers")) +func Provision(ctx context.Context, configDirectory string, alertingService Manager, sqlstore SQLStore, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { + dc := newNotificationProvisioner(sqlstore, alertingService, encryptionService, notificationService, log.New("provisioning.notifiers")) return dc.applyChanges(ctx, configDirectory) } // NotificationProvisioner is responsible for provsioning alert notifiers type NotificationProvisioner struct { - log log.Logger - cfgProvider *configReader - store Store + log log.Logger + cfgProvider *configReader + alertingManager Manager + sqlstore SQLStore } -func newNotificationProvisioner(store Store, encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { +func newNotificationProvisioner(store SQLStore, alertingManager Manager, encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { return NotificationProvisioner{ - log: log, - store: store, + log: log, + alertingManager: alertingManager, cfgProvider: &configReader{ encryptionService: encryptionService, notificationService: notifiationService, log: log, orgStore: store, }, + sqlstore: store, } } @@ -61,7 +73,7 @@ func (dc *NotificationProvisioner) deleteNotifications(ctx context.Context, noti if notification.OrgID == 0 && notification.OrgName != "" { getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} - if err := dc.store.GetOrgByNameHandler(ctx, getOrg); err != nil { + if err := dc.sqlstore.GetOrgByNameHandler(ctx, getOrg); err != nil { return err } notification.OrgID = getOrg.Result.Id @@ -71,13 +83,13 @@ func (dc *NotificationProvisioner) deleteNotifications(ctx context.Context, noti getNotification := &models.GetAlertNotificationsWithUidQuery{Uid: notification.UID, OrgId: notification.OrgID} - if err := dc.store.GetAlertNotificationsWithUid(ctx, getNotification); err != nil { + if err := dc.alertingManager.GetAlertNotificationsWithUid(ctx, getNotification); err != nil { return err } if getNotification.Result != nil { cmd := &models.DeleteAlertNotificationWithUidCommand{Uid: getNotification.Result.Uid, OrgId: getNotification.OrgId} - if err := dc.store.DeleteAlertNotificationWithUid(ctx, cmd); err != nil { + if err := dc.alertingManager.DeleteAlertNotificationWithUid(ctx, cmd); err != nil { return err } } @@ -90,7 +102,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif for _, notification := range notificationToMerge { if notification.OrgID == 0 && notification.OrgName != "" { getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} - if err := dc.store.GetOrgByNameHandler(ctx, getOrg); err != nil { + if err := dc.sqlstore.GetOrgByNameHandler(ctx, getOrg); err != nil { return err } notification.OrgID = getOrg.Result.Id @@ -99,7 +111,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif } cmd := &models.GetAlertNotificationsWithUidQuery{OrgId: notification.OrgID, Uid: notification.UID} - err := dc.store.GetAlertNotificationsWithUid(ctx, cmd) + err := dc.alertingManager.GetAlertNotificationsWithUid(ctx, cmd) if err != nil { return err } @@ -119,7 +131,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif SendReminder: notification.SendReminder, } - if err := dc.store.CreateAlertNotificationCommand(ctx, insertCmd); err != nil { + if err := dc.alertingManager.CreateAlertNotificationCommand(ctx, insertCmd); err != nil { return err } } else { @@ -137,7 +149,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif SendReminder: notification.SendReminder, } - if err := dc.store.UpdateAlertNotificationWithUid(ctx, updateCmd); err != nil { + if err := dc.alertingManager.UpdateAlertNotificationWithUid(ctx, updateCmd); 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 7e8a0dd760b..c8cae8b6886 100644 --- a/pkg/services/provisioning/notifiers/config_reader_test.go +++ b/pkg/services/provisioning/notifiers/config_reader_test.go @@ -6,7 +6,6 @@ import ( "os" "testing" - "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" @@ -37,7 +36,6 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Testing notification as configuration", func(t *testing.T) { setup := func() { sqlStore = sqlstore.InitTestDB(t) - setupBusHandlers(sqlStore) for i := 1; i < 5; i++ { orgCommand := models.CreateOrgCommand{Name: fmt.Sprintf("Main Org. %v", i)} @@ -140,17 +138,14 @@ 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(sqlStore, ossencryption.ProvideService(), nil, logger) + fakeAlertNotification := &fakeAlertNotification{} + fakeAlertNotification.ExpectedAlertNotification = &models.AlertNotification{OrgId: 1} + dc := newNotificationProvisioner(sqlStore, fakeAlertNotification, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) } - notificationsQuery := models.GetAllAlertNotificationsQuery{OrgId: 1} - err = sqlStore.GetAllAlertNotifications(context.Background(), ¬ificationsQuery) - require.NoError(t, err) - require.NotNil(t, notificationsQuery.Result) - require.Equal(t, len(notificationsQuery.Result), 2) }) t.Run("One notification in database with same name and uid", func(t *testing.T) { @@ -171,42 +166,19 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 1) t.Run("should update one notification", func(t *testing.T) { - dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) } - err = sqlStore.GetAllAlertNotifications(context.Background(), ¬ificationsQuery) - require.NoError(t, err) - require.NotNil(t, notificationsQuery.Result) - require.Equal(t, len(notificationsQuery.Result), 2) - - nts := notificationsQuery.Result - nt1 := nts[0] - require.Equal(t, nt1.Type, "email") - require.Equal(t, nt1.Name, "channel1") - require.Equal(t, nt1.Uid, "notifier1") - - nt2 := nts[1] - require.Equal(t, nt2.Type, "slack") - require.Equal(t, nt2.Name, "channel2") - require.Equal(t, nt2.Uid, "notifier2") }) }) t.Run("Two notifications with is_default", func(t *testing.T) { setup() - dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), doubleNotificationsConfig) t.Run("should both be inserted", func(t *testing.T) { require.NoError(t, err) - notificationsQuery := models.GetAllAlertNotificationsQuery{OrgId: 1} - err = sqlStore.GetAllAlertNotifications(context.Background(), ¬ificationsQuery) - require.NoError(t, err) - require.NotNil(t, notificationsQuery.Result) - require.Equal(t, len(notificationsQuery.Result), 2) - - require.True(t, notificationsQuery.Result[0].IsDefault) - require.True(t, notificationsQuery.Result[1].IsDefault) }) }) }) @@ -238,16 +210,11 @@ 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(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) } - notificationsQuery = models.GetAllAlertNotificationsQuery{OrgId: 1} - err = sqlStore.GetAllAlertNotifications(context.Background(), ¬ificationsQuery) - require.NoError(t, err) - require.NotNil(t, notificationsQuery.Result) - require.Equal(t, len(notificationsQuery.Result), 4) }) }) }) @@ -272,26 +239,16 @@ func TestNotificationAsConfig(t *testing.T) { err = sqlStore.CreateAlertNotificationCommand(context.Background(), &existingNotificationCmd) require.NoError(t, err) - dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), correctPropertiesWithOrgName) if err != nil { t.Fatalf("applyChanges return an error %v", err) } - - notificationsQuery := models.GetAllAlertNotificationsQuery{OrgId: existingOrg2.Result.Id} - err = sqlStore.GetAllAlertNotifications(context.Background(), ¬ificationsQuery) - require.NoError(t, err) - require.NotNil(t, notificationsQuery.Result) - require.Equal(t, len(notificationsQuery.Result), 1) - - nt := notificationsQuery.Result[0] - require.Equal(t, nt.Name, "default-notification-create") - require.Equal(t, nt.OrgId, existingOrg2.Result.Id) }) t.Run("Config doesn't contain required field", func(t *testing.T) { setup() - dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), noRequiredFields) require.NotNil(t, err) @@ -305,7 +262,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(sqlStore, ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, &fakeAlertNotification{}, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), emptyFile) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -366,32 +323,45 @@ func TestNotificationAsConfig(t *testing.T) { }) } -func setupBusHandlers(sqlStore *sqlstore.SQLStore) { - bus.AddHandler("getOrg", func(ctx context.Context, q *models.GetOrgByNameQuery) error { - return sqlStore.GetOrgByNameHandler(ctx, q) - }) +type fakeAlertNotification struct { + ExpectedAlertNotification *models.AlertNotification +} - bus.AddHandler("getAlertNotifications", func(ctx context.Context, q *models.GetAlertNotificationsWithUidQuery) error { - return sqlStore.GetAlertNotificationsWithUid(ctx, q) - }) +func (f *fakeAlertNotification) GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error { + query.Result = f.ExpectedAlertNotification + return nil +} +func (f *fakeAlertNotification) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error { + return nil +} +func (f *fakeAlertNotification) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { + return nil +} +func (f *fakeAlertNotification) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { + return nil +} +func (f *fakeAlertNotification) GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error { + return nil +} +func (f *fakeAlertNotification) GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error { + return nil +} +func (f *fakeAlertNotification) SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error { + return nil +} +func (f *fakeAlertNotification) SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error { + return nil +} +func (f *fakeAlertNotification) GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error { + return nil +} +func (f *fakeAlertNotification) DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error { + return nil +} +func (f *fakeAlertNotification) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error { + return nil +} - bus.AddHandler("createAlertNotification", func(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error { - return sqlStore.CreateAlertNotificationCommand(ctx, cmd) - }) - - bus.AddHandler("updateAlertNotification", func(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { - return sqlStore.UpdateAlertNotification(ctx, cmd) - }) - - bus.AddHandler("updateAlertNotification", func(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error { - return sqlStore.UpdateAlertNotificationWithUid(ctx, cmd) - }) - - bus.AddHandler("deleteAlertNotification", func(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error { - return sqlStore.DeleteAlertNotification(ctx, cmd) - }) - - bus.AddHandler("deleteAlertNotification", func(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error { - return sqlStore.DeleteAlertNotificationWithUid(ctx, cmd) - }) +func (f *fakeAlertNotification) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error { + return nil } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 303ab11c17c..ab0678583c3 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/alerting" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards" datasourceservice "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/encryption" @@ -26,6 +27,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p encryptionService encryption.Internal, notificatonService *notifications.NotificationService, dashboardService dashboardservice.DashboardProvisioningService, datasourceService datasourceservice.DataSourceService, + alertingService *alerting.AlertNotificationService, ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, @@ -40,6 +42,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p provisionPlugins: plugins.Provision, dashboardService: dashboardService, datasourceService: datasourceService, + alertingService: alertingService, } return s, nil } @@ -69,7 +72,7 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl { // Used for testing purposes func newProvisioningServiceImpl( newDashboardProvisioner dashboards.DashboardProvisionerFactory, - provisionNotifiers func(context.Context, string, notifiers.Store, encryption.Internal, *notifications.NotificationService) error, + provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error, provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error, provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store) error, ) *ProvisioningServiceImpl { @@ -92,12 +95,13 @@ type ProvisioningServiceImpl struct { pollingCtxCancel context.CancelFunc newDashboardProvisioner dashboards.DashboardProvisionerFactory dashboardProvisioner dashboards.DashboardProvisioner - provisionNotifiers func(context.Context, string, notifiers.Store, encryption.Internal, *notifications.NotificationService) error + provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store) error mutex sync.Mutex dashboardService dashboardservice.DashboardProvisioningService datasourceService datasourceservice.DataSourceService + alertingService *alerting.AlertNotificationService } func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { @@ -170,7 +174,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.SQLStore, ps.EncryptionService, ps.NotificationService); err != nil { + if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.alertingService, ps.SQLStore, 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 From 647c5208d7b855e63fb8c8f80171e9b4fff959c5 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:12:50 +0100 Subject: [PATCH 094/125] Devenv: Add documentation for integration tests (#46056) * Add documentation for integration tests * Update contribute/developer-guide.md --- contribute/developer-guide.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index ea2c2897d45..9eb31be1f00 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -136,6 +136,20 @@ Running the backend tests on Windows currently needs some tweaking, so use the b go run build.go test ``` +### Run PostgreSQL and MySQL integration tests + +To run PostgreSQL and MySQL integration tests locally, you need to start the docker blocks for MySQL and/or PostgreSQL test data sources by running `make devenv sources=mysql_tests,postgres_tests`. When your test data sources are running, you can execute integration tests by running: + +``` +GRAFANA_TEST_DB=mysql go test -covermode=atomic -tags=integration ./pkg/... +``` + +and/or + +``` +GRAFANA_TEST_DB=postgres go test -covermode=atomic -tags=integration ./pkg/... +``` + ### Run end-to-end tests The end to end tests in Grafana use [Cypress](https://www.cypress.io/) to run automated scripts in a headless Chromium browser. Read more about our [e2e framework](/contribute/style-guides/e2e.md). From 8e08128f83568e89f46b8d56bd147ef826d0fb45 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 2 Mar 2022 10:46:17 +0100 Subject: [PATCH 095/125] CloudWatch: List all metrics properly in SQL autocomplete (#45898) * make sure right value is passed for metric * add unit test --- .../datasource/cloudwatch/datasource.test.ts | 20 +++++++++++++++++++ .../datasource/cloudwatch/datasource.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 840e90ffbad..00eb29f9bb7 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -249,6 +249,26 @@ describe('datasource', () => { }); }); }); + describe('resource requests', () => { + it('should map resource response to metric response', async () => { + const datasource = setupMockedDataSource().datasource; + datasource.doMetricResourceRequest = jest.fn().mockResolvedValue([ + { + text: 'AWS/EC2', + value: 'CPUUtilization', + }, + { + text: 'AWS/Redshift', + value: 'CPUPercentage', + }, + ]); + const allMetrics = await datasource.getAllMetrics('us-east-2'); + expect(allMetrics[0].metricName).toEqual('CPUUtilization'); + expect(allMetrics[0].namespace).toEqual('AWS/EC2'); + expect(allMetrics[1].metricName).toEqual('CPUPercentage'); + expect(allMetrics[1].namespace).toEqual('AWS/Redshift'); + }); + }); describe('performTimeSeriesQuery', () => { it('should return the same length of data as result', async () => { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 32f1f374671..4e1c038829e 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -670,7 +670,7 @@ export class CloudWatchDatasource region: this.templateSrv.replace(this.getActualRegion(region)), }); - return values.map((v) => ({ metricName: v.label, namespace: v.text })); + return values.map((v) => ({ metricName: v.value, namespace: v.text })); } async getDimensionKeys( From 5eaf6509c09ea9e9e44cb631787cb245edd98d93 Mon Sep 17 00:00:00 2001 From: Selene Date: Wed, 2 Mar 2022 11:04:29 +0100 Subject: [PATCH 096/125] Dashboard Extractor: Don't fail when using default OSS implementation (#46024) * Don't fail when using default OSS implementation * Check correct error --- pkg/api/datasources.go | 5 +- pkg/services/alerting/extractor.go | 7 +- pkg/services/alerting/extractor_test.go | 65 +++++++++++++++++++ .../permissions/datasource_permissions.go | 5 +- .../datasource_permissions_mocks.go | 5 +- 5 files changed, 79 insertions(+), 8 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 9af7d1c247b..0476e9172e3 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -9,11 +9,12 @@ import ( "sort" "strconv" + "github.com/grafana/grafana/pkg/services/datasources/permissions" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/datasource" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/adapters" @@ -577,7 +578,7 @@ func (hs *HTTPServer) filterDatasourcesByQueryPermission(ctx context.Context, us query.Result = datasources if err := hs.DatasourcePermissionsService.FilterDatasourcesBasedOnQueryPermissions(ctx, &query); err != nil { - if !errors.Is(err, bus.ErrHandlerNotFound) { + if !errors.Is(err, permissions.ErrNotImplemented) { return nil, err } return datasources, nil diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index f8af2b8f858..3238b5b60ac 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -212,9 +212,10 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json } if err := e.datasourcePermissionsService.FilterDatasourcesBasedOnQueryPermissions(ctx, &dsFilterQuery); err != nil { - return nil, err - } - if len(dsFilterQuery.Result) == 0 { + if !errors.Is(err, permissions.ErrNotImplemented) { + return nil, err + } + } else if len(dsFilterQuery.Result) == 0 { return nil, models.ErrDataSourceAccessDenied } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 6b8ea7861a6..90c7499d9e3 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -2,6 +2,7 @@ package alerting import ( "context" + "errors" "io/ioutil" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -295,6 +297,69 @@ func TestAlertRuleExtraction(t *testing.T) { }) } +func TestFilterPermissionsErrors(t *testing.T) { + RegisterCondition("query", func(model *simplejson.Json, index int) (Condition, error) { + return &FakeCondition{}, nil + }) + + // mock data + defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true, Uid: "def-uid"} + + json, err := ioutil.ReadFile("./testdata/graphite-alert.json") + require.Nil(t, err) + dashJSON, err := simplejson.NewJson(json) + require.Nil(t, err) + + dsPermissions := permissions.NewMockDatasourcePermissionService() + dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs} + extractor := ProvideDashAlertExtractorService(dsPermissions, dsService) + + tc := []struct { + name string + result []*models.DataSource + err error + expectedErr error + }{ + { + "Data sources are filtered and return results don't return an error", + []*models.DataSource{defaultDs}, + nil, + nil, + }, + { + "Data sources are filtered but return empty results should return error", + nil, + nil, + models.ErrDataSourceAccessDenied, + }, + { + "Using default OSS implementation doesn't return an error", + nil, + permissions.ErrNotImplemented, + nil, + }, + { + "Returning an error different from ErrNotImplemented should fails", + nil, + errors.New("random error"), + errors.New("random error"), + }, + } + + for _, test := range tc { + t.Run(test.name, func(t *testing.T) { + dsPermissions.DsResult = test.result + dsPermissions.ErrResult = test.err + _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ + User: nil, + Dash: models.NewDashboardFromJson(dashJSON), + OrgID: 1, + }) + assert.Equal(t, err, test.expectedErr) + }) + } +} + type fakeDatasourceService struct { ExpectedDatasource *models.DataSource datasources.DataSourceService diff --git a/pkg/services/datasources/permissions/datasource_permissions.go b/pkg/services/datasources/permissions/datasource_permissions.go index 9dee1b2d0b5..906ef83713d 100644 --- a/pkg/services/datasources/permissions/datasource_permissions.go +++ b/pkg/services/datasources/permissions/datasource_permissions.go @@ -2,17 +2,20 @@ package permissions import ( "context" + "errors" "github.com/grafana/grafana/pkg/models" ) +var ErrNotImplemented = errors.New("not implemented") + type DatasourcePermissionsService interface { FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error } // dummy method func (hs *OSSDatasourcePermissionsService) FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error { - return nil + return ErrNotImplemented } type OSSDatasourcePermissionsService struct{} diff --git a/pkg/services/datasources/permissions/datasource_permissions_mocks.go b/pkg/services/datasources/permissions/datasource_permissions_mocks.go index 767fdda5e5d..69896c8398f 100644 --- a/pkg/services/datasources/permissions/datasource_permissions_mocks.go +++ b/pkg/services/datasources/permissions/datasource_permissions_mocks.go @@ -7,12 +7,13 @@ import ( ) type mockDatasourcePermissionService struct { - DsResult []*models.DataSource + DsResult []*models.DataSource + ErrResult error } func (m *mockDatasourcePermissionService) FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error { cmd.Result = m.DsResult - return nil + return m.ErrResult } func NewMockDatasourcePermissionService() *mockDatasourcePermissionService { From 2e6f14d17f832cdeddbbc4f63fa0f388f877a6d9 Mon Sep 17 00:00:00 2001 From: Selene Date: Wed, 2 Mar 2022 11:05:31 +0100 Subject: [PATCH 097/125] Bus: Tests cleanup (#46025) * Delete unused bus from usagestats * Few updates to remove bus from searchusers test * Fix import --- pkg/api/common_test.go | 12 ++- pkg/api/user_test.go | 88 ++++++-------------- pkg/infra/usagestats/service/service.go | 3 +- pkg/services/searchusers/searchusers.go | 10 +-- pkg/services/sqlstore/mockstore/mockstore.go | 6 ++ pkg/services/sqlstore/store.go | 1 + 6 files changed, 45 insertions(+), 75 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 993b5aa8e0f..7568fa0b75c 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -229,16 +229,16 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin cfg.IsFeatureToggleEnabled = features.IsEnabled cfg.Quota.Enabled = false - bus := bus.GetBus() + mockStore := sqlstore.InitTestDB(t) hs := &HTTPServer{ Cfg: cfg, - Bus: bus, + Bus: bus.GetBus(), Live: newTestLive(t), Features: features, QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routing.NewRouteRegister(), AccessControl: accesscontrolmock.New().WithPermissions(permissions), - searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()), + searchUsersService: searchusers.ProvideUsersService(mockStore, filters.ProvideOSSSearchUserFilter()), ldapGroups: ldap.ProvideGroupsService(), } @@ -349,8 +349,6 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont db := sqlstore.InitTestDB(t) db.Cfg = cfg - bus := bus.GetBus() - dashboardsStore := dashboardsstore.ProvideDashboardStore(db) routeRegister := routing.NewRouteRegister() @@ -358,12 +356,12 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont hs := &HTTPServer{ Cfg: cfg, Features: features, - Bus: bus, + Bus: bus.GetBus(), Live: newTestLive(t), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routeRegister, SQLStore: db, - searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()), + searchUsersService: searchusers.ProvideUsersService(db, filters.ProvideOSSSearchUserFilter()), dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore, nil), } diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index e52fcf09778..82bc8848827 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -148,92 +148,58 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { }, mock) loggedInUserScenario(t, "When calling GET on", "/api/users", "/api/users", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(ctx context.Context, query *models.SearchUsersQuery) error { - query.Result = mockResult + mock.ExpectedSearchUsers = mockResult - sentLimit = query.Limit - sendPage = query.Page - - return nil - }) - - searchUsersService := searchusers.ProvideUsersService(bus.GetBus(), filters.ProvideOSSSearchUserFilter()) + searchUsersService := searchusers.ProvideUsersService(mock, filters.ProvideOSSSearchUserFilter()) sc.handlerFunc = searchUsersService.SearchUsers sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - assert.Equal(t, 1000, sentLimit) - assert.Equal(t, 1, sendPage) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) + assert.Equal(t, 2, len(respJSON.MustArray())) }, mock) loggedInUserScenario(t, "When calling GET with page and limit querystring parameters on", "/api/users", "/api/users", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(ctx context.Context, query *models.SearchUsersQuery) error { - query.Result = mockResult + mock.ExpectedSearchUsers = mockResult - sentLimit = query.Limit - sendPage = query.Page - - return nil - }) - - searchUsersService := searchusers.ProvideUsersService(bus.GetBus(), filters.ProvideOSSSearchUserFilter()) + searchUsersService := searchusers.ProvideUsersService(mock, filters.ProvideOSSSearchUserFilter()) sc.handlerFunc = searchUsersService.SearchUsers sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - assert.Equal(t, 10, sentLimit) - assert.Equal(t, 2, sendPage) - }, mock) - - loggedInUserScenario(t, "When calling GET on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(ctx context.Context, query *models.SearchUsersQuery) error { - query.Result = mockResult - - sentLimit = query.Limit - sendPage = query.Page - - return nil - }) - - searchUsersService := searchusers.ProvideUsersService(bus.GetBus(), filters.ProvideOSSSearchUserFilter()) - sc.handlerFunc = searchUsersService.SearchUsersWithPaging - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - - assert.Equal(t, 1000, sentLimit) - assert.Equal(t, 1, sendPage) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) + assert.Equal(t, 2, len(respJSON.MustArray())) + }, mock) + + loggedInUserScenario(t, "When calling GET on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) { + mock.ExpectedSearchUsers = mockResult + + searchUsersService := searchusers.ProvideUsersService(mock, filters.ProvideOSSSearchUserFilter()) + sc.handlerFunc = searchUsersService.SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.Equal(t, 1, respJSON.Get("page").MustInt()) + assert.Equal(t, 1000, respJSON.Get("perPage").MustInt()) assert.Equal(t, 2, respJSON.Get("totalCount").MustInt()) assert.Equal(t, 2, len(respJSON.Get("users").MustArray())) }, mock) loggedInUserScenario(t, "When calling GET with page and perpage querystring parameters on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(ctx context.Context, query *models.SearchUsersQuery) error { - query.Result = mockResult + mock.ExpectedSearchUsers = mockResult - sentLimit = query.Limit - sendPage = query.Page - - return nil - }) - - searchUsersService := searchusers.ProvideUsersService(bus.GetBus(), filters.ProvideOSSSearchUserFilter()) + searchUsersService := searchusers.ProvideUsersService(mock, filters.ProvideOSSSearchUserFilter()) sc.handlerFunc = searchUsersService.SearchUsersWithPaging sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - assert.Equal(t, 10, sentLimit) - assert.Equal(t, 2, sendPage) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.Equal(t, 2, respJSON.Get("page").MustInt()) + assert.Equal(t, 10, respJSON.Get("perPage").MustInt()) }, mock) } diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 4f79d07d746..ceb8a321fbc 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -6,7 +6,6 @@ import ( "time" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -33,7 +32,7 @@ type UsageStats struct { sendReportCallbacks []usagestats.SendReportCallbackFunc } -func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store, +func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store, socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister, ) *UsageStats { s := &UsageStats{ diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index c6fbfbc9402..79dc1b59196 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -3,8 +3,8 @@ package searchusers import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" ) type Service interface { @@ -13,12 +13,12 @@ type Service interface { } type OSSService struct { - bus bus.Bus + sqlStore sqlstore.Store searchUserFilter models.SearchUserFilter } -func ProvideUsersService(bus bus.Bus, searchUserFilter models.SearchUserFilter) *OSSService { - return &OSSService{bus: bus, searchUserFilter: searchUserFilter} +func ProvideUsersService(sqlStore sqlstore.Store, searchUserFilter models.SearchUserFilter) *OSSService { + return &OSSService{sqlStore: sqlStore, searchUserFilter: searchUserFilter} } func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { @@ -60,7 +60,7 @@ func (s *OSSService) SearchUser(c *models.ReqContext) (*models.SearchUsersQuery, } query := &models.SearchUsersQuery{Query: searchQuery, Filters: filters, Page: page, Limit: perPage} - if err := s.bus.Dispatch(c.Req.Context(), query); err != nil { + if err := s.sqlStore.SearchUsers(c.Req.Context(), query); err != nil { return nil, err } diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 33d5c5f45bc..2490035c0df 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -30,6 +30,7 @@ type SQLStoreMock struct { ExpectedDashboardSnapshot *models.DashboardSnapshot ExpectedTeamsByUser []*models.TeamDTO ExpectedSearchOrgList []*models.OrgDTO + ExpectedSearchUsers models.SearchUserQueryResult ExpectedDatasources []*models.DataSource ExpectedOrg *models.Org ExpectedSystemStats *models.SystemStats @@ -193,6 +194,11 @@ func (m *SQLStoreMock) GetSignedInUser(ctx context.Context, query *models.GetSig return m.ExpectedError } +func (m *SQLStoreMock) SearchUsers(ctx context.Context, query *models.SearchUsersQuery) error { + query.Result = m.ExpectedSearchUsers + return m.ExpectedError +} + func (m *SQLStoreMock) DisableUser(ctx context.Context, cmd *models.DisableUserCommand) error { m.LatestUserId = cmd.UserId return m.ExpectedError diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 82c6f104820..2e2dde3e988 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -42,6 +42,7 @@ type Store interface { GetUserOrgList(ctx context.Context, query *models.GetUserOrgListQuery) error GetSignedInUserWithCacheCtx(ctx context.Context, query *models.GetSignedInUserQuery) error GetSignedInUser(ctx context.Context, query *models.GetSignedInUserQuery) error + SearchUsers(ctx context.Context, query *models.SearchUsersQuery) error DisableUser(ctx context.Context, cmd *models.DisableUserCommand) error BatchDisableUsers(ctx context.Context, cmd *models.BatchDisableUsersCommand) error DeleteUser(ctx context.Context, cmd *models.DeleteUserCommand) error From 2f6c827f5d96e620fc2b20e1503512bafeb43de0 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 2 Mar 2022 13:02:07 +0200 Subject: [PATCH 098/125] CI: Introduce `build-frontend-packages` step (#45824) * Split frontend build * Fix command name * Update grabpl --- .drone.yml | 143 ++++++++++++++++++++------- scripts/drone/pipelines/docs.star | 1 - scripts/drone/pipelines/main.star | 2 + scripts/drone/pipelines/pr.star | 2 + scripts/drone/pipelines/release.star | 2 + scripts/drone/steps/lib.star | 35 ++++++- 6 files changed, 148 insertions(+), 37 deletions(-) diff --git a/.drone.yml b/.drone.yml index 90cdb8819c8..3e3ca330122 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -114,7 +114,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -145,6 +145,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + --no-pull-enterprise + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss depends_on: @@ -178,6 +187,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages environment: null image: grafana/build-container:1.5.1 name: package @@ -187,6 +197,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -266,6 +277,7 @@ steps: - ./bin/grabpl verify-storybook depends_on: - build-frontend + - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.5.1 @@ -346,7 +358,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -410,7 +422,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -466,7 +478,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -519,7 +531,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -620,7 +632,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -662,6 +674,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + --no-pull-enterprise + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: @@ -697,6 +718,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages environment: GITHUB_TOKEN: from_secret: github_token @@ -716,6 +738,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -795,6 +818,7 @@ steps: - ./bin/grabpl verify-storybook depends_on: - build-frontend + - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.5.1 @@ -991,7 +1015,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1059,7 +1083,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1142,7 +1166,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1224,7 +1248,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1258,6 +1282,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} + --edition oss --no-pull-enterprise ${DRONE_TAG} + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: @@ -1293,6 +1326,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages environment: GITHUB_TOKEN: from_secret: github_token @@ -1343,6 +1377,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -1422,6 +1457,7 @@ steps: - ./bin/grabpl verify-storybook depends_on: - build-frontend + - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.5.1 @@ -1520,7 +1556,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1641,7 +1677,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1725,7 +1761,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1784,7 +1820,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1840,6 +1876,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} + --edition enterprise --no-pull-enterprise ${DRONE_TAG} + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin depends_on: @@ -1885,6 +1930,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages - build-backend-enterprise2 environment: GITHUB_TOKEN: @@ -1936,6 +1982,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -2041,6 +2088,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages - build-backend-enterprise2 environment: GITHUB_TOKEN: @@ -2115,7 +2163,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2288,7 +2336,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2416,7 +2464,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2491,7 +2539,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2569,7 +2617,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2630,7 +2678,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2709,7 +2757,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2771,7 +2819,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2807,7 +2855,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2854,7 +2902,7 @@ steps: name: initialize - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2902,7 +2950,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2965,7 +3013,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2996,6 +3044,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + --no-pull-enterprise + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: @@ -3031,6 +3088,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages environment: GITHUB_TOKEN: from_secret: github_token @@ -3081,6 +3139,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -3160,6 +3219,7 @@ steps: - ./bin/grabpl verify-storybook depends_on: - build-frontend + - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.5.1 @@ -3216,7 +3276,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3330,7 +3390,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3407,7 +3467,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -3455,7 +3515,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3505,6 +3565,15 @@ steps: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.1 name: build-frontend +- commands: + - ./bin/grabpl build-frontend-packages --jobs 8 --edition enterprise --build-id + ${DRONE_BUILD_NUMBER} --no-pull-enterprise + depends_on: + - initialize + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: grafana/build-container:1.5.1 + name: build-frontend-packages - commands: - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin depends_on: @@ -3548,6 +3617,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages - build-backend-enterprise2 environment: GITHUB_TOKEN: @@ -3599,6 +3669,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages detach: true environment: ARCH: linux-amd64 @@ -3679,6 +3750,7 @@ steps: - ./bin/grabpl verify-storybook depends_on: - build-frontend + - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 image: grafana/build-container:1.5.1 @@ -3712,6 +3784,7 @@ steps: - build-plugins - build-backend - build-frontend + - build-frontend-packages - build-backend-enterprise2 environment: GITHUB_TOKEN: @@ -3779,7 +3852,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3942,7 +4015,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4060,7 +4133,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.5/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.7/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4257,6 +4330,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: fb2a26bf088c9ff2b7cc63cee4fa1f1da06baf560265df4703414f4db9c90708 +hmac: c2da781acf9dc4a0178c13a5d9029d9ef3af96a6171ceaafc3c35c59dcfd1ac3 ... diff --git a/scripts/drone/pipelines/docs.star b/scripts/drone/pipelines/docs.star index be19f140c73..f87023444a8 100644 --- a/scripts/drone/pipelines/docs.star +++ b/scripts/drone/pipelines/docs.star @@ -5,7 +5,6 @@ load( 'lint_frontend_step', 'codespell_step', 'shellcheck_step', - 'build_frontend_step', 'test_frontend_step', 'build_storybook_step', 'build_frontend_docs_step', diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index 9220d81e366..5821a2f0c7b 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -12,6 +12,7 @@ load( 'test_frontend_step', 'build_backend_step', 'build_frontend_step', + 'build_frontend_package_step', 'build_plugins_step', 'package_step', 'grafana_server_step', @@ -79,6 +80,7 @@ def get_steps(edition, is_downstream=False): enterprise_downstream_step(edition=edition), build_backend_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), build_frontend_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), + build_frontend_package_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), build_plugins_step(edition=edition, sign=True), validate_scuemata_step(), ensure_cuetsified_step(), diff --git a/scripts/drone/pipelines/pr.star b/scripts/drone/pipelines/pr.star index f60e3332fc0..9515ce83fed 100644 --- a/scripts/drone/pipelines/pr.star +++ b/scripts/drone/pipelines/pr.star @@ -9,6 +9,7 @@ load( 'shellcheck_step', 'build_backend_step', 'build_frontend_step', + 'build_frontend_package_step', 'build_plugins_step', 'test_backend_step', 'test_backend_integration_step', @@ -71,6 +72,7 @@ def pr_pipelines(edition): build_steps = [ build_backend_step(edition=edition, ver_mode=ver_mode, variants=variants), build_frontend_step(edition=edition, ver_mode=ver_mode), + build_frontend_package_step(edition=edition, ver_mode=ver_mode), build_plugins_step(edition=edition), validate_scuemata_step(), ensure_cuetsified_step(), diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 237f903e005..7759af67833 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -16,6 +16,7 @@ load( 'test_frontend_step', 'build_backend_step', 'build_frontend_step', + 'build_frontend_package_step', 'build_plugins_step', 'package_step', 'grafana_server_step', @@ -179,6 +180,7 @@ def get_steps(edition, ver_mode): build_steps = [ build_backend_step(edition=edition, ver_mode=ver_mode), build_frontend_step(edition=edition, ver_mode=ver_mode), + build_frontend_package_step(edition=edition, ver_mode=ver_mode), build_plugins_step(edition=edition, sign=True), validate_scuemata_step(), ensure_cuetsified_step(), diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 5ca6fb1ae63..35a7cc97a6c 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.9.5' +grabpl_version = 'v2.9.7' build_image = 'grafana/build-container:1.5.1' publish_image = 'grafana/grafana-ci-deploy:1.3.1' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' @@ -241,6 +241,7 @@ def build_storybook_step(edition, ver_mode): 'depends_on': [ # Best to ensure that this step doesn't mess with what's getting built and packaged 'build-frontend', + 'build-frontend-packages', ], 'environment': { 'NODE_OPTIONS': '--max_old_space_size=4096', @@ -421,6 +422,36 @@ def build_frontend_step(edition, ver_mode, is_downstream=False): 'commands': cmds, } +def build_frontend_package_step(edition, ver_mode, is_downstream=False): + if not is_downstream: + build_no = '${DRONE_BUILD_NUMBER}' + else: + build_no = '$${SOURCE_BUILD_NUMBER}' + + # TODO: Use percentage for num jobs + if ver_mode == 'release': + cmds = [ + './bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} ' + \ + '--edition {} --no-pull-enterprise ${{DRONE_TAG}}'.format(edition), + ] + else: + cmds = [ + './bin/grabpl build-frontend-packages --jobs 8 --edition {} '.format(edition) + \ + '--build-id {} --no-pull-enterprise'.format(build_no), + ] + + return { + 'name': 'build-frontend-packages', + 'image': build_image, + 'depends_on': [ + 'initialize', + ], + 'environment': { + 'NODE_OPTIONS': '--max_old_space_size=8192', + }, + 'commands': cmds, + } + def build_frontend_docs_step(edition): return { @@ -607,6 +638,7 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None, is 'build-plugins', 'build-backend', 'build-frontend', + 'build-frontend-packages', ] if include_enterprise2: sfx = '-enterprise2' @@ -682,6 +714,7 @@ def grafana_server_step(edition, port=3001): 'build-plugins', 'build-backend', 'build-frontend', + 'build-frontend-packages', ], 'environment': environment, 'commands': [ From bc26d42980dae148a24b687170af8a2a3922abf2 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 2 Mar 2022 11:57:17 +0000 Subject: [PATCH 099/125] Update stale.yml (#46082) --- .github/workflows/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ab2a547c8af..74c0bfaa1d7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,6 +19,7 @@ jobs: days-before-issue-stale: -1 exempt-issue-labels: no stalebot exempt-pr-labels: no stalebot + operations-per-run: 100 stale-issue-label: stale stale-pr-label: stale stale-pr-message: > From aeec08706584e24bfc7ad6776b6a5ecfc8968f29 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 2 Mar 2022 13:06:35 +0100 Subject: [PATCH 100/125] Alerting: Fix silence url in notifications (#46031) * Update silence url generation * Update tests * Update test to the new silence params format * Fix tests --- .../channels/default_template_test.go | 16 ++++----- .../notifier/channels/dingding_test.go | 2 +- .../ngalert/notifier/channels/discord_test.go | 4 +-- .../ngalert/notifier/channels/email_test.go | 2 +- .../notifier/channels/googlechat_test.go | 4 +-- .../ngalert/notifier/channels/kafka_test.go | 4 +-- .../ngalert/notifier/channels/line_test.go | 4 +-- .../notifier/channels/opsgenie_test.go | 8 ++--- .../notifier/channels/pagerduty_test.go | 4 +-- .../notifier/channels/pushover_test.go | 2 +- .../ngalert/notifier/channels/sensugo_test.go | 2 +- .../ngalert/notifier/channels/slack_test.go | 8 ++--- .../ngalert/notifier/channels/teams_test.go | 2 +- .../notifier/channels/telegram_test.go | 4 +-- .../notifier/channels/template_data.go | 10 +++++- .../ngalert/notifier/channels/threema_test.go | 4 +-- .../notifier/channels/victorops_test.go | 4 +-- .../ngalert/notifier/channels/webhook_test.go | 10 +++--- .../ngalert/notifier/channels/wecom_test.go | 2 +- .../alerting/api_notification_channel_test.go | 34 +++++++++---------- 20 files changed, 69 insertions(+), 61 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/default_template_test.go b/pkg/services/ngalert/notifier/channels/default_template_test.go index 4e61ac59015..e2eee8533e0 100644 --- a/pkg/services/ngalert/notifier/channels/default_template_test.go +++ b/pkg/services/ngalert/notifier/channels/default_template_test.go @@ -99,7 +99,7 @@ Labels: Annotations: - ann1 = annv1 Source: http://localhost/alert1 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1 Dashboard: http://localhost/grafana/d/dbuid123 Panel: http://localhost/grafana/d/dbuid123?viewPanel=puid123 @@ -110,7 +110,7 @@ Labels: Annotations: - ann1 = annv2 Source: http://localhost/alert2 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2 **Resolved** @@ -122,7 +122,7 @@ Labels: Annotations: - ann1 = annv3 Source: http://localhost/alert3 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3 Dashboard: http://localhost/grafana/d/dbuid456 Panel: http://localhost/grafana/d/dbuid456?viewPanel=puid456 @@ -133,7 +133,7 @@ Labels: Annotations: - ann1 = annv4 Source: http://localhost/alert4 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4 `, }, { @@ -150,7 +150,7 @@ Annotations: Source: http://localhost/alert1 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1 Dashboard: http://localhost/grafana/d/dbuid123 @@ -168,7 +168,7 @@ Annotations: Source: http://localhost/alert2 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2 @@ -185,7 +185,7 @@ Annotations: Source: http://localhost/alert3 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval3 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval3 Dashboard: http://localhost/grafana/d/dbuid456 @@ -203,7 +203,7 @@ Annotations: Source: http://localhost/alert4 -Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval4 +Silence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval4 `, diff --git a/pkg/services/ngalert/notifier/channels/dingding_test.go b/pkg/services/ngalert/notifier/channels/dingding_test.go index a868eca502e..64df838fdcf 100644 --- a/pkg/services/ngalert/notifier/channels/dingding_test.go +++ b/pkg/services/ngalert/notifier/channels/dingding_test.go @@ -44,7 +44,7 @@ func TestDingdingNotifier(t *testing.T) { "msgtype": "link", "link": map[string]interface{}{ "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%2Falerting%2Flist", - "text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: 1234\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "title": "[FIRING:1] (val1)", }, }, diff --git a/pkg/services/ngalert/notifier/channels/discord_test.go b/pkg/services/ngalert/notifier/channels/discord_test.go index 51a89589e64..95454a1f79e 100644 --- a/pkg/services/ngalert/notifier/channels/discord_test.go +++ b/pkg/services/ngalert/notifier/channels/discord_test.go @@ -43,7 +43,7 @@ func TestDiscordNotifier(t *testing.T) { }, }, expMsg: map[string]interface{}{ - "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "embeds": []interface{}{map[string]interface{}{ "color": 1.4037554e+07, "footer": map[string]interface{}{ @@ -123,7 +123,7 @@ func TestDiscordNotifier(t *testing.T) { }, }, expMsg: map[string]interface{}{ - "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "content": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "embeds": []interface{}{map[string]interface{}{ "color": 1.4037554e+07, "footer": map[string]interface{}{ diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 7e6da5adb7e..9cd438cfd66 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -90,7 +90,7 @@ func TestEmailNotifier(t *testing.T) { Labels: template.KV{"alertname": "AlwaysFiring", "severity": "warning"}, Annotations: template.KV{"runbook_url": "http://fix.me"}, Fingerprint: "15a37193dce72bab", - SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DAlwaysFiring%2Cseverity%3Dwarning", + SilenceURL: "http://localhost/base/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DAlwaysFiring&matcher=severity%3Dwarning", DashboardURL: "http://localhost/base/d/abc", PanelURL: "http://localhost/base/d/abc?viewPanel=5", }, diff --git a/pkg/services/ngalert/notifier/channels/googlechat_test.go b/pkg/services/ngalert/notifier/channels/googlechat_test.go index 15a70034dfe..527d77be16f 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat_test.go +++ b/pkg/services/ngalert/notifier/channels/googlechat_test.go @@ -58,7 +58,7 @@ func TestGoogleChatNotifier(t *testing.T) { Widgets: []widget{ textParagraphWidget{ Text: text{ - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, }, buttonWidget{ @@ -117,7 +117,7 @@ func TestGoogleChatNotifier(t *testing.T) { Widgets: []widget{ textParagraphWidget{ Text: text{ - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, }, buttonWidget{ diff --git a/pkg/services/ngalert/notifier/channels/kafka_test.go b/pkg/services/ngalert/notifier/channels/kafka_test.go index 9e30bfebdd1..34bf944e592 100644 --- a/pkg/services/ngalert/notifier/channels/kafka_test.go +++ b/pkg/services/ngalert/notifier/channels/kafka_test.go @@ -51,7 +51,7 @@ func TestKafkaNotifier(t *testing.T) { "client": "Grafana", "client_url": "http://localhost/alerting/list", "description": "[FIRING:1] (val1)", - "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733" } } @@ -86,7 +86,7 @@ func TestKafkaNotifier(t *testing.T) { "client": "Grafana", "client_url": "http://localhost/alerting/list", "description": "[FIRING:2] ", - "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "details": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "incident_key": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733" } } diff --git a/pkg/services/ngalert/notifier/channels/line_test.go b/pkg/services/ngalert/notifier/channels/line_test.go index 7d6a50cfd81..467e6bf3621 100644 --- a/pkg/services/ngalert/notifier/channels/line_test.go +++ b/pkg/services/ngalert/notifier/channels/line_test.go @@ -46,7 +46,7 @@ func TestLineNotifier(t *testing.T) { "Authorization": "Bearer sometoken", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, - expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A", + expMsg: "message=%5BFIRING%3A1%5D++%28val1%29%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A", expMsgError: nil, }, { name: "Multiple alerts", @@ -68,7 +68,7 @@ func TestLineNotifier(t *testing.T) { "Authorization": "Bearer sometoken", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, - expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A", + expMsg: "message=%5BFIRING%3A2%5D++%0Ahttp%3A%2Flocalhost%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A", expMsgError: nil, }, { name: "Token missing", diff --git a/pkg/services/ngalert/notifier/channels/opsgenie_test.go b/pkg/services/ngalert/notifier/channels/opsgenie_test.go index c45ad66ff6b..5dbb8db481a 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie_test.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie_test.go @@ -44,7 +44,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "details": { "url": "http://localhost/alerting/list" }, @@ -69,7 +69,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", "details": { "url": "http://localhost/alerting/list" }, @@ -94,7 +94,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", "details": { "alertname": "alert1", "lbl1": "val1", @@ -126,7 +126,7 @@ func TestOpsgenieNotifier(t *testing.T) { }, expMsg: `{ "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", - "description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "details": { "alertname": "alert1", "url": "http://localhost/alerting/list" diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 3703b83910f..44e1e3bffc6 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -59,7 +59,7 @@ func TestPagerdutyNotifier(t *testing.T) { Component: "Grafana", Group: "default", CustomDetails: map[string]string{ - "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "num_firing": "1", "num_resolved": "0", "resolved": "", @@ -105,7 +105,7 @@ func TestPagerdutyNotifier(t *testing.T) { Component: "My Grafana", Group: "my_group", CustomDetails: map[string]string{ - "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", "num_firing": "2", "num_resolved": "0", "resolved": "", diff --git a/pkg/services/ngalert/notifier/channels/pushover_test.go b/pkg/services/ngalert/notifier/channels/pushover_test.go index 2b81a43079d..b9543720ff2 100644 --- a/pkg/services/ngalert/notifier/channels/pushover_test.go +++ b/pkg/services/ngalert/notifier/channels/pushover_test.go @@ -59,7 +59,7 @@ func TestPushoverNotifier(t *testing.T) { "title": "[FIRING:1] (val1)", "url": "http://localhost/alerting/list", "url_title": "Show alert rule", - "message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "html": "1", }, expMsgError: nil, diff --git a/pkg/services/ngalert/notifier/channels/sensugo_test.go b/pkg/services/ngalert/notifier/channels/sensugo_test.go index 2277f09aed5..271265c1d30 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo_test.go +++ b/pkg/services/ngalert/notifier/channels/sensugo_test.go @@ -60,7 +60,7 @@ func TestSensuGoNotifier(t *testing.T) { "ruleURL": "http://localhost/alerting/list", }, }, - "output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "output": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", "issued": timeNow().Unix(), "interval": 86400, "status": 2, diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index c30b87bf64b..62419671619 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -59,7 +59,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -94,7 +94,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -136,7 +136,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "2 firing, 0 resolved", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", Fallback: "2 firing, 0 resolved", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, @@ -184,7 +184,7 @@ func TestSlackNotifier(t *testing.T) { { Title: "[FIRING:1] (val1)", TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n", Fallback: "[FIRING:1] (val1)", Fields: nil, Footer: "Grafana v" + setting.BuildVersion, diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index 92bf8e15fdd..7f743f6fe91 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -49,7 +49,7 @@ func TestTeamsNotifier(t *testing.T) { "sections": []map[string]interface{}{ { "title": "Details", - "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, }, "potentialAction": []map[string]interface{}{ diff --git a/pkg/services/ngalert/notifier/channels/telegram_test.go b/pkg/services/ngalert/notifier/channels/telegram_test.go index 1e7d5f24a20..c3d7c9ba921 100644 --- a/pkg/services/ngalert/notifier/channels/telegram_test.go +++ b/pkg/services/ngalert/notifier/channels/telegram_test.go @@ -48,7 +48,7 @@ func TestTelegramNotifier(t *testing.T) { expMsg: map[string]string{ "chat_id": "someid", "parse_mode": "html", - "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, expMsgError: nil, }, { @@ -75,7 +75,7 @@ func TestTelegramNotifier(t *testing.T) { expMsg: map[string]string{ "chat_id": "someid", "parse_mode": "html", - "text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "text": "__Custom Firing__\n2 Firing\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, expMsgError: nil, }, { diff --git a/pkg/services/ngalert/notifier/channels/template_data.go b/pkg/services/ngalert/notifier/channels/template_data.go index 1533866b90a..10cd9c6bc47 100644 --- a/pkg/services/ngalert/notifier/channels/template_data.go +++ b/pkg/services/ngalert/notifier/channels/template_data.go @@ -99,7 +99,15 @@ func extendAlert(alert template.Alert, externalURL string, logger log.Logger) *E } sort.Strings(matchers) u.Path = path.Join(externalPath, "/alerting/silence/new") - u.RawQuery = "alertmanager=grafana&matchers=" + url.QueryEscape(strings.Join(matchers, ",")) + + query := make(url.Values) + query.Add("alertmanager", "grafana") + for _, matcher := range matchers { + query.Add("matcher", matcher) + } + + u.RawQuery = query.Encode() + extended.SilenceURL = u.String() return extended diff --git a/pkg/services/ngalert/notifier/channels/threema_test.go b/pkg/services/ngalert/notifier/channels/threema_test.go index 01e4f530411..47364e677f2 100644 --- a/pkg/services/ngalert/notifier/channels/threema_test.go +++ b/pkg/services/ngalert/notifier/channels/threema_test.go @@ -45,7 +45,7 @@ func TestThreemaNotifier(t *testing.T) { }, }, }, - expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", + expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D++%28val1%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0ADashboard%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%0APanel%3A+http%3A%2F%2Flocalhost%2Fd%2Fabcd%3FviewPanel%3Defgh%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", expMsgError: nil, }, { name: "Multiple alerts", @@ -67,7 +67,7 @@ func TestThreemaNotifier(t *testing.T) { }, }, }, - expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253Dalert1%252Clbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", + expMsg: "from=%2A1234567&secret=supersecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A2%5D++%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val1%0AAnnotations%3A%0A+-+ann1+%3D+annv1%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval1%0A%0AValue%3A+%5Bno+value%5D%0ALabels%3A%0A+-+alertname+%3D+alert1%0A+-+lbl1+%3D+val2%0AAnnotations%3A%0A+-+ann1+%3D+annv2%0ASilence%3A+http%3A%2F%2Flocalhost%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253Dalert1%26matcher%3Dlbl1%253Dval2%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%2Falerting%2Flist%0A&to=87654321", expMsgError: nil, }, { name: "Invalid gateway id", diff --git a/pkg/services/ngalert/notifier/channels/victorops_test.go b/pkg/services/ngalert/notifier/channels/victorops_test.go index 2db53055964..3f11e25429b 100644 --- a/pkg/services/ngalert/notifier/channels/victorops_test.go +++ b/pkg/services/ngalert/notifier/channels/victorops_test.go @@ -47,7 +47,7 @@ func TestVictoropsNotifier(t *testing.T) { "entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", "message_type": "CRITICAL", "monitoring_tool": "Grafana v" + setting.BuildVersion, - "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", }, expMsgError: nil, }, { @@ -72,7 +72,7 @@ func TestVictoropsNotifier(t *testing.T) { "entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", "message_type": "CRITICAL", "monitoring_tool": "Grafana v" + setting.BuildVersion, - "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + "state_message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", }, expMsgError: nil, }, { diff --git a/pkg/services/ngalert/notifier/channels/webhook_test.go b/pkg/services/ngalert/notifier/channels/webhook_test.go index e043abc4757..97de78fe33c 100644 --- a/pkg/services/ngalert/notifier/channels/webhook_test.go +++ b/pkg/services/ngalert/notifier/channels/webhook_test.go @@ -68,7 +68,7 @@ func TestWebhookNotifier(t *testing.T) { Fingerprint: "fac0861a85de433a", DashboardURL: "http://localhost/d/abcd", PanelURL: "http://localhost/d/abcd?viewPanel=efgh", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1", }, }, GroupLabels: template.KV{ @@ -87,7 +87,7 @@ func TestWebhookNotifier(t *testing.T) { GroupKey: "alertname", Title: "[FIRING:1] (val1)", State: "alerting", - Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", OrgID: orgID, }, expMsgError: nil, @@ -137,7 +137,7 @@ func TestWebhookNotifier(t *testing.T) { "ann1": "annv1", }, Fingerprint: "fac0861a85de433a", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1", }, { Status: "firing", Labels: template.KV{ @@ -148,7 +148,7 @@ func TestWebhookNotifier(t *testing.T) { "ann1": "annv2", }, Fingerprint: "fab6861a85d5eeb5", - SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2", + SilenceURL: "http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2", }, }, GroupLabels: template.KV{ @@ -165,7 +165,7 @@ func TestWebhookNotifier(t *testing.T) { TruncatedAlerts: 1, Title: "[FIRING:2] ", State: "alerting", - Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval2\n", + Message: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n", OrgID: orgID, }, expMsgError: nil, diff --git a/pkg/services/ngalert/notifier/channels/wecom_test.go b/pkg/services/ngalert/notifier/channels/wecom_test.go index 46676e9b656..77b045107b9 100644 --- a/pkg/services/ngalert/notifier/channels/wecom_test.go +++ b/pkg/services/ngalert/notifier/channels/wecom_test.go @@ -45,7 +45,7 @@ func TestWeComNotifier(t *testing.T) { }, expMsg: map[string]interface{}{ "markdown": map[string]interface{}{ - "content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matchers=alertname%3Dalert1%2Clbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n", + "content": "# [FIRING:1] (val1)\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n\n", }, "msgtype": "markdown", }, diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 402add9ca2c..686de9827a3 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2099,7 +2099,7 @@ var expEmailNotifications = []*models.SendEmailCommandSync{ EndsAt: time.Time{}, GeneratorURL: "http://localhost:3000/alerting/UID_EmailAlert/edit", Fingerprint: "08c220aa26cd0cf5", - SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DEmailAlert", + SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DEmailAlert", DashboardURL: "", PanelURL: "", ValueString: "[ var='A' labels={} value=1 ]", @@ -2157,7 +2157,7 @@ var expNonEmailNotifications = map[string][]string{ { "title": "[FIRING:1] SlackAlert2 ", "title_link": "http://localhost:3000/alerting/list", - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSlackAlert2\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SlackAlert2\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SlackAlert2/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSlackAlert2\n", "fallback": "[FIRING:1] SlackAlert2 ", "footer": "Grafana v", "footer_icon": "https://grafana.com/assets/img/fav32.png", @@ -2190,7 +2190,7 @@ var expNonEmailNotifications = map[string][]string{ "component": "Integration Test", "group": "testgroup", "custom_details": { - "firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DPagerdutyAlert\n", + "firing": "\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PagerdutyAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PagerdutyAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DPagerdutyAlert\n", "num_firing": "1", "num_resolved": "0", "resolved": "" @@ -2210,7 +2210,7 @@ var expNonEmailNotifications = map[string][]string{ `{ "link": { "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%3A3000%2Falerting%2Flist", - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDingDingAlert\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DingDingAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DingDingAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDingDingAlert\n", "title": "[FIRING:1] DingDingAlert " }, "msgtype": "link" @@ -2235,7 +2235,7 @@ var expNonEmailNotifications = map[string][]string{ ], "sections": [ { - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTeamsAlert\n", + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TeamsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTeamsAlert\n", "title": "Details" } ], @@ -2261,7 +2261,7 @@ var expNonEmailNotifications = map[string][]string{ "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "http://localhost:3000/alerting/UID_WebhookAlert/edit", "fingerprint": "929467973978d053", - "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert", + "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert", "dashboardURL": "", "panelURL": "" } @@ -2279,12 +2279,12 @@ var expNonEmailNotifications = map[string][]string{ "truncatedAlerts": 0, "title": "[FIRING:1] WebhookAlert ", "state": "alerting", - "message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DWebhookAlert\n" + "message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = WebhookAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_WebhookAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert\n" }`, }, "discord_recv/discord_test": { `{ - "content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DDiscordAlert\n", + "content": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = DiscordAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_DiscordAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDiscordAlert\n", "embeds": [ { "color": 14037554, @@ -2312,7 +2312,7 @@ var expNonEmailNotifications = map[string][]string{ }, "name": "default" }, - "output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DSensuGoAlert\n", + "output": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = SensuGoAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_SensuGoAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSensuGoAlert\n", "status": 2 }, "entity": { @@ -2325,10 +2325,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "pushover_recv/pushover_test": { - "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert \r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = PushoverAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_PushoverAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DPushoverAlert\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", }, "telegram_recv/bot6sh027hs034h": { - "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DTelegramAlert\n\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TelegramAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_TelegramAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTelegramAlert\n\r\n--abcd--\r\n", }, "googlechat_recv/googlechat_test": { `{ @@ -2344,7 +2344,7 @@ var expNonEmailNotifications = map[string][]string{ "widgets": [ { "textParagraph": { - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DGoogleChatAlert\n" + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = GoogleChatAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_GoogleChatAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DGoogleChatAlert\n" } }, { @@ -2382,7 +2382,7 @@ var expNonEmailNotifications = map[string][]string{ "client": "Grafana", "client_url": "http://localhost:3000/alerting/list", "description": "[FIRING:1] KafkaAlert ", - "details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DKafkaAlert\n", + "details": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = KafkaAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_KafkaAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DKafkaAlert\n", "incident_key": "35c0bdb1715f9162a20d7b2a01cb2e3a4c5b1dc663571701e3f67212b696332f" } } @@ -2390,10 +2390,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "line_recv/line_test": { - `message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DLineAlert%0A`, + `message=%5BFIRING%3A1%5D+LineAlert+%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_LineAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DLineAlert%0A`, }, "threema_recv/threema_test": { - `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matchers%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, + `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+%5B+var%3D%27A%27+labels%3D%7B%7D+value%3D1+%5D%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2FUID_ThreemaAlert%2Fedit%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DThreemaAlert%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, }, "victorops_recv/victorops_test": { `{ @@ -2402,14 +2402,14 @@ var expNonEmailNotifications = map[string][]string{ "entity_id": "633ae988fa7074bcb51f3d1c5fef2ba1c5c4ccb45b3ecbf681f7d507b078b1ae", "message_type": "CRITICAL", "monitoring_tool": "Grafana v", - "state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%%3DVictorOpsAlert\n", + "state_message": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = VictorOpsAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_VictorOpsAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DVictorOpsAlert\n", "timestamp": %s }`, }, "opsgenie_recv/opsgenie_test": { `{ "alias": "47e92f0f6ef9fe99f3954e0d6155f8d09c4b9a038d8c3105e82c0cee4c62956e", - "description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matchers=alertname%3DOpsGenieAlert\n", + "description": "[FIRING:1] OpsGenieAlert \nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = OpsGenieAlert\nAnnotations:\nSource: http://localhost:3000/alerting/UID_OpsGenieAlert/edit\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DOpsGenieAlert\n", "details": { "url": "http://localhost:3000/alerting/list" }, From a53f16935069338b01f559f959e2b62587624c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Mar 2022 13:43:56 +0100 Subject: [PATCH 101/125] Loki: Update syntax highlighting to colorize strings (#45962) * Loki: Update syntax highlighting to colorize strings * Updated patterns --- public/app/plugins/datasource/loki/syntax.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts index abb521b02e5..f347c4699b6 100644 --- a/public/app/plugins/datasource/loki/syntax.ts +++ b/public/app/plugins/datasource/loki/syntax.ts @@ -239,9 +239,19 @@ export const lokiGrammar: Grammar = { }, }, ], + quote: { + pattern: /"(?:\\.|[^\\"])*"/, + alias: 'string', + greedy: true, + }, + backticks: { + pattern: /`(?:\\.|[^\\`])*`/, + alias: 'string', + greedy: true, + }, number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/, operator: /\s?(\|[=~]?|!=?|<(?:=>?|<|>)?|>[>=]?)\s?/i, - punctuation: /[{}()`,.]/, + punctuation: /[{}(),.]/, }; export default lokiGrammar; From 590ea19c3f28bff4157c29105c3ead50a49a54ab Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Wed, 2 Mar 2022 08:48:51 -0500 Subject: [PATCH 102/125] Cloudwatch: use CheckHealth for testing datasource (#45974) Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com> --- pkg/tsdb/cloudwatch/cloudwatch.go | 43 +++++++ pkg/tsdb/cloudwatch/cloudwatch_test.go | 110 ++++++++++++++++++ pkg/tsdb/cloudwatch/test_utils.go | 27 +++++ .../datasource/cloudwatch/datasource.ts | 19 --- 4 files changed, 180 insertions(+), 19 deletions(-) diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index a2d65a23bde..bbd68f269e7 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -169,6 +169,49 @@ func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.Call return e.resourceHandler.CallResource(ctx, req, sender) } +func (e *cloudWatchExecutor) checkHealthMetrics(pluginCtx backend.PluginContext) error { + namespace := "AWS/Billing" + metric := "EstimatedCharges" + params := &cloudwatch.ListMetricsInput{ + Namespace: &namespace, + MetricName: &metric, + } + _, err := e.listMetrics(pluginCtx, defaultRegion, params) + return err +} + +func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error { + logsClient, err := e.getCWLogsClient(pluginCtx, defaultRegion) + if err != nil { + return err + } + _, err = e.handleDescribeLogGroups(ctx, logsClient, simplejson.NewFromAny(map[string]interface{}{"limit": "1"})) + return err +} + +func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + status := backend.HealthStatusOk + metricsTest := "Successfully queried the CloudWatch metrics API." + logsTest := "Successfully queried the CloudWatch logs API." + + err := e.checkHealthMetrics(req.PluginContext) + if err != nil { + status = backend.HealthStatusError + metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error()) + } + + err = e.checkHealthLogs(ctx, req.PluginContext) + if err != nil { + status = backend.HealthStatusError + logsTest = fmt.Sprintf("CloudWatch logs query failed: %s", err.Error()) + } + + return &backend.CheckHealthResult{ + Status: status, + Message: fmt.Sprintf("1. %s\n2. %s", metricsTest, logsTest), + }, nil +} + func (e *cloudWatchExecutor) newSession(pluginCtx backend.PluginContext, region string) (*session.Session, error) { dsInfo, err := e.getDSInfo(pluginCtx) if err != nil { diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index 928667d61f9..b0d6a187da3 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -1,12 +1,24 @@ package cloudwatch import ( + "context" + "fmt" "testing" + "github.com/aws/aws-sdk-go/aws" + awsrequest "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,3 +84,101 @@ func TestNewInstanceSettings(t *testing.T) { }) } } + +func Test_CheckHealth(t *testing.T) { + origNewCWClient := NewCWClient + origNewCWLogsClient := NewCWLogsClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + NewCWLogsClient = origNewCWLogsClient + }) + + var client fakeCheckHealthClient + NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { + return client + } + NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { + return client + } + + t.Run("successfully query metrics and logs", func(t *testing.T) { + client = fakeCheckHealthClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "1. Successfully queried the CloudWatch metrics API.\n2. Successfully queried the CloudWatch logs API.", + }, resp) + }) + + t.Run("successfully queries metrics, fails during logs query", func(t *testing.T) { + client = fakeCheckHealthClient{ + describeLogGroupsWithContext: func(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput, + options ...awsrequest.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + return nil, fmt.Errorf("some logs query error") + }} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. Successfully queried the CloudWatch metrics API.\n2. CloudWatch logs query failed: some logs query error", + }, resp) + }) + + t.Run("successfully queries logs, fails during metrics query", func(t *testing.T) { + client = fakeCheckHealthClient{ + listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { + return fmt.Errorf("some list metrics error") + }} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. CloudWatch metrics query failed: some list metrics error\n2. Successfully queried the CloudWatch logs API.", + }, resp) + }) + + t.Run("fail to get clients", func(t *testing.T) { + client = fakeCheckHealthClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { + return nil, fmt.Errorf("some sessions error") + }}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. CloudWatch metrics query failed: some sessions error\n2. CloudWatch logs query failed: some sessions error", + }, resp) + }) +} diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index d8c8ce1f07a..eeb34a5ad5c 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -172,6 +172,29 @@ func (c fakeRGTAClient) GetResourcesPages(in *resourcegroupstaggingapi.GetResour return nil } +type fakeCheckHealthClient struct { + cloudwatchiface.CloudWatchAPI + cloudwatchlogsiface.CloudWatchLogsAPI + + listMetricsPages func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error + describeLogGroupsWithContext func(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput, + options ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) +} + +func (c fakeCheckHealthClient) ListMetricsPages(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { + if c.listMetricsPages != nil { + return c.listMetricsPages(input, fn) + } + return nil +} + +func (c fakeCheckHealthClient) DescribeLogGroupsWithContext(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput, options ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + if c.describeLogGroupsWithContext != nil { + return c.describeLogGroupsWithContext(ctx, input, options...) + } + return nil, nil +} + func chunkSlice(slice []*cloudwatch.Metric, chunkSize int) [][]*cloudwatch.Metric { var chunks [][]*cloudwatch.Metric for { @@ -194,9 +217,13 @@ func newTestConfig() *setting.Cfg { } type fakeSessionCache struct { + getSession func(c awsds.SessionConfig) (*session.Session, error) } func (s fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session, error) { + if s.getSession != nil { + return s.getSession(c) + } return &session.Session{ Config: &aws.Config{}, }, nil diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 4e1c038829e..734fa4c93fe 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -17,7 +17,6 @@ import { toLegacyResponseData, } from '@grafana/data'; import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; -import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; @@ -870,24 +869,6 @@ export class CloudWatchDatasource ); } - async testDatasource() { - // use billing metrics for test - const region = this.defaultRegion; - const namespace = 'AWS/Billing'; - const metricName = 'EstimatedCharges'; - const dimensions = {}; - - try { - await this.getDimensionValues(region ?? '', namespace, metricName, 'ServiceName', dimensions); - return { - status: 'success', - message: 'Data source is working', - }; - } catch (error) { - return toTestingStatus(error); - } - } - awsRequest(url: string, data: MetricRequest, headers: Record = {}): Observable { const options = { method: 'POST', From 47d1d83673e491b3eb14da8c73db94a845bd9159 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Wed, 2 Mar 2022 14:53:34 +0100 Subject: [PATCH 103/125] feat: add a new `` component (#46073) --- .../SecretInput/SecretInput.story.tsx | 60 +++++++++++++++++ .../SecretInput/SecretInput.test.tsx | 65 +++++++++++++++++++ .../components/SecretInput/SecretInput.tsx | 26 ++++++++ .../src/components/SecretInput/index.tsx | 1 + 4 files changed, 152 insertions(+) create mode 100644 packages/grafana-ui/src/components/SecretInput/SecretInput.story.tsx create mode 100644 packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx create mode 100644 packages/grafana-ui/src/components/SecretInput/SecretInput.tsx create mode 100644 packages/grafana-ui/src/components/SecretInput/index.tsx diff --git a/packages/grafana-ui/src/components/SecretInput/SecretInput.story.tsx b/packages/grafana-ui/src/components/SecretInput/SecretInput.story.tsx new file mode 100644 index 00000000000..60da75ef7de --- /dev/null +++ b/packages/grafana-ui/src/components/SecretInput/SecretInput.story.tsx @@ -0,0 +1,60 @@ +import React, { useState, ChangeEvent } from 'react'; +import { Story, Meta } from '@storybook/react'; +import { SecretInput, Props } from './SecretInput'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +export default { + title: 'Forms/SecretInput', + component: SecretInput, + decorators: [withCenteredStory], + parameters: { + controls: { + exclude: [ + 'prefix', + 'suffix', + 'addonBefore', + 'addonAfter', + 'type', + 'disabled', + 'invalid', + 'loading', + 'before', + 'after', + ], + }, + }, + args: { + width: 50, + placeholder: 'Enter your secret...', + }, + argTypes: { + width: { control: { type: 'range', min: 10, max: 200, step: 10 } }, + }, +} as Meta; + +const Template: Story = (args) => { + const [secret, setSecret] = useState(''); + + return ( + ) => setSecret(event.target.value.trim())} + onReset={() => setSecret('')} + /> + ); +}; + +export const basic = Template.bind({}); + +basic.args = { + isConfigured: false, +}; + +export const secretIsConfigured = Template.bind({}); + +secretIsConfigured.args = { + isConfigured: true, +}; diff --git a/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx b/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx new file mode 100644 index 00000000000..010ed2fdf59 --- /dev/null +++ b/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SecretInput, RESET_BUTTON_TEXT, CONFIGURED_TEXT } from './SecretInput'; + +const PLACEHOLDER_TEXT = 'Your secret...'; + +describe('', () => { + it('should render an input if the secret is not configured', () => { + render( {}} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />); + + const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); + + // Should show an enabled input + expect(input).toBeInTheDocument(); + expect(input).not.toBeDisabled(); + + // Should not show a "Reset" button + expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument(); + }); + + it('should render a disabled input with a reset button if the secret is already configured', () => { + render( {}} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />); + + const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); + + // Should show a disabled input + expect(input).toBeInTheDocument(); + expect(input).toBeDisabled(); + expect(input).toHaveValue(CONFIGURED_TEXT); + + // Should show a reset button + expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); + }); + + it('should be possible to reset a configured secret', () => { + const onReset = jest.fn(); + + render( {}} onReset={onReset} placeholder={PLACEHOLDER_TEXT} />); + + // Should show a reset button and a disabled input + expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled(); + expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); + + // Click on "Reset" + userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT })); + + expect(onReset).toHaveBeenCalledTimes(1); + }); + + it('should be possible to change the value of the secret', () => { + const onChange = jest.fn(); + + render( {}} placeholder={PLACEHOLDER_TEXT} />); + + const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); + + expect(input).toHaveValue(''); + + userEvent.type(input, 'Foo'); + + expect(onChange).toHaveBeenCalled(); + expect(input).toHaveValue('Foo'); + }); +}); diff --git a/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx b/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx new file mode 100644 index 00000000000..d46a70903a1 --- /dev/null +++ b/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Input } from '../Input/Input'; +import { HorizontalGroup } from '../Layout/Layout'; +import { Button } from '../Button'; + +export type Props = React.ComponentProps & { + /** TRUE if the secret was already configured. (It is needed as often the backend doesn't send back the actual secret, only the information that it was configured) */ + isConfigured: boolean; + /** Called when the user clicks on the "Reset" button in order to clear the secret */ + onReset: () => void; +}; + +export const CONFIGURED_TEXT = 'configured'; +export const RESET_BUTTON_TEXT = 'Reset'; + +export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => ( + + {!isConfigured && } + {isConfigured && } + {isConfigured && ( + + )} + +); diff --git a/packages/grafana-ui/src/components/SecretInput/index.tsx b/packages/grafana-ui/src/components/SecretInput/index.tsx new file mode 100644 index 00000000000..ff621221c96 --- /dev/null +++ b/packages/grafana-ui/src/components/SecretInput/index.tsx @@ -0,0 +1 @@ +export { SecretInput } from './SecretInput'; From f530775e45b576fd0df1987833ed91192642f037 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:02:09 +0000 Subject: [PATCH 104/125] Chore: Remove several 'as' type assertions (#45913) --- public/app/core/components/Footer/Footer.tsx | 4 ++-- .../core/components/PageActionBar/PageActionBar.tsx | 4 ++-- .../core/components/PermissionList/AddPermission.tsx | 4 ++-- .../core/components/RolePicker/RolePickerInput.tsx | 4 ++-- public/app/core/components/Select/SortPicker.tsx | 4 ++-- .../components/SplitPaneWrapper/SplitPaneWrapper.tsx | 11 ++++------- .../app/core/history/RichHistoryLocalStorage.test.ts | 8 +++----- public/app/core/store.ts | 4 +++- public/app/features/canvas/runtime/root.tsx | 4 ++-- public/app/features/explore/state/utils.ts | 2 +- 10 files changed, 23 insertions(+), 26 deletions(-) diff --git a/public/app/core/components/Footer/Footer.tsx b/public/app/core/components/Footer/Footer.tsx index 895d0d93650..b3c045e2a37 100644 --- a/public/app/core/components/Footer/Footer.tsx +++ b/public/app/core/components/Footer/Footer.tsx @@ -5,7 +5,7 @@ import { Icon, IconName } from '@grafana/ui'; export interface FooterLink { text: string; id?: string; - icon?: string; + icon?: IconName; url?: string; target?: string; } @@ -77,7 +77,7 @@ export const Footer: FC = React.memo(() => { {links.map((link) => (
  • - {link.icon && } {link.text} + {link.icon && } {link.text}
  • ))} diff --git a/public/app/core/components/PageActionBar/PageActionBar.tsx b/public/app/core/components/PageActionBar/PageActionBar.tsx index 1c771a4e0f0..a3471875063 100644 --- a/public/app/core/components/PageActionBar/PageActionBar.tsx +++ b/public/app/core/components/PageActionBar/PageActionBar.tsx @@ -12,10 +12,10 @@ export interface Props { export default class PageActionBar extends PureComponent { render() { const { searchQuery, linkButton, setSearchQuery, target, placeholder = 'Search by name or type' } = this.props; - const linkProps = { href: linkButton?.href, disabled: linkButton?.disabled }; + const linkProps: typeof LinkButton.defaultProps = { href: linkButton?.href, disabled: linkButton?.disabled }; if (target) { - (linkProps as any).target = target; + linkProps.target = target; } return ( diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index dcdd9bb6628..3555c6ef4db 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -41,8 +41,8 @@ class AddPermissions extends Component { }; } - onTypeChanged = (item: any) => { - const type = item.value as AclTarget; + onTypeChanged = (item: SelectableValue) => { + const type = item.value; switch (type) { case AclTarget.User: diff --git a/public/app/core/components/RolePicker/RolePickerInput.tsx b/public/app/core/components/RolePicker/RolePickerInput.tsx index f0b2d52c24c..3108e2c0b2e 100644 --- a/public/app/core/components/RolePicker/RolePickerInput.tsx +++ b/public/app/core/components/RolePicker/RolePickerInput.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent, HTMLProps, MutableRefObject, useEffect, useRef } from 'react'; +import React, { FormEvent, HTMLProps, useEffect, useRef } from 'react'; import { css, cx } from '@emotion/css'; import { useStyles2, getInputStyles, sharedInputStyle, styleMixins, Tooltip, Icon } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; @@ -36,7 +36,7 @@ export const RolePickerInput = ({ useEffect(() => { if (isFocused) { - (inputRef as MutableRefObject).current?.focus(); + inputRef.current?.focus(); } }); diff --git a/public/app/core/components/Select/SortPicker.tsx b/public/app/core/components/Select/SortPicker.tsx index 9ecdc6b6c60..e2f2a682939 100644 --- a/public/app/core/components/Select/SortPicker.tsx +++ b/public/app/core/components/Select/SortPicker.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { useAsync } from 'react-use'; -import { Icon, IconName, Select } from '@grafana/ui'; +import { Icon, Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { DEFAULT_SORT } from 'app/features/search/constants'; import { SearchSrv } from '../../services/search_srv'; @@ -36,7 +36,7 @@ export const SortPicker: FC = ({ onChange, value, placeholder, filter }) options={options} aria-label="Sort" placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`} - prefix={} + prefix={} /> ) : null; }; diff --git a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx index 18d143e6476..4efc15bc8da 100644 --- a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx +++ b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx @@ -19,7 +19,7 @@ interface Props { } export class SplitPaneWrapper extends PureComponent { - rafToken = createRef(); + rafToken: MutableRefObject = createRef(); static defaultProps = { rightPaneVisible: true, }; @@ -36,7 +36,7 @@ export class SplitPaneWrapper extends PureComponent { if (this.rafToken.current !== undefined) { window.cancelAnimationFrame(this.rafToken.current!); } - (this.rafToken as MutableRefObject).current = window.requestAnimationFrame(() => { + this.rafToken.current = window.requestAnimationFrame(() => { this.forceUpdate(); }); }; @@ -68,8 +68,7 @@ export class SplitPaneWrapper extends PureComponent { renderHorizontalSplit() { const { leftPaneComponents, uiState } = this.props; const styles = getStyles(config.theme); - const topPaneSize = - uiState.topPaneSize >= 1 ? (uiState.topPaneSize as number) : (uiState.topPaneSize as number) * window.innerHeight; + const topPaneSize = uiState.topPaneSize >= 1 ? uiState.topPaneSize : uiState.topPaneSize * window.innerHeight; /* Guesstimate the height of the browser window minus @@ -104,9 +103,7 @@ export class SplitPaneWrapper extends PureComponent { // Need to handle when width is relative. ie a percentage of the viewport const rightPaneSize = - uiState.rightPaneSize <= 1 - ? (uiState.rightPaneSize as number) * window.innerWidth - : (uiState.rightPaneSize as number); + uiState.rightPaneSize <= 1 ? uiState.rightPaneSize * window.innerWidth : uiState.rightPaneSize; if (!rightPaneVisible) { return this.renderHorizontalSplit(); diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index a7b18c317da..1bbc7f7b30f 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -151,12 +151,10 @@ describe('RichHistoryLocalStorage', () => { // one not starred replaced with a newly added starred item const removedNotStarredItems = extraItems + 1; // + 1 to make space for the new item - const newHistory = store.getObject(key); + const newHistory = store.getObject(key)!; expect(newHistory).toHaveLength(MAX_HISTORY_ITEMS); // starred item added - expect(newHistory.filter((h: RichHistoryQuery) => h.starred)).toHaveLength(starredItemsInHistory + 1); // starred item added - expect(newHistory.filter((h: RichHistoryQuery) => !h.starred)).toHaveLength( - starredItemsInHistory - removedNotStarredItems - ); + expect(newHistory.filter((h) => h.starred)).toHaveLength(starredItemsInHistory + 1); // starred item added + expect(newHistory.filter((h) => !h.starred)).toHaveLength(starredItemsInHistory - removedNotStarredItems); }); }); diff --git a/public/app/core/store.ts b/public/app/core/store.ts index d66444a0d6b..bcbb338dc23 100644 --- a/public/app/core/store.ts +++ b/public/app/core/store.ts @@ -16,7 +16,9 @@ export class Store { return window.localStorage[key] === 'true'; } - getObject(key: string, def?: any) { + getObject(key: string): T | undefined; + getObject(key: string, def: T): T; + getObject(key: string, def?: T) { let ret = def; if (this.exists(key)) { const json = window.localStorage[key]; diff --git a/public/app/features/canvas/runtime/root.tsx b/public/app/features/canvas/runtime/root.tsx index 25b45a8bba5..1106adddd93 100644 --- a/public/app/features/canvas/runtime/root.tsx +++ b/public/app/features/canvas/runtime/root.tsx @@ -27,12 +27,12 @@ export class RootElement extends GroupState { this.changeCallback(); } - getSaveModel() { + getSaveModel(): CanvasGroupOptions { const { placement, anchor, ...rest } = this.options; return { ...rest, // everything except placement & anchor elements: this.elements.map((v) => v.getSaveModel()), - } as CanvasGroupOptions; + }; } } diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 9bb6b1d968e..49825d2a8d8 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -96,7 +96,7 @@ export async function loadAndInitDatasource( } const historyKey = `grafana.explore.history.${instance.meta?.id}`; - const history = store.getObject(historyKey, []); + const history = store.getObject(historyKey, []); // Save last-used datasource store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.uid); From e738896316d2a83f02c6c3e0da6491b414e6dcec Mon Sep 17 00:00:00 2001 From: Marco Dalalba Date: Wed, 2 Mar 2022 11:35:36 -0300 Subject: [PATCH 105/125] Jaeger: Add support for traceID field to accept variables. (#45559) --- .../datasource/jaeger/datasource.test.ts | 26 +++++++++++++++++++ .../plugins/datasource/jaeger/datasource.ts | 4 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index 9011f9f3895..6d7284f635c 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -159,6 +159,32 @@ describe('JaegerDatasource', () => { }); }); + it('should resolve templates in traceID', async () => { + const mock = setupFetchMock({ data: [testResponse] }); + const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + + await lastValueFrom( + ds.query({ + ...defaultQuery, + scopedVars: { + $traceid: { + text: 'traceid', + value: '5311b0dd0ca8df3463df93c99cb805a6', + }, + }, + targets: [ + { + query: '$traceid', + refId: '1', + }, + ], + }) + ); + expect(mock).toBeCalledWith({ + url: `${defaultSettings.url}/api/traces/5311b0dd0ca8df3463df93c99cb805a6`, + }); + }); + it('should resolve templates in tags', async () => { const mock = setupFetchMock({ data: [testResponse] }); const ds = new JaegerDatasource(defaultSettings, timeSrvStub); diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index d95b2ace82e..83581fcc816 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -52,7 +52,9 @@ export class JaegerDatasource extends DataSourceApi } if (target.queryType !== 'search' && target.query) { - return this._request(`/api/traces/${encodeURIComponent(target.query)}`).pipe( + return this._request( + `/api/traces/${encodeURIComponent(getTemplateSrv().replace(target.query, options.scopedVars))}` + ).pipe( map((response) => { const traceData = response?.data?.data?.[0]; if (!traceData) { From 3427ae463dfd73d2e8e335b4a0772aa74ca8c979 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 2 Mar 2022 14:35:52 +0000 Subject: [PATCH 106/125] Dashboard: Add feature reporting for dashboard import (#46080) * Add feature reporting for dashboard import * Update tracking event names --- .../manage-dashboards/DashboardImportPage.tsx | 15 +++++++++++++++ .../components/ImportDashboardOverview.tsx | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index a8744ae3291..378b9fe4cc5 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -3,6 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { css } from '@emotion/css'; import { AppEvents, GrafanaTheme2, LoadingState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { reportInteraction } from '@grafana/runtime'; import { Button, Field, @@ -33,6 +34,8 @@ type DashboardImportPageRouteSearchParams = { type OwnProps = Themeable2 & GrafanaRouteComponentProps<{}, DashboardImportPageRouteSearchParams>; +const IMPORT_STARTED_EVENT_NAME = 'dashboard_import_loaded'; + const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'import', undefined, true), loadingState: state.importDashboard.state, @@ -63,6 +66,10 @@ class UnthemedDashboardImport extends PureComponent { } onFileUpload = (event: FormEvent) => { + reportInteraction(IMPORT_STARTED_EVENT_NAME, { + import_source: 'json_uploaded', + }); + const { importDashboardJson } = this.props; const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0]; @@ -89,10 +96,18 @@ class UnthemedDashboardImport extends PureComponent { }; getDashboardFromJson = (formData: { dashboardJson: string }) => { + reportInteraction(IMPORT_STARTED_EVENT_NAME, { + import_source: 'json_pasted', + }); + this.props.importDashboardJson(JSON.parse(formData.dashboardJson)); }; getGcomDashboard = (formData: { gcomDashboard: string }) => { + reportInteraction(IMPORT_STARTED_EVENT_NAME, { + import_source: 'gcom', + }); + let dashboardId; const match = /(^\d+$)|dashboards\/(\d+)/.exec(formData.gcomDashboard); if (match && match[1]) { diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index c94f75c5b5b..275a12d18ac 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -6,7 +6,9 @@ import { ImportDashboardForm } from './ImportDashboardForm'; import { clearLoadedDashboard, importDashboard } from '../state/actions'; import { DashboardSource, ImportDashboardDTO } from '../state/reducers'; import { StoreState } from 'app/types'; -import { locationService } from '@grafana/runtime'; +import { locationService, reportInteraction } from '@grafana/runtime'; + +const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported'; const mapStateToProps = (state: StoreState) => { const searchObj = locationService.getSearchObject(); @@ -39,6 +41,8 @@ class ImportDashboardOverviewUnConnected extends PureComponent { }; onSubmit = (form: ImportDashboardDTO) => { + reportInteraction(IMPORT_FINISHED_EVENT_NAME); + this.props.importDashboard(form); }; From 700f6863f2cccd998498ce5a50c55d342ee27bed Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 2 Mar 2022 06:41:07 -0800 Subject: [PATCH 107/125] AzureMonitor: Move Application Insights and Insight Analytics to a deprecated package (#45834) --- pkg/tsdb/azuremonitor/azlog/azlog.go | 23 +++ .../azuremonitor-resource-handler.go | 16 +- .../azuremonitor-resource-handler_test.go | 8 +- pkg/tsdb/azuremonitor/azuremonitor.go | 89 ++++------ pkg/tsdb/azuremonitor/azuremonitor_test.go | 51 ++++-- .../applicationinsights-datasource.go | 23 +-- .../applicationinsights-datasource_test.go | 7 +- .../applicationinsights-metrics.go | 2 +- .../applicationinsights-metrics_test.go | 4 +- .../azuremonitor/deprecated/httpclient.go | 20 +++ .../insights-analytics-datasource.go | 30 ++-- .../insights-analytics-datasource_test.go | 7 +- pkg/tsdb/azuremonitor/deprecated/routes.go | 23 +++ pkg/tsdb/azuremonitor/deprecated/types.go | 72 +++++++++ pkg/tsdb/azuremonitor/httpclient.go | 16 +- pkg/tsdb/azuremonitor/httpclient_test.go | 20 +-- .../azure-log-analytics-datasource.go | 36 +++-- .../azure-log-analytics-datasource_test.go | 39 ++--- .../azure-response-table-frame.go | 7 +- .../azure-response-table-frame_test.go | 4 +- pkg/tsdb/azuremonitor/{ => macros}/macros.go | 10 +- .../azuremonitor/{ => macros}/macros_test.go | 5 +- .../{ => metrics}/azuremonitor-datasource.go | 55 +++---- .../azuremonitor-datasource_test.go | 48 +++--- .../azuremonitor/{ => metrics}/url-builder.go | 2 +- .../{ => metrics}/url-builder_test.go | 2 +- .../azure-resource-graph-datasource.go | 44 +++-- .../azure-resource-graph-datasource_test.go | 11 +- pkg/tsdb/azuremonitor/routes.go | 62 +++---- .../{ => time}/azuremonitor-time.go | 17 +- .../azuremonitor/{ => time}/time-grain.go | 2 +- .../{ => time}/time-grain_test.go | 2 +- pkg/tsdb/azuremonitor/{ => types}/types.go | 153 +++++++----------- 33 files changed, 523 insertions(+), 387 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/azlog/azlog.go rename pkg/tsdb/azuremonitor/{ => deprecated}/applicationinsights-datasource.go (90%) rename pkg/tsdb/azuremonitor/{ => deprecated}/applicationinsights-datasource_test.go (97%) rename pkg/tsdb/azuremonitor/{ => deprecated}/applicationinsights-metrics.go (99%) rename pkg/tsdb/azuremonitor/{ => deprecated}/applicationinsights-metrics_test.go (99%) create mode 100644 pkg/tsdb/azuremonitor/deprecated/httpclient.go rename pkg/tsdb/azuremonitor/{ => deprecated}/insights-analytics-datasource.go (82%) rename pkg/tsdb/azuremonitor/{ => deprecated}/insights-analytics-datasource_test.go (82%) create mode 100644 pkg/tsdb/azuremonitor/deprecated/routes.go create mode 100644 pkg/tsdb/azuremonitor/deprecated/types.go rename pkg/tsdb/azuremonitor/{ => loganalytics}/azure-log-analytics-datasource.go (87%) rename pkg/tsdb/azuremonitor/{ => loganalytics}/azure-log-analytics-datasource_test.go (91%) rename pkg/tsdb/azuremonitor/{ => loganalytics}/azure-response-table-frame.go (95%) rename pkg/tsdb/azuremonitor/{ => loganalytics}/azure-response-table-frame_test.go (98%) rename pkg/tsdb/azuremonitor/{ => macros}/macros.go (92%) rename pkg/tsdb/azuremonitor/{ => macros}/macros_test.go (96%) rename pkg/tsdb/azuremonitor/{ => metrics}/azuremonitor-datasource.go (87%) rename pkg/tsdb/azuremonitor/{ => metrics}/azuremonitor-datasource_test.go (94%) rename pkg/tsdb/azuremonitor/{ => metrics}/url-builder.go (98%) rename pkg/tsdb/azuremonitor/{ => metrics}/url-builder_test.go (99%) rename pkg/tsdb/azuremonitor/{ => resourcegraph}/azure-resource-graph-datasource.go (82%) rename pkg/tsdb/azuremonitor/{ => resourcegraph}/azure-resource-graph-datasource_test.go (94%) rename pkg/tsdb/azuremonitor/{ => time}/azuremonitor-time.go (66%) rename pkg/tsdb/azuremonitor/{ => time}/time-grain.go (98%) rename pkg/tsdb/azuremonitor/{ => time}/time-grain_test.go (98%) rename pkg/tsdb/azuremonitor/{ => types}/types.go (53%) diff --git a/pkg/tsdb/azuremonitor/azlog/azlog.go b/pkg/tsdb/azuremonitor/azlog/azlog.go new file mode 100644 index 00000000000..bb946053175 --- /dev/null +++ b/pkg/tsdb/azuremonitor/azlog/azlog.go @@ -0,0 +1,23 @@ +package azlog + +import "github.com/grafana/grafana/pkg/infra/log" + +var ( + azlog = log.New("tsdb.azuremonitor") +) + +func Warn(msg string, args ...interface{}) { + azlog.Warn(msg, args) +} + +func Debug(msg string, args ...interface{}) { + azlog.Debug(msg, args) +} + +func Error(msg string, args ...interface{}) { + azlog.Error(msg, args) +} + +func Info(msg string, args ...interface{}) { + azlog.Info(msg, args) +} diff --git a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go index 92ee30ec8cb..a07295868e5 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go @@ -8,6 +8,9 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) func getTarget(original string) (target string, err error) { @@ -63,16 +66,16 @@ func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *ht return rw } -func (s *Service) getDataSourceFromHTTPReq(req *http.Request) (datasourceInfo, error) { +func (s *Service) getDataSourceFromHTTPReq(req *http.Request) (types.DatasourceInfo, error) { ctx := req.Context() pluginContext := httpadapter.PluginConfigFromContext(ctx) i, err := s.im.Get(pluginContext) if err != nil { - return datasourceInfo{}, nil + return types.DatasourceInfo{}, nil } - ds, ok := i.(datasourceInfo) + ds, ok := i.(types.DatasourceInfo) if !ok { - return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance") + return types.DatasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance") } return ds, nil } @@ -111,7 +114,7 @@ func (s *Service) handleResourceReq(subDataSource string) func(rw http.ResponseW req.URL.Host = serviceURL.Host req.URL.Scheme = serviceURL.Scheme - s.executors[subDataSource].resourceRequest(rw, req, service.HTTPClient) + s.executors[subDataSource].ResourceRequest(rw, req, service.HTTPClient) } } @@ -120,8 +123,9 @@ func (s *Service) handleResourceReq(subDataSource string) func(rw http.ResponseW func (s *Service) newResourceMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/azuremonitor/", s.handleResourceReq(azureMonitor)) - mux.HandleFunc("/appinsights/", s.handleResourceReq(appInsights)) mux.HandleFunc("/loganalytics/", s.handleResourceReq(azureLogAnalytics)) mux.HandleFunc("/resourcegraph/", s.handleResourceReq(azureResourceGraph)) + // Remove with Grafana 9 + mux.HandleFunc("/appinsights/", s.handleResourceReq(deprecated.AppInsights)) return mux } diff --git a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go index 849bb3c9981..efe20cc8f30 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/metrics" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) @@ -95,7 +97,7 @@ func Test_handleResourceReq(t *testing.T) { proxy := &fakeProxy{} s := Service{ im: &fakeInstance{ - services: map[string]datasourceService{ + services: map[string]types.DatasourceService{ azureMonitor: { URL: routes[setting.AzurePublic][azureMonitor].URL, HTTPClient: &http.Client{}, @@ -103,8 +105,8 @@ func Test_handleResourceReq(t *testing.T) { }, }, executors: map[string]azDatasourceExecutor{ - azureMonitor: &AzureMonitorDatasource{ - proxy: proxy, + azureMonitor: &metrics.AzureMonitorDatasource{ + Proxy: proxy, }, }, } diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index b40ecda8695..10d54716027 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -5,39 +5,38 @@ import ( "encoding/json" "fmt" "net/http" - "regexp" + "github.com/Masterminds/semver" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" -) - -const ( - timeSeries = "time_series" -) - -var ( - azlog = log.New("tsdb.azuremonitor") - legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/metrics" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/resourcegraph" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) func ProvideService(cfg *setting.Cfg, httpClientProvider *httpclient.Provider, tracer tracing.Tracer) *Service { proxy := &httpServiceProxy{} executors := map[string]azDatasourceExecutor{ - azureMonitor: &AzureMonitorDatasource{proxy: proxy}, - appInsights: &ApplicationInsightsDatasource{proxy: proxy}, - azureLogAnalytics: &AzureLogAnalyticsDatasource{proxy: proxy}, - insightsAnalytics: &InsightsAnalyticsDatasource{proxy: proxy}, - azureResourceGraph: &AzureResourceGraphDatasource{proxy: proxy}, + azureMonitor: &metrics.AzureMonitorDatasource{Proxy: proxy}, + azureLogAnalytics: &loganalytics.AzureLogAnalyticsDatasource{Proxy: proxy}, + azureResourceGraph: &resourcegraph.AzureResourceGraphDatasource{Proxy: proxy}, } + + // Insights Analytics and Application Insights were deprecated in Grafana 8.x and + // will be finally removed with Grafana 9 + if setting.BuildVersion != "" && semver.MustParse(setting.BuildVersion).Compare(semver.MustParse("9.0.0-beta1")) < 0 { + executors[deprecated.InsightsAnalytics] = &deprecated.InsightsAnalyticsDatasource{Proxy: proxy} + executors[deprecated.AppInsights] = &deprecated.ApplicationInsightsDatasource{Proxy: proxy} + } + im := datasource.NewInstanceManager(NewInstanceSettings(cfg, *httpClientProvider, executors)) s := &Service{ @@ -60,10 +59,6 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq return s.resourceHandler.CallResource(ctx, req, sender) } -type serviceProxy interface { - Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter -} - type Service struct { im instancemgmt.InstanceManager executors map[string]azDatasourceExecutor @@ -73,37 +68,13 @@ type Service struct { tracer tracing.Tracer } -type azureMonitorSettings struct { - SubscriptionId string `json:"subscriptionId"` - LogAnalyticsDefaultWorkspace string `json:"logAnalyticsDefaultWorkspace"` - AppInsightsAppId string `json:"appInsightsAppId"` -} - -type datasourceInfo struct { - Cloud string - Credentials azcredentials.AzureCredentials - Settings azureMonitorSettings - Routes map[string]azRoute - Services map[string]datasourceService - - JSONData map[string]interface{} - DecryptedSecureJSONData map[string]string - DatasourceID int64 - OrgID int64 -} - -type datasourceService struct { - URL string - HTTPClient *http.Client -} - -func getDatasourceService(cfg *setting.Cfg, clientProvider httpclient.Provider, dsInfo datasourceInfo, routeName string) (datasourceService, error) { +func getDatasourceService(cfg *setting.Cfg, clientProvider httpclient.Provider, dsInfo types.DatasourceInfo, routeName string) (types.DatasourceService, error) { route := dsInfo.Routes[routeName] client, err := newHTTPClient(route, dsInfo, cfg, clientProvider) if err != nil { - return datasourceService{}, err + return types.DatasourceService{}, err } - return datasourceService{ + return types.DatasourceService{ URL: dsInfo.Routes[routeName].URL, HTTPClient: client, }, nil @@ -122,7 +93,7 @@ func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, e return nil, fmt.Errorf("error reading settings: %w", err) } - azMonitorSettings := azureMonitorSettings{} + azMonitorSettings := types.AzureMonitorSettings{} err = json.Unmarshal(settings.JSONData, &azMonitorSettings) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) @@ -138,7 +109,7 @@ func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, e return nil, fmt.Errorf("error getting credentials: %w", err) } - model := datasourceInfo{ + model := types.DatasourceInfo{ Cloud: cloud, Credentials: credentials, Settings: azMonitorSettings, @@ -146,7 +117,7 @@ func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, e DecryptedSecureJSONData: settings.DecryptedSecureJSONData, DatasourceID: settings.ID, Routes: routes[cloud], - Services: map[string]datasourceService{}, + Services: map[string]types.DatasourceService{}, } for routeName := range executors { @@ -162,18 +133,18 @@ func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, e } type azDatasourceExecutor interface { - executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) - resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) + ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) + ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) } -func (s *Service) getDataSourceFromPluginReq(req *backend.QueryDataRequest) (datasourceInfo, error) { +func (s *Service) getDataSourceFromPluginReq(req *backend.QueryDataRequest) (types.DatasourceInfo, error) { i, err := s.im.Get(req.PluginContext) if err != nil { - return datasourceInfo{}, err + return types.DatasourceInfo{}, err } - dsInfo, ok := i.(datasourceInfo) + dsInfo, ok := i.(types.DatasourceInfo) if !ok { - return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance") + return types.DatasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance") } dsInfo.OrgID = req.PluginContext.OrgID return dsInfo, nil @@ -194,7 +165,7 @@ func (s *Service) newQueryMux() *datasource.QueryTypeMux { if !ok { return nil, fmt.Errorf("missing service for %s", dst) } - return executor.executeTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL, s.tracer) + return executor.ExecuteTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL, s.tracer) }) } return mux diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index 5b556242295..656e221d3d0 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -12,14 +12,45 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestProvideService(t *testing.T) { + t.Run("it should skip insight analytics and app insights with Grafana 9", func(t *testing.T) { + currentV := setting.BuildVersion + t.Cleanup(func() { + setting.BuildVersion = currentV + }) + versions := []struct { + version string + shouldIncludeInsights bool + }{ + {"8.5.0", true}, + {"9.0.0-beta1", false}, + {"9.0.0", false}, + } + for _, v := range versions { + setting.BuildVersion = v.version + s := ProvideService(setting.NewCfg(), httpclient.NewProvider(), nil) + if v.shouldIncludeInsights { + assert.NotNil(t, s.executors[deprecated.InsightsAnalytics]) + assert.NotNil(t, s.executors[deprecated.AppInsights]) + } else { + assert.Nil(t, s.executors[deprecated.InsightsAnalytics]) + assert.Nil(t, s.executors[deprecated.AppInsights]) + } + } + }) +} + func TestNewInstanceSettings(t *testing.T) { tests := []struct { name string settings backend.DataSourceInstanceSettings - expectedModel datasourceInfo + expectedModel types.DatasourceInfo Err require.ErrorAssertionFunc }{ { @@ -29,15 +60,15 @@ func TestNewInstanceSettings(t *testing.T) { DecryptedSecureJSONData: map[string]string{"key": "value"}, ID: 40, }, - expectedModel: datasourceInfo{ + expectedModel: types.DatasourceInfo{ Cloud: setting.AzurePublic, Credentials: &azcredentials.AzureManagedIdentityCredentials{}, - Settings: azureMonitorSettings{}, + Settings: types.AzureMonitorSettings{}, Routes: routes[setting.AzurePublic], JSONData: map[string]interface{}{"azureAuthType": "msi"}, DatasourceID: 40, DecryptedSecureJSONData: map[string]string{"key": "value"}, - Services: map[string]datasourceService{}, + Services: map[string]types.DatasourceService{}, }, Err: require.NoError, }, @@ -62,12 +93,12 @@ func TestNewInstanceSettings(t *testing.T) { } type fakeInstance struct { - routes map[string]azRoute - services map[string]datasourceService + routes map[string]types.AzRoute + services map[string]types.DatasourceService } func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) { - return datasourceInfo{ + return types.DatasourceInfo{ Routes: f.routes, Services: f.services, }, nil @@ -83,10 +114,10 @@ type fakeExecutor struct { expectedURL string } -func (f *fakeExecutor) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { +func (f *fakeExecutor) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { } -func (f *fakeExecutor) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (f *fakeExecutor) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { if client == nil { f.t.Errorf("The HTTP client for %s is missing", f.queryType) @@ -124,7 +155,7 @@ func Test_newMux(t *testing.T) { s := &Service{ im: &fakeInstance{ routes: routes[azureMonitorPublic], - services: map[string]datasourceService{ + services: map[string]types.DatasourceService{ tt.queryType: { URL: routes[azureMonitorPublic][tt.queryType].URL, HTTPClient: &http.Client{}, diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource.go similarity index 90% rename from pkg/tsdb/azuremonitor/applicationinsights-datasource.go rename to pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource.go index 26905bfe81e..c75b79ea252 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go +++ b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource.go @@ -1,4 +1,4 @@ -package azuremonitor +package deprecated import ( "context" @@ -15,6 +15,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/util/errutil" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/context/ctxhttp" @@ -22,7 +25,7 @@ import ( // ApplicationInsightsDatasource calls the application insights query API. type ApplicationInsightsDatasource struct { - proxy serviceProxy + Proxy types.ServiceProxy } // ApplicationInsightsQuery is the model that holds the information @@ -44,12 +47,12 @@ type ApplicationInsightsQuery struct { aggregation string } -func (e *ApplicationInsightsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { - e.proxy.Do(rw, req, cli) +func (e *ApplicationInsightsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { + e.Proxy.Do(rw, req, cli) } -func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Context, - originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (e *ApplicationInsightsDatasource) ExecuteTimeSeriesQuery(ctx context.Context, + originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() @@ -93,7 +96,7 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []backend.DataQuery // Previous versions of the query model don't specify a time grain, so we // need to fallback to a default value if timeGrain == "auto" || timeGrain == "" { - timeGrain, err = setAutoTimeGrain(query.Interval.Milliseconds(), timeGrains) + timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains) if err != nil { return nil, err } @@ -130,7 +133,7 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []backend.DataQuery return applicationInsightsQueries, nil } -func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo datasourceInfo, client *http.Client, url string, tracer tracing.Tracer) ( +func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) ( backend.DataResponse, error) { dataResponse := backend.DataResponse{} @@ -194,7 +197,7 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query return dataResponse, nil } -func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) { +func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo types.DatasourceInfo, url string) (*http.Request, error) { appInsightsAppID := dsInfo.Settings.AppInsightsAppId req, err := http.NewRequest(http.MethodGet, url, nil) @@ -221,7 +224,7 @@ func formatApplicationInsightsLegendKey(alias string, metricName string, labels } keys = sort.StringSlice(keys) - result := legendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte { + result := types.LegendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte { metaPartName := strings.Replace(string(in), "{{", "", 1) metaPartName = strings.Replace(metaPartName, "}}", "", 1) metaPartName = strings.ToLower(strings.TrimSpace(metaPartName)) diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource_test.go similarity index 97% rename from pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go rename to pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource_test.go index 6084036404a..6728c6b721e 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go +++ b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-datasource_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package deprecated import ( "context" @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) @@ -204,8 +205,8 @@ func TestInsightsDimensionsUnmarshalJSON(t *testing.T) { func TestAppInsightsCreateRequest(t *testing.T) { ctx := context.Background() url := "http://ds" - dsInfo := datasourceInfo{ - Settings: azureMonitorSettings{AppInsightsAppId: "foo"}, + dsInfo := types.DatasourceInfo{ + Settings: types.AzureMonitorSettings{AppInsightsAppId: "foo"}, DecryptedSecureJSONData: map[string]string{ "appInsightsApiKey": "key", }, diff --git a/pkg/tsdb/azuremonitor/applicationinsights-metrics.go b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics.go similarity index 99% rename from pkg/tsdb/azuremonitor/applicationinsights-metrics.go rename to pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics.go index f6f547f3eb4..316151ab643 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-metrics.go +++ b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics.go @@ -1,4 +1,4 @@ -package azuremonitor +package deprecated import ( "encoding/json" diff --git a/pkg/tsdb/azuremonitor/applicationinsights-metrics_test.go b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics_test.go similarity index 99% rename from pkg/tsdb/azuremonitor/applicationinsights-metrics_test.go rename to pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics_test.go index 9b9bb3b6718..a56c2bff4c3 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-metrics_test.go +++ b/pkg/tsdb/azuremonitor/deprecated/applicationinsights-metrics_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package deprecated import ( "encoding/json" @@ -173,7 +173,7 @@ func TestInsightsMetricsResultToFrame(t *testing.T) { func loadInsightsMetricsResponse(t *testing.T, name string) MetricsResult { t.Helper() - path := filepath.Join("testdata", name) + path := filepath.Join("../testdata", name) // Ignore gosec warning G304 since it's a test // nolint:gosec f, err := os.Open(path) diff --git a/pkg/tsdb/azuremonitor/deprecated/httpclient.go b/pkg/tsdb/azuremonitor/deprecated/httpclient.go new file mode 100644 index 00000000000..39eecf9458e --- /dev/null +++ b/pkg/tsdb/azuremonitor/deprecated/httpclient.go @@ -0,0 +1,20 @@ +package deprecated + +import ( + "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" +) + +func GetAppInsightsMiddleware(url, appInsightsApiKey string) httpclient.Middleware { + if appInsightsApiKey != "" && url == AzAppInsights.URL || url == AzChinaAppInsights.URL { + // Inject API-Key for AppInsights + return httpclient.MiddlewareFunc(func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { + return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + req.Header.Set("X-API-Key", appInsightsApiKey) + return next.RoundTrip(req) + }) + }) + } + return nil +} diff --git a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go b/pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource.go similarity index 82% rename from pkg/tsdb/azuremonitor/insights-analytics-datasource.go rename to pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource.go index 4b4ee49edb6..27a6c866e67 100644 --- a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource.go @@ -1,4 +1,4 @@ -package azuremonitor +package deprecated import ( "bytes" @@ -13,13 +13,17 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/util/errutil" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/context/ctxhttp" ) type InsightsAnalyticsDatasource struct { - proxy serviceProxy + Proxy types.ServiceProxy } type InsightsAnalyticsQuery struct { @@ -34,12 +38,12 @@ type InsightsAnalyticsQuery struct { Target string } -func (e *InsightsAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { - e.proxy.Do(rw, req, cli) +func (e *InsightsAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { + e.Proxy.Do(rw, req, cli) } -func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, - originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (e *InsightsAnalyticsDatasource) ExecuteTimeSeriesQuery(ctx context.Context, + originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() @@ -55,7 +59,7 @@ func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context return result, nil } -func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery, dsInfo datasourceInfo) ([]*InsightsAnalyticsQuery, error) { +func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*InsightsAnalyticsQuery, error) { iaQueries := []*InsightsAnalyticsQuery{} for _, query := range queries { @@ -74,7 +78,7 @@ func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery, return nil, fmt.Errorf("query is missing query string property") } - qm.InterpolatedQuery, err = KqlInterpolate(query, dsInfo, qm.RawQuery) + qm.InterpolatedQuery, err = macros.KqlInterpolate(query, dsInfo, qm.RawQuery) if err != nil { return nil, err } @@ -88,7 +92,7 @@ func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery, return iaQueries, nil } -func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, +func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) backend.DataResponse { dataResponse := backend.DataResponse{} @@ -136,7 +140,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I azlog.Debug("Request failed", "status", res.Status, "body", string(body)) return dataResponseError(fmt.Errorf("request failed, status: %s, body: %s", res.Status, body)) } - var logResponse AzureLogAnalyticsResponse + var logResponse loganalytics.AzureLogAnalyticsResponse d := json.NewDecoder(bytes.NewReader(body)) d.UseNumber() err = d.Decode(&logResponse) @@ -149,12 +153,12 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I return dataResponseError(err) } - frame, err := ResponseTableToFrame(t) + frame, err := loganalytics.ResponseTableToFrame(t) if err != nil { return dataResponseError(err) } - if query.ResultFormat == timeSeries { + if query.ResultFormat == types.TimeSeries { tsSchema := frame.TimeSeriesSchema() if tsSchema.Type == data.TimeSeriesTypeLong { wideFrame, err := data.LongToWide(frame, nil) @@ -173,7 +177,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I return dataResponse } -func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) { +func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo types.DatasourceInfo, url string) (*http.Request, error) { appInsightsAppID := dsInfo.Settings.AppInsightsAppId req, err := http.NewRequest(http.MethodGet, url, nil) diff --git a/pkg/tsdb/azuremonitor/insights-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource_test.go similarity index 82% rename from pkg/tsdb/azuremonitor/insights-analytics-datasource_test.go rename to pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource_test.go index 8b0e118e987..959c8cfe9c1 100644 --- a/pkg/tsdb/azuremonitor/insights-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/deprecated/insights-analytics-datasource_test.go @@ -1,18 +1,19 @@ -package azuremonitor +package deprecated import ( "context" "net/http" "testing" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) func TestInsightsAnalyticsCreateRequest(t *testing.T) { ctx := context.Background() url := "http://ds" - dsInfo := datasourceInfo{ - Settings: azureMonitorSettings{AppInsightsAppId: "foo"}, + dsInfo := types.DatasourceInfo{ + Settings: types.AzureMonitorSettings{AppInsightsAppId: "foo"}, DecryptedSecureJSONData: map[string]string{ "appInsightsApiKey": "key", }, diff --git a/pkg/tsdb/azuremonitor/deprecated/routes.go b/pkg/tsdb/azuremonitor/deprecated/routes.go new file mode 100644 index 00000000000..9b2d5e58823 --- /dev/null +++ b/pkg/tsdb/azuremonitor/deprecated/routes.go @@ -0,0 +1,23 @@ +package deprecated + +import ( + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) + +// Azure cloud query types +const ( + AppInsights = "Application Insights" + InsightsAnalytics = "Insights Analytics" +) + +var AzAppInsights = types.AzRoute{ + URL: "https://api.applicationinsights.io", + Scopes: []string{}, + Headers: map[string]string{"x-ms-app": "Grafana"}, +} + +var AzChinaAppInsights = types.AzRoute{ + URL: "https://api.applicationinsights.azure.cn", + Scopes: []string{}, + Headers: map[string]string{"x-ms-app": "Grafana"}, +} diff --git a/pkg/tsdb/azuremonitor/deprecated/types.go b/pkg/tsdb/azuremonitor/deprecated/types.go new file mode 100644 index 00000000000..487f28bfdfc --- /dev/null +++ b/pkg/tsdb/azuremonitor/deprecated/types.go @@ -0,0 +1,72 @@ +package deprecated + +import ( + "encoding/json" + "fmt" + "strings" +) + +// insightsJSONQuery is the frontend JSON query model for an Azure Application Insights query. +type insightsJSONQuery struct { + AppInsights struct { + Aggregation string `json:"aggregation"` + Alias string `json:"alias"` + AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` + Dimensions InsightsDimensions `json:"dimension"` + DimensionFilter string `json:"dimensionFilter"` + MetricName string `json:"metricName"` + TimeGrain string `json:"timeGrain"` + } `json:"appInsights"` + Raw *bool `json:"raw"` +} + +// InsightsDimensions will unmarshal from a JSON string, or an array of strings, +// into a string array. This exists to support an older query format which is updated +// when a user saves the query or it is sent from the front end, but may not be when +// alerting fetches the model. +type InsightsDimensions []string + +// UnmarshalJSON fulfills the json.Unmarshaler interface type. +func (s *InsightsDimensions) UnmarshalJSON(data []byte) error { + *s = InsightsDimensions{} + if string(data) == "null" || string(data) == "" { + return nil + } + if strings.ToLower(string(data)) == `"none"` { + return nil + } + if data[0] == '[' { + var sa []string + err := json.Unmarshal(data, &sa) + if err != nil { + return err + } + dimensions := []string{} + for _, v := range sa { + if v == "none" || v == "None" { + continue + } + dimensions = append(dimensions, v) + } + *s = InsightsDimensions(dimensions) + return nil + } + + var str string + err := json.Unmarshal(data, &str) + if err != nil { + return fmt.Errorf("could not parse %q as string or array: %w", string(data), err) + } + if str != "" { + *s = InsightsDimensions{str} + return nil + } + return nil +} + +type insightsAnalyticsJSONQuery struct { + InsightsAnalytics struct { + Query string `json:"query"` + ResultFormat string `json:"resultFormat"` + } `json:"insightsAnalytics"` +} diff --git a/pkg/tsdb/azuremonitor/httpclient.go b/pkg/tsdb/azuremonitor/httpclient.go index af615680ae5..ad031264f89 100644 --- a/pkg/tsdb/azuremonitor/httpclient.go +++ b/pkg/tsdb/azuremonitor/httpclient.go @@ -6,9 +6,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) -func getMiddlewares(route azRoute, model datasourceInfo, cfg *setting.Cfg) ([]httpclient.Middleware, error) { +func getMiddlewares(route types.AzRoute, model types.DatasourceInfo, cfg *setting.Cfg) ([]httpclient.Middleware, error) { middlewares := []httpclient.Middleware{} if len(route.Scopes) > 0 { @@ -19,21 +21,15 @@ func getMiddlewares(route azRoute, model datasourceInfo, cfg *setting.Cfg) ([]ht middlewares = append(middlewares, aztokenprovider.AuthMiddleware(tokenProvider, route.Scopes)) } - if _, ok := model.DecryptedSecureJSONData["appInsightsApiKey"]; ok && (route.URL == azAppInsights.URL || route.URL == azChinaAppInsights.URL) { - // Inject API-Key for AppInsights - apiKeyMiddleware := httpclient.MiddlewareFunc(func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { - return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { - req.Header.Set("X-API-Key", model.DecryptedSecureJSONData["appInsightsApiKey"]) - return next.RoundTrip(req) - }) - }) + // Remove with Grafana 9 + if apiKeyMiddleware := deprecated.GetAppInsightsMiddleware(route.URL, model.DecryptedSecureJSONData["appInsightsApiKey"]); apiKeyMiddleware != nil { middlewares = append(middlewares, apiKeyMiddleware) } return middlewares, nil } -func newHTTPClient(route azRoute, model datasourceInfo, cfg *setting.Cfg, clientProvider httpclient.Provider) (*http.Client, error) { +func newHTTPClient(route types.AzRoute, model types.DatasourceInfo, cfg *setting.Cfg, clientProvider httpclient.Provider) (*http.Client, error) { m, err := getMiddlewares(route, model, cfg) if err != nil { return nil, err diff --git a/pkg/tsdb/azuremonitor/httpclient_test.go b/pkg/tsdb/azuremonitor/httpclient_test.go index 895d2fd7722..5679529459c 100644 --- a/pkg/tsdb/azuremonitor/httpclient_test.go +++ b/pkg/tsdb/azuremonitor/httpclient_test.go @@ -5,6 +5,8 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) @@ -12,18 +14,18 @@ func Test_httpCliProvider(t *testing.T) { cfg := &setting.Cfg{} tests := []struct { name string - route azRoute - model datasourceInfo + route types.AzRoute + model types.DatasourceInfo expectedMiddlewares int Err require.ErrorAssertionFunc }{ { name: "creates an HTTP client with a middleware due to the scope", - route: azRoute{ + route: types.AzRoute{ URL: "http://route", Scopes: []string{"http://route/.default"}, }, - model: datasourceInfo{ + model: types.DatasourceInfo{ Credentials: &azcredentials.AzureClientSecretCredentials{}, }, expectedMiddlewares: 1, @@ -31,11 +33,11 @@ func Test_httpCliProvider(t *testing.T) { }, { name: "creates an HTTP client with a middleware due to an app key", - route: azRoute{ - URL: azAppInsights.URL, + route: types.AzRoute{ + URL: deprecated.AzAppInsights.URL, Scopes: []string{}, }, - model: datasourceInfo{ + model: types.DatasourceInfo{ Credentials: &azcredentials.AzureClientSecretCredentials{}, DecryptedSecureJSONData: map[string]string{ "appInsightsApiKey": "foo", @@ -46,11 +48,11 @@ func Test_httpCliProvider(t *testing.T) { }, { name: "creates an HTTP client without a middleware", - route: azRoute{ + route: types.AzRoute{ URL: "http://route", Scopes: []string{}, }, - model: datasourceInfo{ + model: types.DatasourceInfo{ Credentials: &azcredentials.AzureClientSecretCredentials{}, }, expectedMiddlewares: 0, diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go similarity index 87% rename from pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go rename to pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 964ec995c78..cc96a88ebac 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -1,4 +1,4 @@ -package azuremonitor +package loganalytics import ( "bytes" @@ -17,6 +17,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/util/errutil" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/context/ctxhttp" @@ -24,7 +27,7 @@ import ( // AzureLogAnalyticsDatasource calls the Azure Log Analytics API's type AzureLogAnalyticsDatasource struct { - proxy serviceProxy + Proxy types.ServiceProxy } // AzureLogAnalyticsQuery is the query request that is built from the saved values for @@ -39,15 +42,15 @@ type AzureLogAnalyticsQuery struct { TimeRange backend.TimeRange } -func (e *AzureLogAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { - e.proxy.Do(rw, req, cli) +func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { + e.Proxy.Do(rw, req, cli) } // executeTimeSeriesQuery does the following: // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (e *AzureLogAnalyticsDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() @@ -63,7 +66,7 @@ func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context return result, nil } -func getApiURL(queryJSONModel logJSONQuery) string { +func getApiURL(queryJSONModel types.LogJSONQuery) string { // Legacy queries only specify a Workspace GUID, which we need to use the old workspace-centric // API URL for, and newer queries specifying a resource URI should use resource-centric API. // However, legacy workspace queries using a `workspaces()` template variable will be resolved @@ -86,11 +89,11 @@ func getApiURL(queryJSONModel logJSONQuery) string { } } -func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery, dsInfo datasourceInfo) ([]*AzureLogAnalyticsQuery, error) { +func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*AzureLogAnalyticsQuery, error) { azureLogAnalyticsQueries := []*AzureLogAnalyticsQuery{} for _, query := range queries { - queryJSONModel := logJSONQuery{} + queryJSONModel := types.LogJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) if err != nil { return nil, fmt.Errorf("failed to decode the Azure Log Analytics query object from JSON: %w", err) @@ -101,13 +104,13 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery, resultFormat := azureLogAnalyticsTarget.ResultFormat if resultFormat == "" { - resultFormat = timeSeries + resultFormat = types.TimeSeries } apiURL := getApiURL(queryJSONModel) params := url.Values{} - rawQuery, err := KqlInterpolate(query, dsInfo, azureLogAnalyticsTarget.Query, "TimeGenerated") + rawQuery, err := macros.KqlInterpolate(query, dsInfo, azureLogAnalyticsTarget.Query, "TimeGenerated") if err != nil { return nil, err } @@ -127,7 +130,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery, return azureLogAnalyticsQueries, nil } -func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, +func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) backend.DataResponse { dataResponse := backend.DataResponse{} @@ -204,7 +207,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A azlog.Warn("failed to add custom metadata to azure log analytics response", err) } - if query.ResultFormat == timeSeries { + if query.ResultFormat == types.TimeSeries { tsSchema := frame.TimeSeriesSchema() if tsSchema.Type == data.TimeSeriesTypeLong { wideFrame, err := data.LongToWide(frame, nil) @@ -220,7 +223,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A return dataResponse } -func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) { +func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo types.DatasourceInfo, url string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { azlog.Debug("Failed to create request", "error", err) @@ -232,9 +235,14 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo return req, nil } +// AzureLogAnalyticsResponse is the json response object from the Azure Log Analytics API. +type AzureLogAnalyticsResponse struct { + Tables []types.AzureResponseTable `json:"tables"` +} + // GetPrimaryResultTable returns the first table in the response named "PrimaryResult", or an // error if there is no table by that name. -func (ar *AzureLogAnalyticsResponse) GetPrimaryResultTable() (*AzureResponseTable, error) { +func (ar *AzureLogAnalyticsResponse) GetPrimaryResultTable() (*types.AzureResponseTable, error) { for _, t := range ar.Tables { if t.Name == "PrimaryResult" { return &t, nil diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go similarity index 91% rename from pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go rename to pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index 26001270be5..95b8c14f3af 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package loganalytics import ( "context" @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) @@ -37,7 +38,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), RefID: "A", TimeRange: timeRange, }, @@ -45,7 +46,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: timeSeries, + ResultFormat: types.TimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -54,7 +55,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), Params: url.Values{"query": {"query=Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer"}}, Target: "query=query%3DPerf+%7C+where+%5B%27TimeGenerated%27%5D+%3E%3D+datetime%28%272018-03-15T13%3A00%3A00Z%27%29+and+%5B%27TimeGenerated%27%5D+%3C%3D+datetime%28%272018-03-15T13%3A34%3A00Z%27%29+%7C+where+%5B%27Computer%27%5D+in+%28%27comp1%27%2C%27comp2%27%29+%7C+summarize+avg%28CounterValue%29+by+bin%28TimeGenerated%2C+34000ms%29%2C+Computer", TimeRange: timeRange, @@ -74,14 +75,14 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), RefID: "A", }, }, azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: timeSeries, + ResultFormat: types.TimeSeries, URL: "v1/workspaces/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -90,7 +91,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), Params: url.Values{"query": {"query=Perf"}}, Target: "query=query%3DPerf", }, @@ -109,14 +110,14 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), RefID: "A", }, }, azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: timeSeries, + ResultFormat: types.TimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -125,7 +126,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), Params: url.Values{"query": {"query=Perf"}}, Target: "query=query%3DPerf", }, @@ -144,14 +145,14 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), RefID: "A", }, }, azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ { RefID: "A", - ResultFormat: timeSeries, + ResultFormat: types.TimeSeries, URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", @@ -160,7 +161,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "query": "query=Perf", "resultFormat": "%s" } - }`, timeSeries)), + }`, types.TimeSeries)), Params: url.Values{"query": {"query=Perf"}}, Target: "query=query%3DPerf", }, @@ -171,7 +172,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - queries, err := datasource.buildQueries(tt.queryModel, datasourceInfo{}) + queries, err := datasource.buildQueries(tt.queryModel, types.DatasourceInfo{}) tt.Err(t, err) if diff := cmp.Diff(tt.azureLogAnalyticsQueries[0], queries[0]); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) @@ -183,7 +184,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { func TestLogAnalyticsCreateRequest(t *testing.T) { ctx := context.Background() url := "http://ds" - dsInfo := datasourceInfo{} + dsInfo := types.DatasourceInfo{} tests := []struct { name string @@ -216,9 +217,9 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { ds := AzureLogAnalyticsDatasource{} - dsInfo := datasourceInfo{ - Services: map[string]datasourceService{ - azureLogAnalytics: {URL: "http://ds"}, + dsInfo := types.DatasourceInfo{ + Services: map[string]types.DatasourceService{ + "Azure Log Analytics": {URL: "http://ds"}, }, JSONData: map[string]interface{}{ "azureLogAnalyticsSameAs": false, @@ -231,7 +232,7 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { } tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - res := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services[azureLogAnalytics].URL, tracer) + res := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL, tracer) if res.Error == nil { t.Fatal("expecting an error") } diff --git a/pkg/tsdb/azuremonitor/azure-response-table-frame.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go similarity index 95% rename from pkg/tsdb/azuremonitor/azure-response-table-frame.go rename to pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go index 94b9508c1d0..e26b336c794 100644 --- a/pkg/tsdb/azuremonitor/azure-response-table-frame.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go @@ -1,4 +1,4 @@ -package azuremonitor +package loganalytics import ( "encoding/json" @@ -8,10 +8,11 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) // ResponseTableToFrame converts an AzureResponseTable to a data.Frame. -func ResponseTableToFrame(table *AzureResponseTable) (*data.Frame, error) { +func ResponseTableToFrame(table *types.AzureResponseTable) (*data.Frame, error) { converterFrame, err := converterFrameForTable(table) if err != nil { return nil, err @@ -27,7 +28,7 @@ func ResponseTableToFrame(table *AzureResponseTable) (*data.Frame, error) { return converterFrame.Frame, nil } -func converterFrameForTable(t *AzureResponseTable) (*data.FrameInputConverter, error) { +func converterFrameForTable(t *types.AzureResponseTable) (*data.FrameInputConverter, error) { converters := []data.FieldConverter{} colNames := make([]string, len(t.Columns)) colTypes := make([]string, len(t.Columns)) // for metadata diff --git a/pkg/tsdb/azuremonitor/azure-response-table-frame_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go similarity index 98% rename from pkg/tsdb/azuremonitor/azure-response-table-frame_test.go rename to pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go index a6e0a797cc6..281111fbd3d 100644 --- a/pkg/tsdb/azuremonitor/azure-response-table-frame_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package loganalytics import ( "encoding/json" @@ -156,7 +156,7 @@ func TestLogTableToFrame(t *testing.T) { func loadLogAnalyticsTestFileWithNumber(t *testing.T, name string) AzureLogAnalyticsResponse { t.Helper() - path := filepath.Join("testdata", name) + path := filepath.Join("../testdata", name) // Ignore gosec warning G304 since it's a test // nolint:gosec f, err := os.Open(path) diff --git a/pkg/tsdb/azuremonitor/macros.go b/pkg/tsdb/azuremonitor/macros/macros.go similarity index 92% rename from pkg/tsdb/azuremonitor/macros.go rename to pkg/tsdb/azuremonitor/macros/macros.go index 1adaa115336..d83f746c046 100644 --- a/pkg/tsdb/azuremonitor/macros.go +++ b/pkg/tsdb/azuremonitor/macros/macros.go @@ -1,4 +1,4 @@ -package azuremonitor +package macros import ( "fmt" @@ -9,6 +9,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" ) @@ -31,7 +33,7 @@ type kqlMacroEngine struct { // - $__escapeMulti('\\vm\eth0\Total','\\vm\eth2\Total') -> @'\\vm\eth0\Total',@'\\vm\eth2\Total' // KqlInterpolate interpolates macros for Kusto Query Language (KQL) queries -func KqlInterpolate(query backend.DataQuery, dsInfo datasourceInfo, kql string, defaultTimeField ...string) (string, error) { +func KqlInterpolate(query backend.DataQuery, dsInfo types.DatasourceInfo, kql string, defaultTimeField ...string) (string, error) { engine := kqlMacroEngine{} defaultTimeFieldForAllDatasources := "timestamp" @@ -41,7 +43,7 @@ func KqlInterpolate(query backend.DataQuery, dsInfo datasourceInfo, kql string, return engine.Interpolate(query, dsInfo, kql, defaultTimeFieldForAllDatasources) } -func (m *kqlMacroEngine) Interpolate(query backend.DataQuery, dsInfo datasourceInfo, kql string, defaultTimeField string) (string, error) { +func (m *kqlMacroEngine) Interpolate(query backend.DataQuery, dsInfo types.DatasourceInfo, kql string, defaultTimeField string) (string, error) { m.timeRange = query.TimeRange m.query = query rExp, _ := regexp.Compile(sExpr) @@ -86,7 +88,7 @@ func (m *kqlMacroEngine) Interpolate(query backend.DataQuery, dsInfo datasourceI return kql, nil } -func (m *kqlMacroEngine) evaluateMacro(name string, defaultTimeField string, args []string, dsInfo datasourceInfo) (string, error) { +func (m *kqlMacroEngine) evaluateMacro(name string, defaultTimeField string, args []string, dsInfo types.DatasourceInfo) (string, error) { switch name { case "timeFilter": timeColumn := defaultTimeField diff --git a/pkg/tsdb/azuremonitor/macros_test.go b/pkg/tsdb/azuremonitor/macros/macros_test.go similarity index 96% rename from pkg/tsdb/azuremonitor/macros_test.go rename to pkg/tsdb/azuremonitor/macros/macros_test.go index 866d270627f..7fa92c15067 100644 --- a/pkg/tsdb/azuremonitor/macros_test.go +++ b/pkg/tsdb/azuremonitor/macros/macros_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package macros import ( "testing" @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ) @@ -125,7 +126,7 @@ func TestAzureLogAnalyticsMacros(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defaultTimeField := "TimeGenerated" - rawQuery, err := KqlInterpolate(tt.query, datasourceInfo{}, tt.kql, defaultTimeField) + rawQuery, err := KqlInterpolate(tt.query, types.DatasourceInfo{}, tt.kql, defaultTimeField) tt.Err(t, err) if diff := cmp.Diff(tt.expected, rawQuery, cmpopts.EquateNaNs()); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go similarity index 87% rename from pkg/tsdb/azuremonitor/azuremonitor-datasource.go rename to pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index ff16e0b55e6..df190981bab 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -1,4 +1,4 @@ -package azuremonitor +package metrics import ( "context" @@ -16,6 +16,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/resourcegraph" + azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/util/errutil" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/context/ctxhttp" @@ -23,28 +27,25 @@ import ( // AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported type AzureMonitorDatasource struct { - proxy serviceProxy + Proxy types.ServiceProxy } var ( - // 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds - defaultAllowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} - // Used to convert the aggregation value to the Azure enum for deep linking aggregationTypeMap = map[string]int{"None": 0, "Total": 1, "Minimum": 2, "Maximum": 3, "Average": 4, "Count": 7} ) const azureMonitorAPIVersion = "2018-01-01" -func (e *AzureMonitorDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { - e.proxy.Do(rw, req, cli) +func (e *AzureMonitorDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { + e.Proxy.Do(rw, req, cli) } // executeTimeSeriesQuery does the following: // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (e *AzureMonitorDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() @@ -60,12 +61,12 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori return result, nil } -func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInfo datasourceInfo) ([]*AzureMonitorQuery, error) { - azureMonitorQueries := []*AzureMonitorQuery{} +func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*types.AzureMonitorQuery, error) { + azureMonitorQueries := []*types.AzureMonitorQuery{} for _, query := range queries { var target string - queryJSONModel := azureMonitorJSONQuery{} + queryJSONModel := types.AzureMonitorJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) if err != nil { return nil, fmt.Errorf("failed to decode the Azure Monitor query object from JSON: %w", err) @@ -93,7 +94,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf timeGrain := azJSONModel.TimeGrain timeGrains := azJSONModel.AllowedTimeGrainsMs if timeGrain == "auto" { - timeGrain, err = setAutoTimeGrain(query.Interval.Milliseconds(), timeGrains) + timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains) if err != nil { return nil, err } @@ -135,7 +136,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf azlog.Debug("Azuremonitor request", "params", params) } - azureMonitorQueries = append(azureMonitorQueries, &AzureMonitorQuery{ + azureMonitorQueries = append(azureMonitorQueries, &types.AzureMonitorQuery{ URL: azureURL, UrlComponents: urlComponents, Target: target, @@ -149,7 +150,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf return azureMonitorQueries, nil } -func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, dsInfo datasourceInfo, cli *http.Client, +func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *types.AzureMonitorQuery, dsInfo types.DatasourceInfo, cli *http.Client, url string, tracer tracing.Tracer) backend.DataResponse { dataResponse := backend.DataResponse{} @@ -191,7 +192,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM return dataResponse } - azurePortalUrl, err := getAzurePortalUrl(dsInfo.Cloud) + azurePortalUrl, err := resourcegraph.GetAzurePortalUrl(dsInfo.Cloud) if err != nil { dataResponse.Error = err return dataResponse @@ -206,7 +207,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM return dataResponse } -func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) { +func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo types.DatasourceInfo, url string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { azlog.Debug("Failed to create request", "error", err) @@ -218,28 +219,28 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datas return req, nil } -func (e *AzureMonitorDatasource) unmarshalResponse(res *http.Response) (AzureMonitorResponse, error) { +func (e *AzureMonitorDatasource) unmarshalResponse(res *http.Response) (types.AzureMonitorResponse, error) { body, err := ioutil.ReadAll(res.Body) if err != nil { - return AzureMonitorResponse{}, err + return types.AzureMonitorResponse{}, err } if res.StatusCode/100 != 2 { azlog.Debug("Request failed", "status", res.Status, "body", string(body)) - return AzureMonitorResponse{}, fmt.Errorf("request failed, status: %s", res.Status) + return types.AzureMonitorResponse{}, fmt.Errorf("request failed, status: %s", res.Status) } - var data AzureMonitorResponse + var data types.AzureMonitorResponse err = json.Unmarshal(body, &data) if err != nil { azlog.Debug("Failed to unmarshal AzureMonitor response", "error", err, "status", res.Status, "body", string(body)) - return AzureMonitorResponse{}, err + return types.AzureMonitorResponse{}, err } return data, nil } -func (e *AzureMonitorDatasource) parseResponse(amr AzureMonitorResponse, query *AzureMonitorQuery, azurePortalUrl string) (data.Frames, error) { +func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, query *types.AzureMonitorQuery, azurePortalUrl string) (data.Frames, error) { if len(amr.Value) == 0 { return nil, nil } @@ -303,7 +304,7 @@ func (e *AzureMonitorDatasource) parseResponse(amr AzureMonitorResponse, query * frame.SetRow(i, point.TimeStamp, value) } - frameWithLink := addConfigLinks(*frame, queryUrl) + frameWithLink := resourcegraph.AddConfigLinks(*frame, queryUrl) frames = append(frames, &frameWithLink) } @@ -311,7 +312,7 @@ func (e *AzureMonitorDatasource) parseResponse(amr AzureMonitorResponse, query * } // Gets the deep link for the given query -func getQueryUrl(query *AzureMonitorQuery, azurePortalUrl string) (string, error) { +func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl string) (string, error) { aggregationType := aggregationTypeMap["Average"] aggregation := query.Params.Get("aggregation") if aggregation != "" { @@ -343,7 +344,7 @@ func getQueryUrl(query *AzureMonitorQuery, azurePortalUrl string) (string, error chartDef, err := json.Marshal(map[string]interface{}{ "v2charts": []interface{}{ map[string]interface{}{ - "metrics": []metricChartDefinition{ + "metrics": []types.MetricChartDefinition{ { ResourceMetadata: map[string]string{ "id": id, @@ -351,7 +352,7 @@ func getQueryUrl(query *AzureMonitorQuery, azurePortalUrl string) (string, error Name: query.Params.Get("metricnames"), AggregationType: aggregationType, Namespace: query.Params.Get("metricnamespace"), - MetricVisualization: metricVisualization{ + MetricVisualization: types.MetricVisualization{ DisplayName: query.Params.Get("metricnames"), ResourceDisplayName: query.UrlComponents["resourceName"], }, @@ -387,7 +388,7 @@ func formatAzureMonitorLegendKey(alias string, resourceName string, metricName s } keys = sort.StringSlice(keys) - result := legendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte { + result := types.LegendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte { metaPartName := strings.Replace(string(in), "{{", "", 1) metaPartName = strings.Replace(metaPartName, "}}", "", 1) metaPartName = strings.ToLower(strings.TrimSpace(metaPartName)) diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go similarity index 94% rename from pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go rename to pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go index 7afca8e6d7d..1081b21a619 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package metrics import ( "context" @@ -16,14 +16,16 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" + azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" ) func TestAzureMonitorBuildQueries(t *testing.T) { datasource := &AzureMonitorDatasource{} - dsInfo := datasourceInfo{ - Settings: azureMonitorSettings{ + dsInfo := types.DatasourceInfo{ + Settings: types.AzureMonitorSettings{ SubscriptionId: "default-subscription", }, } @@ -96,7 +98,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with one dimension", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []azureMonitorDimensionFilter{{"blob", "eq", "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}}, "top": "30", }, queryInterval: duration, @@ -107,7 +109,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with two dimensions", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []azureMonitorDimensionFilter{{"blob", "eq", "*"}, {"tier", "eq", "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}, {Dimension: "tier", Operator: "eq", Filter: "*"}}, "top": "30", }, queryInterval: duration, @@ -149,7 +151,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, } - azureMonitorQuery := &AzureMonitorQuery{ + azureMonitorQuery := &types.AzureMonitorQuery{ URL: "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", UrlComponents: map[string]string{ "metricDefinition": "Microsoft.Compute/virtualMachines", @@ -168,7 +170,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { queries, err := datasource.buildQueries(tsdbQuery, dsInfo) require.NoError(t, err) - if diff := cmp.Diff(azureMonitorQuery, queries[0], cmpopts.IgnoreUnexported(simplejson.Json{}), cmpopts.IgnoreFields(AzureMonitorQuery{}, "Params")); diff != "" { + if diff := cmp.Diff(azureMonitorQuery, queries[0], cmpopts.IgnoreUnexported(simplejson.Json{}), cmpopts.IgnoreFields(types.AzureMonitorQuery{}, "Params")); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) } @@ -219,14 +221,14 @@ func TestAzureMonitorParseResponse(t *testing.T) { tests := []struct { name string responseFile string - mockQuery *AzureMonitorQuery + mockQuery *types.AzureMonitorQuery expectedFrames data.Frames queryIntervalMS int64 }{ { name: "average aggregate time series response", responseFile: "1-azure-monitor-response-avg.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -247,7 +249,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "total aggregate time series response", responseFile: "2-azure-monitor-response-total.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -268,7 +270,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "maximum aggregate time series response", responseFile: "3-azure-monitor-response-maximum.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -289,7 +291,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "minimum aggregate time series response", responseFile: "4-azure-monitor-response-minimum.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -310,7 +312,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "count aggregate time series response", responseFile: "5-azure-monitor-response-count.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -331,7 +333,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "single dimension time series response", responseFile: "6-azure-monitor-response-single-dimension.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ UrlComponents: map[string]string{ "resourceName": "grafana", }, @@ -365,7 +367,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "with alias patterns in the query", responseFile: "2-azure-monitor-response-total.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", UrlComponents: map[string]string{ "resourceName": "grafana", @@ -387,7 +389,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "single dimension with alias", responseFile: "6-azure-monitor-response-single-dimension.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ Alias: "{{dimensionname}}={{DimensionValue}}", UrlComponents: map[string]string{ "resourceName": "grafana", @@ -424,7 +426,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "multiple dimension time series response with label alias", responseFile: "7-azure-monitor-response-multi-dimension.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ Alias: "{{resourcegroup}} {Blob Type={{blobtype}}, Tier={{Tier}}}", UrlComponents: map[string]string{ "resourceName": "grafana", @@ -462,7 +464,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { { name: "unspecified unit with alias should not panic", responseFile: "8-azure-monitor-response-unspecified-unit.json", - mockQuery: &AzureMonitorQuery{ + mockQuery: &types.AzureMonitorQuery{ Alias: "custom", UrlComponents: map[string]string{ "resourceName": "grafana", @@ -540,22 +542,22 @@ func TestFindClosestAllowIntervalMS(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - interval := findClosestAllowedIntervalMS(tt.inputInterval, tt.allowedTimeGrains) + interval := azTime.FindClosestAllowedIntervalMS(tt.inputInterval, tt.allowedTimeGrains) require.Equal(t, tt.expectedInterval, interval) }) } } -func loadTestFile(t *testing.T, name string) AzureMonitorResponse { +func loadTestFile(t *testing.T, name string) types.AzureMonitorResponse { t.Helper() - path := filepath.Join("testdata", name) + path := filepath.Join("../testdata", name) // Ignore gosec warning G304 since it's a test // nolint:gosec jsonBody, err := ioutil.ReadFile(path) require.NoError(t, err) - var azData AzureMonitorResponse + var azData types.AzureMonitorResponse err = json.Unmarshal(jsonBody, &azData) require.NoError(t, err) return azData @@ -563,7 +565,7 @@ func loadTestFile(t *testing.T, name string) AzureMonitorResponse { func TestAzureMonitorCreateRequest(t *testing.T) { ctx := context.Background() - dsInfo := datasourceInfo{} + dsInfo := types.DatasourceInfo{} url := "http://ds/" tests := []struct { diff --git a/pkg/tsdb/azuremonitor/url-builder.go b/pkg/tsdb/azuremonitor/metrics/url-builder.go similarity index 98% rename from pkg/tsdb/azuremonitor/url-builder.go rename to pkg/tsdb/azuremonitor/metrics/url-builder.go index a595daf8bad..53bee4f2269 100644 --- a/pkg/tsdb/azuremonitor/url-builder.go +++ b/pkg/tsdb/azuremonitor/metrics/url-builder.go @@ -1,4 +1,4 @@ -package azuremonitor +package metrics import ( "fmt" diff --git a/pkg/tsdb/azuremonitor/url-builder_test.go b/pkg/tsdb/azuremonitor/metrics/url-builder_test.go similarity index 99% rename from pkg/tsdb/azuremonitor/url-builder_test.go rename to pkg/tsdb/azuremonitor/metrics/url-builder_test.go index a2e6e58ed55..f6bb94a4664 100644 --- a/pkg/tsdb/azuremonitor/url-builder_test.go +++ b/pkg/tsdb/azuremonitor/metrics/url-builder_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package metrics import ( "testing" diff --git a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go similarity index 82% rename from pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go rename to pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index c501fa9e057..39ce41b8b22 100644 --- a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -1,4 +1,4 @@ -package azuremonitor +package resourcegraph import ( "bytes" @@ -17,14 +17,23 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azlog" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/macros" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/grafana/grafana/pkg/util/errutil" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/context/ctxhttp" ) +// AzureResourceGraphResponse is the json response object from the Azure Resource Graph Analytics API. +type AzureResourceGraphResponse struct { + Data types.AzureResponseTable `json:"data"` +} + // AzureResourceGraphDatasource calls the Azure Resource Graph API's type AzureResourceGraphDatasource struct { - proxy serviceProxy + Proxy types.ServiceProxy } // AzureResourceGraphQuery is the query request that is built from the saved values for @@ -41,15 +50,15 @@ type AzureResourceGraphQuery struct { const argAPIVersion = "2021-06-01-preview" const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources" -func (e *AzureResourceGraphDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { - e.proxy.Do(rw, req, cli) +func (e *AzureResourceGraphDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { + e.Proxy.Do(rw, req, cli) } // executeTimeSeriesQuery does the following: // 1. builds the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API // 3. parses the responses for each query into data frames -func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, +func (e *AzureResourceGraphDatasource) ExecuteTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo types.DatasourceInfo, client *http.Client, url string, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := &backend.QueryDataResponse{ Responses: map[string]backend.DataResponse{}, @@ -67,7 +76,14 @@ func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Contex return result, nil } -func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, dsInfo datasourceInfo) ([]*AzureResourceGraphQuery, error) { +type argJSONQuery struct { + AzureResourceGraph struct { + Query string `json:"query"` + ResultFormat string `json:"resultFormat"` + } `json:"azureResourceGraph"` +} + +func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*AzureResourceGraphQuery, error) { var azureResourceGraphQueries []*AzureResourceGraphQuery for _, query := range queries { @@ -85,7 +101,7 @@ func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, resultFormat = "table" } - interpolatedQuery, err := KqlInterpolate(query, dsInfo, azureResourceGraphTarget.Query) + interpolatedQuery, err := macros.KqlInterpolate(query, dsInfo, azureResourceGraphTarget.Query) if err != nil { return nil, err @@ -103,7 +119,7 @@ func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, return azureResourceGraphQueries, nil } -func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo datasourceInfo, client *http.Client, +func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo types.DatasourceInfo, client *http.Client, dsURL string, tracer tracing.Tracer) backend.DataResponse { dataResponse := backend.DataResponse{} @@ -173,18 +189,18 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query * return dataResponseErrorWithExecuted(err) } - frame, err := ResponseTableToFrame(&argResponse.Data) + frame, err := loganalytics.ResponseTableToFrame(&argResponse.Data) if err != nil { return dataResponseErrorWithExecuted(err) } - azurePortalUrl, err := getAzurePortalUrl(dsInfo.Cloud) + azurePortalUrl, err := GetAzurePortalUrl(dsInfo.Cloud) if err != nil { return dataResponseErrorWithExecuted(err) } url := azurePortalUrl + "/#blade/HubsExtension/ArgQueryBlade/query/" + url.PathEscape(query.InterpolatedQuery) - frameWithLink := addConfigLinks(*frame, url) + frameWithLink := AddConfigLinks(*frame, url) if frameWithLink.Meta == nil { frameWithLink.Meta = &data.FrameMeta{} } @@ -194,7 +210,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query * return dataResponse } -func addConfigLinks(frame data.Frame, dl string) data.Frame { +func AddConfigLinks(frame data.Frame, dl string) data.Frame { for i := range frame.Fields { if frame.Fields[i].Config == nil { frame.Fields[i].Config = &data.FieldConfig{} @@ -209,7 +225,7 @@ func addConfigLinks(frame data.Frame, dl string) data.Frame { return frame } -func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte, url string) (*http.Request, error) { +func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo types.DatasourceInfo, reqBody []byte, url string) (*http.Request, error) { req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) if err != nil { azlog.Debug("Failed to create request", "error", err) @@ -250,7 +266,7 @@ func (e *AzureResourceGraphDatasource) unmarshalResponse(res *http.Response) (Az return data, nil } -func getAzurePortalUrl(azureCloud string) (string, error) { +func GetAzurePortalUrl(azureCloud string) (string, error) { switch azureCloud { case setting.AzurePublic: return "https://portal.azure.com", nil diff --git a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource_test.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go similarity index 94% rename from pkg/tsdb/azuremonitor/azure-resource-graph-datasource_test.go rename to pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go index 42466777a9d..a742a066a8f 100644 --- a/pkg/tsdb/azuremonitor/azure-resource-graph-datasource_test.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package resourcegraph import ( "context" @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -68,7 +69,7 @@ func TestBuildingAzureResourceGraphQueries(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - queries, err := datasource.buildQueries(tt.queryModel, datasourceInfo{}) + queries, err := datasource.buildQueries(tt.queryModel, types.DatasourceInfo{}) tt.Err(t, err) if diff := cmp.Diff(tt.azureResourceGraphQueries, queries, cmpopts.IgnoreUnexported(simplejson.Json{})); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) @@ -80,7 +81,7 @@ func TestBuildingAzureResourceGraphQueries(t *testing.T) { func TestAzureResourceGraphCreateRequest(t *testing.T) { ctx := context.Background() url := "http://ds" - dsInfo := datasourceInfo{} + dsInfo := types.DatasourceInfo{} tests := []struct { name string @@ -120,7 +121,7 @@ func TestAddConfigData(t *testing.T) { frame := data.Frame{ Fields: []*data.Field{&field}, } - frameWithLink := addConfigLinks(frame, "http://ds") + frameWithLink := AddConfigLinks(frame, "http://ds") expectedFrameWithLink := data.Frame{ Fields: []*data.Field{ { @@ -145,7 +146,7 @@ func TestGetAzurePortalUrl(t *testing.T) { } for _, cloud := range clouds { - azurePortalUrl, err := getAzurePortalUrl(cloud) + azurePortalUrl, err := GetAzurePortalUrl(cloud) if err != nil { t.Errorf("The cloud not supported") } diff --git a/pkg/tsdb/azuremonitor/routes.go b/pkg/tsdb/azuremonitor/routes.go index 9c834f94955..e2a370a36dc 100644 --- a/pkg/tsdb/azuremonitor/routes.go +++ b/pkg/tsdb/azuremonitor/routes.go @@ -1,71 +1,55 @@ package azuremonitor -import "github.com/grafana/grafana/pkg/setting" +import ( + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/deprecated" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) // Azure cloud query types const ( azureMonitor = "Azure Monitor" - appInsights = "Application Insights" azureLogAnalytics = "Azure Log Analytics" - insightsAnalytics = "Insights Analytics" azureResourceGraph = "Azure Resource Graph" ) -type azRoute struct { - URL string - Scopes []string - Headers map[string]string -} - -var azManagement = azRoute{ +var azManagement = types.AzRoute{ URL: "https://management.azure.com", Scopes: []string{"https://management.azure.com/.default"}, Headers: map[string]string{"x-ms-app": "Grafana"}, } -var azUSGovManagement = azRoute{ +var azUSGovManagement = types.AzRoute{ URL: "https://management.usgovcloudapi.net", Scopes: []string{"https://management.usgovcloudapi.net/.default"}, Headers: map[string]string{"x-ms-app": "Grafana"}, } -var azGermanyManagement = azRoute{ +var azGermanyManagement = types.AzRoute{ URL: "https://management.microsoftazure.de", Scopes: []string{"https://management.microsoftazure.de/.default"}, Headers: map[string]string{"x-ms-app": "Grafana"}, } -var azChinaManagement = azRoute{ +var azChinaManagement = types.AzRoute{ URL: "https://management.chinacloudapi.cn", Scopes: []string{"https://management.chinacloudapi.cn/.default"}, Headers: map[string]string{"x-ms-app": "Grafana"}, } -var azAppInsights = azRoute{ - URL: "https://api.applicationinsights.io", - Scopes: []string{}, - Headers: map[string]string{"x-ms-app": "Grafana"}, -} - -var azChinaAppInsights = azRoute{ - URL: "https://api.applicationinsights.azure.cn", - Scopes: []string{}, - Headers: map[string]string{"x-ms-app": "Grafana"}, -} - -var azLogAnalytics = azRoute{ +var azLogAnalytics = types.AzRoute{ URL: "https://api.loganalytics.io", Scopes: []string{"https://api.loganalytics.io/.default"}, Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"}, } -var azChinaLogAnalytics = azRoute{ +var azChinaLogAnalytics = types.AzRoute{ URL: "https://api.loganalytics.azure.cn", Scopes: []string{"https://api.loganalytics.azure.cn/.default"}, Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"}, } -var azUSGovLogAnalytics = azRoute{ +var azUSGovLogAnalytics = types.AzRoute{ URL: "https://api.loganalytics.us", Scopes: []string{"https://api.loganalytics.us/.default"}, Headers: map[string]string{"x-ms-app": "Grafana", "Cache-Control": "public, max-age=60"}, @@ -74,13 +58,13 @@ var azUSGovLogAnalytics = azRoute{ var ( // The different Azure routes are identified by its cloud (e.g. public or gov) // and the service to query (e.g. Azure Monitor or Azure Log Analytics) - routes = map[string]map[string]azRoute{ + routes = map[string]map[string]types.AzRoute{ setting.AzurePublic: { - azureMonitor: azManagement, - azureLogAnalytics: azLogAnalytics, - azureResourceGraph: azManagement, - appInsights: azAppInsights, - insightsAnalytics: azAppInsights, + azureMonitor: azManagement, + azureLogAnalytics: azLogAnalytics, + azureResourceGraph: azManagement, + deprecated.AppInsights: deprecated.AzAppInsights, + deprecated.InsightsAnalytics: deprecated.AzAppInsights, }, setting.AzureUSGovernment: { azureMonitor: azUSGovManagement, @@ -91,11 +75,11 @@ var ( azureMonitor: azGermanyManagement, }, setting.AzureChina: { - azureMonitor: azChinaManagement, - azureLogAnalytics: azChinaLogAnalytics, - azureResourceGraph: azChinaManagement, - appInsights: azChinaAppInsights, - insightsAnalytics: azChinaAppInsights, + azureMonitor: azChinaManagement, + azureLogAnalytics: azChinaLogAnalytics, + azureResourceGraph: azChinaManagement, + deprecated.AppInsights: deprecated.AzChinaAppInsights, + deprecated.InsightsAnalytics: deprecated.AzChinaAppInsights, }, } ) diff --git a/pkg/tsdb/azuremonitor/azuremonitor-time.go b/pkg/tsdb/azuremonitor/time/azuremonitor-time.go similarity index 66% rename from pkg/tsdb/azuremonitor/azuremonitor-time.go rename to pkg/tsdb/azuremonitor/time/azuremonitor-time.go index f631b864c9c..3a29f24f8dc 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-time.go +++ b/pkg/tsdb/azuremonitor/time/azuremonitor-time.go @@ -1,10 +1,15 @@ -package azuremonitor +package time -// setAutoTimeGrain tries to find the closest interval to the query's intervalMs value +var ( + // 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds + defaultAllowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} +) + +// SetAutoTimeGrain tries to find the closest interval to the query's intervalMs value // if the metric has a limited set of possible intervals/time grains then use those // instead of the default list of intervals -func setAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) { - autoInterval := findClosestAllowedIntervalMS(intervalMs, timeGrains) +func SetAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) { + autoInterval := FindClosestAllowedIntervalMS(intervalMs, timeGrains) tg := &TimeGrain{} autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval) if err != nil { @@ -14,12 +19,12 @@ func setAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) { return autoTimeGrain, nil } -// findClosestAllowedIntervalMs is used for the auto time grain setting. +// FindClosestAllowedIntervalMS is used for the auto time grain setting. // It finds the closest time grain from the list of allowed time grains for Azure Monitor // using the Grafana interval in milliseconds // Some metrics only allow a limited list of time grains. The allowedTimeGrains parameter // allows overriding the default list of allowed time grains. -func findClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 { +func FindClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 { allowedIntervals := defaultAllowedIntervalsMS if len(allowedTimeGrains) > 0 { diff --git a/pkg/tsdb/azuremonitor/time-grain.go b/pkg/tsdb/azuremonitor/time/time-grain.go similarity index 98% rename from pkg/tsdb/azuremonitor/time-grain.go rename to pkg/tsdb/azuremonitor/time/time-grain.go index 1e7ad5aff50..c7be8890fe5 100644 --- a/pkg/tsdb/azuremonitor/time-grain.go +++ b/pkg/tsdb/azuremonitor/time/time-grain.go @@ -1,4 +1,4 @@ -package azuremonitor +package time import ( "fmt" diff --git a/pkg/tsdb/azuremonitor/time-grain_test.go b/pkg/tsdb/azuremonitor/time/time-grain_test.go similarity index 98% rename from pkg/tsdb/azuremonitor/time-grain_test.go rename to pkg/tsdb/azuremonitor/time/time-grain_test.go index 58485342962..4ff6813bec7 100644 --- a/pkg/tsdb/azuremonitor/time-grain_test.go +++ b/pkg/tsdb/azuremonitor/time/time-grain_test.go @@ -1,4 +1,4 @@ -package azuremonitor +package time import ( "testing" diff --git a/pkg/tsdb/azuremonitor/types.go b/pkg/tsdb/azuremonitor/types/types.go similarity index 53% rename from pkg/tsdb/azuremonitor/types.go rename to pkg/tsdb/azuremonitor/types/types.go index 373955050a5..6d310f850fd 100644 --- a/pkg/tsdb/azuremonitor/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -1,15 +1,54 @@ -package azuremonitor +package types import ( - "encoding/json" "fmt" + "net/http" "net/url" - "strings" + "regexp" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" ) +const ( + TimeSeries = "time_series" +) + +var ( + LegendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) +) + +type AzRoute struct { + URL string + Scopes []string + Headers map[string]string +} + +type AzureMonitorSettings struct { + SubscriptionId string `json:"subscriptionId"` + LogAnalyticsDefaultWorkspace string `json:"logAnalyticsDefaultWorkspace"` + AppInsightsAppId string `json:"appInsightsAppId"` +} + +type DatasourceService struct { + URL string + HTTPClient *http.Client +} + +type DatasourceInfo struct { + Cloud string + Credentials azcredentials.AzureCredentials + Settings AzureMonitorSettings + Routes map[string]AzRoute + Services map[string]DatasourceService + + JSONData map[string]interface{} + DecryptedSecureJSONData map[string]string + DatasourceID int64 + OrgID int64 +} + // AzureMonitorQuery is the query for all the services as they have similar queries // with a url, a querystring and an alias field type AzureMonitorQuery struct { @@ -57,16 +96,6 @@ type AzureMonitorResponse struct { Resourceregion string `json:"resourceregion"` } -// AzureLogAnalyticsResponse is the json response object from the Azure Log Analytics API. -type AzureLogAnalyticsResponse struct { - Tables []AzureResponseTable `json:"tables"` -} - -// AzureResourceGraphResponse is the json response object from the Azure Resource Graph Analytics API. -type AzureResourceGraphResponse struct { - Data AzureResponseTable `json:"data"` -} - // AzureResponseTable is the table format for Azure responses type AzureResponseTable struct { Name string `json:"name"` @@ -77,8 +106,8 @@ type AzureResponseTable struct { Rows [][]interface{} `json:"rows"` } -// azureMonitorJSONQuery is the frontend JSON query model for an Azure Monitor query. -type azureMonitorJSONQuery struct { +// AzureMonitorJSONQuery is the frontend JSON query model for an Azure Monitor query. +type AzureMonitorJSONQuery struct { AzureMonitor struct { Aggregation string `json:"aggregation"` Alias string `json:"alias"` @@ -94,20 +123,20 @@ type azureMonitorJSONQuery struct { TimeGrain string `json:"timeGrain"` Top string `json:"top"` - DimensionFilters []azureMonitorDimensionFilter `json:"dimensionFilters"` // new model + DimensionFilters []AzureMonitorDimensionFilter `json:"dimensionFilters"` // new model } `json:"azureMonitor"` Subscription string `json:"subscription"` } -// azureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric +// AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric // queries like "BlobType", "eq", "*" -type azureMonitorDimensionFilter struct { +type AzureMonitorDimensionFilter struct { Dimension string `json:"dimension"` Operator string `json:"operator"` Filter string `json:"filter"` } -func (a azureMonitorDimensionFilter) String() string { +func (a AzureMonitorDimensionFilter) String() string { filter := "*" if a.Filter != "" { filter = a.Filter @@ -115,29 +144,8 @@ func (a azureMonitorDimensionFilter) String() string { return fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter) } -// insightsJSONQuery is the frontend JSON query model for an Azure Application Insights query. -type insightsJSONQuery struct { - AppInsights struct { - Aggregation string `json:"aggregation"` - Alias string `json:"alias"` - AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` - Dimensions InsightsDimensions `json:"dimension"` - DimensionFilter string `json:"dimensionFilter"` - MetricName string `json:"metricName"` - TimeGrain string `json:"timeGrain"` - } `json:"appInsights"` - Raw *bool `json:"raw"` -} - -type insightsAnalyticsJSONQuery struct { - InsightsAnalytics struct { - Query string `json:"query"` - ResultFormat string `json:"resultFormat"` - } `json:"insightsAnalytics"` -} - -// logJSONQuery is the frontend JSON query model for an Azure Log Analytics query. -type logJSONQuery struct { +// LogJSONQuery is the frontend JSON query model for an Azure Log Analytics query. +type LogJSONQuery struct { AzureLogAnalytics struct { Query string `json:"query"` ResultFormat string `json:"resultFormat"` @@ -148,69 +156,22 @@ type logJSONQuery struct { } `json:"azureLogAnalytics"` } -type argJSONQuery struct { - AzureResourceGraph struct { - Query string `json:"query"` - ResultFormat string `json:"resultFormat"` - } `json:"azureResourceGraph"` -} - -// metricChartDefinition is the JSON model for a metrics chart definition -type metricChartDefinition struct { +// MetricChartDefinition is the JSON model for a metrics chart definition +type MetricChartDefinition struct { ResourceMetadata map[string]string `json:"resourceMetadata"` Name string `json:"name"` AggregationType int `json:"aggregationType"` Namespace string `json:"namespace"` - MetricVisualization metricVisualization `json:"metricVisualization"` + MetricVisualization MetricVisualization `json:"metricVisualization"` } -// metricVisualization is the JSON model for the visualization field of a +// MetricVisualization is the JSON model for the visualization field of a // metricChartDefinition -type metricVisualization struct { +type MetricVisualization struct { DisplayName string `json:"displayName"` ResourceDisplayName string `json:"resourceDisplayName"` } -// InsightsDimensions will unmarshal from a JSON string, or an array of strings, -// into a string array. This exists to support an older query format which is updated -// when a user saves the query or it is sent from the front end, but may not be when -// alerting fetches the model. -type InsightsDimensions []string - -// UnmarshalJSON fulfills the json.Unmarshaler interface type. -func (s *InsightsDimensions) UnmarshalJSON(data []byte) error { - *s = InsightsDimensions{} - if string(data) == "null" || string(data) == "" { - return nil - } - if strings.ToLower(string(data)) == `"none"` { - return nil - } - if data[0] == '[' { - var sa []string - err := json.Unmarshal(data, &sa) - if err != nil { - return err - } - dimensions := []string{} - for _, v := range sa { - if v == "none" || v == "None" { - continue - } - dimensions = append(dimensions, v) - } - *s = InsightsDimensions(dimensions) - return nil - } - - var str string - err := json.Unmarshal(data, &str) - if err != nil { - return fmt.Errorf("could not parse %q as string or array: %w", string(data), err) - } - if str != "" { - *s = InsightsDimensions{str} - return nil - } - return nil +type ServiceProxy interface { + Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter } From d4b05f9421cbadf9f318a5cd71be4d2f767dcbf5 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 2 Mar 2022 06:42:30 -0800 Subject: [PATCH 108/125] AzureMonitor: Remove deprecated configuration parameters (#45860) --- .betterer.results | 2 +- .../components/ConfigEditor.tsx | 58 +++++-------------- .../components}/AnalyticsConfig.test.tsx | 5 +- .../components}/AnalyticsConfig.tsx | 9 +-- .../components}/InsightsConfig.test.tsx | 13 +++-- .../components}/InsightsConfig.tsx | 49 ++++++++++------ .../InsightsConfig.test.tsx.snap | 5 ++ .../components/deprecated/utils.tsx | 13 +++++ .../credentials.ts | 5 +- 9 files changed, 81 insertions(+), 78 deletions(-) rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{ => deprecated/components}/AnalyticsConfig.test.tsx (99%) rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{ => deprecated/components}/AnalyticsConfig.tsx (89%) rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{ => deprecated/components}/InsightsConfig.test.tsx (93%) rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{ => deprecated/components}/InsightsConfig.tsx (65%) rename public/app/plugins/datasource/grafana-azure-monitor-datasource/components/{ => deprecated/components}/__snapshots__/InsightsConfig.test.tsx.snap (96%) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/utils.tsx diff --git a/.betterer.results b/.betterer.results index f228dec2e0f..7919254746f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -326,7 +326,7 @@ exports[`no enzyme tests`] = { "public/app/plugins/datasource/elasticsearch/configuration/DataLinks.test.tsx:2916632804": [ [1, 17, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.test.tsx:866257119": [ + "public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.test.tsx:1635510338": [ [1, 19, 13, "RegExp match", "2409514259"] ], "public/app/plugins/datasource/influxdb/components/ConfigEditor.test.tsx:767000341": [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx index 483960fd8cd..07bf6c63827 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx @@ -1,21 +1,15 @@ -import React, { PureComponent } from 'react'; -import { - DataSourcePluginOptionsEditorProps, - SelectableValue, - updateDatasourcePluginJsonDataOption, - updateDatasourcePluginOption, - updateDatasourcePluginResetOption, - updateDatasourcePluginSecureJsonDataOption, -} from '@grafana/data'; -import { Alert } from '@grafana/ui'; -import { MonitorConfig } from './MonitorConfig'; -import { AnalyticsConfig } from './AnalyticsConfig'; +import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data'; import { getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { InsightsConfig } from './InsightsConfig'; +import { Alert } from '@grafana/ui'; +import React, { PureComponent } from 'react'; + import ResponseParser from '../azure_monitor/response_parser'; import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types'; -import { isAppInsightsConfigured } from '../credentials'; import { routeNames } from '../utils/common'; +import { AnalyticsConfig } from './deprecated/components/AnalyticsConfig'; +import { InsightsConfig } from './deprecated/components/InsightsConfig'; +import { gtGrafana9, isAppInsightsConfigured } from './deprecated/utils'; +import { MonitorConfig } from './MonitorConfig'; export type Props = DataSourcePluginOptionsEditorProps; @@ -27,7 +21,6 @@ interface ErrorMessage { export interface State { unsaved: boolean; - appInsightsInitiallyConfigured: boolean; error?: ErrorMessage; } @@ -40,7 +33,6 @@ export class ConfigEditor extends PureComponent { this.state = { unsaved: false, - appInsightsInitiallyConfigured: isAppInsightsConfigured(props.options), }; this.baseURL = `/api/datasources/${this.props.options.id}/resources/${routeNames.azureMonitor}/subscriptions`; } @@ -90,24 +82,6 @@ export class ConfigEditor extends PureComponent { } }; - // TODO: Used only by InsightsConfig - private onUpdateJsonDataOption = - (key: keyof AzureDataSourceJsonData) => (event: React.SyntheticEvent) => { - updateDatasourcePluginJsonDataOption(this.props, key, event.currentTarget.value); - }; - - // TODO: Used only by InsightsConfig - private onUpdateSecureJsonDataOption = - (key: keyof AzureDataSourceSecureJsonData) => - (event: React.SyntheticEvent) => { - updateDatasourcePluginSecureJsonDataOption(this.props, key, event.currentTarget.value); - }; - - // TODO: Used only by InsightsConfig - private resetSecureKey = (key: keyof AzureDataSourceSecureJsonData) => { - updateDatasourcePluginResetOption(this.props, key); - }; - render() { const { options } = this.props; const { error } = this.state; @@ -115,16 +89,14 @@ export class ConfigEditor extends PureComponent { return ( <> - - {this.state.appInsightsInitiallyConfigured && ( - + {/* Remove with Grafana 9 */} + {!gtGrafana9() && ( + <> + + {isAppInsightsConfigured(options) && } + )} - + {/* ===================== */} {error && (

    {error.description}

    diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.test.tsx similarity index 99% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.test.tsx rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.test.tsx index 5863a0b29d4..b6e1edea44d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.test.tsx @@ -1,7 +1,8 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; -import AnalyticsConfig, { Props } from './AnalyticsConfig'; import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import AnalyticsConfig, { Props } from './AnalyticsConfig'; const setup = (propsFunc?: (props: Props) => Props) => { let props: Props = { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.tsx similarity index 89% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.tsx rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.tsx index 16d36788b9b..17df310498e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/AnalyticsConfig.tsx @@ -1,8 +1,9 @@ +import { Alert, Button } from '@grafana/ui'; import React, { FunctionComponent, useMemo } from 'react'; -import { AzureCredentialsForm } from './AzureCredentialsForm'; -import { Button, Alert } from '@grafana/ui'; -import { AzureDataSourceSettings } from '../types'; -import { getCredentials } from '../credentials'; + +import { getCredentials } from '../../../credentials'; +import { AzureDataSourceSettings } from '../../../types'; +import { AzureCredentialsForm } from '../../AzureCredentialsForm'; export interface Props { options: AzureDataSourceSettings; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.test.tsx similarity index 93% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.test.tsx rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.test.tsx index cddc9a4b348..3c3e579701d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.test.tsx @@ -1,7 +1,10 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import InsightsConfig, { Props } from './InsightsConfig'; import { Button, LegacyForms } from '@grafana/ui'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import { Props } from '../../ConfigEditor'; +import InsightsConfig from './InsightsConfig'; + const { Input } = LegacyForms; const setup = (propOverrides?: object) => { @@ -33,9 +36,7 @@ const setup = (propOverrides?: object) => { version: 1, readOnly: false, }, - onUpdateJsonDataOption: jest.fn(), - onUpdateSecureJsonDataOption: jest.fn(), - onResetOptionKey: jest.fn(), + onOptionsChange: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.tsx similarity index 65% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.tsx rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.tsx index 1e57a9b18a3..52669126a5e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/InsightsConfig.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/InsightsConfig.tsx @@ -1,25 +1,38 @@ +import { + updateDatasourcePluginJsonDataOption, + updateDatasourcePluginResetOption, + updateDatasourcePluginSecureJsonDataOption, +} from '@grafana/data'; +import { Alert, Button, InlineFormLabel, LegacyForms } from '@grafana/ui'; import React, { PureComponent } from 'react'; -import { InlineFormLabel, Button, LegacyForms, Alert } from '@grafana/ui'; -const { Input } = LegacyForms; -import { AzureDataSourceSettings, AzureDataSourceJsonData, AzureDataSourceSecureJsonData } from '../types'; -export interface Props { - options: AzureDataSourceSettings; - onUpdateJsonDataOption: ( - key: keyof AzureDataSourceJsonData - ) => (event: React.SyntheticEvent) => void; - onUpdateSecureJsonDataOption: ( - key: keyof AzureDataSourceSecureJsonData - ) => (event: React.SyntheticEvent) => void; - onResetOptionKey: (key: keyof AzureDataSourceSecureJsonData) => void; -} +import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData } from '../../../types'; +import { Props } from '../../ConfigEditor'; + +const { Input } = LegacyForms; + export class InsightsConfig extends PureComponent { - onAppInsightsResetApiKey = () => { - this.props.onResetOptionKey('appInsightsApiKey'); + private onAppInsightsResetApiKey = () => { + this.resetSecureKey('appInsightsApiKey'); + }; + + private onUpdateJsonDataOption = + (key: keyof AzureDataSourceJsonData) => (event: React.SyntheticEvent) => { + updateDatasourcePluginJsonDataOption(this.props, key, event.currentTarget.value); + }; + + private onUpdateSecureJsonDataOption = + (key: keyof AzureDataSourceSecureJsonData) => + (event: React.SyntheticEvent) => { + updateDatasourcePluginSecureJsonDataOption(this.props, key, event.currentTarget.value); + }; + + private resetSecureKey = (key: keyof AzureDataSourceSecureJsonData) => { + updateDatasourcePluginResetOption(this.props, key); }; render() { - const { options, onUpdateJsonDataOption, onUpdateSecureJsonDataOption } = this.props; + const { options } = this.props; return ( <>

    Azure Application Insights

    @@ -55,7 +68,7 @@ export class InsightsConfig extends PureComponent { className="width-30" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" value={options.secureJsonData!.appInsightsApiKey || ''} - onChange={onUpdateSecureJsonDataOption('appInsightsApiKey')} + onChange={this.onUpdateSecureJsonDataOption('appInsightsApiKey')} disabled={this.props.options.readOnly} />
    @@ -69,7 +82,7 @@ export class InsightsConfig extends PureComponent {
    diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/__snapshots__/InsightsConfig.test.tsx.snap similarity index 96% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/__snapshots__/InsightsConfig.test.tsx.snap index 6d19dc3246a..4685035e90f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/components/__snapshots__/InsightsConfig.test.tsx.snap @@ -65,6 +65,7 @@ exports[`Render should disable insights api key input 1`] = ` >
    @@ -106,6 +107,7 @@ exports[`Render should enable insights api key input 1`] = ` > @@ -128,6 +130,7 @@ exports[`Render should enable insights api key input 1`] = ` >
    @@ -170,6 +173,7 @@ exports[`Render should render component 1`] = ` @@ -193,6 +197,7 @@ exports[`Render should render component 1`] = `
    diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/utils.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/utils.tsx new file mode 100644 index 00000000000..ddf958cb4db --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/deprecated/utils.tsx @@ -0,0 +1,13 @@ +import { config } from '@grafana/runtime'; +import { gt, valid } from 'semver'; + +import { AzureDataSourceSettings } from '../../types'; + +export function isAppInsightsConfigured(options: AzureDataSourceSettings) { + return !!(options.jsonData.appInsightsAppId && options.secureJsonFields.appInsightsApiKey); +} + +export function gtGrafana9() { + // AppInsights configuration will be removed with Grafana 9 + return valid(config.buildInfo.version) && gt(config.buildInfo.version, '9.0.0-beta1'); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/credentials.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/credentials.ts index ad12b1da2ce..e92905fb035 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/credentials.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/credentials.ts @@ -1,4 +1,5 @@ import { config } from '@grafana/runtime'; + import { AzureAuthType, AzureCloud, @@ -165,7 +166,3 @@ export function updateCredentials( return options; } } - -export function isAppInsightsConfigured(options: AzureDataSourceSettings) { - return !!(options.jsonData.appInsightsAppId && options.secureJsonFields.appInsightsApiKey); -} From 67e605c5c0f72d838886b3bdf587902f2e508517 Mon Sep 17 00:00:00 2001 From: Zoe Braiterman Date: Wed, 2 Mar 2022 09:52:45 -0500 Subject: [PATCH 109/125] Update README.md (#45946) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f4873c5b0e4..8e2299d522d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ ![Grafana](docs/logo-horizontal.png) -The open-source platform for monitoring and observability. +The open-source platform for monitoring and observability [![License](https://img.shields.io/github/license/grafana/grafana)](LICENSE) [![Drone](https://drone.grafana.net/api/badges/grafana/grafana/status.svg)](https://drone.grafana.net/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) -Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored. Create, explore, and share dashboards with your team and foster a data driven culture: +Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored. Create, explore, and share dashboards with your team and foster a data-driven culture: -- **Visualize:** Fast and flexible client side graphs with a multitude of options. Panel plugins offer many different ways to visualize metrics and logs. +- **Visualizations:** Fast and flexible client side graphs with a multitude of options. Panel plugins offer many different ways to visualize metrics and logs. - **Dynamic Dashboards:** Create dynamic & reusable dashboards with template variables that appear as dropdowns at the top of the dashboard. - **Explore Metrics:** Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. - **Explore Logs:** Experience the magic of switching from metrics to logs with preserved label filters. Quickly search through all your logs or streaming them live. From 5c05a3deb9730cee7f8eaca5a77ab6d21e095e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Mar 2022 16:16:19 +0100 Subject: [PATCH 110/125] Prometheus: Save query editor mode default (#46074) --- .../PromQueryBuilderOptions.test.tsx | 2 +- .../components/PromQueryEditorSelector.tsx | 7 +-- .../prometheus/querybuilder/shared/types.ts | 6 +- .../prometheus/querybuilder/state.test.ts | 34 +++++++++++ .../prometheus/querybuilder/state.ts | 60 +++++++++++++++++++ .../prometheus/querybuilder/types.ts | 33 +--------- 6 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/state.test.ts create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/state.ts diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx index 153a72ce5ee..dcc03f3e4aa 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { PromQuery } from '../../types'; -import { getQueryWithDefaults } from '../types'; +import { getQueryWithDefaults } from '../state'; import { CoreApp } from '@grafana/data'; import { PromQueryBuilderOptions } from './PromQueryBuilderOptions'; import { selectOptionInTest } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index 2062f6a71e4..49a98f1b025 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -13,7 +13,7 @@ import { buildVisualQueryFromString } from '../parsing'; import { PromQueryCodeEditor } from './PromQueryCodeEditor'; import { PromQueryBuilderContainer } from './PromQueryBuilderContainer'; import { PromQueryBuilderOptions } from './PromQueryBuilderOptions'; -import { getQueryWithDefaults } from '../types'; +import { changeEditorMode, getQueryWithDefaults } from '../state'; export const PromQueryEditorSelector = React.memo((props) => { const { onChange, onRunQuery, data } = props; @@ -24,7 +24,6 @@ export const PromQueryEditorSelector = React.memo((props) const onEditorModeChange = useCallback( (newMetricEditorMode: QueryEditorMode) => { - const change = { ...query, editorMode: newMetricEditorMode }; if (newMetricEditorMode === QueryEditorMode.Builder) { const result = buildVisualQueryFromString(query.expr || ''); // If there are errors, give user a chance to decide if they want to go to builder as that can loose some data. @@ -33,7 +32,7 @@ export const PromQueryEditorSelector = React.memo((props) return; } } - onChange(change); + changeEditorMode(query, newMetricEditorMode, onChange); }, [onChange, query] ); @@ -52,7 +51,7 @@ export const PromQueryEditorSelector = React.memo((props) body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query." confirmText="Continue" onConfirm={() => { - onChange({ ...query, editorMode: QueryEditorMode.Builder }); + changeEditorMode(query, QueryEditorMode.Builder, onChange); setParseModalOpen(false); }} onDismiss={() => setParseModalOpen(false)} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts index eec8fb91fe5..e86988500f8 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts @@ -87,9 +87,9 @@ export interface QueryBuilderOperationParamEditorProps { } export enum QueryEditorMode { - Builder, - Code, - Explain, + Code = 'code', + Builder = 'builder', + Explain = 'explain', } export interface VisualQueryModeller { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts new file mode 100644 index 00000000000..46d056845bd --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts @@ -0,0 +1,34 @@ +import { CoreApp } from '@grafana/data'; +import { QueryEditorMode } from './shared/types'; +import { changeEditorMode, getQueryWithDefaults } from './state'; + +describe('getQueryWithDefaults(', () => { + it('should set defaults', () => { + expect(getQueryWithDefaults({ refId: 'A' } as any, CoreApp.Dashboard)).toEqual({ + editorMode: 'builder', + expr: '', + legendFormat: '__auto', + range: true, + refId: 'A', + }); + }); + + it('should set both range and instant to true when in Explore', () => { + expect(getQueryWithDefaults({ refId: 'A' } as any, CoreApp.Explore)).toEqual({ + editorMode: 'builder', + expr: '', + legendFormat: '__auto', + range: true, + instant: true, + refId: 'A', + }); + }); + + it('Changing editor mode with blank query should change default', () => { + changeEditorMode({ refId: 'A', expr: '' }, QueryEditorMode.Code, (query) => { + expect(query.editorMode).toBe(QueryEditorMode.Code); + }); + + expect(getQueryWithDefaults({ refId: 'A' } as any, CoreApp.Dashboard).editorMode).toEqual(QueryEditorMode.Code); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.ts new file mode 100644 index 00000000000..9a69b6ab11d --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.ts @@ -0,0 +1,60 @@ +import { CoreApp } from '@grafana/data'; +import store from 'app/core/store'; +import { LegendFormatMode, PromQuery } from '../types'; +import { QueryEditorMode } from './shared/types'; + +const queryEditorModeDefaultLocalStorageKey = 'PrometheusQueryEditorModeDefault'; + +export function changeEditorMode(query: PromQuery, editorMode: QueryEditorMode, onChange: (query: PromQuery) => void) { + // If empty query store new mode as default + if (query.expr === '') { + store.set(queryEditorModeDefaultLocalStorageKey, editorMode); + } + + onChange({ ...query, editorMode }); +} + +export function getDefaultEditorMode(expr: string) { + // If we already have an expression default to code view + if (expr != null && expr !== '') { + return QueryEditorMode.Code; + } + + const value = store.get(queryEditorModeDefaultLocalStorageKey) as QueryEditorMode; + switch (value) { + case QueryEditorMode.Builder: + case QueryEditorMode.Code: + case QueryEditorMode.Explain: + return value; + default: + return QueryEditorMode.Builder; + } +} + +/** + * Returns query with defaults, and boolean true/false depending on change was required + */ +export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined): PromQuery { + // If no expr (ie new query) then default to builder + let result = query; + + if (!query.editorMode) { + result = { ...query, editorMode: getDefaultEditorMode(query.expr) }; + } + + if (query.expr == null) { + result = { ...result, expr: '', legendFormat: LegendFormatMode.Auto }; + } + + if (query.range == null && query.instant == null) { + // Default to range query + result = { ...result, range: true }; + + // In explore we default to both instant & range + if (app === CoreApp.Explore) { + result.instant = true; + } + } + + return result; +} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/types.ts index e51ad1ff144..e621313dd1e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/types.ts @@ -1,7 +1,5 @@ -import { CoreApp } from '@grafana/data'; -import { LegendFormatMode, PromQuery } from '../types'; import { VisualQueryBinary } from './shared/LokiAndPromQueryModellerBase'; -import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryEditorMode } from './shared/types'; +import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types'; /** * Visual query model @@ -114,32 +112,3 @@ export interface PromQueryPattern { name: string; operations: QueryBuilderOperation[]; } - -/** - * Returns query with defaults, and boolean true/false depending on change was required - */ -export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined): PromQuery { - // If no expr (ie new query) then default to builder - let result = query; - const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder); - - if (result.editorMode !== editorMode) { - result = { ...result, editorMode }; - } - - if (query.expr == null) { - result = { ...result, expr: '', legendFormat: LegendFormatMode.Auto }; - } - - if (query.range == null && query.instant == null) { - // Default to range query - result = { ...result, range: true }; - - // In explore we default to both instant & range - if (app === CoreApp.Explore) { - result.instant = true; - } - } - - return result; -} From 9067715d1d2e4405d8ecb89c7baba1571261f978 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Wed, 2 Mar 2022 08:33:14 -0700 Subject: [PATCH 111/125] UI: Add focus styles to QueryField component (#45933) * Add focus styles to QueryField component --- .betterer.results | 2 +- .../components/QueryField/QueryField.test.tsx | 56 ++++++++++++++----- .../src/components/QueryField/QueryField.tsx | 37 ++++++++++-- 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/.betterer.results b/.betterer.results index 7919254746f..a61180ce407 100644 --- a/.betterer.results +++ b/.betterer.results @@ -44,7 +44,7 @@ exports[`no enzyme tests`] = { "packages/grafana-ui/src/components/Logs/LogRows.test.tsx:2288254498": [ [3, 17, 13, "RegExp match", "2409514259"] ], - "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:1906163280": [ + "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:1297745712": [ [1, 19, 13, "RegExp match", "2409514259"] ], "packages/grafana-ui/src/components/Slider/Slider.test.tsx:2110443485": [ diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx index 1e4deaf59e6..fdfee01c17f 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx @@ -1,30 +1,43 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { QueryField } from './QueryField'; +import { UnThemedQueryField } from './QueryField'; import { Editor } from 'slate'; +import { createTheme } from '@grafana/data'; describe('', () => { it('should render with null initial value', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with empty initial value', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with initial value', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should execute query on blur', () => { const onRun = jest.fn(); const wrapper = shallow( - + ); - const field = wrapper.instance() as QueryField; + const field = wrapper.instance() as UnThemedQueryField; expect(onRun.mock.calls.length).toBe(0); field.handleBlur(new Event('bogus'), new Editor({}), () => {}); expect(onRun.mock.calls.length).toBe(1); @@ -33,9 +46,15 @@ describe('', () => { it('should run onChange with clean text', () => { const onChange = jest.fn(); const wrapper = shallow( - + ); - const field = wrapper.instance() as QueryField; + const field = wrapper.instance() as UnThemedQueryField; field.runOnChange(); expect(onChange.mock.calls.length).toBe(1); expect(onChange.mock.calls[0][0]).toBe('my clean query '); @@ -45,7 +64,8 @@ describe('', () => { const onBlur = jest.fn(); const onRun = jest.fn(); const wrapper = shallow( - ', () => { portalOrigin="mock-origin" /> ); - const field = wrapper.instance() as QueryField; + const field = wrapper.instance() as UnThemedQueryField; expect(onBlur.mock.calls.length).toBe(0); expect(onRun.mock.calls.length).toBe(0); field.handleBlur(new Event('bogus'), new Editor({}), () => {}); @@ -62,14 +82,18 @@ describe('', () => { }); describe('syntaxLoaded', () => { it('should re-render the editor after syntax has fully loaded', () => { - const wrapper: any = shallow(); + const wrapper: any = shallow( + + ); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; wrapper.setProps({ syntaxLoaded: true }); expect(spyOnChange).toHaveBeenCalledWith('fooo', true); }); it('should not re-render the editor if syntax is already loaded', () => { - const wrapper: any = shallow(); + const wrapper: any = shallow( + + ); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); wrapper.setProps({ syntaxLoaded: true }); wrapper.instance().editor = {}; @@ -77,14 +101,18 @@ describe('', () => { expect(spyOnChange).not.toBeCalled(); }); it('should not re-render the editor if editor itself is not defined', () => { - const wrapper: any = shallow(); + const wrapper: any = shallow( + + ); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); wrapper.setProps({ syntaxLoaded: true }); expect(wrapper.instance().editor).toBeFalsy(); expect(spyOnChange).not.toBeCalled(); }); it('should not re-render the editor twice once syntax is fully loaded', () => { - const wrapper: any = shallow(); + const wrapper: any = shallow( + + ); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; wrapper.setProps({ syntaxLoaded: true }); diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx index 1191a70380e..bf22dac6fe0 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -16,10 +16,22 @@ import { SuggestionsPlugin, } from '../../slate-plugins'; -import { makeValue, SCHEMA, CompletionItemGroup, TypeaheadOutput, TypeaheadInput, SuggestionsState } from '../..'; +import { + makeValue, + SCHEMA, + CompletionItemGroup, + TypeaheadOutput, + TypeaheadInput, + SuggestionsState, + Themeable2, +} from '../..'; import { selectors } from '@grafana/e2e-selectors'; +import { css, cx } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { withTheme2 } from '../../themes'; +import { getFocusStyles } from '../../themes/mixins'; -export interface QueryFieldProps { +export interface QueryFieldProps extends Themeable2 { additionalPlugins?: Plugin[]; cleanText?: (text: string) => string; disabled?: boolean; @@ -38,6 +50,7 @@ export interface QueryFieldProps { portalOrigin: string; syntax?: string; syntaxLoaded?: boolean; + theme: GrafanaTheme2; } export interface QueryFieldState { @@ -54,7 +67,7 @@ export interface QueryFieldState { * This component can only process strings. Internally it uses Slate Value. * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example. */ -export class QueryField extends React.PureComponent { +export class UnThemedQueryField extends React.PureComponent { plugins: Plugin[]; runOnChangeDebounced: Function; lastExecutedValue: Value | null = null; @@ -197,13 +210,14 @@ export class QueryField extends React.PureComponent +
    (this.editor = editor!)} @@ -227,4 +241,15 @@ export class QueryField extends React.PureComponent { + const focusStyles = getFocusStyles(theme); + return { + wrapper: css` + &:focus-within { + ${focusStyles} + } + `, + }; +}; From 855979aac552b1f614fbe80b10a910c1d279f060 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 2 Mar 2022 09:04:19 -0800 Subject: [PATCH 112/125] Heatmap: add scale display to legend (#45571) Co-authored-by: Adela Almasan --- .../plugins/panel/heatmap-new/ColorScale.tsx | 113 ++++++++++++++++++ .../panel/heatmap-new/HeatmapPanel.tsx | 34 +++++- .../app/plugins/panel/heatmap-new/module.tsx | 47 +++++--- 3 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 public/app/plugins/panel/heatmap-new/ColorScale.tsx diff --git a/public/app/plugins/panel/heatmap-new/ColorScale.tsx b/public/app/plugins/panel/heatmap-new/ColorScale.tsx new file mode 100644 index 00000000000..82064168b44 --- /dev/null +++ b/public/app/plugins/panel/heatmap-new/ColorScale.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect } from 'react'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useTheme2, VizTooltipContainer } from '@grafana/ui'; + +type Props = { + colorPalette: string[]; + min: number; + max: number; + + // Show a value as string -- when not defined, the raw values will not be shown + display?: (v: number) => string; +}; + +type HoverState = { + isShown: boolean; + value: number; +}; + +export const ColorScale = ({ colorPalette, min, max, display }: Props) => { + const [colors, setColors] = useState([]); + const [hover, setHover] = useState({ isShown: false, value: 0 }); + const [cursor, setCursor] = useState({ clientX: 0, clientY: 0 }); + + useEffect(() => { + setColors(getGradientStops({ colorArray: colorPalette })); + }, [colorPalette]); + + const theme = useTheme2(); + const styles = getStyles(theme, colors); + + const onScaleMouseMove = (event: React.MouseEvent) => { + const divOffset = event.nativeEvent.offsetX; + const offsetWidth = (event.target as any).offsetWidth as number; + const normPercentage = Math.floor((divOffset * 100) / offsetWidth + 1); + const scaleValue = Math.floor(((max - min) * normPercentage) / 100 + min); + setHover({ isShown: true, value: scaleValue }); + setCursor({ clientX: event.clientX, clientY: event.clientY }); + }; + + const onScaleMouseLeave = () => { + setHover({ isShown: false, value: 0 }); + }; + + return ( +
    +
    +
    + {display && hover.isShown && ( + + {display(hover.value)} + + )} +
    + {display && ( +
    + {display(min)} + {display(max)} +
    + )} +
    +
    + ); +}; + +const getGradientStops = ({ colorArray, stops = 10 }: { colorArray: string[]; stops?: number }): string[] => { + const colorCount = colorArray.length; + if (colorCount <= 20) { + const incr = (1 / colorCount) * 100; + let per = 0; + const stops: string[] = []; + for (const color of colorArray) { + if (per > 0) { + stops.push(`${color} ${per}%`); + } else { + stops.push(color); + } + per += incr; + stops.push(`${color} ${per}%`); + } + return stops; + } + + const gradientEnd = colorArray[colorCount - 1]; + const skip = Math.ceil(colorCount / stops); + const gradientStops = new Set(); + + for (let i = 0; i < colorCount; i += skip) { + gradientStops.add(colorArray[i]); + } + + gradientStops.add(gradientEnd); + + return [...gradientStops]; +}; + +const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({ + scaleWrapper: css` + margin: 0 16px; + padding-top: 4px; + width: 100%; + max-width: 300px; + color: #ccccdc; + font-size: 11px; + `, + scaleGradient: css` + background: linear-gradient(90deg, ${colors.join()}); + height: 6px; + `, + maxDisplay: css` + float: right; + `, +}); diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 7835e4d3d77..ea0ce691456 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -1,7 +1,15 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { css } from '@emotion/css'; -import { GrafanaTheme2, PanelProps } from '@grafana/data'; -import { Portal, UPlotChart, useStyles2, useTheme2, VizLayout, VizTooltipContainer } from '@grafana/ui'; +import { formattedValueToString, GrafanaTheme2, PanelProps, reduceField, ReducerID } from '@grafana/data'; +import { + Portal, + UPlotChart, + useStyles2, + useTheme2, + VizLayout, + VizTooltipContainer, + LegendDisplayMode, +} from '@grafana/ui'; import { PanelDataErrorView } from '@grafana/runtime'; import { HeatmapData, prepareHeatmapData } from './fields'; @@ -10,6 +18,7 @@ import { quantizeScheme } from './palettes'; import { HeatmapHoverEvent, prepConfig } from './utils'; import { HeatmapHoverView } from './HeatmapHoverView'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { ColorScale } from './ColorScale'; interface HeatmapPanelProps extends PanelProps {} @@ -81,17 +90,30 @@ export const HeatmapPanel: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, data.structureRev]); + const renderLegend = () => { + if (options.legend.displayMode === LegendDisplayMode.Hidden || !info.heatmap) { + return null; + } + + const field = info.heatmap.fields[2]; + const { min, max } = reduceField({ field, reducers: [ReducerID.min, ReducerID.max] }); + const display = field.display ? (v: number) => formattedValueToString(field.display!(v)) : (v: number) => `${v}`; + + return ( + + + + ); + }; + if (info.warning || !info.heatmap) { return ; } return ( <> - + {(vizWidth: number, vizHeight: number) => ( - //
    -          //   {JSON.stringify(scatterData, null, 2)}
    -          // 
    {/*children ? children(config, alignedFrame) : null*/} diff --git a/public/app/plugins/panel/heatmap-new/module.tsx b/public/app/plugins/panel/heatmap-new/module.tsx index d9f4d345540..ce31ca3a638 100644 --- a/public/app/plugins/panel/heatmap-new/module.tsx +++ b/public/app/plugins/panel/heatmap-new/module.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { GraphFieldConfig, VisibilityMode } from '@grafana/schema'; import { Field, FieldType, PanelPlugin } from '@grafana/data'; import { commonOptionsBuilder } from '@grafana/ui'; @@ -12,7 +13,9 @@ import { import { HeatmapSuggestionsSupplier } from './suggestions'; import { heatmapChangedHandler } from './migrations'; import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper'; -import { colorSchemes } from './palettes'; +import { colorSchemes, quantizeScheme } from './palettes'; +import { config } from '@grafana/runtime'; +import { ColorScale } from './ColorScale'; export const plugin = new PanelPlugin(HeatmapPanel) .useFieldConfig() @@ -39,11 +42,6 @@ export const plugin = new PanelPlugin(HeatmapPan if (opts.source === HeatmapSourceMode.Calculate) { addHeatmapCalculationOptions('heatmap.', builder, opts.heatmap, category); - } else if (opts.source === HeatmapSourceMode.Data) { - // builder.addSliderInput({ - // name: 'heatmap from the data...', - // path: 'xxx', - // }); } category = ['Colors']; @@ -125,17 +123,32 @@ export const plugin = new PanelPlugin(HeatmapPan showIf: (opts) => opts.color.mode !== HeatmapColorMode.Opacity, }); - builder.addSliderInput({ - path: 'color.steps', - name: 'Max steps', - defaultValue: defaultPanelOptions.color.steps, - category, - settings: { - min: 2, // 1 for on/off? - max: 128, - step: 1, - }, - }); + builder + .addSliderInput({ + path: 'color.steps', + name: 'Steps', + defaultValue: defaultPanelOptions.color.steps, + category, + settings: { + min: 2, + max: 128, + step: 1, + }, + }) + .addCustomEditor({ + id: '__scale__', + path: `__scale__`, + name: 'Scale', + category, + editor: () => { + const palette = quantizeScheme(opts.color, config.theme2); + return ( +
    + +
    + ); + }, + }); category = ['Display']; From cd3fb8e8321259c41d566b7fda394dd4b8c61398 Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Wed, 2 Mar 2022 18:53:24 +0100 Subject: [PATCH 113/125] Document available Alerting annotations (#46081) * Document available Alerting annotations * Update docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- .../unified-alerting/alerting-rules/alert-annotation-label.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md index e7b634905c5..0b88bb1b866 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md +++ b/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md @@ -11,7 +11,7 @@ Annotations and labels are key value pairs associated with alerts originating fr ## Annotations -Annotations are key-value pairs that provide additional meta-information about an alert. For example: a description, a summary, and runbook URL. These are displayed in rule and alert details in the UI and can be used in contact point message templates. +Annotations are key-value pairs that provide additional meta-information about an alert. You can use the following annotations: `description`, `summary`, `runbook_url`, `alertId`, `dashboardUid`, and `panelId`. For example, a description, a summary, and a runbook URL. These are displayed in rule and alert details in the UI and can be used in contact point message templates. ## Labels From 141b1fad20553ee1fdb2ca141c1cde78869d3b14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 18:35:16 +0000 Subject: [PATCH 114/125] Update dependency rollup to v2.69.0 (#46098) Co-authored-by: Renovate Bot --- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index f701ef6d9de..52c61f6d3f6 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -67,7 +67,7 @@ "@types/tinycolor2": "1.4.3", "react-test-renderer": "17.0.2", "rimraf": "3.0.2", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "sinon": "13.0.1", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index c5ba384cce8..098d82aeb36 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -28,7 +28,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@types/node": "16.11.22", "rimraf": "3.0.2", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2" }, diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 7d622aced9f..59f06bc2958 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -37,7 +37,7 @@ "@types/lodash": "4.14.178", "@types/node": "16.11.22", "@types/uuid": "8.3.4", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-copy": "3.4.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 7891e1b0e63..555355895f8 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -50,7 +50,7 @@ "@types/systemjs": "^0.20.6", "lodash": "4.17.21", "rimraf": "3.0.2", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "typescript": "4.4.4" diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index a04897c510c..0fb4f1b2c95 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -28,7 +28,7 @@ "@rollup/plugin-node-resolve": "13.1.3", "@swc/helpers": "0.3.2", "rimraf": "3.0.2", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "typescript": "4.4.4" diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index b97e7f39544..9b45ba633d7 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -166,7 +166,7 @@ "react-docgen-typescript-loader": "3.7.2", "react-test-renderer": "17.0.2", "rimraf": "3.0.2", - "rollup": "2.68.0", + "rollup": "2.69.0", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-terser": "7.0.2", "sass-loader": "12.6.0", diff --git a/yarn.lock b/yarn.lock index 51433d02d57..6abf8dd95d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4042,7 +4042,7 @@ __metadata: react-test-renderer: 17.0.2 regenerator-runtime: 0.13.9 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -4064,7 +4064,7 @@ __metadata: "@rollup/plugin-node-resolve": 13.1.3 "@types/node": 16.11.22 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 tslib: 2.3.1 @@ -4100,7 +4100,7 @@ __metadata: mocha: 9.2.0 resolve-as-bin: 2.1.0 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-copy: 3.4.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 @@ -4181,7 +4181,7 @@ __metadata: react: 17.0.2 react-dom: 17.0.2 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -4201,7 +4201,7 @@ __metadata: "@rollup/plugin-node-resolve": 13.1.3 "@swc/helpers": 0.3.2 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 tslib: 2.3.1 @@ -4462,7 +4462,7 @@ __metadata: react-use: 17.3.2 react-window: 1.8.6 rimraf: 3.0.2 - rollup: 2.68.0 + rollup: 2.69.0 rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.5.2 @@ -32451,9 +32451,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:2.68.0": - version: 2.68.0 - resolution: "rollup@npm:2.68.0" +"rollup@npm:2.69.0": + version: 2.69.0 + resolution: "rollup@npm:2.69.0" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -32461,7 +32461,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: c883f6fb2e10e1c79a32527da0c50ef47a7beb8ddacfdae4197ff2d1911fb8d10bb2704496cf878d3048fbf3524d613bc87f25c5be0afc667fe30b7d04fa8092 + checksum: 65a207c04ae900da58b86a108e5c438a01d2db1fee9cf8811e6c1b3433214739419b5ebd3eba0b8bb6402e38c04d42f7365298f67d7cd6e683b168c7594b7ea8 languageName: node linkType: hard From 1fa45e2c1629d775dc57ee0b0bf4064fe8578feb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 18:39:17 +0000 Subject: [PATCH 115/125] Update dependency eslint to v8.10.0 (#45939) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5e5244e7861..5abbdfabcab 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "cypress": "9.5.0", "enzyme": "3.11.0", "enzyme-to-json": "3.6.2", - "eslint": "8.9.0", + "eslint": "8.10.0", "eslint-config-prettier": "8.4.0", "eslint-plugin-jest": "^26.1.0", "eslint-plugin-jsdoc": "37.9.1", diff --git a/yarn.lock b/yarn.lock index 6abf8dd95d8..5de9f503e99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3878,9 +3878,9 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.1.0": - version: 1.1.0 - resolution: "@eslint/eslintrc@npm:1.1.0" +"@eslint/eslintrc@npm:^1.2.0": + version: 1.2.0 + resolution: "@eslint/eslintrc@npm:1.2.0" dependencies: ajv: ^6.12.4 debug: ^4.3.2 @@ -3891,7 +3891,7 @@ __metadata: js-yaml: ^4.1.0 minimatch: ^3.0.4 strip-json-comments: ^3.1.1 - checksum: 784aa2157e2808b52bbbaf1d1cfca9a6ba0b2faaa3696eb7a1229d4b357400fbd8a6aa09a16e7ae0868ea075d3a8f365cf5928b6d05a1df47f40a1167423a4fa + checksum: a5e51dcf02627363567094456d7880b46b6a14a285d7a057f083ca903bdd862483bb6314cbc9fb6fa2d2c4537d50e0d28bd5e39650840241ae4796faaec65d2e languageName: node linkType: hard @@ -18787,11 +18787,11 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.9.0": - version: 8.9.0 - resolution: "eslint@npm:8.9.0" +"eslint@npm:8.10.0": + version: 8.10.0 + resolution: "eslint@npm:8.10.0" dependencies: - "@eslint/eslintrc": ^1.1.0 + "@eslint/eslintrc": ^1.2.0 "@humanwhocodes/config-array": ^0.9.2 ajv: ^6.10.0 chalk: ^4.0.0 @@ -18828,7 +18828,7 @@ __metadata: v8-compile-cache: ^2.0.3 bin: eslint: bin/eslint.js - checksum: 8efecdb9752ee6cb4d2787a14e00cbeab29562ed95dd71c6f3f8ac410426a067d5aa659416d2290e46ca44bc5607e6a6e6c62f814694d8639f80666f522022a7 + checksum: 8b31ab3de5b48b6828bf13c09c9e62ee0045fa0afa017efaa73eedcf4dc33bc204ee4c467d4677e37967d1645f73816ddef4271422e691fded352040f8f83093 languageName: node linkType: hard @@ -20813,7 +20813,7 @@ __metadata: emotion: 11.0.0 enzyme: 3.11.0 enzyme-to-json: 3.6.2 - eslint: 8.9.0 + eslint: 8.10.0 eslint-config-prettier: 8.4.0 eslint-plugin-jest: ^26.1.0 eslint-plugin-jsdoc: 37.9.1 From 1f332a846b414946967e674ca85fe23f6b98c142 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:45:08 +0100 Subject: [PATCH 116/125] Update dependency lezer-promql to v0.22.0 (#45838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency lezer-promql to v0.22.0 * Add missing peerdep + fixes in code Co-authored-by: Renovate Bot Co-authored-by: Zoltán Bedi --- package.json | 6 +-- .../monaco-completion-provider/situation.ts | 2 +- .../prometheus/querybuilder/parsing.ts | 2 +- yarn.lock | 48 +++++++++---------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 5abbdfabcab..9061278b902 100644 --- a/package.json +++ b/package.json @@ -247,6 +247,8 @@ "@grafana/ui": "workspace:*", "@jaegertracing/jaeger-ui-components": "workspace:*", "@kusto/monaco-kusto": "4.1.3", + "@lezer/common": "^0.15.11", + "@lezer/lr": "^0.15.8", "@lingui/core": "3.13.2", "@lingui/react": "3.13.2", "@opentelemetry/api": "1.1.0", @@ -305,9 +307,7 @@ "jquery": "3.6.0", "json-source-map": "0.6.1", "jsurl": "^0.1.5", - "lezer": "0.13.5", - "lezer-promql": "0.20.0", - "lezer-tree": "0.13.2", + "lezer-promql": "0.22.0", "lodash": "4.17.21", "logfmt": "^1.3.2", "lru-cache": "7.4.0", diff --git a/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.ts b/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.ts index cef1009353f..8b37ca2cd08 100644 --- a/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.ts +++ b/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.ts @@ -1,5 +1,5 @@ import { parser } from 'lezer-promql'; -import type { Tree, SyntaxNode } from 'lezer-tree'; +import type { Tree, SyntaxNode } from '@lezer/common'; import { NeverCaseError } from './util'; type Direction = 'parent' | 'firstChild' | 'lastChild' | 'nextSibling'; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts index cf0cd342adf..e185475ff83 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts @@ -1,5 +1,5 @@ import { parser } from 'lezer-promql'; -import { SyntaxNode } from 'lezer-tree'; +import { SyntaxNode } from '@lezer/common'; import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types'; import { PromVisualQuery } from './types'; diff --git a/yarn.lock b/yarn.lock index 5de9f503e99..04aec2b62a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6013,6 +6013,22 @@ __metadata: languageName: node linkType: hard +"@lezer/common@npm:^0.15.0, @lezer/common@npm:^0.15.11": + version: 0.15.11 + resolution: "@lezer/common@npm:0.15.11" + checksum: 5cabce5493b9392bb54816d6b921dae20d154b175423479b408e990fdf572fd2ed77a6b2df0ed6ef26d779eeb66ec737d10aa2312e1ffecbcec22e14b19f7be3 + languageName: node + linkType: hard + +"@lezer/lr@npm:^0.15.8": + version: 0.15.8 + resolution: "@lezer/lr@npm:0.15.8" + dependencies: + "@lezer/common": ^0.15.0 + checksum: e741225d6ac9cf08f8016bad49622fbd4a4e0d20c2e8c2b38a0abf0ddca69c58275b0ebdb9d5dde2905cf84f6977bc302f7ed5e5ba42c23afa27e9e65b900f36 + languageName: node + linkType: hard + "@lingui/babel-plugin-extract-messages@npm:^3.13.2": version: 3.13.2 resolution: "@lingui/babel-plugin-extract-messages@npm:3.13.2" @@ -20686,6 +20702,8 @@ __metadata: "@grafana/ui": "workspace:*" "@jaegertracing/jaeger-ui-components": "workspace:*" "@kusto/monaco-kusto": 4.1.3 + "@lezer/common": ^0.15.11 + "@lezer/lr": ^0.15.8 "@lingui/cli": 3.13.2 "@lingui/core": 3.13.2 "@lingui/macro": 3.13.2 @@ -20850,9 +20868,7 @@ __metadata: json-source-map: 0.6.1 jsurl: ^0.1.5 lerna: ^4.0.0 - lezer: 0.13.5 - lezer-promql: 0.20.0 - lezer-tree: 0.13.2 + lezer-promql: 0.22.0 lint-staged: 12.3.3 lodash: 4.17.21 logfmt: ^1.3.2 @@ -24990,28 +25006,12 @@ __metadata: languageName: node linkType: hard -"lezer-promql@npm:0.20.0": - version: 0.20.0 - resolution: "lezer-promql@npm:0.20.0" +"lezer-promql@npm:0.22.0": + version: 0.22.0 + resolution: "lezer-promql@npm:0.22.0" peerDependencies: - lezer: ^0.13.0 - checksum: 9cf76d60aa84b27a1ed5ff7a3d2e407a2d752fd0282777d740bf5e8bf00764a1c8e9e3a835246d9257447231fa4ce5dc4f85d4acb91c6819089ff16874a75382 - languageName: node - linkType: hard - -"lezer-tree@npm:0.13.2, lezer-tree@npm:^0.13.2": - version: 0.13.2 - resolution: "lezer-tree@npm:0.13.2" - checksum: b8be213c780191e0669c7f440aa563218ada762d2cf399b94e755a563cc7da8951929fa3ee65df9ef6586a81223c55cd662e1ec5b49060d892acca4198cf3596 - languageName: node - linkType: hard - -"lezer@npm:0.13.5": - version: 0.13.5 - resolution: "lezer@npm:0.13.5" - dependencies: - lezer-tree: ^0.13.2 - checksum: a5c3aa01c539aba3377a927063bcd63b311737a7abfd71ad2c2229ed4e48b7858f2e8e11e925f8f5286c2629251f77dfabf7505ea6c707499cd9917ca90934c8 + "@lezer/lr": ^0.15.8 + checksum: cdce054700874ef95c779899bc8a6a774f83bc613d509011b7d3b8003f07fa853962291c44edf9232e7a927d0753fec77ec55d795abddda9d2efc044f78bb58a languageName: node linkType: hard From ccdb904e3b8098709f2d99d7087f10c8228b5483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:46:19 +0100 Subject: [PATCH 117/125] Update dependency @types/react-calendar to v3.5.0 (#46117) Co-authored-by: Renovate Bot --- packages/grafana-ui/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 9b45ba633d7..b4dee767060 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -136,7 +136,7 @@ "@types/prismjs": "1.26.0", "@types/react": "17.0.39", "@types/react-beautiful-dnd": "13.1.2", - "@types/react-calendar": "3.4.5", + "@types/react-calendar": "3.5.0", "@types/react-color": "3.0.6", "@types/react-dom": "17.0.11", "@types/react-router-dom": "5.3.3", diff --git a/yarn.lock b/yarn.lock index 04aec2b62a0..a9091c470f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4393,7 +4393,7 @@ __metadata: "@types/prismjs": 1.26.0 "@types/react": 17.0.39 "@types/react-beautiful-dnd": 13.1.2 - "@types/react-calendar": 3.4.5 + "@types/react-calendar": 3.5.0 "@types/react-color": 3.0.6 "@types/react-dom": 17.0.11 "@types/react-router-dom": 5.3.3 @@ -10732,12 +10732,12 @@ __metadata: languageName: node linkType: hard -"@types/react-calendar@npm:3.4.5": - version: 3.4.5 - resolution: "@types/react-calendar@npm:3.4.5" +"@types/react-calendar@npm:3.5.0": + version: 3.5.0 + resolution: "@types/react-calendar@npm:3.5.0" dependencies: "@types/react": "*" - checksum: 710a0b9d4a7517da2d501cd413f7988d391d0f40ae8dd5e67675c38c79f0410fe11e8fffc57a4a566900f9f4c17fae02ec071378645afd71f966595487e0731e + checksum: 0dc7eadd0b27c91f0e90372e7aa9392e86dd80f137d13af10cf3692983a8391d85c2287c383ed5281cce10f646f2162ebd49002378a2e525bcd90ea2722b2923 languageName: node linkType: hard From 50fb63a46887a6692c90d047d812e653a1004f91 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:18:48 -0500 Subject: [PATCH 118/125] ReleaseNotes: Updated changelog and release notes for 8.4.3 (#46119) --- CHANGELOG.md | 22 ++++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes/release-notes-8-4-3.md | 25 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-4-3.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b38a0224cee..9a1556f5db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ + + +# 8.4.3 (2022-03-02) + +### Features and enhancements + +- **Alerting:** Grafana uses > instead of >= when checking the For duration. [#46010](https://github.com/grafana/grafana/issues/46010) +- **Alerting:** Use expanded labels in dashboard annotations. [#45726](https://github.com/grafana/grafana/pull/45726), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Logs:** Escape windows newline into single newline. [#45771](https://github.com/grafana/grafana/pull/45771), [@perosb](https://github.com/perosb) + +### Bug fixes + +- **Alerting:** Fix use of > instead of >= when checking the For duration. [#46011](https://github.com/grafana/grafana/pull/46011), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Azure Monitor:** Fixes broken log queries that use workspace. [#45820](https://github.com/grafana/grafana/pull/45820), [@sunker](https://github.com/sunker) +- **CloudWatch:** Remove error message when using multi-valued template vars in region field. [#45886](https://github.com/grafana/grafana/pull/45886), [@sunker](https://github.com/sunker) +- **Middleware:** Fix IPv6 host parsing in CSRF check. [#45911](https://github.com/grafana/grafana/pull/45911), [@ying-jeanne](https://github.com/ying-jeanne) + +### Plugin development fixes & changes + +- **ClipboardButton:** Use a fallback when the Clipboard API is unavailable. [#45831](https://github.com/grafana/grafana/pull/45831), [@ashharrison90](https://github.com/ashharrison90) + + # 8.4.2 (2022-02-23) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 563ccef4c4f..0c22f453f16 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.3]({{< relref "release-notes-8-4-3" >}}) - [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}}) - [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}}) - [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-3.md b/docs/sources/release-notes/release-notes-8-4-3.md new file mode 100644 index 00000000000..90f989988f3 --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-3.md @@ -0,0 +1,25 @@ ++++ +title = "Release notes for Grafana 8.4.3" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.3 + +### Features and enhancements + +- **Alerting:** Grafana uses > instead of >= when checking the For duration. [#46010](https://github.com/grafana/grafana/issues/46010) +- **Alerting:** Use expanded labels in dashboard annotations. [#45726](https://github.com/grafana/grafana/pull/45726), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Logs:** Escape windows newline into single newline. [#45771](https://github.com/grafana/grafana/pull/45771), [@perosb](https://github.com/perosb) + +### Bug fixes + +- **Alerting:** Fix use of > instead of >= when checking the For duration. [#46011](https://github.com/grafana/grafana/pull/46011), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Azure Monitor:** Fixes broken log queries that use workspace. [#45820](https://github.com/grafana/grafana/pull/45820), [@sunker](https://github.com/sunker) +- **CloudWatch:** Remove error message when using multi-valued template vars in region field. [#45886](https://github.com/grafana/grafana/pull/45886), [@sunker](https://github.com/sunker) +- **Middleware:** Fix IPv6 host parsing in CSRF check. [#45911](https://github.com/grafana/grafana/pull/45911), [@ying-jeanne](https://github.com/ying-jeanne) + +### Plugin development fixes & changes + +- **ClipboardButton:** Use a fallback when the Clipboard API is unavailable. [#45831](https://github.com/grafana/grafana/pull/45831), [@ashharrison90](https://github.com/ashharrison90) From e8a54d58c56554c807a0a27060e1a370742063ef Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Wed, 2 Mar 2022 19:45:07 +0000 Subject: [PATCH 119/125] chore: update latest.json (#46122) --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 901499dabe9..360c9f7b7d9 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "8.4.2", - "testing": "8.4.2" + "stable": "8.4.3", + "testing": "8.4.3" } From b677318277a4b8f88854543846e036ad1de52a8e Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:47:39 -0600 Subject: [PATCH 120/125] docs: modify user preferences refactor (#45553) * docs: modify user preferences refactor * yarn prettier * add missing procedures * fix link * Update docs/sources/manage-user-preferences/_index.md Co-authored-by: Ursula Kallio * minor edits and moved docs to admin chapter * Eve comment out note relref * yarn prettier Co-authored-by: Mitchel Seaman Co-authored-by: Ursula Kallio Co-authored-by: Eve832 --- .../manage-user-preferences/_index.md | 100 ++++++++++++++++++ .../preferences/change-grafana-name.md | 2 +- .../sources/manage-users/user-admin/_index.md | 10 -- .../user-admin/change-your-password.md | 21 ---- .../manage-users/user-admin/switch-org.md | 12 --- .../manage-users/user-admin/user-profile.md | 42 -------- 6 files changed, 101 insertions(+), 86 deletions(-) create mode 100644 docs/sources/administration/manage-user-preferences/_index.md delete mode 100644 docs/sources/manage-users/user-admin/_index.md delete mode 100644 docs/sources/manage-users/user-admin/change-your-password.md delete mode 100644 docs/sources/manage-users/user-admin/switch-org.md delete mode 100644 docs/sources/manage-users/user-admin/user-profile.md diff --git a/docs/sources/administration/manage-user-preferences/_index.md b/docs/sources/administration/manage-user-preferences/_index.md new file mode 100644 index 00000000000..6c4089851af --- /dev/null +++ b/docs/sources/administration/manage-user-preferences/_index.md @@ -0,0 +1,100 @@ ++++ +title = "Manage user preferences" +weight = 400 +description = "Learn how to update your user preferences and switch organizations" +keywords = ["password", "change", "organization", "change"] +aliases = ["/docs/grafana/latest/administration/change-your-password/", "docs/sources/administration/manage-user-preferences/_index.md"] ++++ + +# Manage user preferences + +Grafana allows you to manage certain aspects of your user account, including your user name, email, and password. + +You can also view important information about your account, such as the organizations and roles to which you are assigned and the Grafana sessions associated with your account. + +## Change your Grafana password + +You can change your Grafana password at any time. + +> **Note**: If your Grafana instance uses an external authentication provider, then you might not be able to change your password in Grafana. Contact your Grafana administrator for more information. + +**To change your password**: + +1. Sign in to Grafana. +1. Hover your mouse over the user icon in the lower-left corner of the page. +1. Click **Change Password**. + Grafana opens the **Change Password** tab. + +1. Enter your old password and a new password. +1. Confirm your new password. +1. Click **Change Password**. + +## Edit your profile + +Your profile includes your name, user name, and email address, which you can update. + +**To edit your profile**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page and click **Preferences**. +1. In the **Edit Profile** section, update your profile and click **Save**. + +## Edit your preferences + +You can choose the way you would like data to appear in Grafana, including the UI theme, home dashboard, timezone, and first day of the week. You can set these preferences for your own account, for a team, for an organization, or Grafana-wide using configuration settings. Your user preferences take precedence over team, organization, and Grafana default preferences. For more information, see [Grafana preferences]({{< relref "../../administration/preferences/_index.md">}}). + +- **UI theme** determines whether Grafana appears in light mode or dark mode. By default, UI theme is set to dark mode. +- **Home dashboard** refers to the dashboard you see when you sign in to Grafana. By default, this is set to the Home dashboard. +- **Timezone** is used by dashboards when you set time ranges, so that you view data in your timezone instead of UTC. +- **Week start** is the first day of the week you want to use in dashboard time ranges, for example, `This week`. + +**To edit your preferences**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page, and click **Preferences**. +1. Update any of the values in the **Preferences** section. +1. Click **Save** at the bottom of the Preferences section. + +## Switch organizations + +When you sign in to Grafana, the system signs you in to a default organization. If you are assigned to multiple organizations, then you might need to switch organizations. For example, if you need to view a dashboard not associated with your current organization, then you should switch organizations to view associated dashboards. + +**To switch organizations**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page and click **Switch organization**. +1. Next to the organization that you want to sign in to, click **Switch to**. + +## View your assigned organizations + +Every user is a member of at least one organization. You can have different roles in each organization of which you are a member. + +**To view your assigned organizations**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page and click **Preferences**. +1. Scroll down to the **Organizations** section and review the following information: + - **Name**: The name of the organizations of which you are a member. + - **Role**: The role to which you are assigned in the organization. For more information about roles and permissions, refer to [Organization users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}). + - **Current**: Grafana indicates the organization that you are currently signed into as _Current_. If you are a member of multiple organizations, you can click **Select** to switch to that organization. + +## View your Grafana sessions + +Grafana logs your sessions in each Grafana instance. You can review this section if you suspect someone has misused your Grafana credentials. + +**To view your Grafana sessions**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page, and click **Preferences**. +1. Scroll down to the **Sessions** section. + +## Sign out a user session + +You can sign out other sessions using your account in order to prevent other people from accessing Grafana using your credentials. + +**To sign out one of your Grafana sessions**: + +1. Sign in to Grafana. +1. Hover your cursor over the user icon in the lower-left corner of the page, and click **Preferences**. +1. Scroll down to the **Sessions** section. +1. Click the red "sign out" icon next to the session you would like to sign out. diff --git a/docs/sources/administration/preferences/change-grafana-name.md b/docs/sources/administration/preferences/change-grafana-name.md index 69d4394a8e2..c4a6de72aee 100644 --- a/docs/sources/administration/preferences/change-grafana-name.md +++ b/docs/sources/administration/preferences/change-grafana-name.md @@ -53,4 +53,4 @@ To change the team name or email, follow these steps: ## Change user name or email -To learn how to edit your user information, refer to [Grafana user account profile]({{< relref "../../manage-users/user-admin/user-profile.md" >}}). +To learn how to edit your user information, refer to [Edit your profile]({{< relref "../../manage-user-preferences/_index.md#edit-your-profile" >}}). diff --git a/docs/sources/manage-users/user-admin/_index.md b/docs/sources/manage-users/user-admin/_index.md deleted file mode 100644 index 1734df64364..00000000000 --- a/docs/sources/manage-users/user-admin/_index.md +++ /dev/null @@ -1,10 +0,0 @@ -+++ -title = "User account tasks" -weight = 400 -+++ - -# User account tasks - -Grafana allows you to manage certain aspects of your user account, including the user name, email, and password. - -You can also view important aspects of your account, such as the organizations and roles assigned and the Grafana sessions associated with the account. diff --git a/docs/sources/manage-users/user-admin/change-your-password.md b/docs/sources/manage-users/user-admin/change-your-password.md deleted file mode 100644 index efcdcc267a0..00000000000 --- a/docs/sources/manage-users/user-admin/change-your-password.md +++ /dev/null @@ -1,21 +0,0 @@ -+++ -title = "Change your password" -description = "How to change your Grafana password" -keywords = ["grafana", "password", "change", "preferences"] -aliases = ["/docs/grafana/latest/administration/change-your-password/"] -weight = 200 -+++ - -# Change your Grafana password - -You can change your Grafana password in the Change Password tab. - -> **Note:** If your Grafana instance uses an external authentication provider, then you might not be able to change your password. Contact your Grafana administrator for more information. - -## Change your password - -1. Hover your mouse over your user icon in the lower left corner of the screen. -1. Click **Change Password**. Grafana opens the Change Password tab. -1. Enter your **Old password** to authorize the change. -1. Enter your **New password** and then **Confirm password**. -1. Click **Change Password**. diff --git a/docs/sources/manage-users/user-admin/switch-org.md b/docs/sources/manage-users/user-admin/switch-org.md deleted file mode 100644 index a9c7230442f..00000000000 --- a/docs/sources/manage-users/user-admin/switch-org.md +++ /dev/null @@ -1,12 +0,0 @@ -+++ -title = "Switch organization" -description = "Change which organization you are logged in with" -weight = 300 -+++ - -# Change the organization you are signed in to - -When you sign in to Grafana, you are always signed in with a particular organization. If you are assigned to multiple organizations, then you might need to switch which organization you are signed in to. For example, if you need to view a dashboard associated with a different org, then you might switch organizations. - -1. Hover your cursor over your user icon in the lower left corner of the screen, then click **Switch**. -1. Next to the organization that you want to sign in to, click **Switch to**. diff --git a/docs/sources/manage-users/user-admin/user-profile.md b/docs/sources/manage-users/user-admin/user-profile.md deleted file mode 100644 index 95973c1ac80..00000000000 --- a/docs/sources/manage-users/user-admin/user-profile.md +++ /dev/null @@ -1,42 +0,0 @@ -+++ -title = "User account profile" -description = "View and edit your Grafana user profile" -weight = 100 -+++ - -# Grafana user account profile - -You can edit and view important information about your Grafana user account, including your assigned organizations, your sessions, and the information associated with your account. - -## Edit your profile - -Your profile includes your name, user name, and email address. - -1. Navigate to the Preferences tab. Hover your cursor over your user icon in the lower left corner of the screen, and then click **Preferences.** -1. In the Edit Profile section, you can edit any of the following: - - **Name -** Edit this field to change the display name associated with your profile. - - **Email -** Edit this field to change the email address associated with your profile. - - **Username -** Edit this field to change your user name. -1. Click **Save**. - -## View your assigned organizations - -Every user is a member of at least one organization. You can have different roles in every organization that you are a member of. - -1. Navigate to the Preferences tab. Hover your cursor over your user icon in the lower left corner of the screen, and then click **Preferences.** -1. Scroll down to the Organizations section. - - **Name -** The name of the organizations you are a member of in that Grafana instance. - - **Role -** The role you are assigned in the organization. Refer to [Organization users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) for more information about permissions assigned to each role. - - **Current -** Grafana tags the organization that you are currently signed in to as _Current_. If you are part of multiple organizations, then you can click **Select** to switch to that organization. - -## View your Grafana sessions - -Grafana logs your sessions in each Grafana instance. You can review this section if you suspect someone has misused your Grafana credentials. - -1. Navigate to the Preferences tab. Hover your cursor over your user icon in the lower left corner of the screen, and then click **Preferences.** -1. Scroll down to the Sessions section. Grafana displays the following: - - **Last seen -** How long ago you logged on. - - **Logged on -** The date you logged on to the current Grafana instance. - - **IP address -** The IP address that you logged on from. - - **Browser & OS -** The web browser and operating system used to log on to Grafana. - - If you are a Grafana Admin for the instance, then you can revoke a session by clicking the red signout icon in the session row. From 76495374dceecdae3c3bedbaf221627a0f77843e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 2 Mar 2022 20:52:13 +0100 Subject: [PATCH 121/125] Docs: Include ID token example in Forward OAuth identity for the logged-in user section (#46121) Ref #45938 --- .../add-authentication-for-data-source-plugins.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index c4a8e4e0bda..4cb9c2bbc0b 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -303,4 +303,16 @@ func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataReque } ``` +In addition, if the user's token includes an ID token, Grafana will pass the user's ID token to the plugin in an `X-ID-Token` header, available on the `QueryDataRequest` object on the `QueryData` request in your backend data source. + +```go +func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + idToken := req.Headers["X-ID-Token"] + + for _, q := range req.Queries { + // ... + } +} +``` + > **Note:** Due to a bug in Grafana, using this feature with PostgreSQL can cause a deadlock. For more information, refer to [Grafana causes deadlocks in PostgreSQL, while trying to refresh users token](https://github.com/grafana/grafana/issues/20515). From b58f3c8a056aa75db440d81aa28699239dcdd899 Mon Sep 17 00:00:00 2001 From: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:08:46 -0500 Subject: [PATCH 122/125] Docs: Update ldap authentication topic (#46128) * Fixed broken relrefs, and updated password format in ldap authentication topic. * Fixed few more broken links. --- .../administration/preferences/change-grafana-name.md | 2 +- docs/sources/auth/ldap.md | 8 ++++---- .../using-aws-kms-to-encrypt-database-secrets.md | 2 +- .../using-azure-key-vault-to-encrypt-database-secrets.md | 2 +- .../using-google-cloud-kms-to-encrypt-database-secrets.md | 2 +- ...ing-hashicorp-key-vault-to-encrypt-database-secrets.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/administration/preferences/change-grafana-name.md b/docs/sources/administration/preferences/change-grafana-name.md index c4a6de72aee..7924ffcc2b4 100644 --- a/docs/sources/administration/preferences/change-grafana-name.md +++ b/docs/sources/administration/preferences/change-grafana-name.md @@ -53,4 +53,4 @@ To change the team name or email, follow these steps: ## Change user name or email -To learn how to edit your user information, refer to [Edit your profile]({{< relref "../../manage-user-preferences/_index.md#edit-your-profile" >}}). +To learn how to edit your user information, refer to [Edit your profile]({{< relref "../manage-user-preferences/_index.md#edit-your-profile" >}}). diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index 15b3f29cbd3..a289fc12038 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -68,7 +68,7 @@ ssl_skip_verify = false bind_dn = "cn=admin,dc=grafana,dc=org" # Search user bind password # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" -bind_password = 'grafana' +bind_password = "grafana" # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" # Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))" @@ -236,7 +236,7 @@ use_ssl = false start_tls = false ssl_skip_verify = false bind_dn = "cn=admin,dc=grafana,dc=org" -bind_password = 'grafana' +bind_password = "grafana" search_filter = "(cn=%s)" search_base_dns = ["dc=grafana,dc=org"] @@ -263,7 +263,7 @@ use_ssl = false start_tls = false ssl_skip_verify = false bind_dn = "cn=admin,dc=grafana,dc=org" -bind_password = 'grafana' +bind_password = "grafana" search_filter = "(cn=%s)" search_base_dns = ["ou=users,dc=grafana,dc=org"] @@ -286,7 +286,7 @@ start_tls = false ssl_skip_verify = false bind_dn = "cn=admin,dc=grafana,dc=org" -bind_password = 'grafana' +bind_password = "grafana" search_filter = "(cn=%s)" search_base_dns = ["ou=users,dc=grafana,dc=org"] diff --git a/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md b/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md index b91655c753c..9cf0cd04040 100644 --- a/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md +++ b/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md @@ -23,7 +23,7 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets 3. Create a [programmatic credential](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) (access key ID and secret access key), which has permission to view the key that you created.

    In AWS, you can control access to your KMS keys by using [key policies](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html), [IAM policies](https://docs.aws.amazon.com/kms/latest/developerguide/iam-policies.html), and [grants](https://docs.aws.amazon.com/kms/latest/developerguide/grants.html). You can also create [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html), which must provide a session token along with an access key ID and a secret access key. -4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/envelope-encryption.md" >}}). +4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration//database-encryption.md" >}}). 5. Add your AWS KMS details to the Grafana configuration file; depending on your operating system, it is usually named `grafana.ini`:

    a. Add a new section to the configuration file, with a name in the format of `[security.encryption.awskms.]`, where `` is any name that uniquely identifies this key among other provider keys.

    b. Fill in the section with the following values: diff --git a/docs/sources/enterprise/enterprise-encryption/using-azure-key-vault-to-encrypt-database-secrets.md b/docs/sources/enterprise/enterprise-encryption/using-azure-key-vault-to-encrypt-database-secrets.md index 8747fa81394..489649f24cd 100644 --- a/docs/sources/enterprise/enterprise-encryption/using-azure-key-vault-to-encrypt-database-secrets.md +++ b/docs/sources/enterprise/enterprise-encryption/using-azure-key-vault-to-encrypt-database-secrets.md @@ -24,7 +24,7 @@ You can use an encryption key from Azure Key Vault to encrypt secrets in the Gra 5. In the Key Permissions section, set encrypt and decrypt permissions, and click **Save**. -6. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/envelope-encryption.md" >}}). +6. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/database-encryption.md" >}}). 7. Add your Azure Key Vault details to the Grafana configuration file; depending on your operating system, is usually named `grafana.ini`:

    a. Add a new section to the configuration file, with a name in the format of `[security.encryption.azurekv.]`, where `` is any name that uniquely identifies this key among other provider keys. diff --git a/docs/sources/enterprise/enterprise-encryption/using-google-cloud-kms-to-encrypt-database-secrets.md b/docs/sources/enterprise/enterprise-encryption/using-google-cloud-kms-to-encrypt-database-secrets.md index 0d66b4a89a4..0ef4491ce27 100644 --- a/docs/sources/enterprise/enterprise-encryption/using-google-cloud-kms-to-encrypt-database-secrets.md +++ b/docs/sources/enterprise/enterprise-encryption/using-google-cloud-kms-to-encrypt-database-secrets.md @@ -22,7 +22,7 @@ You can use an encryption key from Google Cloud Key Management Service to encryp 4. [Create a service account key and save its JSON file](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating) to you computer, for example, as `~/.config/gcloud/sample-project-credentials.json`. -5. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/envelope-encryption.md" >}}). +5. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/database-encryption.md" >}}). 6. Add your Google Cloud KMS details to the Grafana configuration file; depending on your operating system, is usually named `grafana.ini`:

    a. Add a new section to the configuration file, with a name in the format of `[security.encryption.azurekv.]`, where `` is any name that uniquely identifies this key among other provider keys. diff --git a/docs/sources/enterprise/enterprise-encryption/using-hashicorp-key-vault-to-encrypt-database-secrets.md b/docs/sources/enterprise/enterprise-encryption/using-hashicorp-key-vault-to-encrypt-database-secrets.md index b09bd2d582b..83d843bbab2 100644 --- a/docs/sources/enterprise/enterprise-encryption/using-hashicorp-key-vault-to-encrypt-database-secrets.md +++ b/docs/sources/enterprise/enterprise-encryption/using-hashicorp-key-vault-to-encrypt-database-secrets.md @@ -20,7 +20,7 @@ You can use an encryption key from Hashicorp Vault to encrypt secrets in the Gra 3. [Create a periodic service token](https://learn.hashicorp.com/tutorials/vault/tokens#periodic-service-tokens). -4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/envelope-encryption.md" >}}). +4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/database-encryption.md" >}}). 5. Add your Hashicorp Vault details to the Grafana configuration file; depending on your operating system, is usually named `grafana.ini`:

    a. Add a new section to the configuration file, with a name in the format of `[security.encryption.hashicorpvault.]`, where `` is any name that uniquely identifies this key among other provider keys. From 016d9e14ed876a05644d5acd30b6522e70f28198 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 2 Mar 2022 19:07:55 -0500 Subject: [PATCH 123/125] Add missing option "OK" for Error state (#45262) * Add missing OK option to models * add ok to legacy legacy UI does not support it but it is possible to do so via provisioning. * use enums in migration so linter would catch missing cases --- pkg/models/alert.go | 3 +- .../api/tooling/definitions/cortex-ruler.go | 1 + pkg/services/ngalert/api/tooling/post.json | 6 ++-- pkg/services/ngalert/api/tooling/spec.json | 6 ++-- .../sqlstore/migrations/ualert/alert_rule.go | 33 ++++++++++--------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 452e0fa44c6..013abe82932 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -28,6 +28,7 @@ const ( ) const ( + ExecutionErrorSetOk ExecutionErrorOption = "ok" ExecutionErrorSetAlerting ExecutionErrorOption = "alerting" ExecutionErrorKeepState ExecutionErrorOption = "keep_state" ) @@ -55,7 +56,7 @@ func (s NoDataOption) ToAlertState() AlertStateType { } func (s ExecutionErrorOption) IsValid() bool { - return s == ExecutionErrorSetAlerting || s == ExecutionErrorKeepState + return s == ExecutionErrorSetAlerting || s == ExecutionErrorKeepState || s == ExecutionErrorSetOk } func (s ExecutionErrorOption) ToAlertState() AlertStateType { diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 5707b4d01f2..451c5456fee 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -354,6 +354,7 @@ const ( type ExecutionErrorState string const ( + OkErrState ExecutionErrorState = "OK" AlertingErrState ExecutionErrorState = "Alerting" ErrorErrState ExecutionErrorState = "Error" ) diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 641089a1746..9a875b709ea 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -874,11 +874,12 @@ }, "exec_err_state": { "enum": [ + "OK", "Alerting", "Error" ], "type": "string", - "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState", + "x-go-enum-desc": "OK OkErrState\nAlerting AlertingErrState\nError ErrorErrState", "x-go-name": "ExecErrState" }, "id": { @@ -1836,11 +1837,12 @@ }, "exec_err_state": { "enum": [ + "OK", "Alerting", "Error" ], "type": "string", - "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState", + "x-go-enum-desc": "OK OkErrState\nAlerting AlertingErrState\nError ErrorErrState", "x-go-name": "ExecErrState" }, "no_data_state": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 811d0e52854..b99f7224990 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2627,10 +2627,11 @@ "exec_err_state": { "type": "string", "enum": [ + "OK", "Alerting", "Error" ], - "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState", + "x-go-enum-desc": "OK OkErrState\nAlerting AlertingErrState\nError ErrorErrState", "x-go-name": "ExecErrState" }, "id": { @@ -3590,10 +3591,11 @@ "exec_err_state": { "type": "string", "enum": [ + "OK", "Alerting", "Error" ], - "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState", + "x-go-enum-desc": "OK OkErrState\nAlerting AlertingErrState\nError ErrorErrState", "x-go-name": "ExecErrState" }, "no_data_state": { diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule.go b/pkg/services/sqlstore/migrations/ualert/alert_rule.go index 57aeefeb2db..9846d6630d7 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/expr" + legacymodels "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/tsdb/graphite" "github.com/grafana/grafana/pkg/util" @@ -241,29 +242,29 @@ func ruleAdjustInterval(freq int64) int64 { } func transNoData(s string) (string, error) { - switch s { - case "ok": - return "OK", nil // values from ngalert/models/rule - case "", "no_data": - return "NoData", nil - case "alerting": - return "Alerting", nil - case "keep_state": - return "NoData", nil // "keep last state" translates to no data because we now emit a special alert when the state is "noData". The result is that the evaluation will not return firing and instead we'll raise the special alert. + switch legacymodels.NoDataOption(s) { + case legacymodels.NoDataSetOK: + return string(ngmodels.OK), nil // values from ngalert/models/rule + case "", legacymodels.NoDataSetNoData: + return string(ngmodels.NoData), nil + case legacymodels.NoDataSetAlerting: + return string(ngmodels.Alerting), nil + case legacymodels.NoDataKeepState: + return string(ngmodels.NoData), nil // "keep last state" translates to no data because we now emit a special alert when the state is "noData". The result is that the evaluation will not return firing and instead we'll raise the special alert. } return "", fmt.Errorf("unrecognized No Data setting %v", s) } func transExecErr(s string) (string, error) { - switch s { - case "", "alerting": - return "Alerting", nil - case "keep_state": + switch legacymodels.ExecutionErrorOption(s) { + case "", legacymodels.ExecutionErrorSetAlerting: + return string(ngmodels.AlertingErrState), nil + case legacymodels.ExecutionErrorKeepState: // Keep last state is translated to error as we now emit a // DatasourceError alert when the state is error - return "Error", nil - case "ok": - return "OK", nil + return string(ngmodels.ErrorErrState), nil + case legacymodels.ExecutionErrorSetOk: + return string(ngmodels.OkErrState), nil } return "", fmt.Errorf("unrecognized Execution Error setting %v", s) } From 854f872b40bbf1f41ba4604a48e48d1906bf3a94 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 2 Mar 2022 18:21:12 -0600 Subject: [PATCH 124/125] StateTimeline: insert trailing null value at +interval (#45997) --- .../src/components/GraphNG/GraphNG.tsx | 3 +- .../GraphNG/nullInsertThreshold.test.ts | 34 +++++++++---------- .../components/GraphNG/nullInsertThreshold.ts | 16 +++++++-- .../src/components/GraphNG/utils.ts | 6 ++-- .../app/plugins/panel/state-timeline/utils.ts | 11 ------ 5 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index e33de639ef2..1581c4abd34 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -116,7 +116,8 @@ export class GraphNG extends React.Component { fields || { x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), y: fieldMatchers.get(FieldMatcherID.numeric).get({}), - } + }, + props.timeRange ); pluginLog('GraphNG', false, 'data aligned', alignedFrame); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts index 01ec591f9ca..144f9ddb7d8 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts @@ -97,6 +97,23 @@ describe('nullInsertThreshold Transformer', () => { expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); }); + test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df, null, 13); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8, null]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c', null]); + }); + // TODO: make this work test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { const df = new MutableDataFrame({ @@ -115,23 +132,6 @@ describe('nullInsertThreshold Transformer', () => { expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); }); - test('should insert nulls at midpoints between adjacent > interval: 2', () => { - const df = new MutableDataFrame({ - refId: 'A', - fields: [ - { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, - { name: 'One', type: FieldType.number, values: [4, 6, 8] }, - { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, - ], - }); - - const result = applyNullInsertThreshold(df); - - expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); - expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]); - expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']); - }); - test('should noop on fewer than two values', () => { const df = new MutableDataFrame({ refId: 'A', diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts index 4e22d0af2b1..35b0fcd325f 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts @@ -12,6 +12,7 @@ const INSERT_MODES = { export function applyNullInsertThreshold( frame: DataFrame, refFieldName?: string | null, + refFieldPseudoMax: number | null = null, insertMode: InsertMode = INSERT_MODES.threshold ): DataFrame { if (frame.length < 2) { @@ -48,7 +49,7 @@ export function applyNullInsertThreshold( const frameValues = frame.fields.map((field) => field.values.toArray()); - const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, insertMode); + const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, refFieldPseudoMax, insertMode); if (filledFieldValues === frameValues) { return frame; @@ -70,7 +71,14 @@ export function applyNullInsertThreshold( return frame; } -function nullInsertThreshold(refValues: number[], frameValues: any[][], threshold: number, getInsertValue: InsertMode) { +function nullInsertThreshold( + refValues: number[], + frameValues: any[][], + threshold: number, + // will insert a trailing null when refFieldPseudoMax > last datapoint + threshold + refFieldPseudoMax: number | null = null, + getInsertValue: InsertMode +) { const len = refValues.length; let prevValue: number = refValues[0]; const refValuesNew: number[] = [prevValue]; @@ -87,6 +95,10 @@ function nullInsertThreshold(refValues: number[], frameValues: any[][], threshol prevValue = curValue; } + if (refFieldPseudoMax != null && prevValue + threshold <= refFieldPseudoMax) { + refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + } + const filledLen = refValuesNew.length; if (filledLen === len) { diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index cc7feecd4a0..b67d694d793 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -1,5 +1,5 @@ import { XYFieldMatchers } from './types'; -import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames } from '@grafana/data'; +import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data'; import { nullToUndefThreshold } from './nullToUndefThreshold'; import { applyNullInsertThreshold } from './nullInsertThreshold'; import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; @@ -29,9 +29,9 @@ function applySpanNullsThresholds(frame: DataFrame) { return frame; } -export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) { +export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { let alignedFrame = outerJoinDataFrames({ - frames: frames.map((frame) => applyNullInsertThreshold(frame)), + frames: frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf())), joinBy: dimFields.x, keep: dimFields.y, keepOriginIndices: true, diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index dd71932f9f8..0bf27bd90ba 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -1,5 +1,4 @@ import React from 'react'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; import { ArrayVector, DataFrame, @@ -19,7 +18,6 @@ import { getActiveThreshold, Threshold, getFieldConfigWithMinMax, - outerJoinDataFrames, ThresholdsMode, } from '@grafana/data'; import { @@ -48,15 +46,6 @@ export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityCh return SeriesVisibilityChangeMode.ToggleSelection; } -export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers) { - return outerJoinDataFrames({ - frames: data, - joinBy: dimFields.x, - keep: dimFields.y, - keepOriginIndices: true, - }); -} - export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme, From a8b90d9a2524765c49923c48a7fcf0025c85b733 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Thu, 3 Mar 2022 10:53:26 +0400 Subject: [PATCH 125/125] FileStore: add basic file storage API (#46051) * #45498: fs API alpha * #45498: remove grafanaDS changes for filestorage.go * #45498: fix lint * #45498: fix lint * #45498: remove db file storage migration * #45498: linting * #45498: linting * #45498: linting * #45498: fix imports * #45498: add comment * remove StorageName abstractions * FileStore: add dummy implementation (#46071) * #45498: bring back grafanaDs changes, add dummy filestorage * #45498: rename grafanaDs to public * #45498: modify join * #45498: review fix * #45498: unnecessary leading newline (whitespace) IMPORTANT FIX * #45498: fix belongsToStorage * #45498: fix removeStoragePrefix so that it works with abs paths Co-authored-by: Ryan McKinley --- .github/CODEOWNERS | 3 +- go.mod | 53 +- go.sum | 187 +++- .../src/types/featureToggles.gen.ts | 1 + pkg/infra/filestorage/api.go | 101 +++ pkg/infra/filestorage/api_test.go | 75 ++ pkg/infra/filestorage/cdk_blob_filestorage.go | 491 ++++++++++ pkg/infra/filestorage/db_filestorage.go | 449 +++++++++ pkg/infra/filestorage/dummy.go | 51 ++ pkg/infra/filestorage/filestorage.go | 160 ++++ pkg/infra/filestorage/filestorage_test.go | 46 + pkg/infra/filestorage/fs_integration_test.go | 857 ++++++++++++++++++ pkg/infra/filestorage/test_utils.go | 346 +++++++ pkg/infra/filestorage/wrapper.go | 257 ++++++ pkg/plugins/manager/signature/manifest.go | 3 + pkg/server/wire.go | 2 + pkg/services/featuremgmt/registry.go | 6 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../sqlstore/migrations/db_file_storage.go | 41 + 19 files changed, 3072 insertions(+), 61 deletions(-) create mode 100644 pkg/infra/filestorage/api.go create mode 100644 pkg/infra/filestorage/api_test.go create mode 100644 pkg/infra/filestorage/cdk_blob_filestorage.go create mode 100644 pkg/infra/filestorage/db_filestorage.go create mode 100644 pkg/infra/filestorage/dummy.go create mode 100644 pkg/infra/filestorage/filestorage.go create mode 100644 pkg/infra/filestorage/filestorage_test.go create mode 100644 pkg/infra/filestorage/fs_integration_test.go create mode 100644 pkg/infra/filestorage/test_utils.go create mode 100644 pkg/infra/filestorage/wrapper.go create mode 100644 pkg/services/sqlstore/migrations/db_file_storage.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fee3c1ec2b4..1a3db25c241 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -53,9 +53,10 @@ go.sum @grafana/backend-platform /pkg/services/sqlstore/migrations @grafana/backend-platform @grafana/hosted-grafana-team *_mig.go @grafana/backend-platform @grafana/hosted-grafana-team -# Grafana live +# Grafana edge /pkg/services/live/ @grafana/grafana-edge-squad /pkg/services/searchV2/ @grafana/grafana-edge-squad +/pkg/infra/filestore/ @grafana/grafana-edge-squad # Alerting /pkg/services/ngalert @grafana/alerting-squad-backend diff --git a/go.mod b/go.mod index 19c0d32aa94..04e2a981048 100644 --- a/go.mod +++ b/go.mod @@ -14,16 +14,16 @@ replace k8s.io/client-go => k8s.io/client-go v0.22.1 replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/goxmldsig v1.1.1 require ( - cloud.google.com/go/storage v1.14.0 + cloud.google.com/go/storage v1.18.2 cuelang.org/go v0.4.0 - github.com/Azure/azure-sdk-for-go v57.1.0+incompatible + github.com/Azure/azure-sdk-for-go v59.3.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0 - github.com/Azure/go-autorest/autorest v0.11.20 + github.com/Azure/go-autorest/autorest v0.11.22 github.com/BurntSushi/toml v0.3.1 github.com/Masterminds/semver v1.5.0 github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f - github.com/aws/aws-sdk-go v1.40.37 + github.com/aws/aws-sdk-go v1.42.8 github.com/beevik/etree v1.1.0 github.com/benbjohnson/clock v1.1.0 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b @@ -31,7 +31,7 @@ require ( github.com/cortexproject/cortex v1.10.1-0.20211014125347-85c378182d0d github.com/crewjam/saml v0.4.6-0.20210521115923-29c6295245bd github.com/davecgh/go-spew v1.1.1 - github.com/denisenkom/go-mssqldb v0.10.0 + github.com/denisenkom/go-mssqldb v0.11.0 github.com/dop251/goja v0.0.0-20210804101310-32956a348b49 github.com/fatih/color v1.10.0 github.com/gchaincl/sqlhooks v1.3.0 @@ -66,7 +66,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/jung-kurt/gofpdf v1.16.2 github.com/laher/mergefs v0.1.1 - github.com/lib/pq v1.10.0 + github.com/lib/pq v1.10.4 github.com/linkedin/goavro/v2 v2.10.0 github.com/m3db/prometheus_remote_client_golang v0.4.4 github.com/magefile/mage v1.12.1 @@ -103,16 +103,16 @@ require ( go.opentelemetry.io/otel/exporters/jaeger v1.0.0 go.opentelemetry.io/otel/sdk v1.0.0 go.opentelemetry.io/otel/trace v1.2.0 - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect - golang.org/x/net v0.0.0-20211013171255-e13a2654a71e - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f + golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 + golang.org/x/exp v0.0.0-20210220032938-85be41e4509f + golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac + golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/tools v0.1.5 gonum.org/v1/gonum v0.9.3 - google.golang.org/api v0.58.0 - google.golang.org/grpc v1.41.0 + google.golang.org/api v0.60.0 + google.golang.org/grpc v1.42.0 google.golang.org/protobuf v1.27.1 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.62.0 @@ -168,14 +168,14 @@ require ( github.com/go-openapi/errors v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/loads v0.20.2 // indirect + github.com/go-openapi/loads v0.20.2 github.com/go-openapi/runtime v0.19.29 // indirect - github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/spec v0.20.4 github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/validate v0.20.2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/status v1.1.0 // indirect - github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang-jwt/jwt/v4 v4.1.0 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -207,7 +207,7 @@ require ( github.com/mattn/go-runewidth v0.0.9 // indirect github.com/miekg/dns v1.1.43 // indirect github.com/mitchellh/go-testing-interface v1.14.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mna/redisc v1.3.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -242,27 +242,32 @@ require ( go.mongodb.org/mongo-driver v1.7.0 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 - go.uber.org/goleak v1.1.10 // indirect - golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2 // indirect + google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect ) require ( cloud.google.com/go/kms v1.1.0 github.com/golang-migrate/migrate/v4 v4.7.0 + gocloud.dev v0.24.0 ) require ( - github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.17 // indirect + github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect + github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect + github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect github.com/containerd/containerd v1.5.9 // indirect + github.com/envoyproxy/go-control-plane v0.10.1 // indirect + github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f // indirect github.com/imdario/mergo v0.3.12 // indirect - github.com/klauspost/compress v1.13.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.8 // indirect github.com/segmentio/asm v1.1.1 // indirect @@ -274,3 +279,7 @@ replace github.com/crewjam/saml => github.com/grafana/saml v0.0.0-20211007135653 replace github.com/apache/thrift => github.com/apache/thrift v0.14.1 replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.10.2 + +// TODO: remove once gocloud.dev releases 0.25.x +// `fileblob` implementation has buggy key ordering in 0.24.0 +replace gocloud.dev v0.24.0 => github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a diff --git a/go.sum b/go.sum index 67d93cc8c4c..b409fd39e11 100644 --- a/go.sum +++ b/go.sum @@ -20,10 +20,10 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.82.0/go.mod h1:vlKccHJGuFBFufnAnuB08dfEH9Y3H7dzDzRECFdC2TA= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= @@ -44,25 +44,33 @@ cloud.google.com/go/bigtable v1.3.0/go.mod h1:z5EyKrPE8OQmeg4h5MNdKvuSnI9CCT49Ki cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/kms v1.0.0/go.mod h1:nhUehi+w7zht2XrUfvTRNpxrfayBHqP4lu2NSywui/0= cloud.google.com/go/kms v1.1.0 h1:1yc4rLqCkVDS9Zvc7m+3mJ47kw0Uo5Q5+sMjcmUVUeM= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= +cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.17.1/go.mod h1:4qDxMr1WsM9+aQAz36ltDwCIM+R0QdlseyFjBuNvnss= +cloud.google.com/go/secretmanager v1.0.0/go.mod h1:+Qkm5qxIJ5mk74xxIXA+87fseaY1JLYBcFPQoc/GQxg= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.3.0/go.mod h1:9IAwXhoyBJ7z9LcAwkj0/7NnPzYaPeZxxVp3zm+5IqA= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.18.2 h1:5NQw6tOn3eMm0oE8vTkfjau18kjL79FlMjy/CHTpmoY= +cloud.google.com/go/storage v1.18.2/go.mod h1:AiIj7BWXyhO5gGVmYJ+S8tbkCx3yb0IMjua8Aw4naVM= +cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs= contrib.go.opencensus.io/exporter/prometheus v0.3.0/go.mod h1:rpCPVQKhiyH8oomWgm34ZmgIdZa8OVYO5WAIygPbBBE= +contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= +contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= cuelang.org/go v0.4.0 h1:GLJblw6m2WGGCA3k1v6Wbk9gTOt2qto48ahO2MmSd6I= cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -70,6 +78,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg= +github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= +github.com/Azure/azure-amqp-common-go/v3 v3.2.2/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= github.com/Azure/azure-event-hubs-go/v3 v3.2.0/go.mod h1:BPIIJNH/l/fVHYq3Rm6eg4clbrULrQ3q7+icmqHyyLc= github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= github.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= @@ -88,23 +98,29 @@ github.com/Azure/azure-sdk-for-go v44.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v45.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v46.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v48.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v54.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v55.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v57.1.0+incompatible h1:TKQ3ieyB0vVKkF6t9dsWbMjq56O1xU3eh3Ec09v6ajM= github.com/Azure/azure-sdk-for-go v57.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ= +github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0 h1:jq5Urf8QJK6h0wr8CMiwggo4OSMkXwpArQlkSjSpaBk= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 h1:v9p9TfTbf7AwNb5NYQt7hI41IfPoLFiFkLtb+bmGjT0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= github.com/Azure/azure-storage-queue-go v0.0.0-20181215014128-6ed74e755687/go.mod h1:K6am8mT+5iFXgingS9LUc7TmbsW6XBw3nxaRyaMyWc8= github.com/Azure/go-amqp v0.12.6/go.mod h1:qApuH6OFTSKZFmCOxccvAv5rLizBQf4v8pRmG138DPo= +github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= +github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -123,8 +139,9 @@ github.com/Azure/go-autorest/autorest v0.11.11/go.mod h1:eipySxLmqSyC5s5k1CLupqe github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.20 h1:s8H1PbCZSqg/DH7JMlOz6YMig6htWLNPsjDdlLqCx3M= github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= +github.com/Azure/go-autorest/autorest v0.11.22 h1:bXiQwDjrRmBQOE67bwlvUKAC1EU1yZTPQ38c+bstZws= +github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= github.com/Azure/go-autorest/autorest/adal v0.8.1-0.20191028180845-3492b2aff503/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= @@ -137,12 +154,15 @@ github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyC github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.15 h1:X+p2GF0GWyOiSmqohIaEeuNFNDY4I4EOlVuUQvFdWMk= github.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/adal v0.9.17 h1:esOPl2dhcz9P3jqBSJ8tPGEj2EqzPPT6zfyuloiogKY= +github.com/Azure/go-autorest/autorest/adal v0.9.17/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.4/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= @@ -183,6 +203,7 @@ github.com/FZambia/eagle v0.0.1 h1:FN1yTkPihMb5nE8SrlRjoCf7T9H9bTKJFQOm6ach2YU= github.com/FZambia/eagle v0.0.1/go.mod h1:xq6u/JeNZ5/8mrAQ76MMhzNTodASh9FavQlCgg4j48w= github.com/FZambia/sentinel v1.1.0 h1:qrCBfxc8SvJihYNjBWgwUI93ZCvFe/PJIPTHKmlp8a8= github.com/FZambia/sentinel v1.1.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.27.0/go.mod h1:bn9iHmAjogMoIPkqBGyJ9R1m9cXGCjBE/cuhBs3oEsQ= github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnloDp7xxV0YvstAE7nKTaM= github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I= @@ -306,6 +327,7 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -324,18 +346,57 @@ github.com/aws/aws-sdk-go v1.34.34/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/ github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.35.30/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.35.31/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.37.8/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.3/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.60/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.40.11/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/aws/aws-sdk-go v1.40.37 h1:I+Q6cLctkFyMMrKukcDnj+i2kjrQ37LGiOM6xmsxC48= github.com/aws/aws-sdk-go v1.40.37/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.42.8 h1:Tj2RP4Fas1mYchwbmw0qWLJIEATAseyp5iTa1D+LWYQ= +github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4= +github.com/aws/aws-sdk-go-v2 v1.11.0 h1:HxyD62DyNhCfiFGUHqJ/xITD6rAjJ7Dm/2nLxLmO4Ag= +github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 h1:yVUAwvJC/0WNPbyl0nA3j1L6CW1CN8wBubCRqtG7JLI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0/go.mod h1:Xn6sxgRuIDflLRJFj5Ev7UxABIkNbccFPV/p8itDReM= +github.com/aws/aws-sdk-go-v2/config v1.10.1 h1:z/ViqIjW6ZeuLWgTWMTSyZzaVWo/1cWeVf1Uu+RF01E= +github.com/aws/aws-sdk-go-v2/config v1.10.1/go.mod h1:auIv5pIIn3jIBHNRcVQcsczn6Pfa6Dyv80Fai0ueoJU= +github.com/aws/aws-sdk-go-v2/credentials v1.6.1 h1:A39JYth2fFCx+omN/gib/jIppx3rRnt2r7UKPq7Mh5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.6.1/go.mod h1:QyvQk1IYTqBWSi1T6UgT/W8DMxBVa5pVuLFSRLLhGf8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0 h1:OpZjuUy8Jt3CA1WgJgBC5Bz+uOjE5Ppx4NFTRaooUuA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0/go.mod h1:5E1J3/TTYy6z909QNR0QnXGBpfESYGDqd3O0zqONghU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.1 h1:p9Dys1g2YdaqMalnp6AwCA+tpMMdJNGw5YYKP/u3sUk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.1/go.mod h1:wN/mvkow08GauDwJ70jnzJ1e+hE+Q3Q7TwpYLXOe9oI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0 h1:zY8cNmbBXt3pzjgWgdIbzpQ6qxoCwt+Nx9JbrAf2mbY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0/go.mod h1:NO3Q5ZTTQtO2xIg2+xTXYDiT7knSejfeDm7WGDaOo0U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.0 h1:Z3aR/OXBnkYK9zXkNkfitHX6SmUBzSsx8VMHbH4Lvhw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.0/go.mod h1:anlUzBoEWglcUxUQwZA7HQOEVEnQALVZsizAapB2hq8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.0 h1:c10Z7fWxtJCoyc8rv06jdh9xrKnu7bAJiRaKWvTb2mU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.0/go.mod h1:6oXGy4GLpypD3uCh8wcqztigGgmhLToMfjavgh+VySg= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.0 h1:qGZWS/WgiFY+Zgad2u0gwBHpJxz6Ne401JE7iQI1nKs= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.0/go.mod h1:Mq6AEc+oEjCUlBuLiK5YwW4shSOAKCQ3tXN0sQeYoBA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.0 h1:0BOlTqnNnrEO04oYKzDxMMe68t107pmIotn18HtVonY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.0/go.mod h1:xKCZ4YFSF2s4Hnb/J0TLeOsKuGzICzcElaOKNGrVnx4= +github.com/aws/aws-sdk-go-v2/service/kms v1.10.0/go.mod h1:ZkHWL8m5Nw1g9yMXqpCjnIJtSDToAmNbXXZ9gj0bO7s= +github.com/aws/aws-sdk-go-v2/service/s3 v1.19.0 h1:5mRAms4TjSTOGYsqKYte5kHr1PzpMJSyLThjF3J+hw0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.19.0/go.mod h1:Gwz3aVctJe6mUY9T//bcALArPUaFmNAy2rTB9qN4No8= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.10.0/go.mod h1:qAgsrzF3Z2vvV01j79fs7D75ofCMQe81/OKBJx0rjFY= +github.com/aws/aws-sdk-go-v2/service/sns v1.11.0/go.mod h1:LIPf3BTbSY5UeVli+x/1y2Qw1w8T9DYyp7p18Qt8Zc8= +github.com/aws/aws-sdk-go-v2/service/sqs v1.12.0/go.mod h1:TDqDmQnsbgL2ZMIGUf3z9xTzCMqFX7FP1geAgIlYqvA= +github.com/aws/aws-sdk-go-v2/service/ssm v1.15.0/go.mod h1:kJa2uHklY03rKsNSbEsToeUgWJ1PambXBtRNacorRhg= +github.com/aws/aws-sdk-go-v2/service/sso v1.6.0 h1:JDgKIUZOmLFu/Rv6zXLrVTWCmzA0jcTdvsT8iFIKrAI= +github.com/aws/aws-sdk-go-v2/service/sso v1.6.0/go.mod h1:Q/l0ON1annSU+mc0JybDy1Gy6dnJxIcWjphO6qJPzvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.10.0 h1:1jh8J+JjYRp+QWKOsaZt7rGUgoyrqiiVwIm+w0ymeUw= +github.com/aws/aws-sdk-go-v2/service/sts v1.10.0/go.mod h1:jLKCFqS+1T4i7HDqCP9GM4Uk75YW1cS0o82LdxpMyOE= github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58= +github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= @@ -395,6 +456,7 @@ github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/centrifugal/centrifuge v0.19.0 h1:YHws0dRpgsBiI73tRl1wwaB13gzuaI1AM4IFcQQQqcw= github.com/centrifugal/centrifuge v0.19.0/go.mod h1:O2elf8q3Qkie3z97wkqVqxB52pnOpPsfFUa7L88Lpy0= @@ -432,9 +494,14 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158 h1:CevA8fI91PAnP8vpnXuB8ZYAZ5wqY86nAbxfgK8tWO4= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.1/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -722,11 +789,13 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.9/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.1 h1:4CF52PCseTFt4bE+Yk3dIpdVi7XWuPVMhPtm4FaIJPM= github.com/envoyproxy/protoc-gen-validate v0.6.1/go.mod h1:txg5va2Qkip90uYoSKH+nkAAmXrb2j3iq4FLwdrCbXQ= +github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/ericchiang/k8s v1.2.0/go.mod h1:/OmBgSq2cd9IANnsGHGlEz27nwMZV2YxlpXuQtU3Bz4= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= @@ -761,8 +830,9 @@ github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -783,6 +853,8 @@ github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NB github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/glinton/ping v0.1.4-0.20200311211934-5ac87da8cd96/go.mod h1:uY+1eqFUyotrQxF1wYFNtMeHp/swbYRsoGzfcPZ8x3o= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -865,7 +937,6 @@ github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= @@ -907,7 +978,6 @@ github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeE github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= -github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= @@ -951,8 +1021,13 @@ github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9G github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts= github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v8 v8.0.0-beta.10.0.20200905143926-df7fe4e2ce72/go.mod h1:CJP1ZIHwhosNYwIdaHPZK9vHsM3+roNBaZ7U9Of1DXc= @@ -1042,8 +1117,9 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/gogo/status v1.0.3/go.mod h1:SavQ51ycCLnc7dGyJxp8YAmudx8xqiVrRf+6IXRsugc= github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= -github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -1113,6 +1189,8 @@ github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a h1:Kw18HR30Firrm4cB1BzcVVaFrrdDlAtnrCKgHxemgMI= +github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a/go.mod h1:TqAL9Q5Q8jFUfIDNbDiFbU3w0z3MQ7q4r1wm57tBhmI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1132,13 +1210,16 @@ github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3 github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.0.0/go.mod h1:LJhKoTwS5Wy5Ld/peq8dFFG5OfJyHEz7ft+DsTUv25M= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= @@ -1158,12 +1239,12 @@ github.com/google/pprof v0.0.0-20201007051231-1066cbb265c7/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201117184057-ae444373da19/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210208152844-1612e9be7af6/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210323184331-8eee2492667d/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210504235042-3a04a4d88a10/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -1241,17 +1322,12 @@ github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivG github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f/go.mod h1:uPG2nyK4CtgNDmWv7qyzYcdI+S90kHHRWvHnBtEMBXM= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/grafana/grafana-aws-sdk v0.9.1 h1:jMZlsLsWnqOwLt2UNcLUsJ2z6289hLYlscK35QgS158= -github.com/grafana/grafana-aws-sdk v0.9.1/go.mod h1:6KaQ8uUD4KpXr/b7bAC7zbfSXTVOiTk4XhIrwkGWn4w= -github.com/grafana/grafana-aws-sdk v0.10.0 h1:q7+mJtT/vsU5InDN57yM+BJ2z1kJDf1W4WwWPEZ0Cxw= -github.com/grafana/grafana-aws-sdk v0.10.0/go.mod h1:vFIOHEnY1u5nY0/tge1IHQjPuG6DRKr2ISf/HikUdjE= github.com/grafana/grafana-aws-sdk v0.10.1 h1:Ksguhjx6EuGLN/5Oc7oZoxuDReJ5RxIH99yqSMpLGUs= github.com/grafana/grafana-aws-sdk v0.10.1/go.mod h1:vFIOHEnY1u5nY0/tge1IHQjPuG6DRKr2ISf/HikUdjE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.125.0 h1:wK2zopAaKhVIMkXzgbExKqZtt+x2ZTGfcY+3wvOuyYQ= github.com/grafana/grafana-plugin-sdk-go v0.125.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc= github.com/grafana/grafana-plugin-sdk-go v0.126.0 h1:GFstod7B/r5Ls9QiYV18fnOVtpWAtfR8aYSXfBvbCjE= github.com/grafana/grafana-plugin-sdk-go v0.126.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc= @@ -1284,6 +1360,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.15.0/go.mod h1:vO11I9oWA+KsxmfFQPhLnnI github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= +github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0/go.mod h1:dk23l2BruuUzRP8wbybQbPn3J7sZga2QHICCeaEy5rQ= github.com/hashicorp/consul v1.10.2 h1:9YX5SX3hMifrXIt9wqN2jJsMnESSHfxEjW5N7qMAdjo= github.com/hashicorp/consul v1.10.2/go.mod h1:EJMYpT39ZL2BnxjGRNTjfTH3s9893yd/DCX60PUnGUY= @@ -1424,6 +1502,7 @@ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbc github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/igm/sockjs-go/v3 v3.0.1 h1:rmgEkeKqBHCFf7uIAipYrYSX8x9LBB2nOxAac2sooak= @@ -1570,13 +1649,15 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -1621,14 +1702,16 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/ragel-machinery v0.0.0-20181214104525-299bdde78165/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= github.com/leoluk/perflib_exporter v0.1.0/go.mod h1:rpV0lYj7lemdTm31t7zpCqYqPnw7xs86f+BaaNBVYFM= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.0/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= @@ -1643,10 +1726,10 @@ github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1 github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/iostat v1.1.0/go.mod h1:rEPNA0xXgjHQjuI5Cy05sLlS2oRcSlWHRLrvh/AQ+Pg= github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8= github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc= -github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.12.1 h1:oGdAbhIUd6iKamKlDGVtU6XGdy5SgNuCWn7gCTgHDtU= github.com/magefile/mage v1.12.1/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= @@ -1769,8 +1852,9 @@ github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1-0.20210112042008-8ebf2d61a8b4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/pointerstructure v1.0.0/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -2485,6 +2569,7 @@ go.mongodb.org/mongo-driver v1.5.2/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS go.mongodb.org/mongo-driver v1.7.0 h1:hHrvOBWlWB2c7+8Gh/Xi5jj82AgidK/t7KVXBZ+IyUA= go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -2534,13 +2619,15 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU= go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -2550,6 +2637,7 @@ go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= @@ -2600,8 +2688,11 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2659,6 +2750,7 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2739,9 +2831,12 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e h1:Xj+JO91noE97IN6F/7WZxzC5QE6yENAQPrwIYhW3bsA= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2762,8 +2857,10 @@ golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2927,11 +3024,16 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2961,8 +3063,9 @@ golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3064,7 +3167,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -3129,8 +3231,10 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.58.0 h1:MDkAbYIB1JpSgCTOCYYoIec/coMlKK4oVbpnBLLcyT0= google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk= +google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3199,9 +3303,7 @@ google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -3209,6 +3311,7 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -3227,8 +3330,14 @@ google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2 h1:CUp93KYgL06Y/PdI8aRJaFiAHevPIGWQmijSqaUhue8= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211019152133-63b7e35f4404/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -3268,8 +3377,9 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -3471,6 +3581,7 @@ modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6 modernc.org/memory v1.0.1/go.mod h1:NSjvC08+g3MLOpcAxQbdctcThAEX4YlJ20WWHYEhvRg= modernc.org/sqlite v1.7.4/go.mod h1:xse4RHCm8Fzw0COf5SJqAyiDrVeDwAQthAS1V/woNIA= modernc.org/tcl v1.4.1/go.mod h1:8YCvzidU9SIwkz7RZwlCWK61mhV8X9UwfkRDRp7y5e0= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index c566a4b2998..b838c5c379c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -46,4 +46,5 @@ export interface FeatureToggles { dashboardComments?: boolean; annotationComments?: boolean; migrationLocking?: boolean; + fileStoreApi?: boolean; } diff --git a/pkg/infra/filestorage/api.go b/pkg/infra/filestorage/api.go new file mode 100644 index 00000000000..b89f42c46bb --- /dev/null +++ b/pkg/infra/filestorage/api.go @@ -0,0 +1,101 @@ +package filestorage + +import ( + "context" + "errors" + "strings" + "time" +) + +type StorageName string + +const ( + StorageNamePublic StorageName = "public" +) + +var ( + ErrRelativePath = errors.New("path cant be relative") + ErrNonCanonicalPath = errors.New("path must be canonical") + ErrPathTooLong = errors.New("path is too long") + ErrPathInvalid = errors.New("path is invalid") + ErrPathEndsWithDelimiter = errors.New("path can not end with delimiter") + Delimiter = "/" +) + +func Join(parts ...string) string { + return Delimiter + strings.Join(parts, Delimiter) +} + +func belongsToStorage(path string, storageName StorageName) bool { + return strings.HasPrefix(path, Delimiter+string(storageName)) +} + +type File struct { + Contents []byte + FileMetadata +} + +type FileMetadata struct { + Name string + FullPath string + MimeType string + Modified time.Time + Created time.Time + Size int64 + Properties map[string]string +} + +type ListFilesResponse struct { + Files []FileMetadata + HasMore bool + LastPath string +} + +type Paging struct { + After string + First int +} + +type UpsertFileCommand struct { + Path string + MimeType string + Contents *[]byte + Properties map[string]string +} + +type PathFilters struct { + allowedPrefixes []string +} + +func (f *PathFilters) isAllowed(path string) bool { + if f == nil || f.allowedPrefixes == nil { + return true + } + + for i := range f.allowedPrefixes { + if strings.HasPrefix(path, strings.ToLower(f.allowedPrefixes[i])) { + return true + } + } + + return false +} + +type ListOptions struct { + Recursive bool + PathFilters +} + +type FileStorage interface { + Get(ctx context.Context, path string) (*File, error) + Delete(ctx context.Context, path string) error + Upsert(ctx context.Context, command *UpsertFileCommand) error + + ListFiles(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListFilesResponse, error) + ListFolders(ctx context.Context, folderPath string, options *ListOptions) ([]FileMetadata, error) + + CreateFolder(ctx context.Context, path string) error + DeleteFolder(ctx context.Context, path string) error + + close() error +} diff --git a/pkg/infra/filestorage/api_test.go b/pkg/infra/filestorage/api_test.go new file mode 100644 index 00000000000..a15d1f5edfe --- /dev/null +++ b/pkg/infra/filestorage/api_test.go @@ -0,0 +1,75 @@ +package filestorage + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFilestorageApi_Join(t *testing.T) { + var tests = []struct { + name string + parts []string + expected string + }{ + { + name: "multiple parts", + parts: []string{"prefix", "p1", "p2"}, + expected: "/prefix/p1/p2", + }, + { + name: "no parts", + parts: []string{}, + expected: "/", + }, + { + name: "a single part", + parts: []string{"prefix"}, + expected: "/prefix", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, Join(tt.parts...)) + }) + } +} + +func TestFilestorageApi_belongToStorage(t *testing.T) { + var tests = []struct { + name string + path string + storage StorageName + expected bool + }{ + { + name: "should return true if path is prefixed with delimiter and the storage name", + path: "/public/abc/d", + storage: StorageNamePublic, + expected: true, + }, + { + name: "should return true if path consists just of the delimiter and the storage name", + path: "/public", + storage: StorageNamePublic, + expected: true, + }, + { + name: "should return false if path is not prefixed with delimiter", + path: "public/abc/d", + storage: StorageNamePublic, + expected: false, + }, + { + name: "should return false if storage name does not match", + path: "/notpublic/abc/d", + storage: StorageNamePublic, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, belongsToStorage(tt.path, tt.storage)) + }) + } +} diff --git a/pkg/infra/filestorage/cdk_blob_filestorage.go b/pkg/infra/filestorage/cdk_blob_filestorage.go new file mode 100644 index 00000000000..61691e7ea48 --- /dev/null +++ b/pkg/infra/filestorage/cdk_blob_filestorage.go @@ -0,0 +1,491 @@ +package filestorage + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + "github.com/grafana/grafana/pkg/infra/log" + "gocloud.dev/blob" + "gocloud.dev/gcerrors" +) + +const ( + originalPathAttributeKey = "__gf_original_path__" +) + +type cdkBlobStorage struct { + log log.Logger + bucket *blob.Bucket + rootFolder string +} + +func NewCdkBlobStorage(log log.Logger, bucket *blob.Bucket, rootFolder string, pathFilters *PathFilters) FileStorage { + return &wrapper{ + log: log, + wrapped: &cdkBlobStorage{ + log: log, + bucket: bucket, + rootFolder: rootFolder, + }, + pathFilters: pathFilters, + } +} + +func (c cdkBlobStorage) Get(ctx context.Context, filePath string) (*File, error) { + contents, err := c.bucket.ReadAll(ctx, strings.ToLower(filePath)) + if err != nil { + if gcerrors.Code(err) == gcerrors.NotFound { + return nil, nil + } + return nil, err + } + attributes, err := c.bucket.Attributes(ctx, strings.ToLower(filePath)) + if err != nil { + return nil, err + } + + var originalPath string + var props map[string]string + if attributes.Metadata != nil { + props = attributes.Metadata + if path, ok := attributes.Metadata[originalPathAttributeKey]; ok { + originalPath = path + delete(props, originalPathAttributeKey) + } + } else { + props = make(map[string]string) + originalPath = filePath + } + + return &File{ + Contents: contents, + FileMetadata: FileMetadata{ + Name: getName(originalPath), + FullPath: originalPath, + Created: attributes.CreateTime, + Properties: props, + Modified: attributes.ModTime, + Size: attributes.Size, + MimeType: detectContentType(originalPath, attributes.ContentType), + }, + }, nil +} + +func (c cdkBlobStorage) Delete(ctx context.Context, filePath string) error { + exists, err := c.bucket.Exists(ctx, strings.ToLower(filePath)) + if err != nil { + return err + } + + if !exists { + return nil + } + + err = c.bucket.Delete(ctx, strings.ToLower(filePath)) + return err +} + +func (c cdkBlobStorage) Upsert(ctx context.Context, command *UpsertFileCommand) error { + existing, err := c.Get(ctx, command.Path) + if err != nil { + return err + } + + var contents []byte + var metadata map[string]string + + if existing == nil { + if command.Contents == nil { + contents = make([]byte, 0) + } else { + contents = *command.Contents + } + + metadata = make(map[string]string) + if command.Properties != nil { + for k, v := range command.Properties { + metadata[k] = v + } + } + metadata[originalPathAttributeKey] = command.Path + return c.bucket.WriteAll(ctx, strings.ToLower(command.Path), contents, &blob.WriterOptions{ + Metadata: metadata, + }) + } + + contents = existing.Contents + if command.Contents != nil { + contents = *command.Contents + } + + if command.Properties != nil { + metadata = make(map[string]string) + for k, v := range command.Properties { + metadata[k] = v + } + } else { + metadata = existing.FileMetadata.Properties + } + + metadata[originalPathAttributeKey] = existing.FullPath + return c.bucket.WriteAll(ctx, strings.ToLower(command.Path), contents, &blob.WriterOptions{ + Metadata: metadata, + }) +} + +func (c cdkBlobStorage) listFiles(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListFilesResponse, error) { + iterator := c.bucket.List(&blob.ListOptions{ + Prefix: strings.ToLower(folderPath), + Delimiter: Delimiter, + }) + + recursive := options.Recursive + + pageSize := paging.First + + foundCursor := true + if paging.After != "" { + foundCursor = false + } + + hasMore := true + files := make([]FileMetadata, 0) + for { + obj, err := iterator.Next(ctx) + if obj != nil && strings.HasSuffix(obj.Key, directoryMarker) { + continue + } + + if errors.Is(err, io.EOF) { + hasMore = false + break + } else { + hasMore = true + } + + if err != nil { + c.log.Error("Failed while iterating over files", "err", err) + return nil, err + } + + if len(files) >= pageSize { + break + } + + path := obj.Key + + allowed := options.isAllowed(obj.Key) + if obj.IsDir && recursive { + newPaging := &Paging{ + First: pageSize - len(files), + } + if paging != nil { + newPaging.After = paging.After + } + + resp, err := c.listFiles(ctx, path, newPaging, options) + + if err != nil { + return nil, err + } + + if len(files) > 0 { + foundCursor = true + } + + files = append(files, resp.Files...) + if len(files) >= pageSize { + //nolint: staticcheck + hasMore = resp.HasMore + } + } else if !obj.IsDir && allowed { + if !foundCursor { + res := strings.Compare(obj.Key, paging.After) + if res < 0 { + continue + } else if res == 0 { + foundCursor = true + continue + } else { + foundCursor = true + } + } + + attributes, err := c.bucket.Attributes(ctx, strings.ToLower(path)) + if err != nil { + c.log.Error("Failed while retrieving attributes", "path", path, "err", err) + return nil, err + } + + var originalPath string + var props map[string]string + if attributes.Metadata != nil { + props = attributes.Metadata + if path, ok := attributes.Metadata[originalPathAttributeKey]; ok { + originalPath = path + delete(props, originalPathAttributeKey) + } + } else { + props = make(map[string]string) + originalPath = fixPath(path) + } + + files = append(files, FileMetadata{ + Name: getName(originalPath), + FullPath: originalPath, + Created: attributes.CreateTime, + Properties: props, + Modified: attributes.ModTime, + Size: attributes.Size, + MimeType: detectContentType(originalPath, attributes.ContentType), + }) + } + } + + lastPath := "" + if len(files) > 0 { + lastPath = files[len(files)-1].FullPath + } + + return &ListFilesResponse{ + Files: files, + HasMore: hasMore, + LastPath: lastPath, + }, nil +} + +func (c cdkBlobStorage) fixInputPrefix(path string) string { + if path == Delimiter || path == "" { + return c.rootFolder + } + if strings.HasPrefix(path, Delimiter) { + path = fmt.Sprintf("%s%s", c.rootFolder, strings.TrimPrefix(path, Delimiter)) + } + + return path +} + +func (c cdkBlobStorage) convertFolderPathToPrefix(path string) string { + if path == Delimiter || path == "" { + return c.rootFolder + } + if strings.HasPrefix(path, Delimiter) { + path = fmt.Sprintf("%s%s", c.rootFolder, strings.TrimPrefix(path, Delimiter)) + } + return fmt.Sprintf("%s%s", path, Delimiter) +} + +func fixPath(path string) string { + newPath := strings.TrimSuffix(path, Delimiter) + if !strings.HasPrefix(newPath, Delimiter) { + newPath = fmt.Sprintf("%s%s", Delimiter, newPath) + } + return newPath +} + +func (c cdkBlobStorage) convertListOptions(options *ListOptions) *ListOptions { + if options == nil || options.allowedPrefixes == nil || len(options.allowedPrefixes) == 0 { + return options + } + + newPrefixes := make([]string, len(options.allowedPrefixes)) + for i, prefix := range options.allowedPrefixes { + newPrefixes[i] = c.fixInputPrefix(prefix) + } + + options.PathFilters.allowedPrefixes = newPrefixes + return options +} + +func (c cdkBlobStorage) ListFiles(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListFilesResponse, error) { + paging.After = c.fixInputPrefix(paging.After) + return c.listFiles(ctx, c.convertFolderPathToPrefix(folderPath), paging, c.convertListOptions(options)) +} + +func (c cdkBlobStorage) listFolderPaths(ctx context.Context, parentFolderPath string, options *ListOptions) ([]string, error) { + iterator := c.bucket.List(&blob.ListOptions{ + Prefix: strings.ToLower(parentFolderPath), + Delimiter: Delimiter, + }) + + recursive := options.Recursive + + currentDirPath := "" + foundPaths := make([]string, 0) + for { + obj, err := iterator.Next(ctx) + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + c.log.Error("Failed while iterating over files", "err", err) + return nil, err + } + + if currentDirPath == "" && !obj.IsDir && options.isAllowed(obj.Key) { + attributes, err := c.bucket.Attributes(ctx, obj.Key) + if err != nil { + c.log.Error("Failed while retrieving attributes", "path", obj.Key, "err", err) + return nil, err + } + + if attributes.Metadata != nil { + if path, ok := attributes.Metadata[originalPathAttributeKey]; ok { + currentDirPath = getParentFolderPath(path) + } + } + } + + if obj.IsDir && recursive { + resp, err := c.listFolderPaths(ctx, obj.Key, options) + + if err != nil { + return nil, err + } + + if len(resp) > 0 { + foundPaths = append(foundPaths, resp...) + } + continue + } + } + + if currentDirPath != "" { + foundPaths = append(foundPaths, fixPath(currentDirPath)) + } + return foundPaths, nil +} + +func (c cdkBlobStorage) ListFolders(ctx context.Context, prefix string, options *ListOptions) ([]FileMetadata, error) { + foundPaths, err := c.listFolderPaths(ctx, c.convertFolderPathToPrefix(prefix), c.convertListOptions(options)) + if err != nil { + return nil, err + } + + folders := make([]FileMetadata, 0) + mem := make(map[string]bool) + for i := 0; i < len(foundPaths); i++ { + path := foundPaths[i] + parts := strings.Split(path, Delimiter) + acc := parts[0] + j := 1 + for { + acc = fmt.Sprintf("%s%s%s", acc, Delimiter, parts[j]) + + comparison := strings.Compare(acc, prefix) + if !mem[acc] && comparison > 0 { + folders = append(folders, FileMetadata{ + Name: getName(acc), + FullPath: acc, + }) + } + mem[acc] = true + + j += 1 + if j >= len(parts) { + break + } + } + } + + return folders, err +} + +func precedingFolders(path string) []string { + parts := strings.Split(path, Delimiter) + if len(parts) == 0 { + return []string{} + } + + if len(parts) == 1 { + return []string{path} + } + + currentDirPath := "" + firstPart := 0 + if parts[0] == "" { + firstPart = 1 + currentDirPath = Delimiter + } + + res := make([]string, 0) + for i := firstPart; i < len(parts); i++ { + res = append(res, currentDirPath+parts[i]) + currentDirPath += parts[i] + Delimiter + } + + return res +} + +func (c cdkBlobStorage) CreateFolder(ctx context.Context, path string) error { + c.log.Info("Creating folder", "path", path) + + precedingFolders := precedingFolders(path) + folderToOriginalCasing := make(map[string]string) + foundFolderIndex := -1 + + for i := len(precedingFolders) - 1; i >= 0; i-- { + currentFolder := precedingFolders[i] + att, err := c.bucket.Attributes(ctx, strings.ToLower(currentFolder+Delimiter+directoryMarker)) + if err != nil { + if gcerrors.Code(err) != gcerrors.NotFound { + return err + } + folderToOriginalCasing[currentFolder] = currentFolder + continue + } + + if path, ok := att.Metadata[originalPathAttributeKey]; ok { + folderToOriginalCasing[currentFolder] = getParentFolderPath(path) + foundFolderIndex = i + break + } else { + folderToOriginalCasing[currentFolder] = currentFolder + } + } + + for i := foundFolderIndex + 1; i < len(precedingFolders); i++ { + currentFolder := precedingFolders[i] + + previousFolderOriginalCasing := "" + if i > 0 { + previousFolderOriginalCasing = folderToOriginalCasing[precedingFolders[i-1]] + } + + metadata := make(map[string]string) + currentFolderWithOriginalCasing := previousFolderOriginalCasing + Delimiter + getName(currentFolder) + metadata[originalPathAttributeKey] = currentFolderWithOriginalCasing + Delimiter + directoryMarker + if err := c.bucket.WriteAll(ctx, strings.ToLower(metadata[originalPathAttributeKey]), make([]byte, 0), &blob.WriterOptions{ + Metadata: metadata, + }); err != nil { + return err + } + c.log.Info("Created folder", "path", currentFolderWithOriginalCasing, "marker", metadata[originalPathAttributeKey]) + } + + return nil +} + +func (c cdkBlobStorage) DeleteFolder(ctx context.Context, folderPath string) error { + directoryMarkerPath := fmt.Sprintf("%s%s%s", folderPath, Delimiter, directoryMarker) + exists, err := c.bucket.Exists(ctx, strings.ToLower(directoryMarkerPath)) + + if err != nil { + return err + } + + if !exists { + return nil + } + + err = c.bucket.Delete(ctx, strings.ToLower(directoryMarkerPath)) + return err +} + +func (c cdkBlobStorage) close() error { + return c.bucket.Close() +} diff --git a/pkg/infra/filestorage/db_filestorage.go b/pkg/infra/filestorage/db_filestorage.go new file mode 100644 index 00000000000..ff7ba4ecba2 --- /dev/null +++ b/pkg/infra/filestorage/db_filestorage.go @@ -0,0 +1,449 @@ +package filestorage + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util/errutil" +) + +type file struct { + Path string `xorm:"path"` + ParentFolderPath string `xorm:"parent_folder_path"` + Contents []byte `xorm:"contents"` + Updated time.Time `xorm:"updated"` + Created time.Time `xorm:"created"` + Size int64 `xorm:"size"` + MimeType string `xorm:"mime_type"` +} + +type fileMeta struct { + Path string `xorm:"path"` + Key string `xorm:"key"` + Value string `xorm:"value"` +} + +type dbFileStorage struct { + db *sqlstore.SQLStore + log log.Logger +} + +func NewDbStorage(log log.Logger, db *sqlstore.SQLStore, pathFilters *PathFilters) FileStorage { + return &wrapper{ + log: log, + wrapped: &dbFileStorage{ + log: log, + db: db, + }, + pathFilters: pathFilters, + } +} + +func (s dbFileStorage) getProperties(sess *sqlstore.DBSession, lowerCasePaths []string) (map[string]map[string]string, error) { + attributesByPath := make(map[string]map[string]string) + + entities := make([]*fileMeta, 0) + if err := sess.Table("file_meta").In("path", lowerCasePaths).Find(&entities); err != nil { + return nil, err + } + + for _, entity := range entities { + if _, ok := attributesByPath[entity.Path]; !ok { + attributesByPath[entity.Path] = make(map[string]string) + } + attributesByPath[entity.Path][entity.Key] = entity.Value + } + + return attributesByPath, nil +} + +func (s dbFileStorage) Get(ctx context.Context, filePath string) (*File, error) { + var result *File + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + table := &file{} + exists, err := sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(filePath)).Get(table) + if !exists { + return nil + } + + var meta = make([]*fileMeta, 0) + if err := sess.Table("file_meta").Where("path = ?", strings.ToLower(filePath)).Find(&meta); err != nil { + return err + } + + var metaProperties = make(map[string]string, len(meta)) + + for i := range meta { + metaProperties[meta[i].Key] = meta[i].Value + } + + contents := table.Contents + if contents == nil { + contents = make([]byte, 0) + } + + result = &File{ + Contents: contents, + FileMetadata: FileMetadata{ + Name: getName(table.Path), + FullPath: table.Path, + Created: table.Created, + Properties: metaProperties, + Modified: table.Updated, + Size: table.Size, + MimeType: table.MimeType, + }, + } + return err + }) + + return result, err +} + +func (s dbFileStorage) Delete(ctx context.Context, filePath string) error { + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + table := &file{} + exists, innerErr := sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(filePath)).Get(table) + if innerErr != nil { + return innerErr + } + + if !exists { + return nil + } + + number, innerErr := sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(filePath)).Delete(table) + if innerErr != nil { + return innerErr + } + s.log.Info("Deleted file", "path", filePath, "affectedRecords", number) + + metaTable := &fileMeta{} + number, innerErr = sess.Table("file_meta").Where("path = ?", strings.ToLower(filePath)).Delete(metaTable) + if innerErr != nil { + return innerErr + } + s.log.Info("Deleted metadata", "path", filePath, "affectedRecords", number) + return innerErr + }) + + return err +} + +func (s dbFileStorage) Upsert(ctx context.Context, cmd *UpsertFileCommand) error { + now := time.Now() + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + existing := &file{} + exists, err := sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(cmd.Path)).Get(existing) + if err != nil { + return err + } + + if exists { + existing.Updated = now + if cmd.Contents != nil { + contents := *cmd.Contents + existing.Contents = contents + existing.MimeType = cmd.MimeType + existing.Size = int64(len(contents)) + } + + _, err = sess.Where("LOWER(path) = ?", strings.ToLower(cmd.Path)).Update(existing) + if err != nil { + return err + } + } else { + contentsToInsert := make([]byte, 0) + if cmd.Contents != nil { + contentsToInsert = *cmd.Contents + } + + file := &file{ + Path: cmd.Path, + ParentFolderPath: getParentFolderPath(cmd.Path), + Contents: contentsToInsert, + MimeType: cmd.MimeType, + Size: int64(len(contentsToInsert)), + Updated: now, + Created: now, + } + _, err := sess.Insert(file) + if err != nil { + return err + } + } + + if len(cmd.Properties) != 0 { + if err = upsertProperties(sess, now, cmd); err != nil { + if rollbackErr := sess.Rollback(); rollbackErr != nil { + s.log.Error("failed while rolling back upsert", "path", cmd.Path) + } + return err + } + } + + return err + }) + + return err +} + +func upsertProperties(sess *sqlstore.DBSession, now time.Time, cmd *UpsertFileCommand) error { + fileMeta := &fileMeta{} + _, err := sess.Table("file_meta").Where("path = ?", strings.ToLower(cmd.Path)).Delete(fileMeta) + if err != nil { + return err + } + + for key, val := range cmd.Properties { + if err := upsertProperty(sess, now, cmd.Path, key, val); err != nil { + return err + } + } + return nil +} + +func upsertProperty(sess *sqlstore.DBSession, now time.Time, path string, key string, val string) error { + existing := &fileMeta{} + exists, err := sess.Table("file_meta").Where("path = ? AND key = ?", strings.ToLower(path), key).Get(existing) + if err != nil { + return err + } + + if exists { + existing.Value = val + _, err = sess.Where("path = ? AND key = ?", strings.ToLower(path), key).Update(existing) + } else { + _, err = sess.Insert(&fileMeta{ + Path: strings.ToLower(path), + Key: key, + Value: val, + }) + } + return err +} + +func (s dbFileStorage) ListFiles(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListFilesResponse, error) { + var resp *ListFilesResponse + + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + var foundFiles = make([]*file, 0) + + sess.Table("file") + lowerFolderPath := strings.ToLower(folderPath) + if options.Recursive { + var nestedFolders string + if folderPath == Delimiter { + nestedFolders = "%" + } else { + nestedFolders = fmt.Sprintf("%s%s%s", lowerFolderPath, Delimiter, "%") + } + sess.Where("(LOWER(parent_folder_path) = ?) OR (LOWER(parent_folder_path) LIKE ?)", lowerFolderPath, nestedFolders) + } else { + sess.Where("LOWER(parent_folder_path) = ?", lowerFolderPath) + } + sess.Where("LOWER(path) NOT LIKE ?", fmt.Sprintf("%s%s%s", "%", Delimiter, directoryMarker)) + + for _, prefix := range options.PathFilters.allowedPrefixes { + sess.Where("LOWER(path) LIKE ?", fmt.Sprintf("%s%s", strings.ToLower(prefix), "%")) + } + + sess.OrderBy("path") + + pageSize := paging.First + sess.Limit(pageSize + 1) + + if paging != nil && paging.After != "" { + sess.Where("path > ?", paging.After) + } + + if err := sess.Find(&foundFiles); err != nil { + return err + } + + foundLength := len(foundFiles) + if foundLength > pageSize { + foundLength = pageSize + } + + lowerCasePaths := make([]string, 0) + for i := 0; i < foundLength; i++ { + lowerCasePaths = append(lowerCasePaths, strings.ToLower(foundFiles[i].Path)) + } + propertiesByLowerPath, err := s.getProperties(sess, lowerCasePaths) + if err != nil { + return err + } + + files := make([]FileMetadata, 0) + for i := 0; i < foundLength; i++ { + var props map[string]string + path := foundFiles[i].Path + if foundProps, ok := propertiesByLowerPath[strings.ToLower(path)]; ok { + props = foundProps + } else { + props = make(map[string]string) + } + + files = append(files, FileMetadata{ + Name: getName(path), + FullPath: path, + Created: foundFiles[i].Created, + Properties: props, + Modified: foundFiles[i].Updated, + Size: foundFiles[i].Size, + MimeType: foundFiles[i].MimeType, + }) + } + + lastPath := "" + if len(files) > 0 { + lastPath = files[len(files)-1].FullPath + } + + resp = &ListFilesResponse{ + Files: files, + LastPath: lastPath, + HasMore: len(foundFiles) == pageSize+1, + } + return nil + }) + + return resp, err +} + +func (s dbFileStorage) ListFolders(ctx context.Context, parentFolderPath string, options *ListOptions) ([]FileMetadata, error) { + folders := make([]FileMetadata, 0) + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + var foundPaths []string + + sess.Table("file") + sess.Distinct("parent_folder_path") + + if options.Recursive { + sess.Where("LOWER(parent_folder_path) > ?", strings.ToLower(parentFolderPath)) + } else { + sess.Where("LOWER(parent_folder_path) = ?", strings.ToLower(parentFolderPath)) + } + + for _, prefix := range options.PathFilters.allowedPrefixes { + sess.Where("LOWER(parent_folder_path) LIKE ?", fmt.Sprintf("%s%s", strings.ToLower(prefix), "%")) + } + + sess.OrderBy("parent_folder_path") + sess.Cols("parent_folder_path") + + if err := sess.Find(&foundPaths); err != nil { + return err + } + + mem := make(map[string]bool) + for i := 0; i < len(foundPaths); i++ { + path := foundPaths[i] + parts := strings.Split(path, Delimiter) + acc := parts[0] + j := 1 + for { + acc = fmt.Sprintf("%s%s%s", acc, Delimiter, parts[j]) + comparison := strings.Compare(acc, parentFolderPath) + if !mem[acc] && comparison > 0 { + folders = append(folders, FileMetadata{ + Name: getName(acc), + FullPath: acc, + }) + } + mem[acc] = true + + j += 1 + if j >= len(parts) { + break + } + } + } + + return nil + }) + + return folders, err +} + +func (s dbFileStorage) CreateFolder(ctx context.Context, path string) error { + now := time.Now() + precedingFolders := precedingFolders(path) + + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + var insertErr error + sess.MustLogSQL(true) + previousFolder := "" + for i := 0; i < len(precedingFolders); i++ { + existing := &file{} + directoryMarkerParentPath := previousFolder + Delimiter + getName(precedingFolders[i]) + previousFolder = directoryMarkerParentPath + directoryMarkerPath := fmt.Sprintf("%s%s%s", directoryMarkerParentPath, Delimiter, directoryMarker) + lower := strings.ToLower(directoryMarkerPath) + exists, err := sess.Table("file").Where("LOWER(path) = ?", lower).Get(existing) + if err != nil { + insertErr = err + break + } + + if exists { + previousFolder = existing.ParentFolderPath + continue + } + + file := &file{ + Path: strings.ToLower(directoryMarkerPath), + ParentFolderPath: directoryMarkerParentPath, + Contents: make([]byte, 0), + Updated: now, + Created: now, + } + _, err = sess.Insert(file) + if err != nil { + insertErr = err + break + } + s.log.Info("Created folder", "markerPath", file.Path, "parent", file.ParentFolderPath) + } + + if insertErr != nil { + if rollErr := sess.Rollback(); rollErr != nil { + return errutil.Wrapf(insertErr, "Rolling back transaction due to error failed: %s", rollErr) + } + return insertErr + } + + return sess.Commit() + }) + + return err +} + +func (s dbFileStorage) DeleteFolder(ctx context.Context, folderPath string) error { + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + existing := &file{} + directoryMarkerPath := fmt.Sprintf("%s%s%s", folderPath, Delimiter, directoryMarker) + exists, err := sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(directoryMarkerPath)).Get(existing) + if err != nil { + return err + } + + if !exists { + return nil + } + + _, err = sess.Table("file").Where("LOWER(path) = ?", strings.ToLower(directoryMarkerPath)).Delete(existing) + return err + }) + + return err +} + +func (s dbFileStorage) close() error { + return nil +} diff --git a/pkg/infra/filestorage/dummy.go b/pkg/infra/filestorage/dummy.go new file mode 100644 index 00000000000..73db7a92bfe --- /dev/null +++ b/pkg/infra/filestorage/dummy.go @@ -0,0 +1,51 @@ +package filestorage + +import ( + "context" + + _ "gocloud.dev/blob/fileblob" + _ "gocloud.dev/blob/memblob" +) + +var ( + _ FileStorage = (*dummyFileStorage)(nil) // dummyFileStorage implements FileStorage +) + +type dummyFileStorage struct { +} + +func (d dummyFileStorage) Get(ctx context.Context, path string) (*File, error) { + return nil, nil +} + +func (d dummyFileStorage) Delete(ctx context.Context, path string) error { + return nil +} + +func (d dummyFileStorage) Upsert(ctx context.Context, file *UpsertFileCommand) error { + return nil +} + +func (d dummyFileStorage) ListFiles(ctx context.Context, path string, cursor *Paging, options *ListOptions) (*ListFilesResponse, error) { + return nil, nil +} + +func (d dummyFileStorage) ListFolders(ctx context.Context, path string, options *ListOptions) ([]FileMetadata, error) { + return nil, nil +} + +func (d dummyFileStorage) CreateFolder(ctx context.Context, path string) error { + return nil +} + +func (d dummyFileStorage) DeleteFolder(ctx context.Context, path string) error { + return nil +} + +func (d dummyFileStorage) IsFolderEmpty(ctx context.Context, path string) (bool, error) { + return true, nil +} + +func (d dummyFileStorage) close() error { + return nil +} diff --git a/pkg/infra/filestorage/filestorage.go b/pkg/infra/filestorage/filestorage.go new file mode 100644 index 00000000000..5c9e1ae952f --- /dev/null +++ b/pkg/infra/filestorage/filestorage.go @@ -0,0 +1,160 @@ +package filestorage + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "gocloud.dev/blob" + + _ "gocloud.dev/blob/fileblob" + _ "gocloud.dev/blob/memblob" +) + +const ( + ServiceName = "FileStorage" +) + +func ProvideService(features featuremgmt.FeatureToggles, cfg *setting.Cfg) (FileStorage, error) { + grafanaDsStorageLogger := log.New("grafanaDsStorage") + + path := fmt.Sprintf("file://%s", cfg.StaticRootPath) + grafanaDsStorageLogger.Info("Initializing grafana ds storage", "path", path) + bucket, err := blob.OpenBucket(context.Background(), path) + if err != nil { + currentDir, _ := os.Getwd() + grafanaDsStorageLogger.Error("Failed to initialize grafana ds storage", "path", path, "error", err, "cwd", currentDir) + return nil, err + } + + prefixes := []string{ + "testdata/", + "img/icons/", + "img/bg/", + "gazetteer/", + "maps/", + "upload/", + } + + var grafanaDsStorage FileStorage + if features.IsEnabled(featuremgmt.FlagFileStoreApi) { + grafanaDsStorage = &wrapper{ + log: grafanaDsStorageLogger, + wrapped: cdkBlobStorage{ + log: grafanaDsStorageLogger, + bucket: bucket, + rootFolder: "", + }, + pathFilters: &PathFilters{allowedPrefixes: prefixes}, + } + } else { + grafanaDsStorage = &dummyFileStorage{} + } + + return &service{ + grafanaDsStorage: grafanaDsStorage, + log: log.New("fileStorageService"), + }, nil +} + +type service struct { + log log.Logger + grafanaDsStorage FileStorage +} + +func (b service) Get(ctx context.Context, path string) (*File, error) { + var filestorage FileStorage + if belongsToStorage(path, StorageNamePublic) { + filestorage = b.grafanaDsStorage + path = removeStoragePrefix(path) + } + + if err := validatePath(path); err != nil { + return nil, err + } + + return filestorage.Get(ctx, path) +} + +func removeStoragePrefix(path string) string { + path = strings.TrimPrefix(path, Delimiter) + if path == Delimiter || path == "" { + return Delimiter + } + + if !strings.Contains(path, Delimiter) { + return Delimiter + } + + split := strings.Split(path, Delimiter) + + // root of storage + if len(split) == 2 && split[1] == "" { + return Delimiter + } + + // replace storage + split[0] = "" + return strings.Join(split, Delimiter) +} + +func (b service) Delete(ctx context.Context, path string) error { + return errors.New("not implemented") +} + +func (b service) Upsert(ctx context.Context, file *UpsertFileCommand) error { + return errors.New("not implemented") +} + +func (b service) ListFiles(ctx context.Context, path string, cursor *Paging, options *ListOptions) (*ListFilesResponse, error) { + var filestorage FileStorage + if belongsToStorage(path, StorageNamePublic) { + filestorage = b.grafanaDsStorage + path = removeStoragePrefix(path) + } else { + return nil, errors.New("not implemented") + } + + if err := validatePath(path); err != nil { + return nil, err + } + + return filestorage.ListFiles(ctx, path, cursor, options) +} + +func (b service) ListFolders(ctx context.Context, path string, options *ListOptions) ([]FileMetadata, error) { + var filestorage FileStorage + if belongsToStorage(path, StorageNamePublic) { + filestorage = b.grafanaDsStorage + path = removeStoragePrefix(path) + } else { + return nil, errors.New("not implemented") + } + + if err := validatePath(path); err != nil { + return nil, err + } + + return filestorage.ListFolders(ctx, path, options) +} + +func (b service) CreateFolder(ctx context.Context, path string) error { + return errors.New("not implemented") +} + +func (b service) DeleteFolder(ctx context.Context, path string) error { + return errors.New("not implemented") +} + +func (b service) IsFolderEmpty(ctx context.Context, path string) (bool, error) { + return true, errors.New("not implemented") +} + +func (b service) close() error { + return b.grafanaDsStorage.close() +} diff --git a/pkg/infra/filestorage/filestorage_test.go b/pkg/infra/filestorage/filestorage_test.go new file mode 100644 index 00000000000..f432c849195 --- /dev/null +++ b/pkg/infra/filestorage/filestorage_test.go @@ -0,0 +1,46 @@ +package filestorage + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFilestorage_removeStoragePrefix(t *testing.T) { + var tests = []struct { + name string + path string + expected string + }{ + { + name: "should return root if path is empty", + path: "", + expected: Delimiter, + }, + { + name: "should remove prefix folder from path with multiple parts", + path: "public/abc/d", + expected: "/abc/d", + }, + { + name: "should return root path if path is just the storage name", + path: "public", + expected: Delimiter, + }, + { + name: "should return root path if path is the prefix of storage", + path: "public/", + expected: Delimiter, + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s%s", "absolute: ", tt.name), func(t *testing.T) { + require.Equal(t, tt.expected, removeStoragePrefix(Delimiter+tt.path)) + }) + + t.Run(fmt.Sprintf("%s%s", "relative: ", tt.name), func(t *testing.T) { + require.Equal(t, tt.expected, removeStoragePrefix(tt.path)) + }) + } +} diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go new file mode 100644 index 00000000000..2440ad34463 --- /dev/null +++ b/pkg/infra/filestorage/fs_integration_test.go @@ -0,0 +1,857 @@ +//go:build integration +// +build integration + +package filestorage + +import ( + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "gocloud.dev/blob" +) + +const ( + pngImageBase64 = "iVBORw0KGgoNAANSUhEUgAAAC4AAAAmCAYAAAC76qlaAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAABFSURBVFiF7c5BDQAhEACx4/x7XjzwGELSKuiamfke9N8OnBKvidfEa+I18Zp4TbwmXhOvidfEa+I18Zp4TbwmXhOvidc2lcsESD1LGnUAAAAASUVORK5CYII=" +) + +type fsTestCase struct { + name string + skip *bool + steps []interface{} +} + +func runTestCase(t *testing.T, testCase fsTestCase, ctx context.Context, filestorage FileStorage) { + if testCase.skip != nil { + return + } + for i, step := range testCase.steps { + executeTestStep(t, ctx, step, i, filestorage) + } +} + +func runTests(createCases func() []fsTestCase, t *testing.T) { + var testLogger log.Logger + //var sqlStore *sqlstore.SQLStore + var filestorage FileStorage + var ctx context.Context + var tempDir string + + commonSetup := func() { + testLogger = log.New("testStorageLogger") + ctx = context.Background() + } + + cleanUp := func() { + testLogger = nil + //sqlStore = nil + if filestorage != nil { + _ = filestorage.close() + filestorage = nil + } + + ctx = nil + _ = os.RemoveAll(tempDir) + } + + setupInMemFS := func() { + commonSetup() + bucket, _ := blob.OpenBucket(context.Background(), "mem://") + filestorage = NewCdkBlobStorage(testLogger, bucket, Delimiter, nil) + } + + //setupSqlFS := func() { + // commonSetup() + // sqlStore = sqlstore.InitTestDB(t) + // filestorage = NewDbStorage(testLogger, sqlStore, nil) + //} + + setupLocalFs := func() { + commonSetup() + tmpDir, err := ioutil.TempDir("", "") + tempDir = tmpDir + if err != nil { + t.Fatal(err) + } + + bucket, err := blob.OpenBucket(context.Background(), fmt.Sprintf("file://%s", tmpDir)) + if err != nil { + t.Fatal(err) + } + filestorage = NewCdkBlobStorage(testLogger, bucket, "", nil) + } + + backends := []struct { + setup func() + name string + }{ + { + setup: setupLocalFs, + name: "Local FS", + }, + { + setup: setupInMemFS, + name: "In-mem FS", + }, + //{ + // setup: setupSqlFS, + // name: "SQL FS", + //}, + } + + for _, backend := range backends { + for _, tt := range createCases() { + t.Run(fmt.Sprintf("%s: %s", backend.name, tt.name), func(t *testing.T) { + backend.setup() + defer cleanUp() + runTestCase(t, tt, ctx, filestorage) + }) + } + } +} + +func TestFsStorage(t *testing.T) { + //skipTest := true + emptyFileBytes := make([]byte, 0) + pngImage, _ := base64.StdEncoding.DecodeString(pngImageBase64) + pngImageSize := int64(len(pngImage)) + + createListFilesTests := func() []fsTestCase { + return []fsTestCase{ + { + name: "listing files", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/folder2/file.jpg", + Contents: &[]byte{}, + Properties: map[string]string{"prop1": "val1", "prop2": "val"}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/file-inner.jpg", + Contents: &[]byte{}, + Properties: map[string]string{"prop1": "val1", "prop2": "val"}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/file-inner2.jpg", + Contents: &[]byte{}, + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1", options: &ListOptions{Recursive: true}}, + list: checks(listSize(3), listHasMore(false), listLastPath("/folder1/folder2/file.jpg")), + files: [][]interface{}{ + checks(fPath("/folder1/file-inner.jpg"), fProperties(map[string]string{"prop1": "val1", "prop2": "val"})), + checks(fPath("/folder1/file-inner2.jpg"), fProperties(map[string]string{})), + checks(fPath("/folder1/folder2/file.jpg"), fProperties(map[string]string{"prop1": "val1", "prop2": "val"})), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: false}}, + list: checks(listSize(0), listHasMore(false), listLastPath("")), + files: [][]interface{}{}, + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1", options: &ListOptions{Recursive: false}}, + list: checks(listSize(2), listHasMore(false), listLastPath("/folder1/file-inner2.jpg")), + files: [][]interface{}{ + checks(fPath("/folder1/file-inner.jpg"), fProperties(map[string]string{"prop1": "val1", "prop2": "val"})), + checks(fPath("/folder1/file-inner2.jpg"), fProperties(map[string]string{})), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1/folder2", options: &ListOptions{Recursive: false}}, + list: checks(listSize(1), listHasMore(false), listLastPath("/folder1/folder2/file.jpg")), + files: [][]interface{}{ + checks(fPath("/folder1/folder2/file.jpg"), fProperties(map[string]string{"prop1": "val1", "prop2": "val"})), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1/folder2", options: &ListOptions{Recursive: false}, paging: &Paging{After: "/folder1/folder2/file.jpg"}}, + list: checks(listSize(0), listHasMore(false), listLastPath("")), + files: [][]interface{}{}, + }, + }, + }, + { + name: "path passed to listing files is a folder path, not a prefix", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/ab/a.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/ab/a/a.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/ac/a.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/aba/a.jpg", + Contents: &[]byte{}, + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/ab", options: &ListOptions{Recursive: true}}, + list: checks(listSize(2), listHasMore(false), listLastPath("/ab/a/a.jpg")), + files: [][]interface{}{ + checks(fPath("/ab/a.jpg")), + checks(fPath("/ab/a/a.jpg")), + }, + }, + }, + }, + { + name: "listing files with prefix filter", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/folder2/file.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/file-inner.jpg", + Contents: &[]byte{}, + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1", options: &ListOptions{Recursive: true, PathFilters: PathFilters{allowedPrefixes: []string{"/folder2"}}}}, + list: checks(listSize(0), listHasMore(false), listLastPath("")), + }, + queryListFiles{ + input: queryListFilesInput{path: "/folder1", options: &ListOptions{Recursive: true, PathFilters: PathFilters{allowedPrefixes: []string{"/folder1/folder"}}}}, + list: checks(listSize(1), listHasMore(false)), + files: [][]interface{}{ + checks(fPath("/folder1/folder2/file.jpg")), + }, + }, + }, + }, + { + name: "listing files with pagination", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/a", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/b", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder2/c", + Contents: &[]byte{}, + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 1, After: ""}}, + list: checks(listSize(1), listHasMore(true), listLastPath("/folder1/a")), + files: [][]interface{}{ + checks(fPath("/folder1/a")), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 1, After: "/folder1/a"}}, + list: checks(listSize(1), listHasMore(true), listLastPath("/folder1/b")), + files: [][]interface{}{ + checks(fPath("/folder1/b")), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 1, After: "/folder1/b"}}, + list: checks(listSize(1), listHasMore(false), listLastPath("/folder2/c")), + files: [][]interface{}{ + checks(fPath("/folder2/c")), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: ""}}, + list: checks(listSize(3), listHasMore(false), listLastPath("/folder2/c")), + files: [][]interface{}{ + checks(fPath("/folder1/a")), + checks(fPath("/folder1/b")), + checks(fPath("/folder2/c")), + }, + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: "/folder2"}}, + list: checks(listSize(1), listHasMore(false)), + }, + queryListFiles{ + input: queryListFilesInput{path: "/", options: &ListOptions{Recursive: true}, paging: &Paging{First: 5, After: "/folder2/c"}}, + list: checks(listSize(0), listHasMore(false)), + }, + }, + }, + } + } + + createListFoldersTests := func() []fsTestCase { + return []fsTestCase{ + { + name: "listing folders", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/folder2/file.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder1/file-inner.jpg", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folderX/folderZ/file.txt", + Contents: &[]byte{}, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folderA/folderB/file.txt", + Contents: &[]byte{}, + }, + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder1")), + checks(fPath("/folder1/folder2")), + checks(fPath("/folderA")), + checks(fPath("/folderA/folderB")), + checks(fPath("/folderX")), + checks(fPath("/folderX/folderZ")), + }, + }, + }, + }, + } + } + + createFileCRUDTests := func() []fsTestCase { + return []fsTestCase{ + { + name: "getting a non-existent file", + steps: []interface{}{ + queryGet{ + input: queryGetInput{ + path: "/folder/a.png", + }, + }, + }, + }, + { + name: "inserting a file", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/a.png", + Contents: &pngImage, + Properties: map[string]string{"prop1": "val1", "prop2": "val"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/folder/a.png", + }, + checks: checks( + fPath("/folder/a.png"), + fName("a.png"), + fMimeType("image/png"), + fProperties(map[string]string{"prop1": "val1", "prop2": "val"}), + fSize(pngImageSize), + fContents(pngImage), + ), + }, + }, + }, + { + name: "preserved original path/name casing when getting a file", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/Folder/A.png", + Contents: &emptyFileBytes, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/fOlder/a.png", + }, + checks: checks( + fPath("/Folder/A.png"), + fName("A.png"), + ), + }, + }, + }, + { + name: "modifying file metadata", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/a.png", + Contents: &pngImage, + Properties: map[string]string{"a": "av", "b": "bv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/a.png", + }, + checks: checks( + fContents(pngImage), + fProperties(map[string]string{"a": "av", "b": "bv"}), + ), + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/a.png", + Properties: map[string]string{"b": "bv2", "c": "cv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/a.png", + }, + checks: checks( + fContents(pngImage), + fProperties(map[string]string{"b": "bv2", "c": "cv"}), + ), + }, + }, + }, + { + name: "modifying file metadata preserves original path casing", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/aB.png", + Contents: &emptyFileBytes, + Properties: map[string]string{"a": "av", "b": "bv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/ab.png", + }, + checks: checks( + fPath("/aB.png"), + fName("aB.png"), + ), + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/ab.png", + Properties: map[string]string{"b": "bv2", "c": "cv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/ab.png", + }, + checks: checks( + fPath("/aB.png"), + fName("aB.png"), + fProperties(map[string]string{"b": "bv2", "c": "cv"}), + ), + }, + }, + }, + { + name: "modifying file contents", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/FILE.png", + Contents: &emptyFileBytes, + Properties: map[string]string{"a": "av", "b": "bv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/file.png", + }, + checks: checks( + fName("FILE.png"), + fProperties(map[string]string{"a": "av", "b": "bv"}), + fSize(0), + fContents(emptyFileBytes), + ), + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/file.png", + Contents: &pngImage, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/file.png", + }, + checks: checks( + fName("FILE.png"), + fMimeType("image/png"), + fProperties(map[string]string{"a": "av", "b": "bv"}), + fSize(pngImageSize), + fContents(pngImage), + ), + }, + }, + }, + { + name: "deleting a file", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/FILE.png", + Contents: &emptyFileBytes, + Properties: map[string]string{"a": "av", "b": "bv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/file.png", + }, + checks: checks( + fPath("/FILE.png"), + ), + }, + cmdDelete{ + path: "/file.png", + }, + queryGet{ + input: queryGetInput{ + path: "/file.png", + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/file.png", + Contents: &emptyFileBytes, + Properties: map[string]string{"a": "av", "b": "bv"}, + }, + }, + queryGet{ + input: queryGetInput{ + path: "/file.png", + }, + checks: checks( + fPath("/file.png"), + ), + }, + }, + }, + { + name: "deleting a non-existent file should be no-op", + steps: []interface{}{ + cmdDelete{ + path: "/file.png", + }, + }, + }, + } + } + + createFolderCrudCases := func() []fsTestCase { + return []fsTestCase{ + { + name: "recreating a folder after it was already created via upserting a file is a no-op", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/aB/cD/eF/file.jpg", + Contents: &[]byte{}, + }, + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + checks(fPath("/aB/cD")), + checks(fPath("/aB/cD/eF")), + }, + }, + cmdCreateFolder{ + path: "/ab/cd/ef", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + checks(fPath("/aB/cD")), + checks(fPath("/aB/cD/eF")), + }, + }, + cmdCreateFolder{ + path: "/ab/cd/ef/GH", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + checks(fPath("/aB/cD")), + checks(fPath("/aB/cD/eF")), + checks(fPath("/aB/cD/eF/GH")), + }, + }, + }, + }, + { + name: "creating a folder with the same name or same name but different casing is a no-op", + steps: []interface{}{ + cmdCreateFolder{ + path: "/aB", + }, + cmdCreateFolder{ + path: "/ab", + }, + cmdCreateFolder{ + path: "/aB", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + }, + }, + cmdCreateFolder{ + path: "/Ab", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + }, + }, + }, + }, + { + name: "creating folder is recursive", + steps: []interface{}{ + cmdCreateFolder{ + path: "/a/b/c", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/a")), + checks(fPath("/a/b")), + checks(fPath("/a/b/c")), + }, + }, + }, + }, + { + name: "deleting a leaf directory does not delete parent directories even if they are empty - folders created directly", + steps: []interface{}{ + cmdCreateFolder{ + path: "/a/b/c", + }, + cmdDeleteFolder{ + path: "/a/b/c", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/a")), + checks(fPath("/a/b")), + }, + }, + }, + }, + { + name: "deleting a leaf directory does not delete parent directories even if they are empty - folders created via file upsert", + steps: []interface{}{ + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/a/b/c/file.jpg", + Contents: &[]byte{}, + }, + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/a")), + checks(fPath("/a/b")), + checks(fPath("/a/b/c")), + }, + }, + cmdDelete{ + path: "/a/b/c/file.jpg", + error: nil, + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/a")), + checks(fPath("/a/b")), + checks(fPath("/a/b/c")), + }, + }, + cmdDeleteFolder{ + path: "/a/b/c", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/a")), + checks(fPath("/a/b")), + }, + }, + }, + }, + { + name: "folders preserve their original casing", + steps: []interface{}{ + cmdCreateFolder{ + path: "/aB/cD/e", + }, + cmdCreateFolder{ + path: "/ab/cd/f", + }, + queryListFolders{ + input: queryListFoldersInput{ + path: "/", + }, + checks: [][]interface{}{ + checks(fPath("/aB")), + checks(fPath("/aB/cD")), + checks(fPath("/aB/cD/e")), + checks(fPath("/aB/cD/f")), + }, + }, + }, + }, + { + name: "folders can't be deleted through the `delete` method", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder")), + checks(fPath("/folder/dashboards")), + checks(fPath("/folder/dashboards/myNewFolder")), + }, + }, + cmdDelete{ + path: "/folder/dashboards/myNewFolder", + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder")), + checks(fPath("/folder/dashboards")), + checks(fPath("/folder/dashboards/myNewFolder")), + }, + }, + }, + }, + { + name: "folders can not be retrieved through the `get` method", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder", + }, + }, + }, + }, + { + name: "should not be able to delete folders with files", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/dashboards/myNewFolder/file.jpg", + Contents: &[]byte{}, + }, + }, + cmdDeleteFolder{ + path: "/folder/dashboards/myNewFolder", + error: &cmdErrorOutput{ + message: "folder %s is not empty - cant remove it", + args: []interface{}{"/folder/dashboards/myNewFolder"}, + }, + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder")), + checks(fPath("/folder/dashboards")), + checks(fPath("/folder/dashboards/myNewFolder")), + }, + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder/file.jpg", + }, + checks: checks( + fName("file.jpg"), + ), + }, + }, + }, + } + } + + runTests(createListFoldersTests, t) + runTests(createListFilesTests, t) + runTests(createFileCRUDTests, t) + runTests(createFolderCrudCases, t) +} diff --git a/pkg/infra/filestorage/test_utils.go b/pkg/infra/filestorage/test_utils.go new file mode 100644 index 00000000000..2ecbb1054aa --- /dev/null +++ b/pkg/infra/filestorage/test_utils.go @@ -0,0 +1,346 @@ +//go:build integration +// +build integration + +package filestorage + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +type cmdErrorOutput struct { + message string + args []interface{} + instance error +} + +type cmdDelete struct { + path string + error *cmdErrorOutput +} + +type cmdUpsert struct { + cmd UpsertFileCommand + error *cmdErrorOutput +} + +type cmdCreateFolder struct { + path string + error *cmdErrorOutput +} + +type cmdDeleteFolder struct { + path string + error *cmdErrorOutput +} + +type queryGetInput struct { + path string +} + +type fileNameCheck struct { + v string +} + +type filePropertiesCheck struct { + v map[string]string +} + +type fileContentsCheck struct { + v []byte +} + +type fileSizeCheck struct { + v int64 +} + +type fileMimeTypeCheck struct { + v string +} + +type filePathCheck struct { + v string +} + +type listSizeCheck struct { + v int +} + +type listHasMoreCheck struct { + v bool +} + +type listLastPathCheck struct { + v string +} + +func fContents(contents []byte) interface{} { + return fileContentsCheck{v: contents} +} + +func fName(name string) interface{} { + return fileNameCheck{v: name} +} + +func fPath(path string) interface{} { + return filePathCheck{v: path} +} + +func fProperties(properties map[string]string) interface{} { + return filePropertiesCheck{v: properties} +} +func fSize(size int64) interface{} { + return fileSizeCheck{v: size} +} + +func fMimeType(mimeType string) interface{} { + return fileMimeTypeCheck{v: mimeType} +} + +func listSize(size int) interface{} { + return listSizeCheck{v: size} +} + +func listHasMore(hasMore bool) interface{} { + return listHasMoreCheck{v: hasMore} +} + +func listLastPath(path string) interface{} { + return listLastPathCheck{v: path} +} + +func checks(c ...interface{}) []interface{} { + return c +} + +type queryGet struct { + input queryGetInput + checks []interface{} +} + +type queryListFilesInput struct { + path string + paging *Paging + options *ListOptions +} + +type queryListFiles struct { + input queryListFilesInput + list []interface{} + files [][]interface{} +} + +type queryListFoldersInput struct { + path string + options *ListOptions +} + +type queryListFolders struct { + input queryListFoldersInput + checks [][]interface{} +} + +func interfaceName(myvar interface{}) string { + if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr { + return "*" + t.Elem().Name() + } else { + return t.Name() + } +} + +func handleCommand(t *testing.T, ctx context.Context, cmd interface{}, cmdName string, fs FileStorage) { + t.Helper() + + var err error + var expectedErr *cmdErrorOutput + switch c := cmd.(type) { + case cmdDelete: + err = fs.Delete(ctx, c.path) + if c.error == nil { + require.NoError(t, err, "%s: should be able to delete %s", cmdName, c.path) + } + expectedErr = c.error + case cmdUpsert: + err = fs.Upsert(ctx, &c.cmd) + if c.error == nil { + require.NoError(t, err, "%s: should be able to upsert file %s", cmdName, c.cmd.Path) + } + expectedErr = c.error + case cmdCreateFolder: + err = fs.CreateFolder(ctx, c.path) + if c.error == nil { + require.NoError(t, err, "%s: should be able to create folder %s", cmdName, c.path) + } + expectedErr = c.error + case cmdDeleteFolder: + err = fs.DeleteFolder(ctx, c.path) + if c.error == nil { + require.NoError(t, err, "%s: should be able to delete %s", cmdName, c.path) + } + expectedErr = c.error + default: + t.Fatalf("unrecognized command %s", cmdName) + } + + if expectedErr != nil && err != nil { + if expectedErr.instance != nil { + require.ErrorIs(t, err, expectedErr.instance) + } + + if expectedErr.message != "" { + require.Errorf(t, err, expectedErr.message, expectedErr.args...) + } + } +} + +func runChecks(t *testing.T, stepName string, path string, output interface{}, checks []interface{}) { + if checks == nil || len(checks) == 0 { + return + } + + runFileMetadataCheck := func(file FileMetadata, check interface{}, checkName string) { + switch c := check.(type) { + case filePropertiesCheck: + require.Equal(t, c.v, file.Properties, "%s-%s %s", stepName, checkName, path) + case fileNameCheck: + require.Equal(t, c.v, file.Name, "%s-%s %s", stepName, checkName, path) + case fileSizeCheck: + require.Equal(t, c.v, file.Size, "%s-%s %s", stepName, checkName, path) + case fileMimeTypeCheck: + require.Equal(t, c.v, file.MimeType, "%s-%s %s", stepName, checkName, path) + case filePathCheck: + require.Equal(t, c.v, file.FullPath, "%s-%s %s", stepName, checkName, path) + default: + t.Fatalf("unrecognized file check %s", checkName) + } + } + + switch o := output.(type) { + case File: + for _, check := range checks { + checkName := interfaceName(check) + if fileContentsCheck, ok := check.(fileContentsCheck); ok { + require.Equal(t, fileContentsCheck.v, o.Contents, "%s-%s %s", stepName, checkName, path) + } else { + runFileMetadataCheck(o.FileMetadata, check, checkName) + } + } + case FileMetadata: + for _, check := range checks { + runFileMetadataCheck(o, check, interfaceName(check)) + } + case ListFilesResponse: + for _, check := range checks { + c := check + checkName := interfaceName(c) + switch c := check.(type) { + case listSizeCheck: + require.Equal(t, c.v, len(o.Files), "%s %s", stepName, path) + case listHasMoreCheck: + require.Equal(t, c.v, o.HasMore, "%s %s", stepName, path) + case listLastPathCheck: + require.Equal(t, c.v, o.LastPath, "%s %s", stepName, path) + default: + t.Fatalf("unrecognized list check %s", checkName) + } + } + default: + t.Fatalf("unrecognized output %s", interfaceName(output)) + } + +} + +func formatPathStructure(files []FileMetadata) string { + if len(files) == 0 { + return "<>" + } + res := "\n" + for _, f := range files { + res = fmt.Sprintf("%s%s\n", res, f.FullPath) + } + return res +} + +func handleQuery(t *testing.T, ctx context.Context, query interface{}, queryName string, fs FileStorage) { + t.Helper() + + switch q := query.(type) { + case queryGet: + inputPath := q.input.path + file, err := fs.Get(ctx, inputPath) + require.NoError(t, err, "%s: should be able to get file %s", queryName, inputPath) + + if q.checks != nil && len(q.checks) > 0 { + require.NotNil(t, file, "%s %s", queryName, inputPath) + require.Equal(t, strings.ToLower(inputPath), strings.ToLower(file.FullPath), "%s %s", queryName, inputPath) + runChecks(t, queryName, inputPath, *file, q.checks) + } else { + require.Nil(t, file, "%s %s", queryName, inputPath) + } + case queryListFiles: + inputPath := q.input.path + resp, err := fs.ListFiles(ctx, inputPath, q.input.paging, q.input.options) + require.NoError(t, err, "%s: should be able to list files in %s", queryName, inputPath) + require.NotNil(t, resp) + if q.list != nil && len(q.list) > 0 { + runChecks(t, queryName, inputPath, *resp, q.list) + } else { + require.NotNil(t, resp, "%s %s", queryName, inputPath) + require.Equal(t, false, resp.HasMore, "%s %s", queryName, inputPath) + require.Equal(t, 0, len(resp.Files), "%s %s", queryName, inputPath) + require.Equal(t, "", resp.LastPath, "%s %s", queryName, inputPath) + } + + if q.files != nil { + require.Equal(t, len(resp.Files), len(q.files), "%s expected a check for each actual file at path: \"%s\". actual: %s", queryName, inputPath, formatPathStructure(resp.Files)) + for i, file := range resp.Files { + runChecks(t, queryName, inputPath, file, q.files[i]) + } + } + case queryListFolders: + inputPath := q.input.path + resp, err := fs.ListFolders(ctx, inputPath, q.input.options) + require.NotNil(t, resp) + require.NoError(t, err, "%s: should be able to list folders in %s", queryName, inputPath) + + if q.checks != nil { + require.Equal(t, len(resp), len(q.checks), "%s: expected a check for each actual folder at path: \"%s\". actual: %s", queryName, inputPath, formatPathStructure(resp)) + for i, file := range resp { + runChecks(t, queryName, inputPath, file, q.checks[i]) + } + } else { + require.Equal(t, 0, len(resp), "%s %s", queryName, inputPath) + } + default: + t.Fatalf("unrecognized query %s", queryName) + } +} + +func executeTestStep(t *testing.T, ctx context.Context, step interface{}, stepNumber int, fs FileStorage) { + name := fmt.Sprintf("[%d]%s", stepNumber, interfaceName(step)) + + switch s := step.(type) { + case queryGet: + handleQuery(t, ctx, s, name, fs) + case queryListFiles: + handleQuery(t, ctx, s, name, fs) + case queryListFolders: + handleQuery(t, ctx, s, name, fs) + case cmdUpsert: + handleCommand(t, ctx, s, name, fs) + case cmdDelete: + handleCommand(t, ctx, s, name, fs) + case cmdCreateFolder: + handleCommand(t, ctx, s, name, fs) + case cmdDeleteFolder: + handleCommand(t, ctx, s, name, fs) + default: + t.Fatalf("unrecognized step %s", name) + } + +} diff --git a/pkg/infra/filestorage/wrapper.go b/pkg/infra/filestorage/wrapper.go new file mode 100644 index 00000000000..10e769bc95a --- /dev/null +++ b/pkg/infra/filestorage/wrapper.go @@ -0,0 +1,257 @@ +package filestorage + +import ( + "context" + "fmt" + "mime" + "path/filepath" + "regexp" + "strings" + + "github.com/grafana/grafana/pkg/infra/log" + _ "gocloud.dev/blob/fileblob" + _ "gocloud.dev/blob/memblob" +) + +var ( + directoryMarker = ".___gf_dir_marker___" + pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z0-9!\-_.*'()]+)+$)`) +) + +type wrapper struct { + log log.Logger + wrapped FileStorage + pathFilters *PathFilters +} + +var ( + _ FileStorage = (*wrapper)(nil) // wrapper implements FileStorage +) + +func getParentFolderPath(path string) string { + if path == Delimiter || path == "" { + return Delimiter + } + + if !strings.Contains(path, Delimiter) { + return Delimiter + } + + split := strings.Split(path, Delimiter) + splitWithoutLastPart := split[:len(split)-1] + if len(splitWithoutLastPart) == 1 && split[0] == "" { + return Delimiter + } + return strings.Join(splitWithoutLastPart, Delimiter) +} + +func getName(path string) string { + if path == Delimiter || path == "" { + return "" + } + + split := strings.Split(path, Delimiter) + return split[len(split)-1] +} + +func validatePath(path string) error { + if !filepath.IsAbs(path) { + return ErrRelativePath + } + + if path == Delimiter { + return nil + } + + if filepath.Clean(path) != path { + return ErrNonCanonicalPath + } + + if strings.HasSuffix(path, Delimiter) { + return ErrPathEndsWithDelimiter + } + + if len(path) > 1000 { + return ErrPathTooLong + } + + matches := pathRegex.MatchString(path) + if !matches { + return ErrPathInvalid + } + + return nil +} + +func (b wrapper) validatePath(path string) error { + if err := validatePath(path); err != nil { + b.log.Error("Path failed validation", "path", path, "error", err) + return err + } + return nil +} + +func (b wrapper) Get(ctx context.Context, path string) (*File, error) { + if err := b.validatePath(path); err != nil { + return nil, err + } + + if !b.pathFilters.isAllowed(path) { + return nil, nil + } + + return b.wrapped.Get(ctx, path) +} +func (b wrapper) Delete(ctx context.Context, path string) error { + if err := b.validatePath(path); err != nil { + return err + } + + if !b.pathFilters.isAllowed(path) { + return nil + } + + return b.wrapped.Delete(ctx, path) +} + +func detectContentType(path string, originalGuess string) string { + if originalGuess == "application/octet-stream" || originalGuess == "" { + mimeTypeBasedOnExt := mime.TypeByExtension(filepath.Ext(path)) + if mimeTypeBasedOnExt == "" { + return "application/octet-stream" + } + return mimeTypeBasedOnExt + } + return originalGuess +} + +func (b wrapper) Upsert(ctx context.Context, file *UpsertFileCommand) error { + if err := b.validatePath(file.Path); err != nil { + return err + } + + if !b.pathFilters.isAllowed(file.Path) { + return nil + } + + path := getParentFolderPath(file.Path) + b.log.Info("Creating folder before upserting file", "file", file.Path, "folder", path) + if err := b.CreateFolder(ctx, path); err != nil { + return err + } + + if file.Contents != nil && file.MimeType == "" { + file.MimeType = detectContentType(file.Path, "") + } + + return b.wrapped.Upsert(ctx, file) +} + +func (b wrapper) withDefaults(options *ListOptions, folderQuery bool) *ListOptions { + if options == nil { + options = &ListOptions{} + options.Recursive = folderQuery + if b.pathFilters != nil && b.pathFilters.allowedPrefixes != nil { + options.PathFilters = *b.pathFilters + } + + return options + } + + if b.pathFilters != nil && b.pathFilters.allowedPrefixes != nil { + if options.allowedPrefixes != nil { + options.allowedPrefixes = append(options.allowedPrefixes, b.pathFilters.allowedPrefixes...) + } else { + copiedPrefixes := make([]string, len(b.pathFilters.allowedPrefixes)) + copy(copiedPrefixes, b.pathFilters.allowedPrefixes) + options.allowedPrefixes = copiedPrefixes + } + } + + return options +} + +func (b wrapper) ListFiles(ctx context.Context, path string, paging *Paging, options *ListOptions) (*ListFilesResponse, error) { + if err := b.validatePath(path); err != nil { + return nil, err + } + + if paging == nil { + paging = &Paging{ + First: 100, + } + } else if paging.First <= 0 { + paging.First = 100 + } + + return b.wrapped.ListFiles(ctx, path, paging, b.withDefaults(options, false)) +} + +func (b wrapper) ListFolders(ctx context.Context, path string, options *ListOptions) ([]FileMetadata, error) { + if err := b.validatePath(path); err != nil { + return nil, err + } + + return b.wrapped.ListFolders(ctx, path, b.withDefaults(options, true)) +} + +func (b wrapper) CreateFolder(ctx context.Context, path string) error { + if err := b.validatePath(path); err != nil { + return err + } + + if !b.pathFilters.isAllowed(path) { + return nil + } + + return b.wrapped.CreateFolder(ctx, path) +} + +func (b wrapper) DeleteFolder(ctx context.Context, path string) error { + if err := b.validatePath(path); err != nil { + return err + } + + if !b.pathFilters.isAllowed(path) { + return nil + } + + isEmpty, err := b.isFolderEmpty(ctx, path) + if err != nil { + return err + } + + if !isEmpty { + return fmt.Errorf("folder %s is not empty - cant remove it", path) + } + + return b.wrapped.DeleteFolder(ctx, path) +} + +func (b wrapper) isFolderEmpty(ctx context.Context, path string) (bool, error) { + filesInFolder, err := b.ListFiles(ctx, path, &Paging{First: 1}, &ListOptions{Recursive: true}) + if err != nil { + return false, err + } + + if len(filesInFolder.Files) > 0 { + return false, nil + } + + folders, err := b.ListFolders(ctx, path, &ListOptions{ + Recursive: true, + }) + if err != nil { + return false, err + } + + if len(folders) > 0 { + return false, nil + } + + return true, nil +} + +func (b wrapper) close() error { + return b.wrapped.close() +} diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 3a852046421..43e474af440 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -15,7 +15,10 @@ import ( "path/filepath" "strings" + // TODO: replace deprecated `golang.org/x/crypto` package https://github.com/grafana/grafana/issues/46050 + // nolint:staticcheck "golang.org/x/crypto/openpgp" + // nolint:staticcheck "golang.org/x/crypto/openpgp/clearsign" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/server/wire.go b/pkg/server/wire.go index ab8daa4d39a..530d0f43806 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" "github.com/grafana/grafana/pkg/infra/kvstore" @@ -146,6 +147,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), quota.ProvideService, remotecache.ProvideService, + filestorage.ProvideService, loginservice.ProvideService, wire.Bind(new(login.Service), new(*loginservice.Implementation)), authinfoservice.ProvideAuthInfoService, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f0a542c248f..1d02d449c7e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -162,5 +162,11 @@ var ( Description: "Lock database during migrations", State: FeatureStateBeta, }, + { + Name: "fileStoreApi", + Description: "Simple API for managing files", + State: FeatureStateAlpha, + RequiresDevMode: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index bb15e26e660..51aeb9bec63 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -122,4 +122,8 @@ const ( // FlagMigrationLocking // Lock database during migrations FlagMigrationLocking = "migrationLocking" + + // FlagFileStoreApi + // Simple API for managing files + FlagFileStoreApi = "fileStoreApi" ) diff --git a/pkg/services/sqlstore/migrations/db_file_storage.go b/pkg/services/sqlstore/migrations/db_file_storage.go new file mode 100644 index 00000000000..c5cd1bc30bc --- /dev/null +++ b/pkg/services/sqlstore/migrations/db_file_storage.go @@ -0,0 +1,41 @@ +package migrations + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// TODO: remove nolint as part of https://github.com/grafana/grafana/issues/45498 +// nolint:unused,deadcode +func addDbFileStorageMigration(mg *migrator.Migrator) { + filesTable := migrator.Table{ + Name: "file", + Columns: []*migrator.Column{ + {Name: "path", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, + {Name: "parent_folder_path", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, + {Name: "contents", Type: migrator.DB_Blob, Nullable: false}, + {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "size", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "mime_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"path"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create file table", migrator.NewAddTableMigration(filesTable)) + mg.AddMigration("file table idx: path natural pk", migrator.NewAddIndexMigration(filesTable, filesTable.Indices[0])) + + fileMetaTable := migrator.Table{ + Name: "file_meta", + Columns: []*migrator.Column{ + {Name: "path", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, + {Name: "key", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, + {Name: "value", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"path", "key"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create file_meta table", migrator.NewAddTableMigration(fileMetaTable)) + mg.AddMigration("file table idx: path key", migrator.NewAddIndexMigration(fileMetaTable, fileMetaTable.Indices[0])) +}