From 0bdee8757be0e83c307246472fd4ec44147daf23 Mon Sep 17 00:00:00 2001 From: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com> Date: Mon, 5 Dec 2022 05:01:12 -0800 Subject: [PATCH 01/94] Docs: Document Prometheus-Alertmanager integration settings (#57043) * Docs: Document Prometheus AM integration settings * Update docs/sources/datasources/alertmanager/_index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../datasources/alertmanager/_index.md | 17 +++++--- docs/sources/datasources/prometheus/_index.md | 40 ++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/sources/datasources/alertmanager/_index.md b/docs/sources/datasources/alertmanager/_index.md index 120c4cc3dc7..dfacbf4a668 100644 --- a/docs/sources/datasources/alertmanager/_index.md +++ b/docs/sources/datasources/alertmanager/_index.md @@ -17,17 +17,22 @@ weight: 150 # Alertmanager data source -Grafana includes built-in support for Prometheus Alertmanager. Once you add it as a data source, you can use the [Grafana Alerting UI](/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop-down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources. +Grafana includes built-in support for Alertmanager implementations in Prometheus and Mimir. +Once you add it as a data source, you can use the [Grafana Alerting UI](/docs/grafana/latest/alerting/) to manage silences, contact points, and notification policies. +To switch between Grafana and any configured Alertmanager data sources, you can select your preference from a drop-down option in those databases' data source settings pages. ## Alertmanager implementations -[Prometheus](https://prometheus.io/) and [Grafana Mimir](/docs/mimir/latest/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana Alerting UI, as it does not support updating configuration via HTTP API. +The data source supports [Prometheus](https://prometheus.io/) and [Grafana Mimir](https://grafana.com/docs/mimir/latest/) (default) implementations of Alertmanager. +You can specify the implementation in the data source's Settings page. +When using Prometheus, contact points and notification policies are read-only in the Grafana Alerting UI, because it doesn't support updates to the configuration using HTTP API. -## Provision the data source +## Provision the Alertmanager data source -Configure the Alertmanager data sources by updating Grafana's configuration files. For more information on how it works and the settings available, refer to the [provisioning docs page]({{< relref "../../administration/provisioning#data-sources" >}}). +You can provision Alertmanager data sources by updating Grafana's configuration files. +For more information on provisioning, and common settings available, refer to the [provisioning docs page]({{< relref "../administration/provisioning/#datasources" >}}). -For example, this YAML provisions an Alertmanager data source running on port 9093, with proxy access and basic authentication: +Here is an example for provisioning the Alertmanager data source: ```yaml apiVersion: 1 @@ -38,6 +43,8 @@ datasources: url: http://localhost:9093 access: proxy jsonData: + # Options for implementation include prometheus and mimir + implementation: prometheus # optionally basicAuth: true basicAuthUser: my_user diff --git a/docs/sources/datasources/prometheus/_index.md b/docs/sources/datasources/prometheus/_index.md index a275990a658..e6e0394dcc6 100644 --- a/docs/sources/datasources/prometheus/_index.md +++ b/docs/sources/datasources/prometheus/_index.md @@ -43,21 +43,28 @@ For more information on how to query other Prometheus-compatible projects from G Set the data source's basic configuration options carefully: -| Name | Description | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Name** | Sets the name you use to refer to the data source in panels and queries. | -| **Default** | Sets whether the data source is pre-selected for new panels. | -| **Url** | Sets the URL of your Prometheus server, such as `http://prometheus.example.org:9090`. | -| **Access** | Only Server access mode is functional. If Server mode is already selected, this option is hidden. Otherwise, change this to Server mode to prevent errors. | -| **Basic Auth** | Enables basic authentication to the Prometheus data source. | -| **User** | Sets the user name for basic authentication. | -| **Password** | Sets the password for basic authentication. | -| **Scrape interval** | Sets the scrape and evaluation interval. We recommend the same value as the typical configured in Prometheus. Defaults to 15s. | -| **Type** | Defines the type of your Prometheus server. Valid values are `Prometheus`, `Cortex`, `Thanos`, `Mimir`. When selected, the Prometheus version field attempts to detect the version automatically using the Prometheus [buildinfo](https://semver.org/) API. Some Prometheus types, such as Cortex, don't support this API, and you must provide their version. | -| **Version** | Defines the version of your Prometheus server. This field is visible only after the **Type** field is defined. | -| **HTTP method** | Sets the HTTP method used to query your data source. We recommend POST, which is pre-selected, because it allows for larger queries. Use GET if the Prometheus version is older than 2.1, or if POST requests are restricted in your network. | -| **Disable metrics lookup** | Disables the metrics chooser and metric/label support in the query field's autocompletion. This can prevent performance issues with larger Prometheus instances. | -| **Custom query parameters** | Adds custom parameters to the Prometheus query URL, such as `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Concatenate multiple parameters with '&'. | +| Name | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Name` | The data source name. This is how you refer to the data source in panels and queries. | +| `Default` | Default data source that is pre-selected for new panels. | +| `URL` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. | +| `Access` | Only Server access mode is functional. If Server mode is already selected this option is hidden. Otherwise change to Server mode to prevent errors. | +| `Basic Auth` | Enable basic authentication to the Prometheus data source. | +| `User` | User name for basic authentication. | +| `Password` | Password for basic authentication. | +| `Manage alerts via Alerting UI` | Toggle whether to enable Alertmanager integration for this data source. | +| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. | +| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. | +| `Type` | The type of your Prometheus server; `Prometheus`, `Cortex`, `Thanos`, `Mimir`. When selected, the **Version** field attempts to populate automatically using the Prometheus [buildinfo](https://semver.org/) API. Some Prometheus types, such as Cortex, don't support this API and must be manually populated. | +| `Version` | The version of your Prometheus server, note that this field is not visible until the Prometheus type is selected. | +| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. | +| `Custom query parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. | +| **Exemplars configuration** | | +| `Internal link` | Enable this option is you have an internal link. When you enable this option, you will see a data source selector. Select the backend tracing data store for your exemplar data. | +| `Data source` | You will see this option only if you enable `Internal link` option. Select the backend tracing data store for your exemplar data. | +| `URL` | You will see this option only if the `Internal link` option is disabled. Enter the full URL of the external link. You can interpolate the value from the field with `${__value.raw }` macro. | +| `URL Label` | (Optional) add a custom display label to override the value of the `Label name` field. | +| `Label name` | Add a name for the exemplar traceID property. | **Exemplars configuration:** @@ -87,6 +94,9 @@ datasources: url: http://localhost:9090 jsonData: httpMethod: POST + manageAlerts: true + prometheusType: Prometheus + prometheusVersion: 2.37.0 exemplarTraceIdDestinations: # Field with internal link pointing to data source in Grafana. # datasourceUid value can be anything, but it should be unique across all defined data source uids. From b4cf711a7493d3e33baa5a314a0fd9afe82d2185 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 5 Dec 2022 13:49:25 +0000 Subject: [PATCH 02/94] Package: @grafana/e2e updates (#59723) E2E package updates - Update addDashboard flow to work with latest grafana - Update viewport configuration to ensure components aren't hidden --- packages/grafana-e2e/cypress.json | 4 +++- packages/grafana-e2e/src/flows/addDashboard.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/grafana-e2e/cypress.json b/packages/grafana-e2e/cypress.json index eaa48eb162f..7eafaf4bf2b 100644 --- a/packages/grafana-e2e/cypress.json +++ b/packages/grafana-e2e/cypress.json @@ -1,5 +1,7 @@ { "projectId": "zb7k1c", "supportFile": "cypress/support/index.ts", - "videoCompression": 20 + "videoCompression": 20, + "viewportWidth": 1920, + "viewportHeight": 1080 } diff --git a/packages/grafana-e2e/src/flows/addDashboard.ts b/packages/grafana-e2e/src/flows/addDashboard.ts index 36dca49b86b..a8c02d8e1fe 100644 --- a/packages/grafana-e2e/src/flows/addDashboard.ts +++ b/packages/grafana-e2e/src/flows/addDashboard.ts @@ -236,9 +236,9 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2() .should('be.visible') .within(() => { - e2e.components.Select.singleValue().should('have.text', 'Query').click(); + e2e.components.Select.singleValue().should('have.text', 'Query').parent().click(); }); - e2e.components.Select.option().should('be.visible').contains(type).click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().find('input').type(`${type}{enter}`); } if (label) { From 32309a8373fbcf276b24e319010e5ba57818bd9e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 5 Dec 2022 15:44:04 +0100 Subject: [PATCH 03/94] Datasource: Fix - apply default query also to queries in new panels (#59625) * apply default query also to new panels * add comment * add tests --- public/app/core/utils/query.test.ts | 23 ++++++++- public/app/core/utils/query.ts | 17 +++++++ .../features/query/components/QueryGroup.tsx | 8 ++- public/app/features/query/state/runRequest.ts | 7 +++ .../query/state/updateQueries.test.ts | 51 +++++++++++++++++++ .../app/features/query/state/updateQueries.ts | 7 +-- 6 files changed, 107 insertions(+), 6 deletions(-) diff --git a/public/app/core/utils/query.test.ts b/public/app/core/utils/query.test.ts index 4b2028ce544..efd5f1944bf 100644 --- a/public/app/core/utils/query.test.ts +++ b/public/app/core/utils/query.test.ts @@ -1,6 +1,10 @@ import { DataQuery } from '@grafana/data'; -import { getNextRefIdChar } from './query'; +import { getNextRefIdChar, queryIsEmpty } from './query'; + +export interface TestQuery extends DataQuery { + name?: string; +} function dataQueryHelper(ids: string[]): DataQuery[] { return ids.map((letter) => { @@ -29,3 +33,20 @@ describe('Get next refId char', () => { expect(getNextRefIdChar(singleExtendedDataQuery)).toEqual('AA'); }); }); + +describe('queryIsEmpty', () => { + it('should return true if query only includes props that are defined in the DataQuery interface', () => { + const testQuery: DataQuery = { refId: 'A' }; + expect(queryIsEmpty(testQuery)).toEqual(true); + }); + + it('should return true if query only includes props that are defined in the DataQuery interface and a label prop', () => { + const testQuery: DataQuery & { label: string } = { refId: 'A', label: '' }; + expect(queryIsEmpty(testQuery)).toEqual(true); + }); + + it('should return false if query only includes props that are not defined in the DataQuery interface', () => { + const testQuery: TestQuery = { refId: 'A', name: 'test' }; + expect(queryIsEmpty(testQuery)).toEqual(false); + }); +}); diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts index 4a771ca64e3..11fca8e6898 100644 --- a/public/app/core/utils/query.ts +++ b/public/app/core/utils/query.ts @@ -9,6 +9,23 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => { } }; +// This function checks if the query has defined properties beyond those defined in the DataQuery interface. +export function queryIsEmpty(query: DataQuery): boolean { + const dataQueryProps = ['refId', 'hide', 'key', 'queryType', 'datasource']; + + for (const key in query) { + // label is not a DataQuery prop, but it's defined in the query when called from the QueryRunner. + if (key === 'label') { + continue; + } + if (!dataQueryProps.includes(key)) { + return false; + } + } + + return true; +} + export function addQuery(queries: DataQuery[], query?: Partial, datasource?: DataSourceRef): DataQuery[] { const q = query || {}; q.refId = getNextRefIdChar(queries); diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 5aaf14ee762..df1841a3ade 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -17,7 +17,7 @@ import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, style import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; -import { addQuery } from 'app/core/utils/query'; +import { addQuery, queryIsEmpty } from 'app/core/utils/query'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; @@ -96,7 +96,11 @@ export class QueryGroup extends PureComponent { const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource); const defaultDataSource = await this.dataSourceSrv.get(); const datasource = ds.getRef(); - const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource })); + const queries = options.queries.map((q) => ({ + ...(queryIsEmpty(q) && ds?.getDefaultQuery?.(CoreApp.PanelEditor)), + datasource, + ...q, + })); this.setState({ queries, dataSource: ds, diff --git a/public/app/features/query/state/runRequest.ts b/public/app/features/query/state/runRequest.ts index 9f0f0490707..440bf4f4e6c 100644 --- a/public/app/features/query/state/runRequest.ts +++ b/public/app/features/query/state/runRequest.ts @@ -6,6 +6,7 @@ import { catchError, map, mapTo, share, takeUntil, tap } from 'rxjs/operators'; // Utils & Services // Types import { + CoreApp, DataFrame, DataQueryError, DataQueryRequest, @@ -23,6 +24,7 @@ import { import { toDataQueryError } from '@grafana/runtime'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { backendSrv } from 'app/core/services/backend_srv'; +import { queryIsEmpty } from 'app/core/utils/query'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionQuery } from 'app/features/expressions/types'; @@ -174,6 +176,11 @@ export function callQueryMethod( request: DataQueryRequest, queryFunction?: typeof datasource.query ) { + // If the datasource has defined a default query, make sure it's applied if the query is empty + request.targets = request.targets.map((t) => + queryIsEmpty(t) ? { ...datasource?.getDefaultQuery?.(CoreApp.PanelEditor), ...t } : t + ); + // If its a public datasource, just return the result. Expressions will be handled on the backend. if (datasource.type === 'public-ds') { return from(datasource.query(request)); diff --git a/public/app/features/query/state/updateQueries.test.ts b/public/app/features/query/state/updateQueries.test.ts index fd95693c388..ce6ac115453 100644 --- a/public/app/features/query/state/updateQueries.test.ts +++ b/public/app/features/query/state/updateQueries.test.ts @@ -5,6 +5,7 @@ import { DataSourceWithQueryImportSupport, } from '@grafana/data'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; +import { TestQuery } from 'app/core/utils/query.test'; import { updateQueries } from './updateQueries'; @@ -41,6 +42,9 @@ const newUidSameTypeDS = { } as DataSourceApi; describe('updateQueries', () => { + afterEach(() => { + jest.clearAllMocks(); + }); it('Should update all queries except expression query when changing data source with same type', async () => { const updated = await updateQueries( newUidSameTypeDS, @@ -111,6 +115,53 @@ describe('updateQueries', () => { expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); }); + it('Should clear queries and get default query from ds when changing type', async () => { + newUidDS.getDefaultQuery = jest.fn().mockReturnValue({ test: 'default-query1' } as Partial); + const updated = await updateQueries( + newUidDS, + 'new-uid', + [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + ], + oldUidDS + ); + + expect(newUidDS.getDefaultQuery).toHaveBeenCalled(); + expect(updated as TestQuery[]).toEqual([ + { + datasource: { type: 'new-type', uid: 'new-uid' }, + refId: 'A', + test: 'default-query1', + }, + ]); + }); + + it('Should return default query from ds when changing type and no new queries exist', async () => { + newUidDS.getDefaultQuery = jest.fn().mockReturnValue({ test: 'default-query2' } as Partial); + const updated = await updateQueries(newUidDS, 'new-uid', [], oldUidDS); + expect(newUidDS.getDefaultQuery).toHaveBeenCalled(); + expect(updated as TestQuery[]).toEqual([ + { + datasource: { type: 'new-type', uid: 'new-uid' }, + refId: 'A', + test: 'default-query2', + }, + ]); + }); + it('Should preserve query data source when changing to mixed', async () => { const updated = await updateQueries( mixedDS, diff --git a/public/app/features/query/state/updateQueries.ts b/public/app/features/query/state/updateQueries.ts index 6e3015f6b67..b97a54ef280 100644 --- a/public/app/features/query/state/updateQueries.ts +++ b/public/app/features/query/state/updateQueries.ts @@ -1,4 +1,4 @@ -import { DataQuery, DataSourceApi, hasQueryExportSupport, hasQueryImportSupport } from '@grafana/data'; +import { CoreApp, DataQuery, DataSourceApi, hasQueryExportSupport, hasQueryImportSupport } from '@grafana/data'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; export async function updateQueries( @@ -9,6 +9,7 @@ export async function updateQueries( ): Promise { let nextQueries = queries; const datasource = { type: nextDS.type, uid: nextDSUidOrVariableExpression }; + const DEFAULT_QUERY = { ...nextDS?.getDefaultQuery?.(CoreApp.PanelEditor), datasource, refId: 'A' }; // we are changing data source type if (currentDS?.meta.id !== nextDS.meta.id) { @@ -27,12 +28,12 @@ export async function updateQueries( } // Otherwise clear queries else { - return [{ refId: 'A', datasource }]; + return [DEFAULT_QUERY]; } } if (nextQueries.length === 0) { - return [{ refId: 'A', datasource }]; + return [DEFAULT_QUERY]; } // Set data source on all queries except expression queries From 7deaeb0f9e6578f378d922b2290bfa4bdfa42466 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Mon, 5 Dec 2022 16:49:33 +0200 Subject: [PATCH 04/94] CI: Add `identify-runner` step for publish images (#59827) Add missing identify-runner step --- .drone.yml | 14 +++++++++++++- scripts/drone/pipelines/publish_images.star | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index afa440e9753..4d427ed0cfa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3857,6 +3857,10 @@ platform: os: linux services: [] steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15.6 + name: identify-runner - commands: - mkdir -p bin - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl @@ -3949,6 +3953,10 @@ platform: os: linux services: [] steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15.6 + name: identify-runner - commands: - mkdir -p bin - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl @@ -4024,6 +4032,10 @@ platform: os: linux services: [] steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15.6 + name: identify-runner - commands: - mkdir -p bin - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.17/grabpl @@ -6306,6 +6318,6 @@ kind: secret name: packages_secret_access_key --- kind: signature -hmac: e7746a4b35fba9e1a7cb3096b947a874786b082f41e4252448ac6acde7ee3ccf +hmac: efffc2eb55bbfa1cebe950585df1f3f81d2f2a38311e2854f3ce08d1eec16fe3 ... diff --git a/scripts/drone/pipelines/publish_images.star b/scripts/drone/pipelines/publish_images.star index 3481dadcd73..6281890a17c 100644 --- a/scripts/drone/pipelines/publish_images.star +++ b/scripts/drone/pipelines/publish_images.star @@ -1,5 +1,6 @@ load( 'scripts/drone/steps/lib.star', + 'identify_runner_step', 'download_grabpl_step', 'publish_images_step', 'compile_build_cmd', @@ -17,6 +18,7 @@ def publish_image_steps(edition, mode, docker_repo): if edition == 'oss': additional_docker_repo='grafana/grafana-oss' steps = [ + identify_runner_step(), download_grabpl_step(), compile_build_cmd(), fetch_images_step(edition), From 3544250baf0ed84ec4d7d607284f71c720c9f003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 5 Dec 2022 16:03:24 +0100 Subject: [PATCH 05/94] Accessibility: Make QueryEditorHelp examples keyboard interactive (#59355) * Cloudwatch: A11y - Make example log keyboard interactive * Change all cheat sheets to buttons * Make suggestions block --- .../datasource/cloudwatch/components/LogsCheatSheet.tsx | 6 +++--- .../datasource/loki/components/LokiCheatSheet.tsx | 9 +++++++-- .../datasource/prometheus/components/PromCheatSheet.tsx | 5 +++-- public/sass/pages/_explore.scss | 6 +++++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx index 697720b3c35..1e1a1cf4ba2 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx @@ -223,10 +223,10 @@ export default class LogsCheatSheet extends PureComponent< onClickExample(query: CloudWatchQuery) { this.props.onClickExample(query); } - renderExpression(expr: string, keyPrefix: string) { return ( -
@@ -241,7 +241,7 @@ export default class LogsCheatSheet extends PureComponent< } >
{renderHighlightedMarkup(expr, keyPrefix)}
-
+ ); } diff --git a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx index 94e53cef3bb..b6c75f526a4 100644 --- a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx +++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx @@ -81,9 +81,14 @@ export default class LokiCheatSheet extends PureComponent onClick({ refId: 'A', expr })}> + ); } diff --git a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx index c26952aae2d..9142be3e1f7 100644 --- a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx @@ -35,12 +35,13 @@ const PromCheatSheet = (props: QueryEditorHelpProps) => (
{item.title}
{item.expression ? ( -
props.onClickExample({ refId: 'A', expr: item.expression })} > {item.expression} -
+ ) : null}
{item.label}
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 5392c918b3c..1bfac398de9 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -139,7 +139,11 @@ .cheat-sheet-item__example { margin: $space-xs 0; - cursor: pointer; + // element is interactive, clear button styles + text-align: left; + border: none; + background: transparent; + display: block; } .query-type-toggle { From e356a567413534c6ec91bb9cde4893b6458bac79 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 5 Dec 2022 16:06:55 +0100 Subject: [PATCH 06/94] Elasticsearch: Fix removing of empty settings from query in backend implementation (#59704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Elasticsearch: Fix removing of empty settings from query in backend implementation * Update * Update * Update pkg/tsdb/elasticsearch/time_series_query.go Co-authored-by: Gábor Farkas Co-authored-by: Gábor Farkas --- pkg/tsdb/elasticsearch/time_series_query.go | 11 ++++++++++- pkg/tsdb/elasticsearch/time_series_query_test.go | 4 +--- .../datasource/elasticsearch/QueryBuilder.test.ts | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index 7f472eb741a..a8c496a0615 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -454,7 +454,16 @@ func (p *timeSeriesQueryParser) parseMetrics(model *simplejson.Json) ([]*MetricA metric.Hide = metricJSON.Get("hide").MustBool(false) metric.ID = metricJSON.Get("id").MustString() metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString() - metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap()) + // In legacy editors, we were storing empty settings values as "null" + // The new editor doesn't store empty strings at all + // We need to ensures backward compatibility with old queries and remove empty fields + settings := metricJSON.Get("settings").MustMap() + for k, v := range settings { + if v == "null" { + delete(settings, k) + } + } + metric.Settings = simplejson.NewFromAny(settings) metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap()) metric.Type, err = metricJSON.Get("type").String() if err != nil { diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index e3cb75019bc..f6c3e61815d 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -39,7 +39,6 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { require.Equal(t, dateHistogramAgg.ExtendedBounds.Min, fromMs) require.Equal(t, dateHistogramAgg.ExtendedBounds.Max, toMs) }) - t.Run("Should clean settings from null values (from frontend tests)", func(t *testing.T) { c := newFakeClient() _, err := executeTsdbQuery(c, `{ @@ -52,8 +51,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { firstLevel := sr.Aggs[0] secondLevel := firstLevel.Aggregation.Aggs[0] require.Equal(t, secondLevel.Aggregation.Aggregation.(*es.MetricAggregation).Settings["script"], "1") - // FIXME: This is a bug in implementation, missing is set to "null" instead of being removed - // require.Equal(t, secondLevel.Aggregation.Aggregation.(*es.MetricAggregation).Settings["missing"], nil) + require.NotContains(t, secondLevel.Aggregation.Aggregation.(*es.MetricAggregation).Settings, "missing") }) t.Run("With multiple bucket aggs", func(t *testing.T) { diff --git a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts index e05cc478f34..b091f5a9c00 100644 --- a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts @@ -22,7 +22,7 @@ describe('ElasticQueryBuilder', () => { // The following `missing: null as any` is because previous versions of the DS where // storing null in the query model when inputting an empty string, // which were then removed in the query builder. - // The new version doesn't store empty strings at all. This tests ensures backward compatinility. + // The new version doesn't store empty strings at all. This tests ensures backward compatibility. metrics: [{ type: 'avg', id: '0', settings: { missing: null as any, script: '1' } }], timeField: '@timestamp', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], From 5240905f051bde8faab5c381f82937a072603d5b Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Mon, 5 Dec 2022 10:00:20 -0600 Subject: [PATCH 07/94] docs: fixes broken link and spelling error (#59828) fixes broken link and spelling error --- .../rbac-fixed-basic-role-definitions/index.md | 2 +- .../rbac-grafana-provisioning/index.md | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index ee92d91a5cf..9060e7d2517 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -21,7 +21,7 @@ The following tables list permissions associated with basic and fixed roles. | Basic role | Associated fixed roles | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | -| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixes:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | | Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | | Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md index 29a5b6eac9a..7215f5ab73d 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md @@ -26,13 +26,17 @@ Grafana performs provisioning during startup. After you make a change to the con 1. Sign in to the Grafana server. -2. Locate the Grafana provisioning folder. +1. Locate the Grafana provisioning folder. -3. Create a new YAML in the following folder: **provisioning/access-control**. For example, `provisioning/access-control/custom-roles.yml` +1. Create a new YAML in the following folder: **provisioning/access-control**. For example, `provisioning/access-control/custom-roles.yml` -4. Add RBAC provisioning details to the configuration file. See [manage RBAC roles]({{< relref "./manage-rbac-roles/" >}}) and [assign RBAC roles]({{< relref "./assign-rbac-roles/" >}}) for instructions, and see this [example role provisioning file]({{< relref "./rbac-provisioning/#example" >}}) for a complete example of a provisioning file. +1. Add RBAC provisioning details to the configuration file. -5. Reload the provisioning configuration file. + Refer to [Manage RBAC roles]({{< relref "./manage-rbac-roles/" >}}) and [Assign RBAC roles]({{< relref "./assign-rbac-roles/" >}}) for instructions. + + Refer to [example role provisioning file]({{< relref "#example-role-configuration-file-using-grafana-provisioning" >}}) for a complete example of a provisioning file. + +1. Reload the provisioning configuration file. For more information about reloading the provisioning configuration at runtime, refer to [Reload provisioning configurations]({{< relref "../../../../developers/http_api/admin/#reload-provisioning-configurations" >}}). @@ -46,7 +50,7 @@ The following example shows a complete YAML configuration file that: - Assign roles to teams - Revoke assignments of roles to teams -## Example +### Example ```yaml --- From 866aea0db2a762800f68efabb345ef78f45f356e Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 5 Dec 2022 11:02:20 -0500 Subject: [PATCH 08/94] Alerting: fix UI element for PagerDuty's severity field configuration (#58927) * make severity a regular text field * add logs + fallback to critical if empty --- .../ngalert/notifier/channels/pagerduty.go | 14 +++++- .../notifier/channels/pagerduty_test.go | 46 +++++++++++++++++-- .../channels_config/available_channels.go | 25 ++-------- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index 3dd574eb1ce..3765a0696c7 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" @@ -21,9 +22,12 @@ import ( const ( pagerDutyEventTrigger = "trigger" pagerDutyEventResolve = "resolve" + + defaultSeverity = "critical" ) var ( + knownSeverity = map[string]struct{}{defaultSeverity: {}, "error": {}, "warning": {}, "info": {}} PagerdutyEventAPIURL = "https://events.pagerduty.com/v2/enqueue" ) @@ -80,7 +84,7 @@ func newPagerdutyNotifier(fc FactoryConfig) (*PagerdutyNotifier, error) { images: fc.ImageStore, settings: pagerdutySettings{ Key: key, - Severity: fc.Config.Settings.Get("severity").MustString("critical"), + Severity: fc.Config.Settings.Get("severity").MustString(defaultSeverity), customDetails: map[string]string{ "firing": `{{ template "__text_alert_list" .Alerts.Firing }}`, "resolved": `{{ template "__text_alert_list" .Alerts.Resolved }}`, @@ -152,6 +156,12 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m details[k] = detail } + severity := strings.ToLower(tmpl(pn.settings.Severity)) + if _, ok := knownSeverity[severity]; !ok { + pn.log.Warn("Severity is not in the list of known values - using default severity", "actualSeverity", severity, "defaultSeverity", defaultSeverity) + severity = defaultSeverity + } + msg := &pagerDutyMessage{ Client: "Grafana", ClientURL: pn.tmpl.ExternalURL.String(), @@ -165,7 +175,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m Payload: pagerDutyPayload{ Component: tmpl(pn.settings.Component), Summary: tmpl(pn.settings.Summary), - Severity: tmpl(pn.settings.Severity), + Severity: severity, CustomDetails: details, Class: tmpl(pn.settings.Class), Group: tmpl(pn.settings.Group), diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 7e86b040f5b..47c8d49dee5 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -7,14 +7,14 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/secrets/fakes" - secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/secrets/fakes" + secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" ) func TestPagerdutyNotifier(t *testing.T) { @@ -69,7 +69,43 @@ func TestPagerdutyNotifier(t *testing.T) { Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}}, }, expMsgError: nil, - }, { + }, + { + name: "should map unknown severity", + settings: `{"integrationKey": "abcdefgh0123456789", "severity": "{{ .CommonLabels.severity }}"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1", "severity": "invalid-severity"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: &pagerDutyMessage{ + RoutingKey: "abcdefgh0123456789", + DedupKey: "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + EventAction: "trigger", + Payload: pagerDutyPayload{ + Summary: "[FIRING:1] (val1 invalid-severity)", + Source: hostname, + Severity: defaultSeverity, + Class: "default", + Component: "Grafana", + Group: "default", + CustomDetails: map[string]string{ + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\n - severity = invalid-severity\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1&matcher=severity%3Dinvalid-severity\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "num_firing": "1", + "num_resolved": "0", + "resolved": "", + }, + }, + Client: "Grafana", + ClientURL: "http://localhost", + Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}}, + }, + expMsgError: nil, + }, + { name: "Default config with one alert and custom summary", settings: `{"integrationKey": "abcdefgh0123456789", "summary": "Alerts firing: {{ len .Alerts.Firing }}"}`, alerts: []*types.Alert{ diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 49e4c01b93b..f9b063bc022 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -239,26 +239,11 @@ func GetAvailableNotifiers() []*NotifierPlugin { Secure: true, }, { - Label: "Severity", - Element: ElementTypeSelect, - SelectOptions: []SelectOption{ - { - Value: "critical", - Label: "Critical", - }, - { - Value: "error", - Label: "Error", - }, - { - Value: "warning", - Label: "Warning", - }, - { - Value: "info", - Label: "Info", - }, - }, + Label: "Severity", + Element: ElementTypeInput, + InputType: InputTypeText, + Placeholder: "critical", + Description: "Severity of the event. It must be critical, error, warning, info - otherwise, the default is set which is critical. You can use templates", PropertyName: "severity", }, { // New in 8.0. From c6840cfdec6c61d52d3ed613ec9eb8764aa1f4be Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Mon, 5 Dec 2022 17:05:22 +0100 Subject: [PATCH 09/94] Tempo: Fix traceQL suggestions for tag names with dots (#59736) --- .../datasource/tempo/traceql/autocomplete.ts | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts index 96ec31a79d7..374ee42541e 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -56,13 +56,17 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP // to stop it, we use a number-as-string sortkey, // so that monaco keeps the order we use const maxIndexDigits = items.length.toString().length; - const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({ - kind: getMonacoCompletionItemKind(item.type, this.monaco!), - label: item.label, - insertText: item.insertText, - sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have - range, - })); + const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => { + const suggestion = { + kind: getMonacoCompletionItemKind(item.type, this.monaco!), + label: item.label, + insertText: item.insertText, + sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have + range, + }; + fixSuggestion(suggestion, item.type, model, offset, this.monaco!); + return suggestion; + }); return { suggestions }; }); } @@ -379,3 +383,43 @@ function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, const offset = model.getOffsetAt(positionClone); return { offset, range }; } + +/** + * Fix the suggestions range and insert text. For the range we have to adjust because monaco by default replaces just + * the last word which stops at dot while traceQL tags contain dots themselves and we want to replace the whole tag + * name when suggesting. The insert text needs to be adjusted for scope (leading dot) if scope is currently missing. + * This may be doable also when creating the suggestions but for a particular situation this seems to be easier to do + * here. + */ +function fixSuggestion( + suggestion: monacoTypes.languages.CompletionItem & { range: monacoTypes.IRange }, + itemType: CompletionType, + model: monacoTypes.editor.ITextModel, + offset: number, + monaco: Monaco +) { + if (itemType === 'TAG_NAME') { + const match = model + .getValue() + .substring(0, offset) + .match(/(span\.|resource\.|\.)?([\w./-]*)$/); + + if (match) { + const scope = match[1]; + const tag = match[2]; + + if (tag) { + // Add the default scope if needed. + if (!scope && suggestion.insertText[0] !== '.') { + suggestion.insertText = '.' + suggestion.insertText; + } + + // Adjust the range, so that we will replace the whole tag. + suggestion.range = monaco.Range.lift({ + ...suggestion.range, + startColumn: offset - tag.length + 1, + }); + } + } + } +} From 46adfb596ddb09e6d003e4845eb4a3be6473f8f6 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 5 Dec 2022 16:12:56 +0000 Subject: [PATCH 10/94] Command Palette: Maintain page state when changing theme (#59787) * use same function as the change theme keybindings * rename toggleTheme service to just theme --- public/app/core/services/keybindingSrv.ts | 2 +- public/app/core/services/{toggleTheme.ts => theme.ts} | 10 +++++++--- .../commandPalette/actions/global.static.actions.tsx | 11 +++-------- 3 files changed, 11 insertions(+), 12 deletions(-) rename public/app/core/services/{toggleTheme.ts => theme.ts} (90%) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 3920e53eb61..39d935f07fc 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -24,7 +24,7 @@ import { AppChromeService } from '../components/AppChrome/AppChromeService'; import { HelpModal } from '../components/help/HelpModal'; import { contextSrv } from '../core'; -import { toggleTheme } from './toggleTheme'; +import { toggleTheme } from './theme'; import { withFocusedPanel } from './withFocusedPanelId'; export class KeybindingSrv { diff --git a/public/app/core/services/toggleTheme.ts b/public/app/core/services/theme.ts similarity index 90% rename from public/app/core/services/toggleTheme.ts rename to public/app/core/services/theme.ts index ee274847ce9..98d91b3837d 100644 --- a/public/app/core/services/toggleTheme.ts +++ b/public/app/core/services/theme.ts @@ -7,11 +7,10 @@ import { contextSrv } from '../core'; import { PreferencesService } from './PreferencesService'; -export async function toggleTheme(runtimeOnly: boolean) { - const currentTheme = config.theme2; +export async function changeTheme(mode: 'dark' | 'light', runtimeOnly?: boolean) { const newTheme = createTheme({ colors: { - mode: currentTheme.isDark ? 'light' : 'dark', + mode: mode, }, }); @@ -55,3 +54,8 @@ export async function toggleTheme(runtimeOnly: boolean) { theme: newTheme.colors.mode, }); } + +export async function toggleTheme(runtimeOnly: boolean) { + const currentTheme = config.theme2; + changeTheme(currentTheme.isDark ? 'light' : 'dark', runtimeOnly); +} diff --git a/public/app/features/commandPalette/actions/global.static.actions.tsx b/public/app/features/commandPalette/actions/global.static.actions.tsx index 505b89fb798..d376a93b461 100644 --- a/public/app/features/commandPalette/actions/global.static.actions.tsx +++ b/public/app/features/commandPalette/actions/global.static.actions.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { isIconName, NavModelItem } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { Icon } from '@grafana/ui'; +import { changeTheme } from 'app/core/services/theme'; const SECTION_PAGES = 'Pages'; const SECTION_ACTIONS = 'Actions'; @@ -82,10 +83,7 @@ export default (navBarTree: NavModelItem[]) => { name: 'Dark', keywords: 'dark theme', section: '', - perform: () => { - locationService.push({ search: '?theme=dark' }); - location.reload(); - }, + perform: () => changeTheme('dark'), parent: 'preferences/theme', }, { @@ -93,10 +91,7 @@ export default (navBarTree: NavModelItem[]) => { name: 'Light', keywords: 'light theme', section: '', - perform: () => { - locationService.push({ search: '?theme=light' }); - location.reload(); - }, + perform: () => changeTheme('light'), parent: 'preferences/theme', }, ]; From eeb57cd520b1fb6a583f0ddb23daf8ed8e5eddf4 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 5 Dec 2022 11:38:50 -0500 Subject: [PATCH 11/94] Alerting: Refactor PagerDuty and OpsGenie notifiers to use encoding/json to parse settings (#58925) * update pagerduty and opsgenie to deserialize settings using standard JSON library * update pagerduty truncation to use a function from Alertamanger package * update opsgenie to use payload model (same as in Alertmanager) --- .../ngalert/notifier/channels/opsgenie.go | 249 ++++++++++-------- .../notifier/channels/opsgenie_test.go | 31 ++- .../ngalert/notifier/channels/pagerduty.go | 77 ++++-- .../notifier/channels/pagerduty_test.go | 41 ++- 4 files changed, 251 insertions(+), 147 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index ba8480a9c67..bb45abce242 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -13,8 +13,8 @@ import ( "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" + ptr "github.com/xorcare/pointer" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -35,21 +35,14 @@ var ( // OpsgenieNotifier is responsible for sending alert notifications to Opsgenie. type OpsgenieNotifier struct { *Base - APIKey string - APIUrl string - Message string - Description string - AutoClose bool - OverridePriority bool - SendTagsAs string - tmpl *template.Template - log log.Logger - ns notifications.WebhookSender - images ImageStore + tmpl *template.Template + log log.Logger + ns notifications.WebhookSender + images ImageStore + settings *opsgenieSettings } -type OpsgenieConfig struct { - *NotificationChannelConfig +type opsgenieSettings struct { APIKey string APIUrl string Message string @@ -59,62 +52,92 @@ type OpsgenieConfig struct { SendTagsAs string } +func buildOpsgenieSettings(fc FactoryConfig) (*opsgenieSettings, error) { + type rawSettings struct { + APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"` + APIUrl string `json:"apiUrl,omitempty" yaml:"apiUrl,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + AutoClose *bool `json:"autoClose,omitempty" yaml:"autoClose,omitempty"` + OverridePriority *bool `json:"overridePriority,omitempty" yaml:"overridePriority,omitempty"` + SendTagsAs string `json:"sendTagsAs,omitempty" yaml:"sendTagsAs,omitempty"` + } + + raw := rawSettings{} + err := fc.Config.unmarshalSettings(&raw) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal settings: %w", err) + } + + raw.APIKey = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "apiKey", raw.APIKey) + if raw.APIKey == "" { + return nil, errors.New("could not find api key property in settings") + } + if raw.APIUrl == "" { + raw.APIUrl = OpsgenieAlertURL + } + + if strings.TrimSpace(raw.Message) == "" { + raw.Message = DefaultMessageTitleEmbed + } + + switch raw.SendTagsAs { + case OpsgenieSendTags, OpsgenieSendDetails, OpsgenieSendBoth: + case "": + raw.SendTagsAs = OpsgenieSendTags + default: + return nil, fmt.Errorf("invalid value for sendTagsAs: %q", raw.SendTagsAs) + } + + if raw.AutoClose == nil { + raw.AutoClose = ptr.Bool(true) + } + if raw.OverridePriority == nil { + raw.OverridePriority = ptr.Bool(true) + } + + return &opsgenieSettings{ + APIKey: raw.APIKey, + APIUrl: raw.APIUrl, + Message: raw.Message, + Description: raw.Description, + AutoClose: *raw.AutoClose, + OverridePriority: *raw.OverridePriority, + SendTagsAs: raw.SendTagsAs, + }, nil +} + func OpsgenieFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewOpsgenieConfig(fc.Config, fc.DecryptFunc) + notifier, err := NewOpsgenieNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return NewOpsgenieNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template, fc.DecryptFunc), nil -} - -func NewOpsgenieConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*OpsgenieConfig, error) { - apiKey := decryptFunc(context.Background(), config.SecureSettings, "apiKey", config.Settings.Get("apiKey").MustString()) - if apiKey == "" { - return nil, errors.New("could not find api key property in settings") - } - sendTagsAs := config.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags) - if sendTagsAs != OpsgenieSendTags && - sendTagsAs != OpsgenieSendDetails && - sendTagsAs != OpsgenieSendBoth { - return nil, fmt.Errorf("invalid value for sendTagsAs: %q", sendTagsAs) - } - return &OpsgenieConfig{ - NotificationChannelConfig: config, - APIKey: apiKey, - APIUrl: config.Settings.Get("apiUrl").MustString(OpsgenieAlertURL), - AutoClose: config.Settings.Get("autoClose").MustBool(true), - OverridePriority: config.Settings.Get("overridePriority").MustBool(true), - Message: config.Settings.Get("message").MustString(`{{ template "default.title" . }}`), - Description: config.Settings.Get("description").MustString(""), - SendTagsAs: sendTagsAs, - }, nil + return notifier, nil } // NewOpsgenieNotifier is the constructor for the Opsgenie notifier -func NewOpsgenieNotifier(config *OpsgenieConfig, ns notifications.WebhookSender, images ImageStore, t *template.Template, fn GetDecryptedValueFn) *OpsgenieNotifier { +func NewOpsgenieNotifier(fc FactoryConfig) (*OpsgenieNotifier, error) { + settings, err := buildOpsgenieSettings(fc) + if err != nil { + return nil, err + } return &OpsgenieNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: config.UID, - Name: config.Name, - Type: config.Type, - DisableResolveMessage: config.DisableResolveMessage, - Settings: config.Settings, + Uid: fc.Config.UID, + Name: fc.Config.Name, + Type: fc.Config.Type, + DisableResolveMessage: fc.Config.DisableResolveMessage, + Settings: fc.Config.Settings, }), - APIKey: config.APIKey, - APIUrl: config.APIUrl, - Description: config.Description, - Message: config.Message, - AutoClose: config.AutoClose, - OverridePriority: config.OverridePriority, - SendTagsAs: config.SendTagsAs, - tmpl: t, - log: log.New("alerting.notifier." + config.Name), - ns: ns, - images: images, - } + tmpl: fc.Template, + log: log.New("alerting.notifier.opsgenie"), + ns: fc.NotificationService, + images: fc.ImageStore, + settings: settings, + }, nil } // Notify sends an alert notification to Opsgenie @@ -127,7 +150,7 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo return true, nil } - bodyJSON, url, err := on.buildOpsgenieMessage(ctx, alerts, as) + body, url, err := on.buildOpsgenieMessage(ctx, alerts, as) if err != nil { return false, fmt.Errorf("build Opsgenie message: %w", err) } @@ -138,18 +161,13 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo return true, nil } - body, err := json.Marshal(bodyJSON) - if err != nil { - return false, fmt.Errorf("marshal json: %w", err) - } - cmd := &models.SendWebhookSync{ Url: url, Body: string(body), HttpMethod: http.MethodPost, HttpHeader: map[string]string{ "Content-Type": "application/json", - "Authorization": fmt.Sprintf("GenieKey %s", on.APIKey), + "Authorization": fmt.Sprintf("GenieKey %s", on.settings.APIKey), }, } @@ -160,28 +178,24 @@ func (on *OpsgenieNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo return true, nil } -func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts model.Alerts, as []*types.Alert) (payload *simplejson.Json, apiURL string, err error) { +func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts model.Alerts, as []*types.Alert) (payload []byte, apiURL string, err error) { key, err := notify.ExtractGroupKey(ctx) if err != nil { return nil, "", err } - var ( - alias = key.Hash() - bodyJSON = simplejson.New() - details = simplejson.New() - ) - if alerts.Status() == model.AlertResolved { // For resolved notification, we only need the source. // Don't need to run other templates. - if on.AutoClose { - bodyJSON := simplejson.New() - bodyJSON.Set("source", "Grafana") - apiURL = fmt.Sprintf("%s/%s/close?identifierType=alias", on.APIUrl, alias) - return bodyJSON, apiURL, nil + if !on.settings.AutoClose { // TODO This should be handled by DisableResolveMessage? + return nil, "", nil } - return nil, "", nil + msg := opsGenieCloseMessage{ + Source: "Grafana", + } + data, err := json.Marshal(msg) + apiURL = fmt.Sprintf("%s/%s/close?identifierType=alias", on.settings.APIUrl, key.Hash()) + return data, apiURL, err } ruleURL := joinUrlPath(on.tmpl.ExternalURL.String(), "/alerting/list", on.log) @@ -189,17 +203,12 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod var tmplErr error tmpl, data := TmplText(ctx, on.tmpl, as, on.log, &tmplErr) - titleTmpl := on.Message - if strings.TrimSpace(titleTmpl) == "" { - titleTmpl = `{{ template "default.title" . }}` + message, truncated := notify.Truncate(tmpl(on.settings.Message), 130) + if truncated { + on.log.Debug("Truncated message", "originalMessage", message) } - title := tmpl(titleTmpl) - if len(title) > 130 { - title = title[:127] + "..." - } - - description := tmpl(on.Description) + description := tmpl(on.settings.Description) if strings.TrimSpace(description) == "" { description = fmt.Sprintf( "%s\n%s\n\n%s", @@ -215,8 +224,7 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod lbls := make(map[string]string, len(data.CommonLabels)) for k, v := range data.CommonLabels { lbls[k] = tmpl(v) - - if k == "og_priority" { + if k == "og_priority" && on.settings.OverridePriority { if ValidPriorities[v] { priority = v } @@ -229,18 +237,13 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod tmplErr = nil } - bodyJSON.Set("message", title) - bodyJSON.Set("source", "Grafana") - bodyJSON.Set("alias", alias) - bodyJSON.Set("description", description) - details.Set("url", ruleURL) - + details := make(map[string]interface{}) + details["url"] = ruleURL if on.sendDetails() { for k, v := range lbls { - details.Set(k, v) + details[k] = v } - - images := []string{} + var images []string _ = withStoredImages(ctx, on.log, on.images, func(_ int, image ngmodels.Image) error { if len(image.URL) == 0 { @@ -252,7 +255,7 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod as...) if len(images) != 0 { - details.Set("image_urls", images) + details["image_urls"] = images } } @@ -264,19 +267,24 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod } sort.Strings(tags) - if priority != "" && on.OverridePriority { - bodyJSON.Set("priority", priority) + result := opsGenieCreateMessage{ + Alias: key.Hash(), + Description: description, + Tags: tags, + Source: "Grafana", + Message: message, + Details: details, + Priority: priority, } - bodyJSON.Set("tags", tags) - bodyJSON.Set("details", details) - apiURL = tmpl(on.APIUrl) + apiURL = tmpl(on.settings.APIUrl) if tmplErr != nil { - on.log.Warn("failed to template Opsgenie URL", "error", tmplErr.Error(), "fallback", on.APIUrl) - apiURL = on.APIUrl + on.log.Warn("failed to template Opsgenie URL", "error", tmplErr.Error(), "fallback", on.settings.APIUrl) + apiURL = on.settings.APIUrl } - return bodyJSON, apiURL, nil + b, err := json.Marshal(result) + return b, apiURL, err } func (on *OpsgenieNotifier) SendResolved() bool { @@ -284,9 +292,34 @@ func (on *OpsgenieNotifier) SendResolved() bool { } func (on *OpsgenieNotifier) sendDetails() bool { - return on.SendTagsAs == OpsgenieSendDetails || on.SendTagsAs == OpsgenieSendBoth + return on.settings.SendTagsAs == OpsgenieSendDetails || on.settings.SendTagsAs == OpsgenieSendBoth } func (on *OpsgenieNotifier) sendTags() bool { - return on.SendTagsAs == OpsgenieSendTags || on.SendTagsAs == OpsgenieSendBoth + return on.settings.SendTagsAs == OpsgenieSendTags || on.settings.SendTagsAs == OpsgenieSendBoth +} + +type opsGenieCreateMessage struct { + Alias string `json:"alias"` + Message string `json:"message"` + Description string `json:"description,omitempty"` + Details map[string]interface{} `json:"details"` + Source string `json:"source"` + Responders []opsGenieCreateMessageResponder `json:"responders,omitempty"` + Tags []string `json:"tags"` + Note string `json:"note,omitempty"` + Priority string `json:"priority,omitempty"` + Entity string `json:"entity,omitempty"` + Actions []string `json:"actions,omitempty"` +} + +type opsGenieCreateMessageResponder struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + Type string `json:"type"` // team, user, escalation, schedule etc. +} + +type opsGenieCloseMessage struct { + Source string `json:"source"` } diff --git a/pkg/services/ngalert/notifier/channels/opsgenie_test.go b/pkg/services/ngalert/notifier/channels/opsgenie_test.go index 04b507517cb..6236cc81fbf 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie_test.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie_test.go @@ -95,7 +95,7 @@ func TestOpsgenieNotifier(t *testing.T) { "details": { "url": "http://localhost/alerting/list" }, - "message": "IyJnsW78xQoiBJ7L7NqASv31JCFf0At3r9KUykqBVxSiC6qkDhvDLDW9VImiFcq0Iw2XwFy5fX4FcbTmlkaZzUzjVwx9VUuokhzqQlJVhWDYFqhj3a5wX0LjyvNQjsq...", + "message": "IyJnsW78xQoiBJ7L7NqASv31JCFf0At3r9KUykqBVxSiC6qkDhvDLDW9VImiFcq0Iw2XwFy5fX4FcbTmlkaZzUzjVwx9VUuokhzqQlJVhWDYFqhj3a5wX0LjyvNQjsqT9…", "source": "Grafana", "tags": ["alertname:alert1", "lbl1:val1"] }`, @@ -234,28 +234,33 @@ func TestOpsgenieNotifier(t *testing.T) { require.NoError(t, err) secureSettings := make(map[string][]byte) - m := &NotificationChannelConfig{ - Name: "opsgenie_testing", - Type: "opsgenie", - Settings: settingsJSON, - SecureSettings: secureSettings, - } - webhookSender := mockNotificationService() webhookSender.Webhook.Body = "" secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - cfg, err := NewOpsgenieConfig(m, decryptFn) + + fc := FactoryConfig{ + Config: &NotificationChannelConfig{ + Name: "opsgenie_testing", + Type: "opsgenie", + Settings: settingsJSON, + SecureSettings: secureSettings, + }, + NotificationService: webhookSender, + DecryptFunc: decryptFn, + ImageStore: &UnavailableImageStore{}, + Template: tmpl, + } + + ctx := notify.WithGroupKey(context.Background(), "alertname") + ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn, err := NewOpsgenieNotifier(fc) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) return } require.NoError(t, err) - - ctx := notify.WithGroupKey(context.Background(), "alertname") - ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) - pn := NewOpsgenieNotifier(cfg, webhookSender, &UnavailableImageStore{}, tmpl, decryptFn) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index 3765a0696c7..a84347bb1d1 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -24,6 +24,8 @@ const ( pagerDutyEventResolve = "resolve" defaultSeverity = "critical" + defaultClass = "default" + defaultGroup = "default" ) var ( @@ -39,7 +41,7 @@ type PagerdutyNotifier struct { log log.Logger ns notifications.WebhookSender images ImageStore - settings pagerdutySettings + settings *pagerdutySettings } type pagerdutySettings struct { @@ -52,6 +54,44 @@ type pagerdutySettings struct { Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` } +func buildPagerdutySettings(fc FactoryConfig) (*pagerdutySettings, error) { + settings := pagerdutySettings{} + err := fc.Config.unmarshalSettings(&settings) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal settings: %w", err) + } + + settings.Key = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "integrationKey", settings.Key) + if settings.Key == "" { + return nil, errors.New("could not find integration key property in settings") + } + + settings.customDetails = map[string]string{ + "firing": `{{ template "__text_alert_list" .Alerts.Firing }}`, + "resolved": `{{ template "__text_alert_list" .Alerts.Resolved }}`, + "num_firing": `{{ .Alerts.Firing | len }}`, + "num_resolved": `{{ .Alerts.Resolved | len }}`, + } + + if settings.Severity == "" { + settings.Severity = defaultSeverity + } + if settings.Class == "" { + settings.Class = defaultClass + } + if settings.Component == "" { + settings.Component = "Grafana" + } + if settings.Group == "" { + settings.Group = defaultGroup + } + if settings.Summary == "" { + settings.Summary = DefaultMessageTitleEmbed + } + + return &settings, nil +} + func PagerdutyFactory(fc FactoryConfig) (NotificationChannel, error) { pdn, err := newPagerdutyNotifier(fc) if err != nil { @@ -65,9 +105,9 @@ func PagerdutyFactory(fc FactoryConfig) (NotificationChannel, error) { // NewPagerdutyNotifier is the constructor for the PagerDuty notifier func newPagerdutyNotifier(fc FactoryConfig) (*PagerdutyNotifier, error) { - key := fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "integrationKey", fc.Config.Settings.Get("integrationKey").MustString()) - if key == "" { - return nil, errors.New("could not find integration key property in settings") + settings, err := buildPagerdutySettings(fc) + if err != nil { + return nil, err } return &PagerdutyNotifier{ @@ -78,24 +118,11 @@ func newPagerdutyNotifier(fc FactoryConfig) (*PagerdutyNotifier, error) { DisableResolveMessage: fc.Config.DisableResolveMessage, Settings: fc.Config.Settings, }), - tmpl: fc.Template, - log: log.New("alerting.notifier." + fc.Config.Name), - ns: fc.NotificationService, - images: fc.ImageStore, - settings: pagerdutySettings{ - Key: key, - Severity: fc.Config.Settings.Get("severity").MustString(defaultSeverity), - customDetails: map[string]string{ - "firing": `{{ template "__text_alert_list" .Alerts.Firing }}`, - "resolved": `{{ template "__text_alert_list" .Alerts.Resolved }}`, - "num_firing": `{{ .Alerts.Firing | len }}`, - "num_resolved": `{{ .Alerts.Resolved | len }}`, - }, - Class: fc.Config.Settings.Get("class").MustString("default"), - Component: fc.Config.Settings.Get("component").MustString("Grafana"), - Group: fc.Config.Settings.Get("group").MustString("default"), - Summary: fc.Config.Settings.Get("summary").MustString(DefaultMessageTitleEmbed), - }, + tmpl: fc.Template, + log: log.New("alerting.notifier." + fc.Config.Name), + ns: fc.NotificationService, + images: fc.ImageStore, + settings: settings, }, nil } @@ -192,9 +219,9 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m }, as...) - if len(msg.Payload.Summary) > 1024 { - // This is the Pagerduty limit. - msg.Payload.Summary = msg.Payload.Summary[:1021] + "..." + if summary, truncated := notify.Truncate(msg.Payload.Summary, 1024); truncated { + pn.log.Debug("Truncated summary", "original", msg.Payload.Summary) + msg.Payload.Summary = summary } if hostname, err := os.Hostname(); err == nil { diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 47c8d49dee5..21ee9c88acc 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -3,8 +3,11 @@ package channels import ( "context" "encoding/json" + "fmt" + "math/rand" "net/url" "os" + "strings" "testing" "github.com/prometheus/alertmanager/notify" @@ -184,7 +187,43 @@ func TestPagerdutyNotifier(t *testing.T) { Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}}, }, expMsgError: nil, - }, { + }, + { + name: "should truncate long summary", + settings: fmt.Sprintf(`{"integrationKey": "abcdefgh0123456789", "summary": "%s"}`, strings.Repeat("1", rand.Intn(100)+1025)), + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: &pagerDutyMessage{ + RoutingKey: "abcdefgh0123456789", + DedupKey: "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + EventAction: "trigger", + Payload: pagerDutyPayload{ + Summary: fmt.Sprintf("%s…", strings.Repeat("1", 1023)), + Source: hostname, + Severity: "critical", + Class: "default", + 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&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": "", + }, + }, + Client: "Grafana", + ClientURL: "http://localhost", + Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}}, + }, + expMsgError: nil, + }, + { name: "Error in initing", settings: `{}`, expInitError: `could not find integration key property in settings`, From 4e0f95dc3146b44ca2100e44bd2ddcda30c53d42 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 5 Dec 2022 12:48:10 -0500 Subject: [PATCH 12/94] Chore: Update SDK to v0.145.0 with TimeSeriesMulti Constant (#59729) --- go.mod | 6 +- go.sum | 19 +- pkg/tsdb/loki/frame_test.go | 4 +- .../loki/testdata/matrix_gap.golden.jsonc | 4 +- .../loki/testdata/matrix_inf.golden.jsonc | 4 +- .../loki/testdata/matrix_name.golden.jsonc | 4 +- .../loki/testdata/matrix_nan.golden.jsonc | 4 +- .../loki/testdata/matrix_simple.golden.jsonc | 8 +- .../testdata/matrix_small_step.golden.jsonc | 4 +- .../testdata/matrix_with_stats.golden.jsonc | 8 +- .../loki/testdata/vector_simple.golden.jsonc | 8 +- .../vector_special_values.golden.jsonc | 12 +- .../prometheus/buffered/time_series_query.go | 2 +- .../testdata/range_auto.result.golden.jsonc | 4 +- .../range_infinity.result.golden.jsonc | 4 +- .../range_missing.result.golden.jsonc | 4 +- .../testdata/range_nan.result.golden.jsonc | 4 +- .../testdata/range_simple.result.golden.jsonc | 8 +- pkg/util/converter/prom.go | 6 +- .../testdata/loki-streams-a-frame.jsonc | 1 + .../testdata/loki-streams-a-wide-frame.jsonc | 1 + .../testdata/loki-streams-b-frame.jsonc | 1 + .../testdata/loki-streams-b-wide-frame.jsonc | 1 + .../testdata/loki-streams-c-frame.jsonc | 1 + .../testdata/loki-streams-c-wide-frame.jsonc | 1 + .../testdata/prom-exemplars-a-frame.jsonc | 1 + .../prom-exemplars-a-wide-frame.jsonc | 1 + .../testdata/prom-exemplars-b-frame.jsonc | 1 + .../prom-exemplars-b-wide-frame.jsonc | 1 + .../testdata/prom-labels-frame.jsonc | 1 + .../testdata/prom-labels-wide-frame.jsonc | 1 + .../testdata/prom-matrix-frame.jsonc | 9 +- ...rom-matrix-histogram-no-labels-frame.jsonc | 9 +- ...atrix-histogram-no-labels-wide-frame.jsonc | 9 +- ...m-matrix-histogram-partitioned-frame.jsonc | 193 +++++++++--------- ...rix-histogram-partitioned-wide-frame.jsonc | 193 +++++++++--------- .../testdata/prom-matrix-wide-frame.jsonc | 1 + .../prom-matrix-with-nans-frame.jsonc | 5 +- .../prom-matrix-with-nans-wide-frame.jsonc | 1 + .../testdata/prom-scalar-frame.jsonc | 5 +- .../testdata/prom-scalar-wide-frame.jsonc | 5 +- .../testdata/prom-series-frame.jsonc | 1 + .../testdata/prom-series-wide-frame.jsonc | 1 + .../testdata/prom-string-frame.jsonc | 5 +- .../testdata/prom-string-wide-frame.jsonc | 5 +- .../testdata/prom-vector-frame.jsonc | 21 +- ...rom-vector-histogram-no-labels-frame.jsonc | 9 +- ...ector-histogram-no-labels-wide-frame.jsonc | 9 +- .../testdata/prom-vector-wide-frame.jsonc | 1 + .../testdata/prom-warnings-frame.jsonc | 21 +- .../testdata/prom-warnings-wide-frame.jsonc | 1 + 51 files changed, 342 insertions(+), 291 deletions(-) diff --git a/go.mod b/go.mod index 16fb97f6a2f..fc9778ffbe9 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/grafana/cuetsy v0.1.1 github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.3.1 - github.com/grafana/grafana-plugin-sdk-go v0.142.0 + github.com/grafana/grafana-plugin-sdk-go v0.145.0 github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.0.0 @@ -304,7 +304,11 @@ require ( github.com/segmentio/asm v1.1.4 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect + github.com/unknwon/com v1.0.1 // indirect + github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect go.starlark.net v0.0.0-20221020143700-22309ac47eac // indirect + gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect ) require ( diff --git a/go.sum b/go.sum index 91cfa8ec3a9..48560c53758 100644 --- a/go.sum +++ b/go.sum @@ -1342,6 +1342,8 @@ github.com/gophercloud/gophercloud v0.18.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075 github.com/gophercloud/gophercloud v0.20.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4= github.com/gophercloud/gophercloud v0.24.0 h1:jDsIMGJ1KZpAjYfQgGI2coNQj5Q83oPzuiGJRFWgMzw= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU= github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -1376,8 +1378,8 @@ github.com/grafana/grafana-azure-sdk-go v1.3.1/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6 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.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.142.0 h1:fDgA0EmWWy5+/7nX7fdHBfADR6pWuR1TZA5QL36VX7U= -github.com/grafana/grafana-plugin-sdk-go v0.142.0/go.mod h1:srvRQ+de4C5h7FqA5lSFUkFCs5pJolWT+PGV2AyBOFk= +github.com/grafana/grafana-plugin-sdk-go v0.145.0 h1:ZlRxxV3C6RA+wNWeGr+rLVD70pgsZwiLI9etzE0zu+Q= +github.com/grafana/grafana-plugin-sdk-go v0.145.0/go.mod h1:dFof/7GenWBFTmrfcPRCpLau7tgIED0ykzupWAlB0o0= github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293 h1:dJIdfHqu+XjKz+w9zXLqXKPdp6Jjx/UPSOwdeSfWdeQ= github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4= @@ -1665,6 +1667,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -2309,8 +2313,12 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snowflakedb/gosnowflake v1.3.4/go.mod h1:NsRq2QeiMUuoNUJhp5Q6xGC4uBrsS9g6LwZVEkTWgsE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -2435,6 +2443,12 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= +github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= +github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -2979,6 +2993,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/tsdb/loki/frame_test.go b/pkg/tsdb/loki/frame_test.go index e1f57f85a1d..47bbbf7f90a 100644 --- a/pkg/tsdb/loki/frame_test.go +++ b/pkg/tsdb/loki/frame_test.go @@ -95,7 +95,7 @@ func TestAdjustFrame(t *testing.T) { field2.Labels = data.Labels{"app": "Application", "tag2": "tag2"} frame := data.NewFrame("test", field1, field2) - frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMany}) + frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}) query := &lokiQuery{ Expr: "up(ALERTS)", @@ -123,7 +123,7 @@ func TestAdjustFrame(t *testing.T) { field2 := data.NewField("", nil, make([]float64, 0)) frame := data.NewFrame("test", field1, field2) - frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMany}) + frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}) err := adjustFrame(frame, query) require.NoError(t, err) diff --git a/pkg/tsdb/loki/testdata/matrix_gap.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_gap.golden.jsonc index 746d190cd86..42169beb718 100644 --- a/pkg/tsdb/loki/testdata/matrix_gap.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_gap.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {} @@ -28,7 +28,7 @@ "schema": { "name": "{}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_inf.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_inf.golden.jsonc index eda8ef19b83..80da91d0b75 100644 --- a/pkg/tsdb/loki/testdata/matrix_inf.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_inf.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {level="info", location="moon", protocol="http"} @@ -30,7 +30,7 @@ "schema": { "name": "{level=\"info\", location=\"moon\", protocol=\"http\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_name.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_name.golden.jsonc index c5b9961f404..66f4f5db496 100644 --- a/pkg/tsdb/loki/testdata/matrix_name.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_name.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {__name__="moon", level="error"} @@ -23,7 +23,7 @@ "schema": { "name": "{__name__=\"moon\", level=\"error\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_nan.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_nan.golden.jsonc index d3f1dc6a9f0..025b0961691 100644 --- a/pkg/tsdb/loki/testdata/matrix_nan.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_nan.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {} @@ -25,7 +25,7 @@ "schema": { "name": "{}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_simple.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_simple.golden.jsonc index 7f2295b3399..0e37c6ee222 100644 --- a/pkg/tsdb/loki/testdata/matrix_simple.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_simple.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {level="error", location="moon"} @@ -20,7 +20,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {level="info", location="mars"} @@ -46,7 +46,7 @@ "schema": { "name": "{level=\"error\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ @@ -97,7 +97,7 @@ "schema": { "name": "{level=\"info\", location=\"mars\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_small_step.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_small_step.golden.jsonc index 8e19e4e84c7..7699d3aa09e 100644 --- a/pkg/tsdb/loki/testdata/matrix_small_step.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_small_step.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {level="error"} @@ -26,7 +26,7 @@ "schema": { "name": "{level=\"error\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/matrix_with_stats.golden.jsonc b/pkg/tsdb/loki/testdata/matrix_with_stats.golden.jsonc index 48eb212bfc9..8a34e8866ab 100644 --- a/pkg/tsdb/loki/testdata/matrix_with_stats.golden.jsonc +++ b/pkg/tsdb/loki/testdata/matrix_with_stats.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "stats": [ // { // "displayName": "Summary: bytes processed per second", @@ -126,7 +126,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" // } // Name: {level="info", location="mars"} @@ -149,7 +149,7 @@ "schema": { "name": "{level=\"error\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "stats": [ { "displayName": "Summary: bytes processed per second", @@ -304,7 +304,7 @@ "schema": { "name": "{level=\"info\", location=\"mars\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/vector_simple.golden.jsonc b/pkg/tsdb/loki/testdata/vector_simple.golden.jsonc index 300cb9a6457..759e47d5a55 100644 --- a/pkg/tsdb/loki/testdata/vector_simple.golden.jsonc +++ b/pkg/tsdb/loki/testdata/vector_simple.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: query1" // } // Name: {level="error", location="moon"} @@ -17,7 +17,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: query1" // } // Name: {level="info", location="moon"} @@ -39,7 +39,7 @@ "schema": { "name": "{level=\"error\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: query1" }, "fields": [ @@ -81,7 +81,7 @@ "schema": { "name": "{level=\"info\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: query1" }, "fields": [ diff --git a/pkg/tsdb/loki/testdata/vector_special_values.golden.jsonc b/pkg/tsdb/loki/testdata/vector_special_values.golden.jsonc index d362bc4d649..25025839664 100644 --- a/pkg/tsdb/loki/testdata/vector_special_values.golden.jsonc +++ b/pkg/tsdb/loki/testdata/vector_special_values.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: query1" // } // Name: {level="error", location="moon"} @@ -17,7 +17,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: query1" // } // Name: {level="info", location="moon"} @@ -33,7 +33,7 @@ // // // Frame[2] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "executedQueryString": "Expr: query1" // } // Name: {level="debug", location="moon"} @@ -55,7 +55,7 @@ "schema": { "name": "{level=\"error\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: query1" }, "fields": [ @@ -105,7 +105,7 @@ "schema": { "name": "{level=\"info\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: query1" }, "fields": [ @@ -155,7 +155,7 @@ "schema": { "name": "{level=\"debug\", location=\"moon\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "executedQueryString": "Expr: query1" }, "fields": [ diff --git a/pkg/tsdb/prometheus/buffered/time_series_query.go b/pkg/tsdb/prometheus/buffered/time_series_query.go index fa1cdc107ae..a6861ce5a96 100644 --- a/pkg/tsdb/prometheus/buffered/time_series_query.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query.go @@ -688,7 +688,7 @@ func deviation(values []float64) float64 { func newDataFrame(name string, typ string, fields ...*data.Field) *data.Frame { frame := data.NewFrame(name, fields...) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMulti, Custom: map[string]string{ "resultType": typ, // Note: SSE depends on this property and map type }, diff --git a/pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc b/pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc index 658c52ef38e..52e2d2c7be0 100644 --- a/pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc +++ b/pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -35,7 +35,7 @@ "schema": { "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[1m0s])) by (le))", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc index 19e2ade7b27..09ccdfe64d3 100644 --- a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc +++ b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -28,7 +28,7 @@ "schema": { "name": "1 / 0", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc index 9214b554fc8..b6dc63c5d68 100644 --- a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc +++ b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -28,7 +28,7 @@ "schema": { "name": "go_goroutines{job=\"prometheus\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc index b94f094e1e6..44aa5af58df 100644 --- a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc +++ b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -28,7 +28,7 @@ "schema": { "name": "{handler=\"/api/v1/query_range\", job=\"prometheus\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc index 61e74919c29..bb3c642fd05 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -22,7 +22,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // }, @@ -48,7 +48,7 @@ "schema": { "name": "prometheus_http_requests_total{code=\"200\", handler=\"/api/v1/query_range\", job=\"prometheus\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, @@ -102,7 +102,7 @@ "schema": { "name": "prometheus_http_requests_total{code=\"400\", handler=\"/api/v1/query_range\", job=\"prometheus\"}", "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" }, diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 5a51b33f2f1..4b9dda60916 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -330,7 +330,7 @@ func readString(iter *jsoniter.Iterator) *backend.DataResponse { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMulti, Custom: resultTypeToCustomMeta("string"), } @@ -354,7 +354,7 @@ func readScalar(iter *jsoniter.Iterator) *backend.DataResponse { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMulti, Custom: resultTypeToCustomMeta("scalar"), } @@ -544,7 +544,7 @@ func readMatrixOrVectorMulti(iter *jsoniter.Iterator, resultType string) *backen } else { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMulti, Custom: resultTypeToCustomMeta(resultType), } rsp.Frames = append(rsp.Frames, frame) diff --git a/pkg/util/converter/testdata/loki-streams-a-frame.jsonc b/pkg/util/converter/testdata/loki-streams-a-frame.jsonc index ea7680895e7..06559dd9366 100644 --- a/pkg/util/converter/testdata/loki-streams-a-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-a-frame.jsonc @@ -54,6 +54,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/loki-streams-a-wide-frame.jsonc b/pkg/util/converter/testdata/loki-streams-a-wide-frame.jsonc index ea7680895e7..06559dd9366 100644 --- a/pkg/util/converter/testdata/loki-streams-a-wide-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-a-wide-frame.jsonc @@ -54,6 +54,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/loki-streams-b-frame.jsonc b/pkg/util/converter/testdata/loki-streams-b-frame.jsonc index aba2b7ad6dc..fa04ae09377 100644 --- a/pkg/util/converter/testdata/loki-streams-b-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-b-frame.jsonc @@ -53,6 +53,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/loki-streams-b-wide-frame.jsonc b/pkg/util/converter/testdata/loki-streams-b-wide-frame.jsonc index aba2b7ad6dc..fa04ae09377 100644 --- a/pkg/util/converter/testdata/loki-streams-b-wide-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-b-wide-frame.jsonc @@ -53,6 +53,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/loki-streams-c-frame.jsonc b/pkg/util/converter/testdata/loki-streams-c-frame.jsonc index 89b7386f7f9..45c917f3e6f 100644 --- a/pkg/util/converter/testdata/loki-streams-c-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-c-frame.jsonc @@ -15,6 +15,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/loki-streams-c-wide-frame.jsonc b/pkg/util/converter/testdata/loki-streams-c-wide-frame.jsonc index 89b7386f7f9..45c917f3e6f 100644 --- a/pkg/util/converter/testdata/loki-streams-c-wide-frame.jsonc +++ b/pkg/util/converter/testdata/loki-streams-c-wide-frame.jsonc @@ -15,6 +15,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-exemplars-a-frame.jsonc b/pkg/util/converter/testdata/prom-exemplars-a-frame.jsonc index d1c18e4d4d4..5ecc472e7f0 100644 --- a/pkg/util/converter/testdata/prom-exemplars-a-frame.jsonc +++ b/pkg/util/converter/testdata/prom-exemplars-a-frame.jsonc @@ -36,6 +36,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-exemplars-a-wide-frame.jsonc b/pkg/util/converter/testdata/prom-exemplars-a-wide-frame.jsonc index d1c18e4d4d4..5ecc472e7f0 100644 --- a/pkg/util/converter/testdata/prom-exemplars-a-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-exemplars-a-wide-frame.jsonc @@ -36,6 +36,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-exemplars-b-frame.jsonc b/pkg/util/converter/testdata/prom-exemplars-b-frame.jsonc index 2bf2b6e271e..25369baebe5 100644 --- a/pkg/util/converter/testdata/prom-exemplars-b-frame.jsonc +++ b/pkg/util/converter/testdata/prom-exemplars-b-frame.jsonc @@ -3047,6 +3047,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-exemplars-b-wide-frame.jsonc b/pkg/util/converter/testdata/prom-exemplars-b-wide-frame.jsonc index 2bf2b6e271e..25369baebe5 100644 --- a/pkg/util/converter/testdata/prom-exemplars-b-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-exemplars-b-wide-frame.jsonc @@ -3047,6 +3047,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-labels-frame.jsonc b/pkg/util/converter/testdata/prom-labels-frame.jsonc index 099022f1908..80dea07f3bb 100644 --- a/pkg/util/converter/testdata/prom-labels-frame.jsonc +++ b/pkg/util/converter/testdata/prom-labels-frame.jsonc @@ -23,6 +23,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-labels-wide-frame.jsonc b/pkg/util/converter/testdata/prom-labels-wide-frame.jsonc index 099022f1908..80dea07f3bb 100644 --- a/pkg/util/converter/testdata/prom-labels-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-labels-wide-frame.jsonc @@ -23,6 +23,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-frame.jsonc index dc91656053b..34962e54703 100644 --- a/pkg/util/converter/testdata/prom-matrix-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // } @@ -21,7 +21,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // } @@ -41,11 +41,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" } @@ -90,7 +91,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" } diff --git a/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc index a1ffaf6edb9..f02ca0ce646 100644 --- a/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 932 Rows // +-------------------------------+------------------------+------------------------+--------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -21,10 +21,11 @@ // | 2022-04-14 19:08:20 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.712286008031362 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------+------------------------+--------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-wide-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-wide-frame.jsonc index a1ffaf6edb9..f02ca0ce646 100644 --- a/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-histogram-no-labels-wide-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 932 Rows // +-------------------------------+------------------------+------------------------+--------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -21,10 +21,11 @@ // | 2022-04-14 19:08:20 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.712286008031362 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------+------------------------+--------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc index dcd6c49b2c1..49c6a3829a6 100644 --- a/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -11,13 +11,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[1] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -25,13 +25,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[2] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -39,13 +39,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[3] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -53,13 +53,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[4] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -67,13 +67,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[5] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -81,13 +81,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[6] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -95,13 +95,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[7] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -109,13 +109,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[8] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 1 Rows // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -124,13 +124,13 @@ // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ // | 2022-04-14 19:08:20 +0000 UTC | 0.05255602595335715 | 0.057312752700291944 | 0.003508784241348215 | 0 | // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[9] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -138,13 +138,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[10] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 426 Rows // +-------------------------------+------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -162,13 +162,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.002762135864009951 | 0.0030121305183748843 | 0.30175469375429953 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[11] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 1 Rows // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -177,13 +177,13 @@ // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ // | 2022-04-14 19:08:20 +0000 UTC | 1 | 1.0905077326652577 | 0.0035087596183873042 | 0 | // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[12] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 6 Rows // +-------------------------------+-----------------------------------------------------------------------------------+--------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -197,13 +197,13 @@ // | 2022-04-14 19:13:20 +0000 UTC | 0.8408964152537144 | 0.9170040432046711 | 0.003508771929824561 | 0 | // | 2022-04-14 19:13:20 +0000 UTC | 1 | 1.0905077326652577 | 0.003508771929824561 | 0 | // +-------------------------------+-----------------------------------------------------------------------------------+--------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[13] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 269 Rows // +-------------------------------+---------------------------------------------------------------------------+------------------------+--------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -221,13 +221,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.533338799658994 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+---------------------------------------------------------------------------+------------------------+--------------------+---------------+ -// -// -// +// +// +// // Frame[14] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 303 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -245,13 +245,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.005065779510355506 | 0.005524271728019902 | 0.06666662973235489 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[15] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 56 Rows // +-------------------------------+------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -269,13 +269,13 @@ // | 2022-04-14 19:10:00 +0000 UTC | 0.005524271728019902 | 0.0060242610367497685 | 0.00713925 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[16] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 41 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -293,13 +293,13 @@ // | 2022-04-14 19:10:00 +0000 UTC | 0.0060242610367497685 | 0.006569503244169644 | 0.0035300277777777778 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[17] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 29 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -317,13 +317,13 @@ // | 2022-04-14 19:11:40 +0000 UTC | 0.04052623608284405 | 0.044194173824159216 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[18] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 38 Rows // +-------------------------------+------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -341,13 +341,13 @@ // | 2022-04-14 19:11:40 +0000 UTC | 0.0060242610367497685 | 0.006569503244169644 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[19] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 195 Rows // +-------------------------------+---------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -365,13 +365,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.0021298979153618314 | 0.0023226701464896895 | 0.07368416128056675 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+---------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[20] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 261 Rows // +-------------------------------+-------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -389,13 +389,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.0009765625 | 0.0010649489576809157 | 0.03508765774105932 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[21] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 176 Rows // +-------------------------------+-------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -413,13 +413,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.009290680585958758 | 0.010131559020711013 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[22] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 255 Rows // +-------------------------------+-------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -437,13 +437,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.04052623608284405 | 0.044194173824159216 | 0 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[23] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 167 Rows // +-------------------------------+------------------------------------------------------------------------------------+------------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -461,10 +461,11 @@ // | 2022-04-14 19:08:20 +0000 UTC | 1.1766134837401892e-05 | 1.2831061023768835e-05 | 0.1578945767934209 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------+------------------------+-----------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-wide-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-wide-frame.jsonc index dcd6c49b2c1..49c6a3829a6 100644 --- a/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-histogram-partitioned-wide-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -11,13 +11,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[1] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -25,13 +25,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[2] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -39,13 +39,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[3] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -53,13 +53,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[4] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -67,13 +67,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+-----------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[5] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -81,13 +81,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[6] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -95,13 +95,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+------------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[7] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -109,13 +109,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[8] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 1 Rows // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -124,13 +124,13 @@ // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ // | 2022-04-14 19:08:20 +0000 UTC | 0.05255602595335715 | 0.057312752700291944 | 0.003508784241348215 | 0 | // +-------------------------------+--------------------------------------------------------------------------------------------+----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[9] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 0 Rows // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -138,13 +138,13 @@ // | Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []int8 | // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ // +-------------------+---------------------------------------------------------------------------------------------+-----------------+-----------------+---------------+ -// -// -// +// +// +// // Frame[10] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 426 Rows // +-------------------------------+------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -162,13 +162,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.002762135864009951 | 0.0030121305183748843 | 0.30175469375429953 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[11] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 1 Rows // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -177,13 +177,13 @@ // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ // | 2022-04-14 19:08:20 +0000 UTC | 1 | 1.0905077326652577 | 0.0035087596183873042 | 0 | // +-------------------------------+-------------------------------------------------------------------------------------+--------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[12] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 6 Rows // +-------------------------------+-----------------------------------------------------------------------------------+--------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -197,13 +197,13 @@ // | 2022-04-14 19:13:20 +0000 UTC | 0.8408964152537144 | 0.9170040432046711 | 0.003508771929824561 | 0 | // | 2022-04-14 19:13:20 +0000 UTC | 1 | 1.0905077326652577 | 0.003508771929824561 | 0 | // +-------------------------------+-----------------------------------------------------------------------------------+--------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[13] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 269 Rows // +-------------------------------+---------------------------------------------------------------------------+------------------------+--------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -221,13 +221,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.533338799658994 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+---------------------------------------------------------------------------+------------------------+--------------------+---------------+ -// -// -// +// +// +// // Frame[14] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 303 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -245,13 +245,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.005065779510355506 | 0.005524271728019902 | 0.06666662973235489 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[15] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 56 Rows // +-------------------------------+------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -269,13 +269,13 @@ // | 2022-04-14 19:10:00 +0000 UTC | 0.005524271728019902 | 0.0060242610367497685 | 0.00713925 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[16] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 41 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -293,13 +293,13 @@ // | 2022-04-14 19:10:00 +0000 UTC | 0.0060242610367497685 | 0.006569503244169644 | 0.0035300277777777778 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[17] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 29 Rows // +-------------------------------+-----------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -317,13 +317,13 @@ // | 2022-04-14 19:11:40 +0000 UTC | 0.04052623608284405 | 0.044194173824159216 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-----------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[18] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 38 Rows // +-------------------------------+------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -341,13 +341,13 @@ // | 2022-04-14 19:11:40 +0000 UTC | 0.0060242610367497685 | 0.006569503244169644 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------------------------+----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[19] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 195 Rows // +-------------------------------+---------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -365,13 +365,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.0021298979153618314 | 0.0023226701464896895 | 0.07368416128056675 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+---------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[20] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 261 Rows // +-------------------------------+-------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -389,13 +389,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.0009765625 | 0.0010649489576809157 | 0.03508765774105932 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[21] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 176 Rows // +-------------------------------+-------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -413,13 +413,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.009290680585958758 | 0.010131559020711013 | 0.003508771929824561 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------------------+-----------------------+-----------------------+---------------+ -// -// -// +// +// +// // Frame[22] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 255 Rows // +-------------------------------+-------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -437,13 +437,13 @@ // | 2022-04-14 19:08:20 +0000 UTC | 0.04052623608284405 | 0.044194173824159216 | 0 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+-------------------------------------------------------------------------------------------------------------+-----------------------+----------------------+---------------+ -// -// -// +// +// +// // Frame[23] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 167 Rows // +-------------------------------+------------------------------------------------------------------------------------+------------------------+-----------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -461,10 +461,11 @@ // | 2022-04-14 19:08:20 +0000 UTC | 1.1766134837401892e-05 | 1.2831061023768835e-05 | 0.1578945767934209 | 0 | // | ... | ... | ... | ... | ... | // +-------------------------------+------------------------------------------------------------------------------------+------------------------+-----------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-wide-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-wide-frame.jsonc index 5ef32b61126..affb83ab5b2 100644 --- a/pkg/util/converter/testdata/prom-matrix-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-wide-frame.jsonc @@ -21,6 +21,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-matrix-with-nans-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-with-nans-frame.jsonc index 2daafd89b11..f527d47df0d 100644 --- a/pkg/util/converter/testdata/prom-matrix-with-nans-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-with-nans-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "matrix" // } @@ -21,11 +21,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "matrix" } diff --git a/pkg/util/converter/testdata/prom-matrix-with-nans-wide-frame.jsonc b/pkg/util/converter/testdata/prom-matrix-with-nans-wide-frame.jsonc index 8bb09734b4b..c53e80c0c73 100644 --- a/pkg/util/converter/testdata/prom-matrix-with-nans-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-matrix-with-nans-wide-frame.jsonc @@ -21,6 +21,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-scalar-frame.jsonc b/pkg/util/converter/testdata/prom-scalar-frame.jsonc index 898d6f03e73..bd0d2cacdd5 100644 --- a/pkg/util/converter/testdata/prom-scalar-frame.jsonc +++ b/pkg/util/converter/testdata/prom-scalar-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "scalar" // } @@ -19,11 +19,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "scalar" } diff --git a/pkg/util/converter/testdata/prom-scalar-wide-frame.jsonc b/pkg/util/converter/testdata/prom-scalar-wide-frame.jsonc index 898d6f03e73..bd0d2cacdd5 100644 --- a/pkg/util/converter/testdata/prom-scalar-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-scalar-wide-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "scalar" // } @@ -19,11 +19,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "scalar" } diff --git a/pkg/util/converter/testdata/prom-series-frame.jsonc b/pkg/util/converter/testdata/prom-series-frame.jsonc index 49e103ead0d..295b59823ed 100644 --- a/pkg/util/converter/testdata/prom-series-frame.jsonc +++ b/pkg/util/converter/testdata/prom-series-frame.jsonc @@ -16,6 +16,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-series-wide-frame.jsonc b/pkg/util/converter/testdata/prom-series-wide-frame.jsonc index 49e103ead0d..295b59823ed 100644 --- a/pkg/util/converter/testdata/prom-series-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-series-wide-frame.jsonc @@ -16,6 +16,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-string-frame.jsonc b/pkg/util/converter/testdata/prom-string-frame.jsonc index 33b3daf5d9e..6307e2916bc 100644 --- a/pkg/util/converter/testdata/prom-string-frame.jsonc +++ b/pkg/util/converter/testdata/prom-string-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "string" // } @@ -19,11 +19,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "string" } diff --git a/pkg/util/converter/testdata/prom-string-wide-frame.jsonc b/pkg/util/converter/testdata/prom-string-wide-frame.jsonc index 33b3daf5d9e..6307e2916bc 100644 --- a/pkg/util/converter/testdata/prom-string-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-string-wide-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "string" // } @@ -19,11 +19,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "string" } diff --git a/pkg/util/converter/testdata/prom-vector-frame.jsonc b/pkg/util/converter/testdata/prom-vector-frame.jsonc index 587cba760ac..cdd2532fa5f 100644 --- a/pkg/util/converter/testdata/prom-vector-frame.jsonc +++ b/pkg/util/converter/testdata/prom-vector-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // } @@ -19,7 +19,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // } @@ -37,7 +37,7 @@ // // // Frame[2] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // } @@ -55,7 +55,7 @@ // // // Frame[3] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // } @@ -73,7 +73,7 @@ // // // Frame[4] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // } @@ -91,11 +91,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" } @@ -136,7 +137,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" } @@ -177,7 +178,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" } @@ -225,7 +226,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" } @@ -273,7 +274,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" } diff --git a/pkg/util/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc b/pkg/util/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc index e6f3f1ec5bf..21f3e31e35e 100644 --- a/pkg/util/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc +++ b/pkg/util/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 134 Rows // +-----------------------------------+------------------------+------------------------+---------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -21,10 +21,11 @@ // | 2022-04-14 20:21:08.042 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.392982456140351 | 0 | // | ... | ... | ... | ... | ... | // +-----------------------------------+------------------------+------------------------+---------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-vector-histogram-no-labels-wide-frame.jsonc b/pkg/util/converter/testdata/prom-vector-histogram-no-labels-wide-frame.jsonc index e6f3f1ec5bf..21f3e31e35e 100644 --- a/pkg/util/converter/testdata/prom-vector-histogram-no-labels-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-vector-histogram-no-labels-wide-frame.jsonc @@ -1,9 +1,9 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "heatmap-cells" // } -// Name: +// Name: // Dimensions: 5 Fields by 134 Rows // +-----------------------------------+------------------------+------------------------+---------------------+---------------+ // | Name: xMax | Name: yMin | Name: yMax | Name: count | Name: yLayout | @@ -21,10 +21,11 @@ // | 2022-04-14 20:21:08.042 +0000 UTC | 9.07293025972535e-06 | 9.894100606163098e-06 | 3.392982456140351 | 0 | // | ... | ... | ... | ... | ... | // +-----------------------------------+------------------------+------------------------+---------------------+---------------+ -// -// +// +// // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-vector-wide-frame.jsonc b/pkg/util/converter/testdata/prom-vector-wide-frame.jsonc index de2405c1fab..e6cd4985d55 100644 --- a/pkg/util/converter/testdata/prom-vector-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-vector-wide-frame.jsonc @@ -20,6 +20,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { diff --git a/pkg/util/converter/testdata/prom-warnings-frame.jsonc b/pkg/util/converter/testdata/prom-warnings-frame.jsonc index 26f89f1ac1a..6f88319e60b 100644 --- a/pkg/util/converter/testdata/prom-warnings-frame.jsonc +++ b/pkg/util/converter/testdata/prom-warnings-frame.jsonc @@ -1,7 +1,7 @@ // 🌟 This was machine generated. Do not edit. 🌟 // // Frame[0] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // }, @@ -29,7 +29,7 @@ // // // Frame[1] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // }, @@ -57,7 +57,7 @@ // // // Frame[2] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // }, @@ -85,7 +85,7 @@ // // // Frame[3] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // }, @@ -113,7 +113,7 @@ // // // Frame[4] { -// "type": "timeseries-many", +// "type": "timeseries-multi", // "custom": { // "resultType": "vector" // }, @@ -141,11 +141,12 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" }, @@ -196,7 +197,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" }, @@ -247,7 +248,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" }, @@ -305,7 +306,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" }, @@ -363,7 +364,7 @@ { "schema": { "meta": { - "type": "timeseries-many", + "type": "timeseries-multi", "custom": { "resultType": "vector" }, diff --git a/pkg/util/converter/testdata/prom-warnings-wide-frame.jsonc b/pkg/util/converter/testdata/prom-warnings-wide-frame.jsonc index c4b1c7aa96f..36fac2a7969 100644 --- a/pkg/util/converter/testdata/prom-warnings-wide-frame.jsonc +++ b/pkg/util/converter/testdata/prom-warnings-wide-frame.jsonc @@ -30,6 +30,7 @@ // // 🌟 This was machine generated. Do not edit. 🌟 { + "status": 200, "frames": [ { "schema": { From 46143b076472e5d6ae2a39dbdb199c1ea8559cf4 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Mon, 5 Dec 2022 14:48:47 -0500 Subject: [PATCH 13/94] kindsys: Change "Meta" to "Properties" (#59852) --- kinds/gen.go | 28 ++++---- pkg/codegen/generators.go | 2 +- pkg/codegen/jenny_corestructkind.go | 4 +- pkg/codegen/jenny_eachmajor.go | 4 +- pkg/codegen/jenny_rawkind.go | 4 +- pkg/codegen/jenny_tsveneerindex.go | 12 ++-- pkg/codegen/latest_jenny.go | 4 +- pkg/codegen/tmpl/kind_corestructured.tmpl | 43 +++++++----- pkg/codegen/tmpl/kind_raw.tmpl | 25 ++++--- pkg/codegen/tmpl/kind_registry.tmpl | 18 ++--- pkg/kinds/dashboard/dashboard_kind_gen.go | 21 ++++-- pkg/kinds/playlist/playlist_kind_gen.go | 21 ++++-- pkg/kinds/svg/svg_kind_gen.go | 19 +++-- pkg/kinds/team/team_kind_gen.go | 21 ++++-- pkg/kindsys/kind.go | 24 +++++-- pkg/kindsys/kindmetas.go | 85 +++++++++++++---------- pkg/kindsys/load.go | 60 ++++++++-------- 17 files changed, 237 insertions(+), 158 deletions(-) diff --git a/kinds/gen.go b/kinds/gen.go index 71ff849e8da..8a16ebc3858 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -30,7 +30,7 @@ func main() { // Core kinds composite code generator. Produces all generated code in // grafana/grafana that derives from raw and structured core kinds. coreKindsGen := codejen.JennyListWithNamer(func(decl *codegen.DeclForGen) string { - return decl.Meta.Common().MachineName + return decl.Properties.Common().MachineName }) // All the jennies that comprise the core kinds generator pipeline @@ -63,12 +63,12 @@ func main() { continue } rel := filepath.Join(kindsys.CoreStructuredDeclParentPath, ent.Name()) - decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rel, rt.Context(), nil) if err != nil { die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil))) } - if decl.Meta.MachineName != ent.Name() { - die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name())) + if decl.Properties.MachineName != ent.Name() { + die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Properties.Name, ent.Name())) } all = append(all, elsedie(codegen.ForGen(rt, decl.Some()))(rel)) @@ -82,19 +82,19 @@ func main() { continue } rel := filepath.Join(kindsys.RawDeclParentPath, ent.Name()) - decl, err := kindsys.LoadCoreKind[kindsys.RawMeta](rel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.RawProperties](rel, rt.Context(), nil) if err != nil { die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil))) } - if decl.Meta.MachineName != ent.Name() { - die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name())) + if decl.Properties.MachineName != ent.Name() { + die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Properties.Name, ent.Name())) } dfg, _ := codegen.ForGen(nil, decl.Some()) all = append(all, dfg) } sort.Slice(all, func(i, j int) bool { - return nameFor(all[i].Meta) < nameFor(all[j].Meta) + return nameFor(all[i].Properties) < nameFor(all[j].Properties) }) jfs, err := coreKindsGen.GenerateFS(all...) @@ -111,18 +111,18 @@ func main() { } } -func nameFor(m kindsys.SomeKindMeta) string { +func nameFor(m kindsys.SomeKindProperties) string { switch x := m.(type) { - case kindsys.RawMeta: + case kindsys.RawProperties: return x.Name - case kindsys.CoreStructuredMeta: + case kindsys.CoreStructuredProperties: return x.Name - case kindsys.CustomStructuredMeta: + case kindsys.CustomStructuredProperties: return x.Name - case kindsys.ComposableMeta: + case kindsys.ComposableProperties: return x.Name default: - // unreachable so long as all the possibilities in KindMetas have switch branches + // unreachable so long as all the possibilities in KindProperties have switch branches panic("unreachable") } } diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go index 1a56f71a5b4..1cf3079927f 100644 --- a/pkg/codegen/generators.go +++ b/pkg/codegen/generators.go @@ -67,7 +67,7 @@ func (decl *DeclForGen) Lineage() thema.Lineage { // ForLatestSchema returns a [SchemaForGen] for the latest schema in this // DeclForGen's lineage. func (decl *DeclForGen) ForLatestSchema() SchemaForGen { - comm := decl.Meta.Common() + comm := decl.Properties.Common() return SchemaForGen{ Name: comm.Name, Schema: decl.Lineage().Latest(), diff --git a/pkg/codegen/jenny_corestructkind.go b/pkg/codegen/jenny_corestructkind.go index 0d1fad63e17..98b0d318258 100644 --- a/pkg/codegen/jenny_corestructkind.go +++ b/pkg/codegen/jenny_corestructkind.go @@ -21,7 +21,7 @@ func CoreStructuredKindJenny(gokindsdir string, cfg *CoreStructuredKindGenerator } if cfg.GenDirName == nil { cfg.GenDirName = func(decl *DeclForGen) string { - return decl.Meta.Common().MachineName + return decl.Properties.Common().MachineName } } @@ -54,7 +54,7 @@ func (gen *genCoreStructuredKind) Generate(decl *DeclForGen) (*codejen.File, err return nil, nil } - path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go") + path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Properties.Common().MachineName+"_kind_gen.go") buf := new(bytes.Buffer) if err := tmpls.Lookup("kind_corestructured.tmpl").Execute(buf, decl); err != nil { return nil, fmt.Errorf("failed executing kind_corestructured template for %s: %w", path, err) diff --git a/pkg/codegen/jenny_eachmajor.go b/pkg/codegen/jenny_eachmajor.go index 766fcad5bf0..dd95e186ad6 100644 --- a/pkg/codegen/jenny_eachmajor.go +++ b/pkg/codegen/jenny_eachmajor.go @@ -33,7 +33,7 @@ func (j *lmox) Generate(decl *DeclForGen) (codejen.Files, error) { if decl.IsRaw() { return nil, nil } - comm := decl.Meta.Common() + comm := decl.Properties.Common() sfg := SchemaForGen{ Name: comm.Name, IsGroup: comm.LineageIsGroup, @@ -42,7 +42,7 @@ func (j *lmox) Generate(decl *DeclForGen) (codejen.Files, error) { do := func(sfg SchemaForGen, infix string) (codejen.Files, error) { f, err := j.inner.Generate(sfg) if err != nil { - return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Meta.Common().Name, err) + return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Properties.Common().Name, err) } if f == nil || !f.Exists() { return nil, nil diff --git a/pkg/codegen/jenny_rawkind.go b/pkg/codegen/jenny_rawkind.go index 45c1675a01b..b8e967c951f 100644 --- a/pkg/codegen/jenny_rawkind.go +++ b/pkg/codegen/jenny_rawkind.go @@ -21,7 +21,7 @@ func RawKindJenny(gokindsdir string, cfg *RawKindGeneratorConfig) OneToOne { } if cfg.GenDirName == nil { cfg.GenDirName = func(decl *DeclForGen) string { - return decl.Meta.Common().MachineName + return decl.Properties.Common().MachineName } } @@ -51,7 +51,7 @@ func (gen *genRawKind) Generate(decl *DeclForGen) (*codejen.File, error) { return nil, nil } - path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go") + path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Properties.Common().MachineName+"_kind_gen.go") buf := new(bytes.Buffer) if err := tmpls.Lookup("kind_raw.tmpl").Execute(buf, decl); err != nil { return nil, fmt.Errorf("failed executing kind_raw template for %s: %w", path, err) diff --git a/pkg/codegen/jenny_tsveneerindex.go b/pkg/codegen/jenny_tsveneerindex.go index 1d5b11dea2c..cfe70117ca5 100644 --- a/pkg/codegen/jenny_tsveneerindex.go +++ b/pkg/codegen/jenny_tsveneerindex.go @@ -48,15 +48,15 @@ func (gen *genTSVeneerIndex) Generate(decls ...*DeclForGen) (*codejen.File, erro sch := decl.Lineage().Latest() f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{ - RootName: decl.Meta.Common().Name, - Group: decl.Meta.Common().LineageIsGroup, + RootName: decl.Properties.Common().Name, + Group: decl.Properties.Common().LineageIsGroup, }) if err != nil { - return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err) + return nil, fmt.Errorf("%s: %w", decl.Properties.Common().Name, err) } elems, err := gen.extractTSIndexVeneerElements(decl, f) if err != nil { - return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err) + return nil, fmt.Errorf("%s: %w", decl.Properties.Common().Name, err) } tsf.Nodes = append(tsf.Nodes, elems...) } @@ -66,7 +66,7 @@ func (gen *genTSVeneerIndex) Generate(decls ...*DeclForGen) (*codejen.File, erro func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf *ast.File) ([]ast.Decl, error) { lin := decl.Lineage() - comm := decl.Meta.Common() + comm := decl.Properties.Common() // Check the root, then walk the tree rootv := lin.Latest().Underlying() @@ -131,7 +131,7 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf * } vpath := fmt.Sprintf("v%v", thema.LatestVersion(lin)[0]) - if decl.Meta.Common().Maturity.Less(kindsys.MaturityStable) { + if decl.Properties.Common().Maturity.Less(kindsys.MaturityStable) { vpath = "x" } diff --git a/pkg/codegen/latest_jenny.go b/pkg/codegen/latest_jenny.go index 5f24052a46a..327ef398663 100644 --- a/pkg/codegen/latest_jenny.go +++ b/pkg/codegen/latest_jenny.go @@ -35,7 +35,7 @@ func (j *latestj) Generate(decl *DeclForGen) (*codejen.File, error) { if decl.IsRaw() { return nil, nil } - comm := decl.Meta.Common() + comm := decl.Properties.Common() sfg := SchemaForGen{ Name: comm.Name, Schema: decl.Lineage().Latest(), @@ -44,7 +44,7 @@ func (j *latestj) Generate(decl *DeclForGen) (*codejen.File, error) { f, err := j.inner.Generate(sfg) if err != nil { - return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Meta.Common().Name, err) + return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), decl.Properties.Common().Name, err) } if f == nil || !f.Exists() { return nil, nil diff --git a/pkg/codegen/tmpl/kind_corestructured.tmpl b/pkg/codegen/tmpl/kind_corestructured.tmpl index a02a686b687..6e6755224ea 100644 --- a/pkg/codegen/tmpl/kind_corestructured.tmpl +++ b/pkg/codegen/tmpl/kind_corestructured.tmpl @@ -1,4 +1,4 @@ -package {{ .Meta.MachineName }} +package {{ .Properties.MachineName }} import ( "github.com/grafana/grafana/pkg/kindsys" @@ -10,14 +10,14 @@ import ( // directory containing the .cue files in which this kind is declared. Necessary // for runtime errors related to the declaration and/or lineage to provide // a real path to the correct .cue file. -const rootrel string = "kinds/structured/{{ .Meta.MachineName }}" +const rootrel string = "kinds/structured/{{ .Properties.MachineName }}" // TODO standard generated docs type Kind struct { - lin thema.ConvergentLineage[*{{ .Meta.Name }}] + lin thema.ConvergentLineage[*{{ .Properties.Name }}] jcodec vmux.Codec - valmux vmux.ValueMux[*{{ .Meta.Name }}] - decl kindsys.Decl[kindsys.CoreStructuredMeta] + valmux vmux.ValueMux[*{{ .Properties.Name }}] + decl kindsys.Decl[kindsys.CoreStructuredProperties] } // type guard @@ -25,7 +25,7 @@ var _ kindsys.Structured = &Kind{} // TODO standard generated docs func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil) if err != nil { return nil, err } @@ -40,14 +40,14 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { // Get the thema.Schema that the meta says is in the current version (which // codegen ensures is always the latest) - cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) - tsch, err := thema.BindType[*{{ .Meta.Name }}](cursch, &{{ .Meta.Name }}{}) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) + tsch, err := thema.BindType[*{{ .Properties.Name }}](cursch, &{{ .Properties.Name }}{}) if err != nil { // Should be unreachable, modulo bugs in the Thema->Go code generator return nil, err } - k.jcodec = vmux.NewJSONCodec("{{ .Meta.MachineName }}.json") + k.jcodec = vmux.NewJSONCodec("{{ .Properties.MachineName }}.json") k.lin = tsch.ConvergentLineage() k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) return k, nil @@ -55,12 +55,12 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { // TODO standard generated docs func (k *Kind) Name() string { - return "{{ .Meta.MachineName }}" + return "{{ .Properties.MachineName }}" } // TODO standard generated docs func (k *Kind) MachineName() string { - return "{{ .Meta.MachineName }}" + return "{{ .Properties.MachineName }}" } // TODO standard generated docs @@ -69,28 +69,37 @@ func (k *Kind) Lineage() thema.Lineage { } // TODO standard generated docs -func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Meta.Name }}] { +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Properties.Name }}] { return k.lin } // JSONValueMux is a version multiplexer that maps a []byte containing JSON data -// at any schematized dashboard version to an instance of {{ .Meta.Name }}. +// at any schematized dashboard version to an instance of {{ .Properties.Name }}. // // Validation and translation errors emitted from this func will identify the // input bytes as "dashboard.json". // // This is a thin wrapper around Thema's [vmux.ValueMux]. -func (k *Kind) JSONValueMux(b []byte) (*{{ .Meta.Name }}, thema.TranslationLacunas, error) { +func (k *Kind) JSONValueMux(b []byte) (*{{ .Properties.Name }}, thema.TranslationLacunas, error) { return k.valmux(b) } // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// {{ .Properties.MachineName }} declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties], +// representing the static properties declared in the {{ .Properties.MachineName }} kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/codegen/tmpl/kind_raw.tmpl b/pkg/codegen/tmpl/kind_raw.tmpl index a38070c67e3..52380de4500 100644 --- a/pkg/codegen/tmpl/kind_raw.tmpl +++ b/pkg/codegen/tmpl/kind_raw.tmpl @@ -1,4 +1,4 @@ -package {{ .Meta.MachineName }} +package {{ .Properties.MachineName }} import ( "github.com/grafana/grafana/pkg/kindsys" @@ -8,7 +8,7 @@ import ( // TODO standard generated docs type Kind struct { - decl kindsys.Decl[kindsys.RawMeta] + decl kindsys.Decl[kindsys.RawProperties] } // type guard @@ -16,7 +16,7 @@ var _ kindsys.Raw = &Kind{} // TODO standard generated docs func NewKind() (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/{{ .Meta.MachineName }}", nil, nil) + decl, err := kindsys.LoadCoreKind[kindsys.RawProperties]("kinds/raw/{{ .Properties.MachineName }}", nil, nil) if err != nil { return nil, err } @@ -28,21 +28,30 @@ func NewKind() (*Kind, error) { // TODO standard generated docs func (k *Kind) Name() string { - return "{{ .Meta.Name }}" + return "{{ .Properties.Name }}" } // TODO standard generated docs func (k *Kind) MachineName() string { - return "{{ .Meta.MachineName }}" + return "{{ .Properties.MachineName }}" } // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.RawMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// {{ .Properties.MachineName }} declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.RawProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.RawProperties], +// representing the static properties declared in the {{ .Properties.MachineName }} kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/codegen/tmpl/kind_registry.tmpl b/pkg/codegen/tmpl/kind_registry.tmpl index e323903a9f9..dc21571c0ea 100644 --- a/pkg/codegen/tmpl/kind_registry.tmpl +++ b/pkg/codegen/tmpl/kind_registry.tmpl @@ -5,7 +5,7 @@ import ( "sync" {{range .Kinds }} - "{{ $.KindPackagePrefix }}/{{ .Meta.MachineName }}"{{end}} + "{{ $.KindPackagePrefix }}/{{ .Properties.MachineName }}"{{end}} "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/kindsys" "github.com/grafana/thema" @@ -25,19 +25,19 @@ type Base struct { all []kindsys.Interface numRaw, numStructured int {{- range .Kinds }} - {{ .Meta.MachineName }} *{{ .Meta.MachineName }}.Kind{{end}} + {{ .Properties.MachineName }} *{{ .Properties.MachineName }}.Kind{{end}} } // type guards var ( {{- range .Kinds }} - _ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Meta.MachineName }}.Kind{}{{end}} + _ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Properties.MachineName }}.Kind{}{{end}} ) {{range .Kinds }} -// {{ .Meta.Name }} returns the [kindsys.Interface] implementation for the {{ .Meta.MachineName }} kind. -func (b *Base) {{ .Meta.Name }}() *{{ .Meta.MachineName }}.Kind { - return b.{{ .Meta.MachineName }} +// {{ .Properties.Name }} returns the [kindsys.Interface] implementation for the {{ .Properties.MachineName }} kind. +func (b *Base) {{ .Properties.Name }}() *{{ .Properties.MachineName }}.Kind { + return b.{{ .Properties.MachineName }} } {{end}} @@ -49,11 +49,11 @@ func doNewBase(rt *thema.Runtime) *Base { } {{range .Kinds }} - reg.{{ .Meta.MachineName }}, err = {{ .Meta.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }}) + reg.{{ .Properties.MachineName }}, err = {{ .Properties.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }}) if err != nil { - panic(fmt.Sprintf("error while initializing the {{ .Meta.MachineName }} Kind: %s", err)) + panic(fmt.Sprintf("error while initializing the {{ .Properties.MachineName }} Kind: %s", err)) } - reg.all = append(reg.all, reg.{{ .Meta.MachineName }}) + reg.all = append(reg.all, reg.{{ .Properties.MachineName }}) {{end}} return reg diff --git a/pkg/kinds/dashboard/dashboard_kind_gen.go b/pkg/kinds/dashboard/dashboard_kind_gen.go index 71c47986121..f99cd07950c 100644 --- a/pkg/kinds/dashboard/dashboard_kind_gen.go +++ b/pkg/kinds/dashboard/dashboard_kind_gen.go @@ -26,7 +26,7 @@ type Kind struct { lin thema.ConvergentLineage[*Dashboard] jcodec vmux.Codec valmux vmux.ValueMux[*Dashboard] - decl kindsys.Decl[kindsys.CoreStructuredMeta] + decl kindsys.Decl[kindsys.CoreStructuredProperties] } // type guard @@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{} // TODO standard generated docs func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { // Get the thema.Schema that the meta says is in the current version (which // codegen ensures is always the latest) - cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) tsch, err := thema.BindType[*Dashboard](cursch, &Dashboard{}) if err != nil { // Should be unreachable, modulo bugs in the Thema->Go code generator @@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Dashboard, thema.TranslationLacunas, err // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// dashboard declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties], +// representing the static properties declared in the dashboard kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/playlist/playlist_kind_gen.go b/pkg/kinds/playlist/playlist_kind_gen.go index e67a4bc3dc2..ee9026aedcd 100644 --- a/pkg/kinds/playlist/playlist_kind_gen.go +++ b/pkg/kinds/playlist/playlist_kind_gen.go @@ -26,7 +26,7 @@ type Kind struct { lin thema.ConvergentLineage[*Playlist] jcodec vmux.Codec valmux vmux.ValueMux[*Playlist] - decl kindsys.Decl[kindsys.CoreStructuredMeta] + decl kindsys.Decl[kindsys.CoreStructuredProperties] } // type guard @@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{} // TODO standard generated docs func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { // Get the thema.Schema that the meta says is in the current version (which // codegen ensures is always the latest) - cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) tsch, err := thema.BindType[*Playlist](cursch, &Playlist{}) if err != nil { // Should be unreachable, modulo bugs in the Thema->Go code generator @@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Playlist, thema.TranslationLacunas, erro // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// playlist declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties], +// representing the static properties declared in the playlist kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/svg/svg_kind_gen.go b/pkg/kinds/svg/svg_kind_gen.go index b3f6a4f3150..117a525c8a8 100644 --- a/pkg/kinds/svg/svg_kind_gen.go +++ b/pkg/kinds/svg/svg_kind_gen.go @@ -15,7 +15,7 @@ import ( // TODO standard generated docs type Kind struct { - decl kindsys.Decl[kindsys.RawMeta] + decl kindsys.Decl[kindsys.RawProperties] } // type guard @@ -23,7 +23,7 @@ var _ kindsys.Raw = &Kind{} // TODO standard generated docs func NewKind() (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/svg", nil, nil) + decl, err := kindsys.LoadCoreKind[kindsys.RawProperties]("kinds/raw/svg", nil, nil) if err != nil { return nil, err } @@ -45,11 +45,20 @@ func (k *Kind) MachineName() string { // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.RawMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// svg declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.RawProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.RawProperties], +// representing the static properties declared in the svg kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/team/team_kind_gen.go b/pkg/kinds/team/team_kind_gen.go index 0401069ee17..2a8fd883210 100644 --- a/pkg/kinds/team/team_kind_gen.go +++ b/pkg/kinds/team/team_kind_gen.go @@ -26,7 +26,7 @@ type Kind struct { lin thema.ConvergentLineage[*Team] jcodec vmux.Codec valmux vmux.ValueMux[*Team] - decl kindsys.Decl[kindsys.CoreStructuredMeta] + decl kindsys.Decl[kindsys.CoreStructuredProperties] } // type guard @@ -34,7 +34,7 @@ var _ kindsys.Structured = &Kind{} // TODO standard generated docs func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { - decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredProperties](rootrel, rt.Context(), nil) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { // Get the thema.Schema that the meta says is in the current version (which // codegen ensures is always the latest) - cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) tsch, err := thema.BindType[*Team](cursch, &Team{}) if err != nil { // Should be unreachable, modulo bugs in the Thema->Go code generator @@ -95,11 +95,20 @@ func (k *Kind) JSONValueMux(b []byte) (*Team, thema.TranslationLacunas, error) { // TODO standard generated docs func (k *Kind) Maturity() kindsys.Maturity { - return k.decl.Meta.Maturity + return k.decl.Properties.Maturity } -// TODO standard generated docs -func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredMeta] { +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// team declaration in .cue files. +func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreStructuredProperties] { d := k.decl return &d } + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreStructuredProperties], +// representing the static properties declared in the team kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kindsys/kind.go b/pkg/kindsys/kind.go index 9839954fb69..2add53e98ad 100644 --- a/pkg/kindsys/kind.go +++ b/pkg/kindsys/kind.go @@ -41,15 +41,25 @@ func (m Maturity) Less(om Maturity) bool { return maturityIdx(m) < maturityIdx(om) } -// TODO docs +// Interface describes a Grafana kind object: a Go representation of the definition of +// one of Grafana's categories of kinds. type Interface interface { - // TODO docs + // Props returns a [kindsys.SomeKindProps], representing the properties + // of the kind as declared in the .cue source. The underlying type is + // determined by the category of kind. + // + // This method is largely for convenience, as all actual kind categories are + // expected to implement one of the other interfaces, each of which contain + // a Decl() method through which these same properties are accessible. + Props() SomeKindProperties + + // TODO remove, unnecessary with Props() Name() string - // TODO docs + // TODO remove, unnecessary with Props() MachineName() string - // TODO docs + // TODO remove, unnecessary with Props() Maturity() Maturity // TODO unclear if we want maturity for raw kinds } @@ -58,7 +68,7 @@ type Raw interface { Interface // TODO docs - Decl() *Decl[RawMeta] + Decl() *Decl[RawProperties] } type Structured interface { @@ -68,7 +78,7 @@ type Structured interface { Lineage() thema.Lineage // TODO docs - Decl() *Decl[CoreStructuredMeta] // TODO figure out how to reconcile this interface with CustomStructuredMeta + Decl() *Decl[CoreStructuredProperties] // TODO figure out how to reconcile this interface with CustomStructuredProperties } // type Composable interface { @@ -78,5 +88,5 @@ type Structured interface { // Lineage() thema.Lineage // // // TODO docs -// Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta +// Properties() CoreStructuredProperties // TODO figure out how to reconcile this interface with CustomStructuredProperties // } diff --git a/pkg/kindsys/kindmetas.go b/pkg/kindsys/kindmetas.go index 5470d8e26d6..0e366434337 100644 --- a/pkg/kindsys/kindmetas.go +++ b/pkg/kindsys/kindmetas.go @@ -2,8 +2,8 @@ package kindsys import "github.com/grafana/thema" -// CommonMeta contains the metadata common to all categories of kinds. -type CommonMeta struct { +// CommonProperties contains the metadata common to all categories of kinds. +type CommonProperties struct { Name string `json:"name"` PluralName string `json:"pluralName"` MachineName string `json:"machineName"` @@ -12,63 +12,78 @@ type CommonMeta struct { Maturity Maturity `json:"maturity"` } -// TODO generate from type.cue -type RawMeta struct { - CommonMeta +// RawProperties represents the static properties in a #Raw kind declaration that are +// trivially representable with basic Go types. +// +// When a .cue #Raw declaration is loaded through the standard [LoadCoreKind], +// func, it is fully validated and populated according to all rules specified +// in CUE for #Raw kinds. +type RawProperties struct { + CommonProperties Extensions []string `json:"extensions"` } -func (m RawMeta) _private() {} -func (m RawMeta) Common() CommonMeta { - return m.CommonMeta +func (m RawProperties) _private() {} +func (m RawProperties) Common() CommonProperties { + return m.CommonProperties } -// TODO -type CoreStructuredMeta struct { - CommonMeta +// CoreStructuredProperties represents the static properties in the declaration of a +// #CoreStructured kind that are representable with basic Go types. This +// excludes Thema schemas. +// +// When a .cue #CoreStructured declaration is loaded through the standard [LoadCoreKind], +// func, it is fully validated and populated according to all rules specified +// in CUE for #CoreStructured kinds. +type CoreStructuredProperties struct { + CommonProperties CurrentVersion thema.SyntacticVersion `json:"currentVersion"` } -func (m CoreStructuredMeta) _private() {} -func (m CoreStructuredMeta) Common() CommonMeta { - return m.CommonMeta +func (m CoreStructuredProperties) _private() {} +func (m CoreStructuredProperties) Common() CommonProperties { + return m.CommonProperties } -// TODO -type CustomStructuredMeta struct { - CommonMeta +// CustomStructuredProperties represents the static properties in the declaration of a +// #CustomStructured kind that are representable with basic Go types. This +// excludes Thema schemas. +type CustomStructuredProperties struct { + CommonProperties CurrentVersion thema.SyntacticVersion `json:"currentVersion"` } -func (m CustomStructuredMeta) _private() {} -func (m CustomStructuredMeta) Common() CommonMeta { - return m.CommonMeta +func (m CustomStructuredProperties) _private() {} +func (m CustomStructuredProperties) Common() CommonProperties { + return m.CommonProperties } -// TODO -type ComposableMeta struct { - CommonMeta +// ComposableProperties represents the static properties in the declaration of a +// #Composable kind that are representable with basic Go types. This +// excludes Thema schemas. +type ComposableProperties struct { + CommonProperties CurrentVersion thema.SyntacticVersion `json:"currentVersion"` } -func (m ComposableMeta) _private() {} -func (m ComposableMeta) Common() CommonMeta { - return m.CommonMeta +func (m ComposableProperties) _private() {} +func (m ComposableProperties) Common() CommonProperties { + return m.CommonProperties } -// SomeKindMeta is an interface type to abstract over the different kind -// metadata struct types: [RawMeta], [CoreStructuredMeta], -// [CustomStructuredMeta]. +// SomeKindProperties is an interface type to abstract over the different kind +// property struct types: [RawProperties], [CoreStructuredProperties], +// [CustomStructuredProperties], [ComposableProperties]. // // It is the traditional interface counterpart to the generic type constraint -// KindMetas. -type SomeKindMeta interface { +// KindProperties. +type SomeKindProperties interface { _private() - Common() CommonMeta + Common() CommonProperties } -// KindMetas is a type parameter that comprises the base possible set of +// KindProperties is a type parameter that comprises the base possible set of // kind metadata configurations. -type KindMetas interface { - RawMeta | CoreStructuredMeta | CustomStructuredMeta | ComposableMeta +type KindProperties interface { + RawProperties | CoreStructuredProperties | CustomStructuredProperties | ComposableProperties } diff --git a/pkg/kindsys/load.go b/pkg/kindsys/load.go index 81bf9073c9f..710cb425b49 100644 --- a/pkg/kindsys/load.go +++ b/pkg/kindsys/load.go @@ -83,52 +83,52 @@ func CUEFramework(ctx *cue.Context) cue.Value { // ToKindMeta takes a cue.Value expected to represent a kind of the category // specified by the type parameter and populates the Go type from the cue.Value. -func ToKindMeta[T KindMetas](v cue.Value) (T, error) { - meta := new(T) +func ToKindMeta[T KindProperties](v cue.Value) (T, error) { + props := new(T) if !v.Exists() { - return *meta, ErrValueNotExist + return *props, ErrValueNotExist } fw := CUEFramework(v.Context()) var kdef cue.Value - anymeta := any(*meta).(SomeKindMeta) - switch anymeta.(type) { - case RawMeta: + anyprops := any(*props).(SomeKindProperties) + switch anyprops.(type) { + case RawProperties: kdef = fw.LookupPath(cue.MakePath(cue.Def("Raw"))) - case CoreStructuredMeta: + case CoreStructuredProperties: kdef = fw.LookupPath(cue.MakePath(cue.Def("CoreStructured"))) - case CustomStructuredMeta: + case CustomStructuredProperties: kdef = fw.LookupPath(cue.MakePath(cue.Def("CustomStructured"))) - case ComposableMeta: + case ComposableProperties: kdef = fw.LookupPath(cue.MakePath(cue.Def("Composable"))) default: - // unreachable so long as all the possibilities in KindMetas have switch branches + // unreachable so long as all the possibilities in KindProperties have switch branches panic("unreachable") } item := v.Unify(kdef) if err := item.Validate(cue.Concrete(false), cue.All()); err != nil { - return *meta, ewrap(item.Err(), ErrValueNotAKind) + return *props, ewrap(item.Err(), ErrValueNotAKind) } - if err := item.Decode(meta); err != nil { + if err := item.Decode(props); err != nil { // Should only be reachable if CUE and Go framework types have diverged panic(errors.Details(err, nil)) } - return *meta, nil + return *props, nil } // SomeDecl represents a single kind declaration, having been loaded // and validated by a func such as [LoadCoreKind]. // -// The underlying type of the Meta field indicates the category of +// The underlying type of the Properties field indicates the category of // kind. type SomeDecl struct { // V is the cue.Value containing the entire Kind declaration. V cue.Value - // Meta contains the kind's metadata settings. - Meta SomeKindMeta + // Properties contains the kind's declared properties. + Properties SomeKindProperties } // BindKindLineage binds the lineage for the kind declaration. nil, nil is returned @@ -140,10 +140,10 @@ func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOptio if rt == nil { rt = cuectx.GrafanaThemaRuntime() } - switch decl.Meta.(type) { - case RawMeta: + switch decl.Properties.(type) { + case RawProperties: return nil, nil - case CoreStructuredMeta, CustomStructuredMeta, ComposableMeta: + case CoreStructuredProperties, CustomStructuredProperties, ComposableProperties: return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...) default: panic("unreachable") @@ -152,25 +152,25 @@ func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOptio // IsRaw indicates whether the represented kind is a raw kind. func (decl *SomeDecl) IsRaw() bool { - _, is := decl.Meta.(RawMeta) + _, is := decl.Properties.(RawProperties) return is } // IsCoreStructured indicates whether the represented kind is a core structured kind. func (decl *SomeDecl) IsCoreStructured() bool { - _, is := decl.Meta.(CoreStructuredMeta) + _, is := decl.Properties.(CoreStructuredProperties) return is } // IsCustomStructured indicates whether the represented kind is a custom structured kind. func (decl *SomeDecl) IsCustomStructured() bool { - _, is := decl.Meta.(CustomStructuredMeta) + _, is := decl.Properties.(CustomStructuredProperties) return is } // IsComposable indicates whether the represented kind is a composable kind. func (decl *SomeDecl) IsComposable() bool { - _, is := decl.Meta.(ComposableMeta) + _, is := decl.Properties.(ComposableProperties) return is } @@ -178,18 +178,18 @@ func (decl *SomeDecl) IsComposable() bool { // and validated by a func such as [LoadCoreKind]. // // Its type parameter indicates the category of kind. -type Decl[T KindMetas] struct { +type Decl[T KindProperties] struct { // V is the cue.Value containing the entire Kind declaration. V cue.Value - // Meta contains the kind's metadata settings. - Meta T + // Properties contains the kind's declared properties. + Properties T } // Some converts the typed Decl to the equivalent typeless SomeDecl. func (decl *Decl[T]) Some() *SomeDecl { return &SomeDecl{ - V: decl.V, - Meta: any(decl.Meta).(SomeKindMeta), + V: decl.V, + Properties: any(decl.Properties).(SomeKindProperties), } } @@ -210,7 +210,7 @@ func (decl *Decl[T]) Some() *SomeDecl { // This is a low-level function, primarily intended for use in code generation. // For representations of core kinds that are useful in Go programs at runtime, // see ["github.com/grafana/grafana/pkg/registry/corekind"]. -func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) { +func LoadCoreKind[T RawProperties | CoreStructuredProperties](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) { vk, err := cuectx.BuildGrafanaInstance(ctx, declpath, "kind", overlay) if err != nil { return nil, err @@ -218,7 +218,7 @@ func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Cont decl := &Decl[T]{ V: vk, } - decl.Meta, err = ToKindMeta[T](vk) + decl.Properties, err = ToKindMeta[T](vk) if err != nil { return nil, err } From 78ef55eb0635b88c9eb1388eb2a9078d2ea95245 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Mon, 5 Dec 2022 23:03:47 +0200 Subject: [PATCH 14/94] CI: Add enterprise2 mode in `versions.go` (#59851) Add enterprise2 mode --- pkg/build/cmd/genversions.go | 4 ++-- pkg/build/cmd/genversions_test.go | 4 +++- pkg/build/cmd/grafanacom.go | 2 +- pkg/build/cmd/uploadpackages.go | 2 +- pkg/build/config/version_mode.go | 3 ++- pkg/build/config/versions.go | 40 ++++++++++++++++++++++++++++++- 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/pkg/build/cmd/genversions.go b/pkg/build/cmd/genversions.go index 0d293499430..629414b7351 100644 --- a/pkg/build/cmd/genversions.go +++ b/pkg/build/cmd/genversions.go @@ -36,7 +36,7 @@ func GenerateMetadata(c *cli.Context) (config.Metadata, error) { releaseMode = config.ReleaseMode{Mode: mode} case config.Custom: if edition, _ := os.LookupEnv("EDITION"); edition == string(config.EditionEnterprise2) { - releaseMode = config.ReleaseMode{Mode: config.TagMode} + releaseMode = config.ReleaseMode{Mode: config.Enterprise2Mode} if tag != "" { version = strings.TrimPrefix(tag, "v") } @@ -48,7 +48,7 @@ func GenerateMetadata(c *cli.Context) (config.Metadata, error) { } // if there is a custom event targeting the main branch, that's an enterprise downstream build if mode == config.MainBranch { - releaseMode = config.ReleaseMode{Mode: config.CustomMode} + releaseMode = config.ReleaseMode{Mode: config.DownstreamMode} } else { releaseMode = config.ReleaseMode{Mode: mode} } diff --git a/pkg/build/cmd/genversions_test.go b/pkg/build/cmd/genversions_test.go index c34b8915da2..1039c04c89e 100644 --- a/pkg/build/cmd/genversions_test.go +++ b/pkg/build/cmd/genversions_test.go @@ -16,6 +16,7 @@ const ( DroneTag = "DRONE_TAG" DroneSemverPrerelease = "DRONE_SEMVER_PRERELEASE" DroneBuildNumber = "DRONE_BUILD_NUMBER" + Edition = "EDITION" ) const ( @@ -33,7 +34,8 @@ func TestGetMetadata(t *testing.T) { {map[string]string{DroneBuildEvent: config.Push, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.ReleaseBranchMode}}, {map[string]string{DroneBuildEvent: config.Push, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.MainMode}}, {map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.ReleaseBranchMode}}, - {map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.Custom}}, + {map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.DownstreamMode}}, + {map[string]string{DroneBuildEvent: config.Custom, DroneTargetBranch: config.MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345", Edition: "enterprise2"}, hashedGrafanaVersion, config.ReleaseMode{Mode: config.Enterprise2Mode}}, {map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, "9.2.0", config.ReleaseMode{Mode: config.TagMode, IsBeta: false, IsTest: false}}, {map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-beta", DroneSemverPrerelease: "beta", DroneBuildNumber: "12345"}, "9.2.0-beta", config.ReleaseMode{Mode: config.TagMode, IsBeta: true, IsTest: false}}, {map[string]string{DroneBuildEvent: config.Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-test", DroneSemverPrerelease: "test", DroneBuildNumber: "12345"}, "9.2.0-test", config.ReleaseMode{Mode: config.TagMode, IsBeta: false, IsTest: true}}, diff --git a/pkg/build/cmd/grafanacom.go b/pkg/build/cmd/grafanacom.go index 200f9b40b3c..12cf448e26a 100644 --- a/pkg/build/cmd/grafanacom.go +++ b/pkg/build/cmd/grafanacom.go @@ -146,7 +146,7 @@ func publishPackages(cfg packaging.PublishConfig) error { } switch cfg.ReleaseMode.Mode { - case config.MainMode, config.CustomMode, config.CronjobMode: + case config.MainMode, config.DownstreamMode, config.CronjobMode: pth = path.Join(pth, packaging.MainFolder) default: pth = path.Join(pth, packaging.ReleaseFolder) diff --git a/pkg/build/cmd/uploadpackages.go b/pkg/build/cmd/uploadpackages.go index 01a43a43bdf..8da2c22b14e 100644 --- a/pkg/build/cmd/uploadpackages.go +++ b/pkg/build/cmd/uploadpackages.go @@ -142,7 +142,7 @@ func uploadPackages(cfg uploadConfig) error { switch cfg.versionMode { case config.TagMode: versionFolder = releaseFolder - case config.MainMode, config.CustomMode: + case config.MainMode, config.DownstreamMode: versionFolder = mainFolder case config.ReleaseBranchMode: versionFolder = releaseBranchFolder diff --git a/pkg/build/config/version_mode.go b/pkg/build/config/version_mode.go index f7aa6578fe4..a74b5b5f49e 100644 --- a/pkg/build/config/version_mode.go +++ b/pkg/build/config/version_mode.go @@ -8,7 +8,8 @@ const ( TagMode VersionMode = "release" ReleaseBranchMode VersionMode = "branch" PullRequestMode VersionMode = "pull_request" - CustomMode VersionMode = "custom" + DownstreamMode VersionMode = "downstream" + Enterprise2Mode VersionMode = "enterprise2" CronjobMode VersionMode = "cron" ) diff --git a/pkg/build/config/versions.go b/pkg/build/config/versions.go index baf8e6b8f46..874cedef5f5 100644 --- a/pkg/build/config/versions.go +++ b/pkg/build/config/versions.go @@ -59,7 +59,7 @@ var Versions = VersionMap{ Storybook: "grafana-storybook", }, }, - CustomMode: { + DownstreamMode: { Variants: []Variant{ VariantArmV6, VariantArmV7, @@ -165,4 +165,42 @@ var Versions = VersionMap{ StorybookSrcDir: "artifacts/storybook", }, }, + Enterprise2Mode: { + Variants: []Variant{ + VariantArmV6, + VariantArmV7, + VariantArmV7Musl, + VariantArm64, + VariantArm64Musl, + VariantDarwinAmd64, + VariantWindowsAmd64, + VariantLinuxAmd64, + VariantLinuxAmd64Musl, + }, + PluginSignature: PluginSignature{ + Sign: true, + AdminSign: true, + }, + Docker: Docker{ + ShouldSave: true, + Architectures: []Architecture{ + ArchAMD64, + ArchARM64, + ArchARMv7, + }, + Distribution: []Distribution{ + Alpine, + Ubuntu, + }, + PrereleaseBucket: "grafana-prerelease/artifacts/docker", + }, + Buckets: Buckets{ + Artifacts: "grafana-prerelease/artifacts/downloads", + ArtifactsEnterprise2: "grafana-prerelease/artifacts/downloads-enterprise2", + CDNAssets: "grafana-prerelease", + CDNAssetsDir: "artifacts/static-assets", + Storybook: "grafana-prerelease", + StorybookSrcDir: "artifacts/storybook", + }, + }, } From 932349b5ab4c91df3b78f99d78d20e7c5b851032 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 6 Dec 2022 12:30:03 +0100 Subject: [PATCH 15/94] Loki: Fix wrongly escaped label values when using LabelFilter (#59812) * change backticks to quotes * add unescaping to `addFilterAsLabelFilter` * fix removal of wrong quotes * change unescape order --- .../datasource/loki/languageUtils.test.ts | 26 +++++- .../plugins/datasource/loki/languageUtils.ts | 4 + .../datasource/loki/modifyQuery.test.ts | 80 ++++++++++--------- .../plugins/datasource/loki/modifyQuery.ts | 5 +- .../loki/querybuilder/parsing.test.ts | 15 ++++ .../datasource/loki/querybuilder/parsing.ts | 4 +- 6 files changed, 95 insertions(+), 39 deletions(-) diff --git a/public/app/plugins/datasource/loki/languageUtils.test.ts b/public/app/plugins/datasource/loki/languageUtils.test.ts index f9fa3c975e7..80d7b2aa7e6 100644 --- a/public/app/plugins/datasource/loki/languageUtils.test.ts +++ b/public/app/plugins/datasource/loki/languageUtils.test.ts @@ -1,4 +1,4 @@ -import { isBytesString } from './languageUtils'; +import { escapeLabelValueInExactSelector, isBytesString, unescapeLabelValue } from './languageUtils'; describe('isBytesString', () => { it('correctly matches bytes string with integers', () => { @@ -18,3 +18,27 @@ describe('isBytesString', () => { expect(isBytesString('1.234')).toBe(false); }); }); + +describe('escapeLabelValueInExactSelector', () => { + it.each` + value | escapedValue + ${'nothing to escape'} | ${'nothing to escape'} + ${'escape quote: "'} | ${'escape quote: \\"'} + ${'escape newline: \nend'} | ${'escape newline: \\nend'} + ${'escape slash: \\'} | ${'escape slash: \\\\'} + `('when called with $value', ({ value, escapedValue }) => { + expect(escapeLabelValueInExactSelector(value)).toEqual(escapedValue); + }); +}); + +describe('unescapeLabelValueInExactSelector', () => { + it.each` + value | unescapedValue + ${'nothing to unescape'} | ${'nothing to unescape'} + ${'escape quote: \\"'} | ${'escape quote: "'} + ${'escape newline: \\nend'} | ${'escape newline: \nend'} + ${'escape slash: \\\\'} | ${'escape slash: \\'} + `('when called with $value', ({ value, unescapedValue }) => { + expect(unescapeLabelValue(value)).toEqual(unescapedValue); + }); +}); diff --git a/public/app/plugins/datasource/loki/languageUtils.ts b/public/app/plugins/datasource/loki/languageUtils.ts index 0dfcd1b35e2..581d8a991a1 100644 --- a/public/app/plugins/datasource/loki/languageUtils.ts +++ b/public/app/plugins/datasource/loki/languageUtils.ts @@ -35,6 +35,10 @@ export function escapeLabelValueInExactSelector(labelValue: string): string { return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"'); } +export function unescapeLabelValue(labelValue: string): string { + return labelValue.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\'); +} + export function escapeLabelValueInRegexSelector(labelValue: string): string { return escapeLabelValueInExactSelector(escapeLokiRegexp(labelValue)); } diff --git a/public/app/plugins/datasource/loki/modifyQuery.test.ts b/public/app/plugins/datasource/loki/modifyQuery.test.ts index 171b5b02a57..8fcc107c373 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.test.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.test.ts @@ -8,42 +8,50 @@ import { describe('addLabelToQuery()', () => { it.each` - query | description | label | operator | value | expectedResult - ${'{x="y"}'} | ${'no label and value'} | ${''} | ${'='} | ${''} | ${''} - ${'{x="yy"}'} | ${'simple query'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="yy", bar="baz"}'} - ${'{x="yy"}'} | ${'simple query'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="yy", bar="baz"}'} - ${'{x="yy"}'} | ${'custom operator'} | ${'bar'} | ${'!='} | ${'baz'} | ${'{x="yy", bar!="baz"}'} - ${'rate({}[1m])'} | ${'do not modify ranges'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[1m])'} - ${'sum by (host) (rate({} [1m]))'} | ${'detect in-order function use'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum by (host) (rate({bar="baz"}[1m]))'} - ${'{instance="my-host.com:9100"}'} | ${'selectors with punctuation'} | ${'bar'} | ${'='} | ${'baz'} | ${'{instance="my-host.com:9100", bar="baz"}'} - ${'{list="a,b,c"}'} | ${'selectors with punctuation'} | ${'bar'} | ${'='} | ${'baz'} | ${'{list="a,b,c", bar="baz"}'} - ${'rate({}[5m]) + rate({}[5m])'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[5m]) + rate({bar="baz"}[5m])'} - ${'avg(rate({x="y"} [$__interval]))+ sum(rate({}[5m]))'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'avg(rate({x="y", bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} - ${'rate({x="yy"}[5m]) * rate({y="zz",a="bb"}[5m]) * rate({}[5m])'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({x="yy", bar="baz"}[5m]) * rate({y="zz", a="bb", bar="baz"}[5m]) * rate({bar="baz"}[5m])'} - ${'{x="yy", bar!="baz"}'} | ${'do not add duplicate labels'} | ${'bar'} | ${'!='} | ${'baz'} | ${'{x="yy", bar!="baz"}'} - ${'rate({bar="baz"}[1m])'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[1m])'} - ${'{list="a,b,c", bar="baz"}'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'{list="a,b,c", bar="baz"}'} - ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} - ${'{x="y"} |="yy"'} | ${'do not remove filters'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} |="yy"'} - ${'{x="y"} |="yy" !~"xx"'} | ${'do not remove filters'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} |="yy" !~"xx"'} - ${'{x="y"} or {}'} | ${'metric with logical operators'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} or {bar="baz"}'} - ${'{x="y"} and {}'} | ${'metric with logical operators'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} and {bar="baz"}'} - ${'sum(rate({job="foo"}[2m])) by (value $variable)'} | ${'template variables'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)'} - ${'rate({x="y"}[${__range_s}s])'} | ${'metric query with range grafana variable'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({x="y", bar="baz"}[${__range_s}s])'} - ${'max by (id, name, type) ({type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (rate({} [5m])) * 1000'} | ${'metric query with labels in label list with the group modifier'} | ${'bar'} | ${'='} | ${'baz'} | ${'max by (id, name, type) ({type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (rate({bar="baz"}[5m])) * 1000'} - ${'{foo="bar"} | logfmt'} | ${'query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz`'} - ${'{foo="bar"} | logfmt | json'} | ${'query with multiple parsers'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | json | bar=`baz`'} - ${'{foo="bar"} | logfmt | x="y"'} | ${'query with parser and label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | x="y" | bar=`baz`'} - ${'rate({foo="bar"} | logfmt [5m])'} | ${'metric query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({foo="bar"} | logfmt | bar=`baz` [5m])'} - ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | line_format "{{.status}}" [5m]))'} | ${'metric query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | bar=`baz` | line_format "{{.status}}" [5m]))'} - ${'{foo="bar"} | logfmt | line_format "{{.status}}"'} | ${'do not add filter to line_format expressions in query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{.status}}"'} - ${'{foo="bar"} | logfmt | line_format "{{status}}"'} | ${'do not add filter to line_format expressions in query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{status}}"'} - ${'{}'} | ${'query without stream selector'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}'} - ${'{} | logfmt'} | ${'query without stream selector and with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| logfmt'} - ${'{} | x="y"'} | ${'query without stream selector and with label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| x="y"'} - ${'{} | logfmt | x="y"'} | ${'query without stream selector and with parser and label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| logfmt | x="y"'} - ${'sum(rate({x="y"} [5m])) + sum(rate({} | logfmt [5m]))'} | ${'metric query with 1 empty and 1 not empty stream selector with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({x="y", bar="baz"} [5m])) + sum(rate({bar="baz"}| logfmt [5m]))'} - ${'sum(rate({x="y"} | logfmt [5m])) + sum(rate({} [5m]))'} | ${'metric query with 1 non-empty and 1 not empty stream selector with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({x="y", bar="baz"} | logfmt [5m])) + sum(rate({bar="baz"}[5m]))'} + query | description | label | operator | value | expectedResult + ${'{x="y"}'} | ${'no label and value'} | ${''} | ${'='} | ${''} | ${''} + ${'{x="yy"}'} | ${'simple query'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="yy", bar="baz"}'} + ${'{x="yy"}'} | ${'simple query'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="yy", bar="baz"}'} + ${'{x="yy"}'} | ${'custom operator'} | ${'bar'} | ${'!='} | ${'baz'} | ${'{x="yy", bar!="baz"}'} + ${'rate({}[1m])'} | ${'do not modify ranges'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[1m])'} + ${'sum by (host) (rate({} [1m]))'} | ${'detect in-order function use'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum by (host) (rate({bar="baz"}[1m]))'} + ${'{instance="my-host.com:9100"}'} | ${'selectors with punctuation'} | ${'bar'} | ${'='} | ${'baz'} | ${'{instance="my-host.com:9100", bar="baz"}'} + ${'{list="a,b,c"}'} | ${'selectors with punctuation'} | ${'bar'} | ${'='} | ${'baz'} | ${'{list="a,b,c", bar="baz"}'} + ${'rate({}[5m]) + rate({}[5m])'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[5m]) + rate({bar="baz"}[5m])'} + ${'avg(rate({x="y"} [$__interval]))+ sum(rate({}[5m]))'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'avg(rate({x="y", bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} + ${'rate({x="yy"}[5m]) * rate({y="zz",a="bb"}[5m]) * rate({}[5m])'} | ${'arithmetical expressions'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({x="yy", bar="baz"}[5m]) * rate({y="zz", a="bb", bar="baz"}[5m]) * rate({bar="baz"}[5m])'} + ${'{x="yy", bar!="baz"}'} | ${'do not add duplicate labels'} | ${'bar'} | ${'!='} | ${'baz'} | ${'{x="yy", bar!="baz"}'} + ${'rate({bar="baz"}[1m])'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({bar="baz"}[1m])'} + ${'{list="a,b,c", bar="baz"}'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'{list="a,b,c", bar="baz"}'} + ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} | ${'do not add duplicate labels'} | ${'bar'} | ${'='} | ${'baz'} | ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'} + ${'{x="y"} |="yy"'} | ${'do not remove filters'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} |="yy"'} + ${'{x="y"} |="yy" !~"xx"'} | ${'do not remove filters'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} |="yy" !~"xx"'} + ${'{x="y"} or {}'} | ${'metric with logical operators'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} or {bar="baz"}'} + ${'{x="y"} and {}'} | ${'metric with logical operators'} | ${'bar'} | ${'='} | ${'baz'} | ${'{x="y", bar="baz"} and {bar="baz"}'} + ${'sum(rate({job="foo"}[2m])) by (value $variable)'} | ${'template variables'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)'} + ${'rate({x="y"}[${__range_s}s])'} | ${'metric query with range grafana variable'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({x="y", bar="baz"}[${__range_s}s])'} + ${'max by (id, name, type) ({type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (rate({} [5m])) * 1000'} | ${'metric query with labels in label list with the group modifier'} | ${'bar'} | ${'='} | ${'baz'} | ${'max by (id, name, type) ({type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (rate({bar="baz"}[5m])) * 1000'} + ${'{foo="bar"} | logfmt'} | ${'query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz`'} + ${'{foo="bar"} | logfmt | json'} | ${'query with multiple parsers'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | json | bar=`baz`'} + ${'{foo="bar"} | logfmt | x="y"'} | ${'query with parser and label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | x="y" | bar=`baz`'} + ${'rate({foo="bar"} | logfmt [5m])'} | ${'metric query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'rate({foo="bar"} | logfmt | bar=`baz` [5m])'} + ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | line_format "{{.status}}" [5m]))'} | ${'metric query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | bar=`baz` | line_format "{{.status}}" [5m]))'} + ${'{foo="bar"} | logfmt | line_format "{{.status}}"'} | ${'do not add filter to line_format expressions in query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{.status}}"'} + ${'{foo="bar"} | logfmt | line_format "{{status}}"'} | ${'do not add filter to line_format expressions in query with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{status}}"'} + ${'{}'} | ${'query without stream selector'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}'} + ${'{} | logfmt'} | ${'query without stream selector and with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| logfmt'} + ${'{} | x="y"'} | ${'query without stream selector and with label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| x="y"'} + ${'{} | logfmt | x="y"'} | ${'query without stream selector and with parser and label filter'} | ${'bar'} | ${'='} | ${'baz'} | ${'{bar="baz"}| logfmt | x="y"'} + ${'sum(rate({x="y"} [5m])) + sum(rate({} | logfmt [5m]))'} | ${'metric query with 1 empty and 1 not empty stream selector with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({x="y", bar="baz"} [5m])) + sum(rate({bar="baz"}| logfmt [5m]))'} + ${'sum(rate({x="y"} | logfmt [5m])) + sum(rate({} [5m]))'} | ${'metric query with 1 non-empty and 1 not empty stream selector with parser'} | ${'bar'} | ${'='} | ${'baz'} | ${'sum(rate({x="y", bar="baz"} | logfmt [5m])) + sum(rate({bar="baz"}[5m]))'} + ${'{x="yy"}'} | ${'simple query with escaped value'} | ${'bar'} | ${'='} | ${'"baz"'} | ${'{x="yy", bar=""baz""}'} + ${'{x="yy"}'} | ${'simple query with escaped value'} | ${'bar'} | ${'='} | ${'\\"baz\\"'} | ${'{x="yy", bar="\\"baz\\""}'} + ${'{x="yy"}'} | ${'simple query with an other escaped value'} | ${'bar'} | ${'='} | ${'baz\\\\'} | ${'{x="yy", bar="baz\\\\"}'} + ${'{x="yy"}'} | ${'simple query with escaped value and regex operator'} | ${'bar'} | ${'~='} | ${'baz\\\\'} | ${'{x="yy", bar~="baz\\\\"}'} + ${'{foo="bar"} | logfmt'} | ${'query with parser with escaped value'} | ${'bar'} | ${'='} | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar=`"baz"`'} + ${'{foo="bar"} | logfmt'} | ${'query with parser with an other escaped value'} | ${'bar'} | ${'='} | ${'baz\\\\'} | ${'{foo="bar"} | logfmt | bar=`baz\\`'} + ${'{foo="bar"} | logfmt'} | ${'query with parser with escaped value and regex operator'} | ${'bar'} | ${'~='} | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar~=`"baz"`'} + ${'{foo="bar"} | logfmt'} | ${'query with parser with escaped value and regex operator'} | ${'bar'} | ${'~='} | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar~=`"baz"`'} `( 'should add label to query: $query, description: $description', ({ query, description, label, operator, value, expectedResult }) => { diff --git a/public/app/plugins/datasource/loki/modifyQuery.ts b/public/app/plugins/datasource/loki/modifyQuery.ts index 2b3b2a3d021..4329eacb142 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.ts @@ -18,6 +18,7 @@ import { import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types'; +import { unescapeLabelValue } from './languageUtils'; import { LokiQueryModeller } from './querybuilder/LokiQueryModeller'; import { buildVisualQueryFromString } from './querybuilder/parsing'; @@ -329,7 +330,9 @@ function addFilterAsLabelFilter( const start = query.substring(prev, match.to); const end = isLast ? query.substring(match.to) : ''; - const labelFilter = ` | ${filter.label}${filter.op}\`${filter.value}\``; + // we now unescape all escaped values again, because we are using backticks which can handle those cases. + // we also don't care about the operator here, because we need to unescape for both, regex and equal. + const labelFilter = ` | ${filter.label}${filter.op}\`${unescapeLabelValue(filter.value)}\``; newQuery += start + labelFilter + end; prev = match.to; } diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts index f67c5cca42f..4ffe4957364 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.test.ts @@ -571,6 +571,21 @@ describe('buildVisualQueryFromString', () => { }) ); }); + + it('parses simple query with quotes in label value', () => { + expect(buildVisualQueryFromString('{app="\\"frontend\\""}')).toEqual( + noErrors({ + labels: [ + { + op: '=', + value: '\\"frontend\\"', + label: 'app', + }, + ], + operations: [], + }) + ); + }); }); function noErrors(query: LokiVisualQuery) { diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts index 561be84facf..cd428b02c1e 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts @@ -212,7 +212,9 @@ function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter { const labelNode = node.getChild(Identifier); const label = getString(expr, labelNode); const op = getString(expr, labelNode!.nextSibling); - const value = getString(expr, node.getChild(String)).replace(/"/g, ''); + let value = getString(expr, node.getChild(String)); + // `value` is wrapped in double quotes, so we need to remove them. As a value can contain double quotes, we can't use RegEx here. + value = value.substring(1, value.length - 1); return { label, From 3978502d837986a9ad77f4bf303b433f038637b1 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Tue, 6 Dec 2022 13:20:49 +0100 Subject: [PATCH 16/94] Chore: Remote cache key prefix (#59838) * attempt to implement a remote cache key prefix * add a test for the prefix store * oh, linter --- conf/defaults.ini | 3 ++ conf/sample.ini | 3 ++ pkg/infra/remotecache/remotecache.go | 41 +++++++++++++++++------ pkg/infra/remotecache/remotecache_test.go | 25 ++++++++++++++ pkg/setting/setting.go | 3 ++ 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index fd34cb644cc..cba6bc9bb40 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -162,6 +162,9 @@ type = database # memcache: 127.0.0.1:11211 connstr = +# prefix prepended to all the keys in the remote cache +prefix = + #################################### Data proxy ########################### [dataproxy] diff --git a/conf/sample.ini b/conf/sample.ini index 91ac70a0eef..6789bd286a2 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -169,6 +169,9 @@ # memcache: 127.0.0.1:11211 ;connstr = +# prefix prepended to all the keys in the remote cache +; prefix = + #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index bce1b75540f..8f572b05e1a 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -97,20 +97,24 @@ func (ds *RemoteCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (CacheStorage, error) { - if opts.Name == redisCacheType { - return newRedisStorage(opts) +func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (cache CacheStorage, err error) { + switch opts.Name { + case redisCacheType: + cache, err = newRedisStorage(opts) + case memcachedCacheType: + cache = newMemcachedStorage(opts) + case databaseCacheType: + cache = newDatabaseCache(sqlstore) + default: + return nil, ErrInvalidCacheType } - - if opts.Name == memcachedCacheType { - return newMemcachedStorage(opts), nil + if err != nil { + return cache, err } - - if opts.Name == databaseCacheType { - return newDatabaseCache(sqlstore), nil + if opts.Prefix != "" { + cache = &prefixCacheStorage{cache: cache, prefix: opts.Prefix} } - - return nil, ErrInvalidCacheType + return cache, nil } // Register records a type, identified by a value for that type, under its @@ -137,3 +141,18 @@ func decodeGob(data []byte, out *cachedItem) error { buf := bytes.NewBuffer(data) return gob.NewDecoder(buf).Decode(&out) } + +type prefixCacheStorage struct { + cache CacheStorage + prefix string +} + +func (pcs *prefixCacheStorage) Get(ctx context.Context, key string) (interface{}, error) { + return pcs.cache.Get(ctx, pcs.prefix+key) +} +func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { + return pcs.cache.Set(ctx, pcs.prefix+key, value, expire) +} +func (pcs *prefixCacheStorage) Delete(ctx context.Context, key string) error { + return pcs.cache.Delete(ctx, pcs.prefix+key) +} diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 41b6381d813..b72291a519d 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" ) @@ -88,3 +89,27 @@ func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { _, err = client.Get(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } + +func TestCachePrefix(t *testing.T) { + db := db.InitTestDB(t) + cache := &databaseCache{ + SQLStore: db, + log: log.New("remotecache.database"), + } + prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} + + // Set a value (with a prefix) + err := prefixCache.Set(context.Background(), "foo", "bar", time.Hour) + require.NoError(t, err) + // Get a value (with a prefix) + v, err := prefixCache.Get(context.Background(), "foo") + require.NoError(t, err) + require.Equal(t, "bar", v) + // Get a value directly from the underlying cache, ensure the prefix is in the key + v, err = cache.Get(context.Background(), "test/foo") + require.NoError(t, err) + require.Equal(t, "bar", v) + // Get a value directly from the underlying cache without a prefix, should not be there + _, err = cache.Get(context.Background(), "foo") + require.Error(t, err) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c2a90b8f950..81ad5fb72bc 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1120,10 +1120,12 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { cacheServer := iniFile.Section("remote_cache") dbName := valueAsString(cacheServer, "type", "database") connStr := valueAsString(cacheServer, "connstr", "") + prefix := valueAsString(cacheServer, "prefix", "") cfg.RemoteCacheOptions = &RemoteCacheOptions{ Name: dbName, ConnStr: connStr, + Prefix: prefix, } geomapSection := iniFile.Section("geomap") @@ -1159,6 +1161,7 @@ func valueAsString(section *ini.Section, keyName string, defaultValue string) st type RemoteCacheOptions struct { Name string ConnStr string + Prefix string } func (cfg *Cfg) readLDAPConfig() { From 4259b6bf581634145d2c285d75309106c2423a06 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Tue, 6 Dec 2022 15:02:01 +0200 Subject: [PATCH 17/94] API: Fix delete user failure due to quota not enabled (#59875) --- pkg/services/quota/quotaimpl/quota.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/quota/quotaimpl/quota.go b/pkg/services/quota/quotaimpl/quota.go index f26c066b193..9cc8be38af5 100644 --- a/pkg/services/quota/quotaimpl/quota.go +++ b/pkg/services/quota/quotaimpl/quota.go @@ -33,7 +33,7 @@ func (s *serviceDisabled) CheckQuotaReached(ctx context.Context, targetSrv quota } func (s *serviceDisabled) DeleteQuotaForUser(ctx context.Context, userID int64) error { - return quota.ErrDisabled + return nil } func (s *serviceDisabled) RegisterQuotaReporter(e *quota.NewUsageReporter) error { From 982005152aa9a0fbd590e7f5cf6f83596d3a1d59 Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Tue, 6 Dec 2022 13:03:37 +0000 Subject: [PATCH 18/94] Chore: Refactor ScrollManager.test.js to TypeScript (#58661) --- ...lManager.test.js => ScrollManager.test.ts} | 107 +++++++++--------- .../src/ScrollManager.tsx | 4 +- 2 files changed, 56 insertions(+), 55 deletions(-) rename packages/jaeger-ui-components/src/{ScrollManager.test.js => ScrollManager.test.ts} (71%) diff --git a/packages/jaeger-ui-components/src/ScrollManager.test.js b/packages/jaeger-ui-components/src/ScrollManager.test.ts similarity index 71% rename from packages/jaeger-ui-components/src/ScrollManager.test.js rename to packages/jaeger-ui-components/src/ScrollManager.test.ts index f09cebe7c25..7f7e1368279 100644 --- a/packages/jaeger-ui-components/src/ScrollManager.test.js +++ b/packages/jaeger-ui-components/src/ScrollManager.test.ts @@ -14,27 +14,30 @@ jest.mock('./scroll-page'); -import ScrollManager from './ScrollManager'; +import traceGenerator from '../src/demo/trace-generators'; + +import ScrollManager, { Accessors } from './ScrollManager'; import { scrollBy, scrollTo } from './scroll-page'; +import { Trace, TraceSpanData, TraceSpanReference } from './types/trace'; const SPAN_HEIGHT = 2; -function getTrace() { - const spans = []; - const trace = { - spans, - duration: 2000, - startTime: 1000, - }; - for (let i = 0; i < 10; i++) { - spans.push({ duration: 1, startTime: 1000, spanID: i + 1 }); - } - return trace; +function getTrace(): Trace { + const generatedTrace = traceGenerator.trace({ numberOfSpans: 10 }); + generatedTrace.duration = 2000; + generatedTrace.startTime = 1000; + + generatedTrace.spans.forEach((span: TraceSpanData, index: number) => { + span.duration = 1; + span.startTime = 1000; + span.spanID = (index + 1).toString(); + }); + return generatedTrace; } function getAccessors() { return { - getViewRange: jest.fn(() => [0, 1]), + getViewRange: jest.fn(() => [0, 1] as [number, number]), getSearchedSpanIDs: jest.fn(), getCollapsedChildren: jest.fn(), getViewHeight: jest.fn(() => SPAN_HEIGHT * 2), @@ -47,13 +50,13 @@ function getAccessors() { } describe('ScrollManager', () => { - let trace; - let accessors; - let manager; + let trace: Trace; + let accessors: Accessors; + let manager: ScrollManager; beforeEach(() => { - scrollBy.mockReset(); - scrollTo.mockReset(); + jest.mocked(scrollBy).mockReset(); + jest.mocked(scrollTo).mockReset(); trace = getTrace(); accessors = getAccessors(); manager = new ScrollManager(trace, { scrollBy, scrollTo }); @@ -61,14 +64,13 @@ describe('ScrollManager', () => { }); it('saves the accessors', () => { - const n = Math.random(); - manager.setAccessors(n); - expect(manager._accessors).toBe(n); + accessors = getAccessors(); + manager.setAccessors(accessors); + expect(manager._accessors).toBe(accessors); }); describe('_scrollPast()', () => { it('throws if accessors is not set', () => { - manager.setAccessors(null); expect(manager._scrollPast).toThrow(); }); @@ -77,10 +79,10 @@ describe('ScrollManager', () => { const oldWarn = console.warn; // eslint-disable-next-line no-console console.warn = () => {}; - manager._scrollPast(null, null); - expect(accessors.getRowPosition.mock.calls.length).toBe(1); - expect(accessors.getViewHeight.mock.calls.length).toBe(0); - expect(scrollTo.mock.calls.length).toBe(0); + manager._scrollPast(-2, 1); + expect(jest.mocked(accessors.getRowPosition).mock.calls.length).toBe(1); + expect(jest.mocked(accessors.getViewHeight).mock.calls.length).toBe(0); + expect(jest.mocked(scrollTo).mock.calls.length).toBe(0); // eslint-disable-next-line no-console console.warn = oldWarn; }); @@ -88,44 +90,43 @@ describe('ScrollManager', () => { it('scrolls up with direction is `-1`', () => { const y = 10; const expectTo = y - 0.5 * accessors.getViewHeight(); - accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT }); + jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT }); manager._scrollPast(NaN, -1); - expect(scrollTo.mock.calls).toEqual([[expectTo]]); + expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]); }); it('scrolls down with direction `1`', () => { const y = 10; const vh = accessors.getViewHeight(); const expectTo = y + SPAN_HEIGHT - 0.5 * vh; - accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT }); + jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT }); manager._scrollPast(NaN, 1); - expect(scrollTo.mock.calls).toEqual([[expectTo]]); + expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]); }); }); describe('_scrollToVisibleSpan()', () => { - function getRefs(spanID) { - return [{ refType: 'CHILD_OF', spanID }]; + function getRefs(spanID: string | undefined) { + return [{ refType: 'CHILD_OF', spanID }] as TraceSpanReference[]; } - let scrollPastMock; + let scrollPastMock: jest.Mock; beforeEach(() => { scrollPastMock = jest.fn(); manager._scrollPast = scrollPastMock; }); it('throws if accessors is not set', () => { - manager.setAccessors(null); expect(manager._scrollToVisibleSpan).toThrow(); }); it('exits if the trace is not set', () => { manager.setTrace(null); - manager._scrollToVisibleSpan(); + manager._scrollToVisibleSpan(1); expect(scrollPastMock.mock.calls.length).toBe(0); }); it('does nothing if already at the boundary', () => { - accessors.getTopRowIndexVisible.mockReturnValue(0); - accessors.getBottomRowIndexVisible.mockReturnValue(trace.spans.length - 1); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(0); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(trace.spans.length - 1); manager._scrollToVisibleSpan(-1); expect(scrollPastMock.mock.calls.length).toBe(0); manager._scrollToVisibleSpan(1); @@ -133,8 +134,8 @@ describe('ScrollManager', () => { }); it('centers the current top or bottom span', () => { - accessors.getTopRowIndexVisible.mockReturnValue(5); - accessors.getBottomRowIndexVisible.mockReturnValue(5); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(5); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(5); manager._scrollToVisibleSpan(-1); expect(scrollPastMock).lastCalledWith(5, -1); manager._scrollToVisibleSpan(1); @@ -144,8 +145,8 @@ describe('ScrollManager', () => { it('skips spans that are out of view', () => { trace.spans[4].startTime = trace.startTime + trace.duration * 0.5; accessors.getViewRange = () => [0.4, 0.6]; - accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1); - accessors.getBottomRowIndexVisible.mockReturnValue(0); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0); manager._scrollToVisibleSpan(1); expect(scrollPastMock).lastCalledWith(4, 1); manager._scrollToVisibleSpan(-1); @@ -153,8 +154,8 @@ describe('ScrollManager', () => { }); it('skips spans that do not match the text search', () => { - accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1); - accessors.getBottomRowIndexVisible.mockReturnValue(0); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0); accessors.getSearchedSpanIDs = () => new Set([trace.spans[4].spanID]); manager._scrollToVisibleSpan(1); expect(scrollPastMock).lastCalledWith(4, 1); @@ -164,8 +165,8 @@ describe('ScrollManager', () => { it('scrolls to boundary when scrolling away from closest spanID in findMatches', () => { const closetFindMatchesSpanID = 4; - accessors.getTopRowIndexVisible.mockReturnValue(closetFindMatchesSpanID - 1); - accessors.getBottomRowIndexVisible.mockReturnValue(closetFindMatchesSpanID + 1); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(closetFindMatchesSpanID - 1); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(closetFindMatchesSpanID + 1); accessors.getSearchedSpanIDs = () => new Set([trace.spans[closetFindMatchesSpanID].spanID]); manager._scrollToVisibleSpan(1); @@ -177,7 +178,7 @@ describe('ScrollManager', () => { it('scrolls to last visible row when boundary is hidden', () => { const parentOfLastRowWithHiddenChildrenIndex = trace.spans.length - 2; - accessors.getBottomRowIndexVisible.mockReturnValue(0); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0); accessors.getCollapsedChildren = () => new Set([trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID]); accessors.getSearchedSpanIDs = () => new Set([trace.spans[0].spanID]); trace.spans[trace.spans.length - 1].references = getRefs( @@ -204,9 +205,9 @@ describe('ScrollManager', () => { } } // set which spans are "in-view" and which have collapsed children - accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1); - accessors.getBottomRowIndexVisible.mockReturnValue(0); - accessors.getCollapsedChildren.mockReturnValue(new Set([spans[0].spanID, spans[4].spanID])); + jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1); + jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0); + jest.mocked(accessors.getCollapsedChildren).mockReturnValue(new Set([spans[0].spanID, spans[4].spanID])); }); it('skips spans that are hidden because their parent is collapsed', () => { @@ -219,7 +220,7 @@ describe('ScrollManager', () => { it('ignores references with unknown types', () => { // modify spans[2] so that it has an unknown refType const spans = trace.spans; - spans[2].references = [{ refType: 'OTHER' }]; + spans[2].references = [{ refType: 'OTHER' }] as unknown as TraceSpanReference[]; manager.scrollToNextVisibleSpan(); expect(scrollPastMock).lastCalledWith(2, 1); manager.scrollToPrevVisibleSpan(); @@ -239,7 +240,7 @@ describe('ScrollManager', () => { describe('scrollToFirstVisibleSpan', () => { beforeEach(() => { - jest.spyOn(manager, '_scrollToVisibleSpan').mockImplementationOnce(); + jest.spyOn(manager, '_scrollToVisibleSpan'); }); it('calls _scrollToVisibleSpan searching downwards from first span', () => { @@ -261,12 +262,12 @@ describe('ScrollManager', () => { manager._accessors = null; manager.scrollPageDown(); manager.scrollPageUp(); - expect(scrollBy.mock.calls.length).toBe(0); + expect(jest.mocked(scrollBy).mock.calls.length).toBe(0); manager._accessors = accessors; manager._scroller = null; manager.scrollPageDown(); manager.scrollPageUp(); - expect(scrollBy.mock.calls.length).toBe(0); + expect(jest.mocked(scrollBy).mock.calls.length).toBe(0); }); }); diff --git a/packages/jaeger-ui-components/src/ScrollManager.tsx b/packages/jaeger-ui-components/src/ScrollManager.tsx index 262896f21f6..85fc84f9333 100644 --- a/packages/jaeger-ui-components/src/ScrollManager.tsx +++ b/packages/jaeger-ui-components/src/ScrollManager.tsx @@ -87,7 +87,7 @@ function isSpanHidden(span: TraceSpan, childrenAreHidden: Set, spansMap: */ export default class ScrollManager { _trace: Trace | TNil; - _scroller: Scroller; + _scroller: Scroller | TNil; _accessors: Accessors | TNil; constructor(trace: Trace | TNil, scroller: Scroller) { @@ -117,7 +117,7 @@ export default class ScrollManager { y -= vh; } y += direction * 0.5 * vh; - this._scroller.scrollTo(y); + this._scroller?.scrollTo(y); } _scrollToVisibleSpan(direction: 1 | -1, startRow?: number) { From 2ff6ae65dfedba7cbada571a329f3a7567d72c12 Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Tue, 6 Dec 2022 13:13:15 +0000 Subject: [PATCH 19/94] Chore: Refactor the `packages/jaeger-ui-components/src/TracePageHeader` folder to TS (#59722) --- .betterer.results | 8 +- packages/jaeger-ui-components/package.json | 1 + ...Graph.test.js => CanvasSpanGraph.test.tsx} | 0 ...GraphTicks.test.js => GraphTicks.test.tsx} | 6 +- .../TracePageHeader/SpanGraph/GraphTicks.tsx | 2 +- .../{Scrubber.test.js => Scrubber.test.tsx} | 8 +- .../TracePageHeader/SpanGraph/Scrubber.tsx | 2 +- ...ingLayer.test.js => ViewingLayer.test.tsx} | 87 ++++++++++--------- .../SpanGraph/ViewingLayer.tsx | 2 +- .../{index.test.js => index.test.tsx} | 14 +-- .../src/TracePageHeader/SpanGraph/index.tsx | 2 +- ...vas.test.js => render-into-canvas.test.ts} | 41 +++++---- ...ar.test.js => TracePageSearchBar.test.tsx} | 17 ++-- .../TracePageHeader/TracePageSearchBar.tsx | 2 +- yarn.lock | 1 + 15 files changed, 103 insertions(+), 90 deletions(-) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{CanvasSpanGraph.test.js => CanvasSpanGraph.test.tsx} (100%) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{GraphTicks.test.js => GraphTicks.test.tsx} (88%) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{Scrubber.test.js => Scrubber.test.tsx} (89%) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{ViewingLayer.test.js => ViewingLayer.test.tsx} (79%) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{index.test.js => index.test.tsx} (84%) rename packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/{render-into-canvas.test.js => render-into-canvas.test.ts} (78%) rename packages/jaeger-ui-components/src/TracePageHeader/{TracePageSearchBar.test.js => TracePageSearchBar.test.tsx} (77%) diff --git a/.betterer.results b/.betterer.results index 52dfa2aa57c..08df5db3e94 100644 --- a/.betterer.results +++ b/.betterer.results @@ -8,11 +8,11 @@ exports[`no enzyme tests`] = { "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:2976628669": [ [0, 26, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js:1676554632": [ - [14, 19, 13, "RegExp match", "2409514259"] + "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx:793800575": [ + [14, 35, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js:186764954": [ - [14, 19, 13, "RegExp match", "2409514259"] + "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx:596989456": [ + [14, 35, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [ [14, 26, 13, "RegExp match", "2409514259"] diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 151c54674f4..fd0c72d3aa7 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -15,6 +15,7 @@ "@testing-library/user-event": "14.4.3", "@types/classnames": "^2.2.7", "@types/deep-freeze": "^0.1.1", + "@types/enzyme": "3.10.12", "@types/hoist-non-react-statics": "^3.3.1", "@types/jest": "29.2.3", "@types/lodash": "4.14.187", diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.tsx similarity index 88% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.tsx index ffb2af7c879..ade36dcc5dc 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.tsx @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { render, screen, within } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; -import GraphTicks from './GraphTicks'; +import GraphTicks, { GraphTicksProps } from './GraphTicks'; -const setup = (propOverrides) => { +const setup = (propOverrides?: GraphTicksProps) => { const defaultProps = { items: [ { valueWidth: 100, valueOffset: 25, serviceName: 'a' }, diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx index ce2244456ee..5827fd07110 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx @@ -27,7 +27,7 @@ const getStyles = () => { }; }; -type GraphTicksProps = { +export type GraphTicksProps = { numTicks: number; }; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.tsx similarity index 89% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.tsx index 1f1ca955b78..384e047bcc9 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.tsx @@ -15,19 +15,19 @@ import { render, screen, fireEvent, within } from '@testing-library/react'; import React from 'react'; -import Scrubber from './Scrubber'; +import Scrubber, { ScrubberProps } from './Scrubber'; describe('', () => { const defaultProps = { position: 0, }; - let rerender; + let rerender: (arg0: JSX.Element) => void; beforeEach(() => { ({ rerender } = render( - + )); }); @@ -45,7 +45,7 @@ describe('', () => { it('calculates the correct x% for a timestamp', () => { rerender( - + ); const line = screen.getByTestId('scrubber-component-line'); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx index 94bec0f1769..077eb2a9f2d 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx @@ -72,7 +72,7 @@ export const getStyles = () => { }; }; -type ScrubberProps = { +export type ScrubberProps = { isDragging: boolean; position: number; onMouseDown: (evt: React.MouseEvent) => void; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx similarity index 79% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx index b65184021ed..e83e65f778c 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx @@ -12,22 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import { createTheme } from '@grafana/data'; -import { EUpdateTypes } from '../../utils/DraggableManager'; +import { ViewRangeTime } from '../../TraceTimelineViewer/types'; +import DraggableManager, { DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; import GraphTicks from './GraphTicks'; -import Scrubber from './Scrubber'; -import ViewingLayer, { dragTypes, getStyles } from './ViewingLayer'; +import Scrubber, { ScrubberProps } from './Scrubber'; +import ViewingLayer, { dragTypes, getStyles, ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer'; -function getViewRange(viewStart, viewEnd) { +function getViewRange(viewStart: number, viewEnd: number) { return { time: { - current: [viewStart, viewEnd], + current: [viewStart, viewEnd] as [number, number], }, }; } @@ -35,8 +36,8 @@ function getViewRange(viewStart, viewEnd) { describe('', () => { polyfillAnimationFrame(window); - let props; - let wrapper; + let props: ViewingLayerProps; + let wrapper: ShallowWrapper; beforeEach(() => { props = { @@ -45,7 +46,8 @@ describe('', () => { updateNextViewRangeTime: jest.fn(), updateViewRangeTime: jest.fn(), viewRange: getViewRange(0, 1), - }; + } as unknown as ViewingLayerProps; + wrapper = shallow() .dive() .dive(); @@ -57,11 +59,12 @@ describe('', () => { wrapper = shallow() .dive() .dive(); + wrapper.instance()._setRoot({ getBoundingClientRect() { return { left: 10, width: 100 }; }, - }); + } as SVGElement); }); it('throws if _root is not set', () => { @@ -105,81 +108,81 @@ describe('', () => { describe('reframe', () => { it('handles mousemove', () => { const value = 0.5; - wrapper.instance()._handleReframeMouseMove({ value }); - const calls = props.updateNextViewRangeTime.mock.calls; + wrapper.instance()._handleReframeMouseMove({ value } as DraggingUpdate); + const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; expect(calls).toEqual([[{ cursor: value }]]); }); it('handles mouseleave', () => { wrapper.instance()._handleReframeMouseLeave(); - const calls = props.updateNextViewRangeTime.mock.calls; + const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; expect(calls).toEqual([[{ cursor: null }]]); }); describe('drag update', () => { it('handles sans anchor', () => { const value = 0.5; - wrapper.instance()._handleReframeDragUpdate({ value }); - const calls = props.updateNextViewRangeTime.mock.calls; + wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); + const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]); }); it('handles the existing anchor', () => { const value = 0.5; const anchor = 0.1; - const time = { ...props.viewRange.time, reframe: { anchor } }; + const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; props = { ...props, viewRange: { time } }; wrapper = shallow() .dive() .dive(); - wrapper.instance()._handleReframeDragUpdate({ value }); - const calls = props.updateNextViewRangeTime.mock.calls; + wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); + const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]); }); }); describe('drag end', () => { - let manager; + let manager: DraggableManager; beforeEach(() => { - manager = { resetBounds: jest.fn() }; + manager = { resetBounds: jest.fn() } as unknown as DraggableManager; }); it('handles sans anchor', () => { const value = 0.5; - wrapper.instance()._handleReframeDragEnd({ manager, value }); - expect(manager.resetBounds.mock.calls).toEqual([[]]); - const calls = props.updateViewRangeTime.mock.calls; + wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); + expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); + const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; expect(calls).toEqual([[value, value, 'minimap']]); }); it('handles dragged left (anchor is greater)', () => { const value = 0.5; const anchor = 0.6; - const time = { ...props.viewRange.time, reframe: { anchor } }; + const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; props = { ...props, viewRange: { time } }; wrapper = shallow() .dive() .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value }); + wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - expect(manager.resetBounds.mock.calls).toEqual([[]]); - const calls = props.updateViewRangeTime.mock.calls; + expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); + const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; expect(calls).toEqual([[value, anchor, 'minimap']]); }); it('handles dragged right (anchor is less)', () => { const value = 0.5; const anchor = 0.4; - const time = { ...props.viewRange.time, reframe: { anchor } }; + const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; props = { ...props, viewRange: { time } }; wrapper = shallow() .dive() .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value }); + wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - expect(manager.resetBounds.mock.calls).toEqual([[]]); - const calls = props.updateViewRangeTime.mock.calls; + expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); + const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; expect(calls).toEqual([[anchor, value, 'minimap']]); }); }); @@ -187,12 +190,12 @@ describe('', () => { describe('scrubber', () => { it('prevents the cursor from being drawn on scrubber mouseover', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter }); + wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter } as DraggingUpdate); expect(wrapper.state('preventCursorLine')).toBe(true); }); it('prevents the cursor from being drawn on scrubber mouseleave', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave }); + wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave } as DraggingUpdate); expect(wrapper.state('preventCursorLine')).toBe(false); }); @@ -203,7 +206,7 @@ describe('', () => { event: { stopPropagation }, type: EUpdateTypes.DragStart, }; - wrapper.instance()._handleScrubberDragUpdate(update); + wrapper.instance()._handleScrubberDragUpdate(update as unknown as DraggingUpdate); expect(stopPropagation.mock.calls).toEqual([[]]); }); @@ -229,7 +232,7 @@ describe('', () => { }, ]; cases.forEach((_case) => { - instance._handleScrubberDragUpdate(_case.dragUpdate); + instance._handleScrubberDragUpdate(_case.dragUpdate as DraggingUpdate); expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate); }); }); @@ -261,9 +264,9 @@ describe('', () => { const { manager } = _case.dragUpdate; wrapper.setState({ preventCursorLine: true }); expect(wrapper.state('preventCursorLine')).toBe(true); - instance._handleScrubberDragEnd(_case.dragUpdate); + instance._handleScrubberDragEnd(_case.dragUpdate as unknown as DraggingUpdate); expect(wrapper.state('preventCursorLine')).toBe(false); - expect(manager.resetBounds.mock.calls).toEqual([[]]); + expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap'); }); }); @@ -315,7 +318,7 @@ describe('', () => { const leftBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); expect(leftBox.length).toBe(1); - const width = Number(leftBox.prop('width').slice(0, -1)); + const width = Number(leftBox.prop('width')?.toString().slice(0, -1)); const x = leftBox.prop('x'); expect(Math.round(width)).toBe(20); expect(x).toBe(0); @@ -329,17 +332,17 @@ describe('', () => { const rightBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); expect(rightBox.length).toBe(1); - const width = Number(rightBox.prop('width').slice(0, -1)); - const x = Number(rightBox.prop('x').slice(0, -1)); + const width = Number(rightBox.prop('width')?.toString().slice(0, -1)); + const x = Number(rightBox.prop('x')?.toString().slice(0, -1)); expect(Math.round(width)).toBe(20); expect(x).toBe(80); }); it('renders handles for the timeRangeFilter', () => { const [viewStart, viewEnd] = props.viewRange.time.current; - let scrubber = ; + let scrubber = ; expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); - scrubber = ; + scrubber = ; expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); }); }); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx index e4090032a4c..17025ba4a65 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -89,7 +89,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => { }; }); -type ViewingLayerProps = { +export type ViewingLayerProps = { height: number; numTicks: number; updateViewRangeTime: TUpdateViewRangeTimeFunction; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx similarity index 84% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx index d76f4aab944..ba8d949e2fc 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import traceGenerator from '../../demo/trace-generators'; @@ -21,9 +21,9 @@ import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnim import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; -import ViewingLayer from './ViewingLayer'; +import ViewingLayer, { ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer'; -import SpanGraph from './index'; +import SpanGraph, { SpanGraphProps } from './index'; describe('', () => { polyfillAnimationFrame(window); @@ -39,10 +39,10 @@ describe('', () => { }, }; - let wrapper; + let wrapper: ShallowWrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('renders a ', () => { @@ -54,7 +54,7 @@ describe('', () => { }); it('returns a
if a trace is not provided', () => { - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.matchesElement(
)).toBeTruthy(); }); @@ -68,7 +68,7 @@ describe('', () => { it('passes items to CanvasSpanGraph', () => { const canvasGraph = wrapper.find(CanvasSpanGraph).first(); - const items = trace.spans.map((span) => ({ + const items = trace?.spans.map((span) => ({ valueOffset: span.relativeStartTime, valueWidth: span.duration, serviceName: span.process.serviceName, diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx index 6bab52a4b67..8133aaaa8e9 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx @@ -27,7 +27,7 @@ import ViewingLayer from './ViewingLayer'; const DEFAULT_HEIGHT = 60; const TIMELINE_TICK_INTERVAL = 4; -type SpanGraphProps = { +export type SpanGraphProps = { height?: number; trace: Trace; viewRange: ViewRange; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.ts similarity index 78% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.ts index 2b822025a84..a4a5082fa06 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.ts @@ -15,7 +15,6 @@ import { range as _range } from 'lodash'; import renderIntoCanvas, { - BG_COLOR, ITEM_ALPHA, MIN_ITEM_HEIGHT, MAX_TOTAL_HEIGHT, @@ -24,8 +23,10 @@ import renderIntoCanvas, { MAX_ITEM_HEIGHT, } from './render-into-canvas'; +const BG_COLOR = '#FFFFFF'; + const getCanvasWidth = () => window.innerWidth * 2; -const getBgFillRect = (items) => ({ +const getBgFillRect = (items?: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>) => ({ fillStyle: BG_COLOR, height: !items || items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(MAX_TOTAL_HEIGHT, items.length), width: getCanvasWidth(), @@ -37,12 +38,15 @@ describe('renderIntoCanvas()', () => { const basicItem = { valueWidth: 100, valueOffset: 50, serviceName: 'some-name' }; class CanvasContext { + fillStyle: undefined; + fillRectAccumulator: Array<{ fillStyle: undefined; height: number; width: number; x: number; y: number }> = []; + constructor() { this.fillStyle = undefined; this.fillRectAccumulator = []; } - fillRect(x, y, width, height) { + fillRect(x: number, y: number, width: number, height: number) { const fillStyle = this.fillStyle; this.fillRectAccumulator.push({ fillStyle, @@ -55,6 +59,11 @@ describe('renderIntoCanvas()', () => { } class Canvas { + height: number; + width: number; + contexts: CanvasContext[]; + getContext: jest.Mock; + constructor() { this.contexts = []; this.height = NaN; @@ -71,13 +80,13 @@ describe('renderIntoCanvas()', () => { function getColorFactory() { let i = 0; - const inputOutput = []; - function getFakeColor(str) { - const rv = [i, i, i]; + const inputOutput: Array<{ input: string; output: [number, number, number] }> = []; + function getFakeColor(str: string) { + const rv: [number, number, number] = [i, i, i]; i++; inputOutput.push({ input: str, - output: rv.slice(), + output: rv.slice() as [number, number, number], }); return rv; } @@ -88,7 +97,7 @@ describe('renderIntoCanvas()', () => { it('sets the width', () => { const canvas = new Canvas(); expect(canvas.width !== canvas.width).toBe(true); - renderIntoCanvas(canvas, [basicItem], 150, getColorFactory()); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, [basicItem], 150, getColorFactory(), BG_COLOR); expect(canvas.width).toBe(getCanvasWidth()); }); @@ -96,18 +105,18 @@ describe('renderIntoCanvas()', () => { it('sets the height', () => { const canvas = new Canvas(); expect(canvas.height !== canvas.height).toBe(true); - renderIntoCanvas(canvas, [basicItem], 150, getColorFactory()); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, [basicItem], 150, getColorFactory(), BG_COLOR); expect(canvas.height).toBe(MIN_TOTAL_HEIGHT); }); it('draws the background', () => { const expectedDrawing = [getBgFillRect()]; const canvas = new Canvas(); - const items = []; + const items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }> = []; const totalValueWidth = 4000; const getFillColor = getColorFactory(); - renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); - expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR); + expect((canvas.getContext as jest.Mock).mock.calls).toEqual([['2d', { alpha: false }]]); expect(canvas.contexts.length).toBe(1); expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawing); }); @@ -141,7 +150,7 @@ describe('renderIntoCanvas()', () => { ]; const canvas = new Canvas(); const getFillColor = getColorFactory(); - renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR); expect(getFillColor.inputOutput).toEqual(expectedColors); expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); expect(canvas.contexts.length).toBe(1); @@ -157,7 +166,7 @@ describe('renderIntoCanvas()', () => { items.push(basicItem); } expect(canvas.height !== canvas.height).toBe(true); - renderIntoCanvas(canvas, items, 150, getColorFactory()); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, 150, getColorFactory(), BG_COLOR); expect(canvas.height).toBe(items.length); }); @@ -187,9 +196,9 @@ describe('renderIntoCanvas()', () => { ]; const canvas = new Canvas(); const getFillColor = getColorFactory(); - renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); + renderIntoCanvas(canvas as unknown as HTMLCanvasElement, items, totalValueWidth, getFillColor, BG_COLOR); expect(getFillColor.inputOutput).toEqual(expectedColors); - expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); + expect((canvas.getContext as jest.Mock).mock.calls).toEqual([['2d', { alpha: false }]]); expect(canvas.contexts.length).toBe(1); expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings); }); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.tsx similarity index 77% rename from packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js rename to packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.tsx index f0338d287b9..f79f8734233 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.tsx @@ -16,9 +16,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { createTheme } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import TracePageSearchBar, { getStyles } from './TracePageSearchBar'; +import TracePageSearchBar, { getStyles, TracePageSearchBarProps } from './TracePageSearchBar'; const defaultProps = { forwardedRef: React.createRef(), @@ -30,8 +29,8 @@ const defaultProps = { describe('', () => { describe('truthy textFilter', () => { it('renders UiFindInput with correct props', () => { - render(); - expect(screen.getByPlaceholderText('Find...')['value']).toEqual('value'); + render(); + expect((screen.getByPlaceholderText('Find...') as HTMLInputElement)['value']).toEqual('value'); const suffix = screen.getByLabelText('Search bar suffix'); const theme = createTheme(); expect(suffix['className']).toBe(getStyles(theme).TracePageSearchBarSuffix); @@ -39,13 +38,13 @@ describe('', () => { }); it('renders buttons', () => { - render(); + render(); const nextResButton = screen.queryByRole('button', { name: 'Next results button' }); const prevResButton = screen.queryByRole('button', { name: 'Prev results button' }); expect(nextResButton).toBeInTheDocument(); expect(prevResButton).toBeInTheDocument(); - expect(nextResButton['disabled']).toBe(false); - expect(prevResButton['disabled']).toBe(false); + expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false); + expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false); }); it('only shows navigable buttons when navigable is true', () => { @@ -53,7 +52,7 @@ describe('', () => { ...defaultProps, navigable: false, }; - render(); + render(); expect(screen.queryByRole('button', { name: 'Next results button' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Prev results button' })).not.toBeInTheDocument(); }); @@ -65,7 +64,7 @@ describe('', () => { ...defaultProps, searchValue: '', }; - render(); + render(); }); it('does not render suffix', () => { diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index 7c7f5b7e803..84a4a3c6c82 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -69,7 +69,7 @@ export const getStyles = (theme: GrafanaTheme2) => { }; }; -type TracePageSearchBarProps = { +export type TracePageSearchBarProps = { navigable: boolean; searchValue: string; setSearch: (value: string) => void; diff --git a/yarn.lock b/yarn.lock index ed8c5a77922..28f495ade83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4966,6 +4966,7 @@ __metadata: "@testing-library/user-event": 14.4.3 "@types/classnames": ^2.2.7 "@types/deep-freeze": ^0.1.1 + "@types/enzyme": 3.10.12 "@types/hoist-non-react-statics": ^3.3.1 "@types/jest": 29.2.3 "@types/lodash": 4.14.187 From f1fb202284a961bd8e3229742e57cc834a201078 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Tue, 6 Dec 2022 15:12:27 +0100 Subject: [PATCH 20/94] Chore: Add encryption codec to the remote cache (#59871) * add encryption codec to the remote cache * change config files too * fix test constructor * pass codec into the test cache --- conf/defaults.ini | 3 ++ conf/sample.ini | 3 ++ pkg/infra/remotecache/database_storage.go | 8 +-- .../remotecache/database_storage_test.go | 2 + pkg/infra/remotecache/memcached_storage.go | 12 +++-- pkg/infra/remotecache/redis_storage.go | 11 ++-- pkg/infra/remotecache/remotecache.go | 52 ++++++++++++++++--- pkg/infra/remotecache/remotecache_test.go | 6 ++- pkg/infra/remotecache/testing.go | 3 +- .../contexthandler/auth_proxy_test.go | 3 +- pkg/setting/setting.go | 15 +++--- 11 files changed, 87 insertions(+), 31 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index cba6bc9bb40..cedead7db8e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -165,6 +165,9 @@ connstr = # prefix prepended to all the keys in the remote cache prefix = +# This enables encryption of values stored in the remote cache +encryption = + #################################### Data proxy ########################### [dataproxy] diff --git a/conf/sample.ini b/conf/sample.ini index 6789bd286a2..30e4e65cdcc 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -172,6 +172,9 @@ # prefix prepended to all the keys in the remote cache ; prefix = +# This enables encryption of values stored in the remote cache +;encryption = + #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go index 2b63dd6428a..be5be00011d 100644 --- a/pkg/infra/remotecache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -14,12 +14,14 @@ const databaseCacheType = "database" type databaseCache struct { SQLStore db.DB + codec codec log log.Logger } -func newDatabaseCache(sqlstore db.DB) *databaseCache { +func newDatabaseCache(sqlstore db.DB, codec codec) *databaseCache { dc := &databaseCache{ SQLStore: sqlstore, + codec: codec, log: log.New("remotecache.database"), } @@ -78,7 +80,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro } } - if err = decodeGob(cacheHit.Data, item); err != nil { + if err = dc.codec.Decode(ctx, cacheHit.Data, item); err != nil { return err } @@ -90,7 +92,7 @@ func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, erro func (dc *databaseCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { item := &cachedItem{Val: value} - data, err := encodeGob(item) + data, err := dc.codec.Encode(ctx, item) if err != nil { return err } diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go index 87727683180..6a595c5ff7f 100644 --- a/pkg/infra/remotecache/database_storage_test.go +++ b/pkg/infra/remotecache/database_storage_test.go @@ -16,6 +16,7 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, + codec: &gobCodec{}, log: log.New("remotecache.database"), } @@ -64,6 +65,7 @@ func TestSecondSet(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, + codec: &gobCodec{}, log: log.New("remotecache.database"), } diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index 56b331cfe99..b8c1b4e4823 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -11,12 +11,14 @@ import ( const memcachedCacheType = "memcached" type memcachedStorage struct { - c *memcache.Client + c *memcache.Client + codec codec } -func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { +func newMemcachedStorage(opts *setting.RemoteCacheOptions, codec codec) *memcachedStorage { return &memcachedStorage{ - c: memcache.New(opts.ConnStr), + c: memcache.New(opts.ConnStr), + codec: codec, } } @@ -31,7 +33,7 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { // Set sets value to given key in the cache. func (s *memcachedStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} - bytes, err := encodeGob(item) + bytes, err := s.codec.Encode(ctx, item) if err != nil { return err } @@ -58,7 +60,7 @@ func (s *memcachedStorage) Get(ctx context.Context, key string) (interface{}, er item := &cachedItem{} - err = decodeGob(memcachedItem.Value, item) + err = s.codec.Decode(ctx, memcachedItem.Value, item) if err != nil { return nil, err } diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index bc7909b0162..7c76345a5bf 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -15,7 +15,8 @@ import ( const redisCacheType = "redis" type redisStorage struct { - c *redis.Client + c *redis.Client + codec codec } // parseRedisConnStr parses k=v pairs in csv and builds a redis Options object @@ -76,18 +77,18 @@ func parseRedisConnStr(connStr string) (*redis.Options, error) { return options, nil } -func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) { +func newRedisStorage(opts *setting.RemoteCacheOptions, codec codec) (*redisStorage, error) { opt, err := parseRedisConnStr(opts.ConnStr) if err != nil { return nil, err } - return &redisStorage{c: redis.NewClient(opt)}, nil + return &redisStorage{c: redis.NewClient(opt), codec: codec}, nil } // Set sets value to given key in session. func (s *redisStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} - value, err := encodeGob(item) + value, err := s.codec.Encode(ctx, item) if err != nil { return err } @@ -100,7 +101,7 @@ func (s *redisStorage) Get(ctx context.Context, key string) (interface{}, error) v := s.c.Get(ctx, key) item := &cachedItem{} - err := decodeGob([]byte(v.Val()), item) + err := s.codec.Decode(ctx, []byte(v.Val()), item) if err == nil { return item.Val, nil diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index 8f572b05e1a..dcbd81f1dd5 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" ) @@ -29,8 +30,14 @@ const ( ServiceName = "RemoteCache" ) -func ProvideService(cfg *setting.Cfg, sqlStore db.DB) (*RemoteCache, error) { - client, err := createClient(cfg.RemoteCacheOptions, sqlStore) +func ProvideService(cfg *setting.Cfg, sqlStore db.DB, secretsService secrets.Service) (*RemoteCache, error) { + var codec codec + if cfg.RemoteCacheOptions.Encryption { + codec = &encryptionCodec{secretsService} + } else { + codec = &gobCodec{} + } + client, err := createClient(cfg.RemoteCacheOptions, sqlStore, codec) if err != nil { return nil, err } @@ -97,14 +104,14 @@ func (ds *RemoteCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (cache CacheStorage, err error) { +func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) (cache CacheStorage, err error) { switch opts.Name { case redisCacheType: - cache, err = newRedisStorage(opts) + cache, err = newRedisStorage(opts, codec) case memcachedCacheType: - cache = newMemcachedStorage(opts) + cache = newMemcachedStorage(opts, codec) case databaseCacheType: - cache = newDatabaseCache(sqlstore) + cache = newDatabaseCache(sqlstore, codec) default: return nil, ErrInvalidCacheType } @@ -131,17 +138,46 @@ type cachedItem struct { Val interface{} } -func encodeGob(item *cachedItem) ([]byte, error) { +type codec interface { + Encode(context.Context, *cachedItem) ([]byte, error) + Decode(context.Context, []byte, *cachedItem) error +} + +type gobCodec struct{} + +func (c *gobCodec) Encode(_ context.Context, item *cachedItem) ([]byte, error) { buf := bytes.NewBuffer(nil) err := gob.NewEncoder(buf).Encode(item) return buf.Bytes(), err } -func decodeGob(data []byte, out *cachedItem) error { +func (c *gobCodec) Decode(_ context.Context, data []byte, out *cachedItem) error { buf := bytes.NewBuffer(data) return gob.NewDecoder(buf).Decode(&out) } +type encryptionCodec struct { + secretsService secrets.Service +} + +func (c *encryptionCodec) Encode(ctx context.Context, item *cachedItem) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(item) + if err != nil { + return nil, err + } + return c.secretsService.Encrypt(ctx, buf.Bytes(), secrets.WithoutScope()) +} + +func (c *encryptionCodec) Decode(ctx context.Context, data []byte, out *cachedItem) error { + decrypted, err := c.secretsService.Decrypt(ctx, data) + if err != nil { + return err + } + buf := bytes.NewBuffer(decrypted) + return gob.NewDecoder(buf).Decode(&out) +} + type prefixCacheStorage struct { cache CacheStorage prefix string diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index b72291a519d..7e0947c75d7 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) @@ -28,7 +29,7 @@ func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore d cfg := &setting.Cfg{ RemoteCacheOptions: opts, } - dc, err := ProvideService(cfg, sqlstore) + dc, err := ProvideService(cfg, sqlstore, fakes.NewFakeSecretsService()) require.Nil(t, err, "Failed to init client for test") return dc @@ -46,7 +47,7 @@ func TestCachedBasedOnConfig(t *testing.T) { } func TestInvalidCacheTypeReturnsError(t *testing.T) { - _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil) + _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil, &gobCodec{}) assert.Equal(t, err, ErrInvalidCacheType) } @@ -95,6 +96,7 @@ func TestCachePrefix(t *testing.T) { cache := &databaseCache{ SQLStore: db, log: log.New("remotecache.database"), + codec: &gobCodec{}, } prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} diff --git a/pkg/infra/remotecache/testing.go b/pkg/infra/remotecache/testing.go index 008fbaafced..f42252aa7c1 100644 --- a/pkg/infra/remotecache/testing.go +++ b/pkg/infra/remotecache/testing.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) @@ -22,7 +23,7 @@ func NewFakeStore(t *testing.T) *RemoteCache { dc, err := ProvideService(&setting.Cfg{ RemoteCacheOptions: opts, - }, sqlStore) + }, sqlStore, fakes.NewFakeSecretsService()) require.NoError(t, err, "Failed to init remote cache for test") return dc diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 55737a44720..b53d2935273 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" @@ -79,7 +80,7 @@ func getContextHandler(t *testing.T) *ContextHandler { cfg.AuthProxyHeaderName = "X-Killa" cfg.AuthProxyEnabled = true cfg.AuthProxyHeaderProperty = "username" - remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore) + remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore, fakes.NewFakeSecretsService()) require.NoError(t, err) userAuthTokenSvc := authtest.NewFakeUserAuthTokenService() renderSvc := &fakeRenderService{} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 81ad5fb72bc..f9a6e1f96e1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1121,11 +1121,13 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { dbName := valueAsString(cacheServer, "type", "database") connStr := valueAsString(cacheServer, "connstr", "") prefix := valueAsString(cacheServer, "prefix", "") + encryption := cacheServer.Key("encryption").MustBool(false) cfg.RemoteCacheOptions = &RemoteCacheOptions{ - Name: dbName, - ConnStr: connStr, - Prefix: prefix, + Name: dbName, + ConnStr: connStr, + Prefix: prefix, + Encryption: encryption, } geomapSection := iniFile.Section("geomap") @@ -1159,9 +1161,10 @@ func valueAsString(section *ini.Section, keyName string, defaultValue string) st } type RemoteCacheOptions struct { - Name string - ConnStr string - Prefix string + Name string + ConnStr string + Prefix string + Encryption bool } func (cfg *Cfg) readLDAPConfig() { From 1dbc22d18bfb6ba1f695079808661baa75ccbdb6 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Dec 2022 10:36:43 -0500 Subject: [PATCH 21/94] Build: don't remove grafana-server and grafana-cli binaries from deb and rpm packages (#59890) * don't remove grafana-server and grafana-cli binaries from /usr/share/grafana/bin in deb and rpm packages * don't add config overrides in /usr/sbin/grafana-server --- packaging/wrappers/grafana-server | 24 +----------------------- pkg/build/packaging/grafana.go | 12 ------------ 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/packaging/wrappers/grafana-server b/packaging/wrappers/grafana-server index d8a4fa930fc..466b0d7c690 100755 --- a/packaging/wrappers/grafana-server +++ b/packaging/wrappers/grafana-server @@ -5,18 +5,8 @@ # the system-wide Grafana configuration that was bundled with the package as we # use the binary. -DEFAULT=/etc/default/grafana - GRAFANA_HOME="${GRAFANA_HOME:-/usr/share/grafana}" -CONF_DIR=/etc/grafana -DATA_DIR=/var/lib/grafana -PLUGINS_DIR=/var/lib/grafana/plugins -LOG_DIR=/var/log/grafana - -CONF_FILE=$CONF_DIR/grafana.ini -PROVISIONING_CFG_DIR=$CONF_DIR/provisioning - EXECUTABLE="$GRAFANA_HOME/bin/grafana" if [ ! -x $EXECUTABLE ]; then @@ -24,18 +14,6 @@ if [ ! -x $EXECUTABLE ]; then exit 5 fi -# overwrite settings from default file -if [ -f "$DEFAULT" ]; then - . "$DEFAULT" -fi - -OPTS="--homepath=${GRAFANA_HOME} \ - --config=${CONF_FILE} \ - --configOverrides='cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR \ - cfg:default.paths.data=${DATA_DIR} \ - cfg:default.paths.logs=${LOG_DIR} \ - cfg:default.paths.plugins=${PLUGINS_DIR}'" - CMD=server -eval $EXECUTABLE "$CMD" "$OPTS" "$@" +eval $EXECUTABLE "$CMD" "$@" diff --git a/pkg/build/packaging/grafana.go b/pkg/build/packaging/grafana.go index 7c8331f8a70..4500f72f569 100644 --- a/pkg/build/packaging/grafana.go +++ b/pkg/build/packaging/grafana.go @@ -340,18 +340,6 @@ func createPackage(srcDir string, options linuxPackageOptions) error { return err } - // remove unneeded binaries, these are exposed via wrappers that provide the needed configuration - for _, fileName := range []string{ - cliBinary, - cliBinary + ".md5", - serverBinary, - serverBinary + ".md5", - } { - if err := os.Remove(filepath.Join(packageRoot, options.homeBinDir, fileName)); err != nil { - return fmt.Errorf("failed to remove %q: %w", filepath.Join(options.homeBinDir, fileName), err) - } - } - if err := executeFPM(options, packageRoot, srcDir); err != nil { return err } From bc5badbcd8f17f788ac2020f3a07b75b8e648a38 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Tue, 6 Dec 2022 18:42:11 +0200 Subject: [PATCH 22/94] CI: Add `enterprise2` flag to unblock enterprise2 image deployments (#59904) Add hacky flag to unblock enterprise2 --- pkg/build/cmd/main.go | 4 ++++ pkg/build/cmd/uploadpackages.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index d8863710085..bf60c54e7e2 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -168,6 +168,10 @@ func main() { Flags: []cli.Flag{ &jobsFlag, &editionFlag, + &cli.BoolFlag{ + Name: "enterprise2", + Usage: "Declare if the edition is enterprise2", + }, }, }, { diff --git a/pkg/build/cmd/uploadpackages.go b/pkg/build/cmd/uploadpackages.go index 8da2c22b14e..581af791ebc 100644 --- a/pkg/build/cmd/uploadpackages.go +++ b/pkg/build/cmd/uploadpackages.go @@ -86,7 +86,7 @@ func UploadPackages(c *cli.Context) error { distDir: distDir, } - if cfg.edition == config.EditionEnterprise2 { + if cfg.edition == config.EditionEnterprise2 || c.Bool("enterprise2") { if releaseModeConfig.Buckets.ArtifactsEnterprise2 != "" { cfg.Config.Bucket = releaseModeConfig.Buckets.ArtifactsEnterprise2 } else { From 18d09cd3fe676354b9369a44d391748effdd8895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 6 Dec 2022 18:21:03 +0100 Subject: [PATCH 23/94] PanelEdit: Fixes broken panel edit splitter logic (#59859) * Alt panel edit fix * fix fix --- .../SplitPaneWrapper/SplitPaneWrapper.tsx | 6 -- .../components/PanelEditor/PanelEditor.tsx | 76 +++++++++---------- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx index 69816af915f..dc8702752dd 100644 --- a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx +++ b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx @@ -141,12 +141,6 @@ const getStyles = (theme: GrafanaTheme2, hasSplit: boolean) => { `; return { - singleLeftPane: css` - height: 100%; - position: absolute; - overflow: hidden; - width: 100%; - `, resizerV: cx( resizer, css` diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 8dea8470fbe..f6ba01a7ede 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -241,32 +241,45 @@ export class PanelEditorUnconnected extends PureComponent { ); } - renderPanelAndEditor(styles: EditorStyles) { + renderPanelAndEditor(uiState: PanelEditorUIState, styles: EditorStyles) { const { panel, dashboard, plugin, tab } = this.props; const tabs = getPanelEditorTabs(tab, plugin); const isOnlyPanel = tabs.length === 0; const panelPane = this.renderPanel(styles, isOnlyPanel); if (tabs.length === 0) { - return panelPane; + return
{panelPane}
; } - return [ - panelPane, -
{ + if (size) { + updatePanelEditorUIState({ topPaneSize: size / window.innerHeight }); + } + }} > - -
, - ]; + {panelPane} +
+ +
+ + ); } renderTemplateVariables(styles: EditorStyles) { @@ -433,25 +446,6 @@ export class PanelEditorUnconnected extends PureComponent { ); } - renderHorizontalSplit(uiState: PanelEditorUIState, styles: EditorStyles) { - return ( - { - if (size) { - updatePanelEditorUIState({ topPaneSize: size / window.innerHeight }); - } - }} - > - {this.renderPanelAndEditor(styles)} - - ); - } - render() { const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState } = this.props; const styles = getStyles(theme, this.props); @@ -472,7 +466,7 @@ export class PanelEditorUnconnected extends PureComponent {
{!uiState.isPanelOptionsVisible ? ( - this.renderHorizontalSplit(uiState, styles) + this.renderPanelAndEditor(uiState, styles) ) : ( { } }} > - {this.renderHorizontalSplit(uiState, styles)} + {this.renderPanelAndEditor(uiState, styles)} {this.renderOptionsPane()} )} @@ -569,6 +563,12 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, props: Props) => { position: relative; flex-direction: column; `, + onlyPanel: css` + height: 100%; + position: absolute; + overflow: hidden; + width: 100%; + `, }; }); From a85adeed965fe72e3e297b93fa2197d98e64a10b Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 6 Dec 2022 12:33:15 -0500 Subject: [PATCH 24/94] Alerting: Update state history service to filter states transitions (#58863) * rename the method to better reflect its behavior * make historian filter transition on itself * call historian with all changes --- .../ngalert/state/historian/annotation.go | 14 ++- .../state/historian/annotation_test.go | 100 ++++++++++++++++++ pkg/services/ngalert/state/manager.go | 25 +---- pkg/services/ngalert/state/persist.go | 2 +- pkg/services/ngalert/state/state.go | 2 +- pkg/services/ngalert/state/testing.go | 2 +- 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 pkg/services/ngalert/state/historian/annotation_test.go diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index c94805178af..9687b463592 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" @@ -34,7 +35,7 @@ func NewAnnotationHistorian(annotations annotations.Repository, dashboards dashb } // RecordStates writes a number of state transitions for a given rule to state history. -func (h *AnnotationStateHistorian) RecordStates(ctx context.Context, rule *ngmodels.AlertRule, states []state.StateTransition) { +func (h *AnnotationStateHistorian) RecordStatesAsync(ctx context.Context, rule *ngmodels.AlertRule, states []state.StateTransition) { logger := h.log.FromContext(ctx) // Build annotations before starting goroutine, to make sure all data is copied and won't mutate underneath us. annotations := h.buildAnnotations(rule, states, logger) @@ -45,6 +46,9 @@ func (h *AnnotationStateHistorian) RecordStates(ctx context.Context, rule *ngmod func (h *AnnotationStateHistorian) buildAnnotations(rule *ngmodels.AlertRule, states []state.StateTransition, logger log.Logger) []annotations.Item { items := make([]annotations.Item, 0, len(states)) for _, state := range states { + if !shouldAnnotate(state) { + continue + } logger.Debug("Alert state changed creating annotation", "newState", state.Formatted(), "oldState", state.PreviousFormatted()) annotationText, annotationData := buildAnnotationTextAndData(rule, state.State) @@ -154,3 +158,11 @@ func removePrivateLabels(labels data.Labels) data.Labels { } return result } + +func shouldAnnotate(transition state.StateTransition) bool { + // Do not log not transitioned states normal states if it was marked as stale + if !transition.Changed() || transition.StateReason == ngmodels.StateReasonMissingSeries && transition.PreviousState == eval.Normal && transition.State.State == eval.Normal { + return false + } + return true +} diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go new file mode 100644 index 00000000000..eb008035c3c --- /dev/null +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -0,0 +1,100 @@ +package historian + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/ngalert/eval" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state" +) + +func TestShouldAnnotate(t *testing.T) { + allStates := []eval.State{ + eval.Normal, + eval.Alerting, + eval.Pending, + eval.NoData, + eval.Error, + } + + type Transition struct { + State eval.State + StateReason string + PreviousState eval.State + PreviousStateReason string + } + + transition := func(from eval.State, fromReason string, to eval.State, toReason string) Transition { + return Transition{ + PreviousState: from, + PreviousStateReason: fromReason, + State: to, + StateReason: toReason, + } + } + + noTransition := func(state eval.State, stateReason string) Transition { + return transition(state, stateReason, state, stateReason) + } + + knownReasons := []string{ + "", + models.StateReasonMissingSeries, + eval.Error.String(), + eval.NoData.String(), + } + + allCombinations := make([]Transition, 0, len(allStates)*len(allStates)*len(knownReasons)*len(knownReasons)) + for _, from := range allStates { + for _, reasonFrom := range knownReasons { + for _, to := range allStates { + for _, reasonTo := range knownReasons { + allCombinations = append(allCombinations, transition(from, reasonFrom, to, reasonTo)) + } + } + } + } + + negativeTransitions := map[Transition]struct{}{ + noTransition(eval.Normal, ""): {}, + noTransition(eval.Normal, eval.Error.String()): {}, + noTransition(eval.Normal, eval.NoData.String()): {}, + noTransition(eval.Normal, models.StateReasonMissingSeries): {}, + noTransition(eval.Alerting, ""): {}, + noTransition(eval.Alerting, eval.Error.String()): {}, + noTransition(eval.Alerting, eval.NoData.String()): {}, + noTransition(eval.Alerting, models.StateReasonMissingSeries): {}, + noTransition(eval.Pending, ""): {}, + noTransition(eval.Pending, eval.Error.String()): {}, + noTransition(eval.Pending, eval.NoData.String()): {}, + noTransition(eval.Pending, models.StateReasonMissingSeries): {}, + noTransition(eval.NoData, ""): {}, + noTransition(eval.NoData, eval.Error.String()): {}, + noTransition(eval.NoData, eval.NoData.String()): {}, + noTransition(eval.NoData, models.StateReasonMissingSeries): {}, + noTransition(eval.Error, ""): {}, + noTransition(eval.Error, eval.Error.String()): {}, + noTransition(eval.Error, eval.NoData.String()): {}, + noTransition(eval.Error, models.StateReasonMissingSeries): {}, + + transition(eval.Normal, "", eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, eval.Error.String(), eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, eval.NoData.String(), eval.Normal, models.StateReasonMissingSeries): {}, + } + + for _, tc := range allCombinations { + _, ok := negativeTransitions[tc] + trans := state.StateTransition{ + State: &state.State{State: tc.State, StateReason: tc.StateReason}, + PreviousState: tc.PreviousState, + PreviousStateReason: tc.PreviousStateReason, + } + + t.Run(fmt.Sprintf("%s -> %s should be %v", trans.PreviousFormatted(), trans.Formatted(), !ok), func(t *testing.T) { + require.Equal(t, !ok, shouldAnnotate(trans)) + }) + } +} diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 388adb81403..11e48ebc804 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -181,7 +181,10 @@ func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time st.saveAlertStates(ctx, logger, states...) - st.logStateTransitions(ctx, alertRule, states, staleStates) + allChanges := append(states, staleStates...) + if st.historian != nil { + st.historian.RecordStatesAsync(ctx, alertRule, allChanges) + } nextStates := make([]*State, 0, len(states)) for _, s := range states { @@ -316,26 +319,6 @@ func (st *Manager) saveAlertStates(ctx context.Context, logger log.Logger, state } } -func (st *Manager) logStateTransitions(ctx context.Context, alertRule *ngModels.AlertRule, newStates, staleStates []StateTransition) { - if st.historian == nil { - return - } - changedStates := make([]StateTransition, 0, len(staleStates)) - for _, s := range newStates { - if s.changed() { - changedStates = append(changedStates, s) - } - } - - // TODO refactor further. Let historian decide what to log. Current logic removes states `Normal (reason-X) -> Normal (reason-Y)` - for _, t := range staleStates { - if t.PreviousState == eval.Alerting { - changedStates = append(changedStates, t) - } - } - st.historian.RecordStates(ctx, alertRule, changedStates) -} - func (st *Manager) deleteAlertStates(ctx context.Context, logger log.Logger, states []StateTransition) { if st.instanceStore == nil || len(states) == 0 { return diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index 78bcd181daf..1dd8a0366ab 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -23,7 +23,7 @@ type RuleReader interface { // Historian maintains an audit log of alert state history. type Historian interface { // RecordStates writes a number of state transitions for a given rule to state history. - RecordStates(ctx context.Context, rule *models.AlertRule, states []StateTransition) + RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) } // ImageCapturer captures images. diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 839dc014c2c..d0892db5d74 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -103,7 +103,7 @@ func (c StateTransition) PreviousFormatted() string { return FormatStateAndReason(c.PreviousState, c.PreviousStateReason) } -func (c StateTransition) changed() bool { +func (c StateTransition) Changed() bool { return c.PreviousState != c.State.State || c.PreviousStateReason != c.State.StateReason } diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index 80c2a565e45..d1511fc972a 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -49,7 +49,7 @@ func (f *FakeRuleReader) ListAlertRules(_ context.Context, q *models.ListAlertRu type FakeHistorian struct{} -func (f *FakeHistorian) RecordStates(ctx context.Context, rule *models.AlertRule, states []StateTransition) { +func (f *FakeHistorian) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) { } // NotAvailableImageService is a service that returns ErrScreenshotsUnavailable. From abb49d96b5caac19b1ef4dca64a465d98a4f5034 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 6 Dec 2022 13:07:39 -0500 Subject: [PATCH 25/94] Alerting: update state manager to return StateTransition instead of State (#58867) * improve test for stale states * update state manager return StateTransition * update scheduler to accept state transitions --- pkg/services/ngalert/models/testing.go | 6 + pkg/services/ngalert/schedule/compat.go | 6 +- pkg/services/ngalert/schedule/schedule.go | 2 +- pkg/services/ngalert/state/manager.go | 15 +-- pkg/services/ngalert/state/manager_test.go | 145 ++++++++++++--------- pkg/services/ngalert/state/testing.go | 15 ++- 6 files changed, 108 insertions(+), 81 deletions(-) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index f1c343a75bc..29d538e8f99 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -158,6 +158,12 @@ func WithTitle(title string) AlertRuleMutator { } } +func WithFor(duration time.Duration) AlertRuleMutator { + return func(rule *AlertRule) { + rule.For = duration + } +} + func GenerateAlertLabels(count int, prefix string) data.Labels { labels := make(data.Labels, count) for i := 0; i < count; i++ { diff --git a/pkg/services/ngalert/schedule/compat.go b/pkg/services/ngalert/schedule/compat.go index 5e96e052d69..76eaa061d89 100644 --- a/pkg/services/ngalert/schedule/compat.go +++ b/pkg/services/ngalert/schedule/compat.go @@ -130,7 +130,7 @@ func errorAlert(labels, annotations data.Labels, alertState *state.State, urlStr } } -func FromAlertStateToPostableAlerts(firingStates []*state.State, stateManager *state.Manager, appURL *url.URL) apimodels.PostableAlerts { +func FromStateTransitionToPostableAlerts(firingStates []state.StateTransition, stateManager *state.Manager, appURL *url.URL) apimodels.PostableAlerts { alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))} var sentAlerts []*state.State ts := time.Now() @@ -139,13 +139,13 @@ func FromAlertStateToPostableAlerts(firingStates []*state.State, stateManager *s if !alertState.NeedsSending(stateManager.ResendDelay) { continue } - alert := stateToPostableAlert(alertState, appURL) + alert := stateToPostableAlert(alertState.State, appURL) alerts.PostableAlerts = append(alerts.PostableAlerts, *alert) if alertState.StateReason == ngModels.StateReasonMissingSeries { // do not put stale state back to state manager continue } alertState.LastSentAt = ts - sentAlerts = append(sentAlerts, alertState) + sentAlerts = append(sentAlerts, alertState.State) } stateManager.Put(sentAlerts) return alerts diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 12c6ea4bb62..503885fea5d 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -371,7 +371,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR return } processedStates := sch.stateManager.ProcessEvalResults(ctx, e.scheduledAt, e.rule, results, sch.getRuleExtraLabels(e)) - alerts := FromAlertStateToPostableAlerts(processedStates, sch.stateManager, sch.appURL) + alerts := FromStateTransitionToPostableAlerts(processedStates, sch.stateManager, sch.appURL) if len(alerts.PostableAlerts) > 0 { sch.alertsSender.Send(key, alerts) } diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 11e48ebc804..9e687647135 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -167,7 +167,7 @@ func (st *Manager) ResetStateByRuleUID(ctx context.Context, ruleKey ngModels.Ale // ProcessEvalResults updates the current states that belong to a rule with the evaluation results. // if extraLabels is not empty, those labels will be added to every state. The extraLabels take precedence over rule labels and result labels -func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels) []*State { +func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels) []StateTransition { logger := st.log.FromContext(ctx) logger.Debug("State manager processing evaluation results", "resultCount", len(results)) var states []StateTransition @@ -185,18 +185,7 @@ func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time if st.historian != nil { st.historian.RecordStatesAsync(ctx, alertRule, allChanges) } - - nextStates := make([]*State, 0, len(states)) - for _, s := range states { - nextStates = append(nextStates, s.State) - } - // TODO refactor further. Do not filter because it will be filtered downstream - for _, s := range staleStates { - if s.PreviousState == eval.Alerting { - nextStates = append(nextStates, s.State) - } - } - return nextStates + return allChanges } // Set the current state based on evaluation results diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 69a8d792093..075f3b3a822 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -2309,7 +2309,18 @@ func TestStaleResults(t *testing.T) { return key } - checkExpectedStates := func(t *testing.T, actual []*state.State, expected map[string]struct{}) { + checkExpectedStates := func(t *testing.T, actual []*state.State, expected map[string]struct{}) map[string]*state.State { + t.Helper() + result := make(map[string]*state.State) + require.Len(t, actual, len(expected)) + for _, currentState := range actual { + _, ok := expected[currentState.CacheID] + result[currentState.CacheID] = currentState + require.Truef(t, ok, "State %s is not expected. States: %v", currentState.CacheID, expected) + } + return result + } + checkExpectedStateTransitions := func(t *testing.T, actual []state.StateTransition, expected map[string]struct{}) { t.Helper() require.Len(t, actual, len(expected)) for _, currentState := range actual { @@ -2318,75 +2329,83 @@ func TestStaleResults(t *testing.T) { } } + ctx := context.Background() + clk := clock.NewMock() + + store := &state.FakeInstanceStore{} + + st := state.NewManager(testMetrics.GetStateMetrics(), nil, store, &state.NoopImageService{}, clk, &state.FakeHistorian{}) + + rule := models.AlertRuleGen(models.WithFor(0))() + + initResults := eval.Results{ + eval.ResultGen(eval.WithEvaluatedAt(clk.Now()))(), + eval.ResultGen(eval.WithState(eval.Alerting), eval.WithEvaluatedAt(clk.Now()))(), + eval.ResultGen(eval.WithState(eval.Normal), eval.WithEvaluatedAt(clk.Now()))(), + } + + state1 := getCacheID(t, rule, initResults[0]) + state2 := getCacheID(t, rule, initResults[1]) + state3 := getCacheID(t, rule, initResults[2]) + + initStates := map[string]struct{}{ + state1: {}, + state2: {}, + state3: {}, + } + + // Init + processed := st.ProcessEvalResults(ctx, clk.Now(), rule, initResults, nil) + checkExpectedStateTransitions(t, processed, initStates) + + currentStates := st.GetStatesForRuleUID(rule.OrgID, rule.UID) + statesMap := checkExpectedStates(t, currentStates, initStates) + require.Equal(t, eval.Alerting, statesMap[state2].State) // make sure the state is alerting because we need it to be resolved later + + staleDuration := 2 * time.Duration(rule.IntervalSeconds) * time.Second + clk.Add(staleDuration) + result := initResults[0] + result.EvaluatedAt = clk.Now() + results := eval.Results{ + result, + } + + var expectedStaleKeys []models.AlertInstanceKey t.Run("should mark missing states as stale", func(t *testing.T) { - // init - ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) - clk := clock.NewMock() - clk.Set(time.Now()) - - st := state.NewManager(testMetrics.GetStateMetrics(), nil, dbstore, &state.NoopImageService{}, clk, &state.FakeHistorian{}) - - orgID := rand.Int63() - rule := tests.CreateTestAlertRule(t, ctx, dbstore, 10, orgID) - - initResults := eval.Results{ - eval.Result{ - Instance: data.Labels{"test1": "testValue1"}, - State: eval.Alerting, - EvaluatedAt: clk.Now(), - }, - eval.Result{ - Instance: data.Labels{"test1": "testValue2"}, - State: eval.Alerting, - EvaluatedAt: clk.Now(), - }, - eval.Result{ - Instance: data.Labels{"test1": "testValue3"}, - State: eval.Normal, - EvaluatedAt: clk.Now(), - }, - } - - initStates := map[string]struct{}{ - getCacheID(t, rule, initResults[0]): {}, - getCacheID(t, rule, initResults[1]): {}, - getCacheID(t, rule, initResults[2]): {}, - } - - // Init - processed := st.ProcessEvalResults(ctx, clk.Now(), rule, initResults, nil) - checkExpectedStates(t, processed, initStates) - currentStates := st.GetStatesForRuleUID(orgID, rule.UID) - checkExpectedStates(t, currentStates, initStates) - - staleDuration := 2 * time.Duration(rule.IntervalSeconds) * time.Second - clk.Add(staleDuration) - results := eval.Results{ - eval.Result{ - Instance: data.Labels{"test1": "testValue1"}, - State: eval.Alerting, - EvaluatedAt: clk.Now(), - }, - } - clk.Add(time.Nanosecond) // we use time now when calculate stale states. Evaluation tick and real time are not the same. usually, difference is way greater than nanosecond. - expectedStaleReturned := getCacheID(t, rule, initResults[1]) processed = st.ProcessEvalResults(ctx, clk.Now(), rule, results, nil) - checkExpectedStates(t, processed, map[string]struct{}{ - getCacheID(t, rule, results[0]): {}, - expectedStaleReturned: {}, - }) + checkExpectedStateTransitions(t, processed, initStates) for _, s := range processed { - if s.CacheID == expectedStaleReturned { - assert.Truef(t, s.Resolved, "Returned stale state should have Resolved set to true") - assert.Equal(t, eval.Normal, s.State) - assert.Equal(t, models.StateReasonMissingSeries, s.StateReason) - break + if s.CacheID == state1 { + continue } + assert.Equal(t, eval.Normal, s.State.State) + assert.Equal(t, models.StateReasonMissingSeries, s.StateReason) + assert.Equal(t, clk.Now(), s.EndsAt) + if s.CacheID == state2 { + assert.Truef(t, s.Resolved, "Returned stale state should have Resolved set to true") + } + key, err := s.GetAlertInstanceKey() + require.NoError(t, err) + expectedStaleKeys = append(expectedStaleKeys, key) } - currentStates = st.GetStatesForRuleUID(orgID, rule.UID) + }) + + t.Run("should remove stale states from cache", func(t *testing.T) { + currentStates = st.GetStatesForRuleUID(rule.OrgID, rule.UID) checkExpectedStates(t, currentStates, map[string]struct{}{ getCacheID(t, rule, results[0]): {}, }) }) + + t.Run("should delete stale states from the database", func(t *testing.T) { + for _, op := range store.RecordedOps { + switch q := op.(type) { + case state.FakeInstanceStoreOp: + keys, ok := q.Args[1].([]models.AlertInstanceKey) + require.Truef(t, ok, "Failed to parse fake store operations") + require.Len(t, keys, 2) + require.EqualValues(t, expectedStaleKeys, keys) + } + } + }) } diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index d1511fc972a..917c785eed4 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -15,6 +15,11 @@ type FakeInstanceStore struct { RecordedOps []interface{} } +type FakeInstanceStoreOp struct { + Name string + Args []interface{} +} + func (f *FakeInstanceStore) ListAlertInstances(_ context.Context, q *models.ListAlertInstancesQuery) error { f.mtx.Lock() defer f.mtx.Unlock() @@ -33,7 +38,15 @@ func (f *FakeInstanceStore) SaveAlertInstances(_ context.Context, q ...models.Al func (f *FakeInstanceStore) FetchOrgIds(_ context.Context) ([]int64, error) { return []int64{}, nil } -func (f *FakeInstanceStore) DeleteAlertInstances(_ context.Context, _ ...models.AlertInstanceKey) error { +func (f *FakeInstanceStore) DeleteAlertInstances(ctx context.Context, q ...models.AlertInstanceKey) error { + f.mtx.Lock() + defer f.mtx.Unlock() + f.RecordedOps = append(f.RecordedOps, FakeInstanceStoreOp{ + Name: "DeleteAlertInstances", Args: []interface{}{ + ctx, + q, + }, + }) return nil } From 7e339decfe3d3a75af55ed63fc4f665b58899e83 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 6 Dec 2022 19:38:55 +0100 Subject: [PATCH 26/94] JavaScript Unit Tests: Enable shouldFailOnLog only for CI environments (#59901) Tests: enable shouldFailOnLog only for CI environments --- public/test/setupTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index 20052ded8bc..31f322f1d4a 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -6,7 +6,7 @@ import { initReactI18next } from 'react-i18next'; import { matchers } from './matchers'; failOnConsole({ - //shouldFailOnLog: true, + shouldFailOnLog: process.env.CI ? true : false, }); expect.extend(matchers); From 3c249e1b99403daeda7ba1bfb70191a1a11be391 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 6 Dec 2022 18:44:06 +0000 Subject: [PATCH 27/94] Fix incorrect start time for DatasourceError alerts (#59903) --- pkg/services/ngalert/state/manager_test.go | 2 +- pkg/services/ngalert/state/state.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 075f3b3a822..8ee29ab0efd 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -1868,7 +1868,7 @@ func TestProcessEvalResults(t *testing.T) { Values: make(map[string]*float64), }, }, - StartsAt: evaluationTime.Add(20 * time.Second), + StartsAt: evaluationTime.Add(30 * time.Second), EndsAt: evaluationTime.Add(50 * time.Second).Add(state.ResendDelay * 3), LastEvaluationTime: evaluationTime.Add(50 * time.Second), EvaluationDuration: evaluationDuration, diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index d0892db5d74..65ba71c72e2 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -198,6 +198,11 @@ func (a *State) resultError(alertRule *models.AlertRule, result eval.Result) { // is unavailable or queries against the datasource returns errors, and is // then resolved as soon as the datasource is available and queries return // without error + if a.State != execErrState { + // Set the start time if the state changes from Alerting to Error or from + // Error to Alerting + a.StartsAt = result.EvaluatedAt + } a.State = execErrState a.setEndsAt(alertRule, result) case eval.Pending: From 9e349bf9b08947c4b2d54b44c59c66ba09418a3a Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Tue, 6 Dec 2022 23:21:12 +0400 Subject: [PATCH 28/94] EntityStore: support filtering by labels (#59905) * label filtering * filtering in sql * filtering in sql group by * label is unique - no need for distinct * capitalize * fix capitalization --- .../store/entity/sqlstash/querybuilder.go | 5 + .../entity/sqlstash/sql_storage_server.go | 31 +++++-- .../entity/tests/server_integration_test.go | 92 +++++++++++++++++++ .../testdata/dashboard-with-tags-b-g.json | 40 ++++++++ .../testdata/dashboard-with-tags-r-g.json | 40 ++++++++ 5 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 pkg/services/store/entity/tests/testdata/dashboard-with-tags-b-g.json create mode 100644 pkg/services/store/entity/tests/testdata/dashboard-with-tags-r-g.json diff --git a/pkg/services/store/entity/sqlstash/querybuilder.go b/pkg/services/store/entity/sqlstash/querybuilder.go index ac13956f2af..4f4ed45f4eb 100644 --- a/pkg/services/store/entity/sqlstash/querybuilder.go +++ b/pkg/services/store/entity/sqlstash/querybuilder.go @@ -17,6 +17,11 @@ func (q *selectQuery) addWhere(f string, val interface{}) { q.where = append(q.where, f+"=?") } +func (q *selectQuery) addWhereInSubquery(f string, subquery string, subqueryArgs []interface{}) { + q.args = append(q.args, subqueryArgs...) + q.where = append(q.where, f+" IN ("+subquery+")") +} + func (q *selectQuery) addWhereIn(f string, vals []string) { count := len(vals) if count > 1 { diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 022dd9eb938..3dc4d90e7a5 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -623,7 +623,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ return nil, fmt.Errorf("missing user in context") } - if r.NextPageToken != "" || len(r.Sort) > 0 || len(r.Labels) > 0 { + if r.NextPageToken != "" || len(r.Sort) > 0 { return nil, fmt.Errorf("not yet supported") } @@ -637,6 +637,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ if r.WithBody { fields = append(fields, "body") } + if r.WithLabels { fields = append(fields, "labels") } @@ -644,25 +645,40 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ fields = append(fields, "fields") } - selectQuery := selectQuery{ + entityQuery := selectQuery{ fields: fields, from: "entity", // the table args: []interface{}{}, limit: int(r.Limit), oneExtra: true, // request one more than the limit (and show next token if it exists) } - selectQuery.addWhere("tenant_id", user.OrgID) + entityQuery.addWhere("tenant_id", user.OrgID) if len(r.Kind) > 0 { - selectQuery.addWhereIn("kind", r.Kind) + entityQuery.addWhereIn("kind", r.Kind) } // Folder UID or OID? if r.Folder != "" { - selectQuery.addWhere("folder", r.Folder) + entityQuery.addWhere("folder", r.Folder) } - query, args := selectQuery.toQuery() + if len(r.Labels) > 0 { + var args []interface{} + var conditions []string + for labelKey, labelValue := range r.Labels { + args = append(args, labelKey) + args = append(args, labelValue) + conditions = append(conditions, "(label = ? AND value = ?)") + } + joinedConditions := strings.Join(conditions, " OR ") + query := "SELECT grn FROM entity_labels WHERE " + joinedConditions + " GROUP BY grn HAVING COUNT(label) = ?" + args = append(args, len(r.Labels)) + + entityQuery.addWhereInSubquery("grn", query, args) + } + + query, args := entityQuery.toQuery() fmt.Printf("\n\n-------------\n") fmt.Printf("%s\n", query) @@ -704,7 +720,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ } // found one more than requested - if len(rsp.Results) >= selectQuery.limit { + if len(rsp.Results) >= entityQuery.limit { // TODO? should this encode start+offset? rsp.NextPageToken = oid break @@ -732,5 +748,6 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ rsp.Results = append(rsp.Results, result) } + return rsp, err } diff --git a/pkg/services/store/entity/tests/server_integration_test.go b/pkg/services/store/entity/tests/server_integration_test.go index 17f45487399..c83ff12f6ae 100644 --- a/pkg/services/store/entity/tests/server_integration_test.go +++ b/pkg/services/store/entity/tests/server_integration_test.go @@ -1,6 +1,7 @@ package entity_server_tests import ( + _ "embed" "encoding/json" "fmt" "reflect" @@ -15,6 +16,13 @@ import ( "google.golang.org/grpc/metadata" ) +var ( + //go:embed testdata/dashboard-with-tags-b-g.json + dashboardWithTagsBlueGreen string + //go:embed testdata/dashboard-with-tags-r-g.json + dashboardWithTagsRedGreen string +) + type rawEntityMatcher struct { grn *entity.GRN createdRange []time.Time @@ -379,4 +387,88 @@ func TestIntegrationEntityServer(t *testing.T) { w2.Entity.Version, }, version) }) + + t.Run("should be able to filter objects based on their labels", func(t *testing.T) { + kind := models.StandardKindDashboard + _, err := testCtx.client.Write(ctx, &entity.WriteEntityRequest{ + GRN: &entity.GRN{ + Kind: kind, + UID: "blue-green", + }, + Body: []byte(dashboardWithTagsBlueGreen), + }) + require.NoError(t, err) + + _, err = testCtx.client.Write(ctx, &entity.WriteEntityRequest{ + GRN: &entity.GRN{ + Kind: kind, + UID: "red-green", + }, + Body: []byte(dashboardWithTagsRedGreen), + }) + require.NoError(t, err) + + search, err := testCtx.client.Search(ctx, &entity.EntitySearchRequest{ + Kind: []string{kind}, + WithBody: false, + WithLabels: true, + Labels: map[string]string{ + "red": "", + }, + }) + require.NoError(t, err) + require.NotNil(t, search) + require.Len(t, search.Results, 1) + require.Equal(t, search.Results[0].GRN.UID, "red-green") + + search, err = testCtx.client.Search(ctx, &entity.EntitySearchRequest{ + Kind: []string{kind}, + WithBody: false, + WithLabels: true, + Labels: map[string]string{ + "red": "", + "green": "", + }, + }) + require.NoError(t, err) + require.NotNil(t, search) + require.Len(t, search.Results, 1) + require.Equal(t, search.Results[0].GRN.UID, "red-green") + + search, err = testCtx.client.Search(ctx, &entity.EntitySearchRequest{ + Kind: []string{kind}, + WithBody: false, + WithLabels: true, + Labels: map[string]string{ + "red": "invalid", + }, + }) + require.NoError(t, err) + require.NotNil(t, search) + require.Len(t, search.Results, 0) + + search, err = testCtx.client.Search(ctx, &entity.EntitySearchRequest{ + Kind: []string{kind}, + WithBody: false, + WithLabels: true, + Labels: map[string]string{ + "green": "", + }, + }) + require.NoError(t, err) + require.NotNil(t, search) + require.Len(t, search.Results, 2) + + search, err = testCtx.client.Search(ctx, &entity.EntitySearchRequest{ + Kind: []string{kind}, + WithBody: false, + WithLabels: true, + Labels: map[string]string{ + "yellow": "", + }, + }) + require.NoError(t, err) + require.NotNil(t, search) + require.Len(t, search.Results, 0) + }) } diff --git a/pkg/services/store/entity/tests/testdata/dashboard-with-tags-b-g.json b/pkg/services/store/entity/tests/testdata/dashboard-with-tags-b-g.json new file mode 100644 index 00000000000..444842ed2fd --- /dev/null +++ b/pkg/services/store/entity/tests/testdata/dashboard-with-tags-b-g.json @@ -0,0 +1,40 @@ +{ + "tags": [ + "blue", + "green" + ], + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 221, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 8, + "title": "Row title", + "type": "row" + } + ], + "schemaVersion": 36, + "style": "dark", + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "special ds", + "uid": "mocpwtR4k", + "version": 1, + "weekStart": "" +} diff --git a/pkg/services/store/entity/tests/testdata/dashboard-with-tags-r-g.json b/pkg/services/store/entity/tests/testdata/dashboard-with-tags-r-g.json new file mode 100644 index 00000000000..2239008252d --- /dev/null +++ b/pkg/services/store/entity/tests/testdata/dashboard-with-tags-r-g.json @@ -0,0 +1,40 @@ +{ + "tags": [ + "red", + "green" + ], + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 221, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 8, + "title": "Row title", + "type": "row" + } + ], + "schemaVersion": 36, + "style": "dark", + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "special ds", + "uid": "mocpwtR4k", + "version": 1, + "weekStart": "" +} From fdc3adb2c920cbf5f06a8092a57151b6b6bd1e00 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 6 Dec 2022 21:15:55 +0100 Subject: [PATCH 29/94] Loki: Add tracking for on dashboard load (#59914) Loki: Add on dashboard load tracking --- .../plugins/datasource/loki/module.test.ts | 90 +++++++++++++ public/app/plugins/datasource/loki/module.ts | 8 +- .../app/plugins/datasource/loki/tracking.ts | 118 ++++++++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/loki/module.test.ts create mode 100644 public/app/plugins/datasource/loki/tracking.ts diff --git a/public/app/plugins/datasource/loki/module.test.ts b/public/app/plugins/datasource/loki/module.test.ts new file mode 100644 index 00000000000..a99b7fb1987 --- /dev/null +++ b/public/app/plugins/datasource/loki/module.test.ts @@ -0,0 +1,90 @@ +import { DashboardLoadedEvent } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import './module'; + +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + getAppEvents: () => ({ + subscribe: jest.fn((_, handler) => { + // Trigger test event + handler( + new DashboardLoadedEvent({ + dashboardId: 'dashboard123', + orgId: 1, + userId: 2, + grafanaVersion: 'v9.0.0', + queries: { + loki: [ + { + datasource: { type: 'loki', uid: 'abc' }, + editorMode: 'builder', + expr: '{place="$place"} |= ``', + queryType: 'range', + refId: 'A', + }, + { + datasource: { type: 'loki', uid: 'abc' }, + editorMode: 'builder', + expr: '{place="$place"} |= `error`', + maxLines: 60, + queryType: 'instant', + refId: 'A', + }, + { + datasource: { type: 'loki', uid: 'abc' }, + editorMode: 'builder', + expr: 'count_over_time({place="$place"} [5m])', + legendFormat: '{{place}}', + maxLines: 60, + queryType: 'range', + refId: 'A', + resolution: 1, + }, + { + datasource: { type: 'loki', uid: 'abc' }, + editorMode: 'code', + expr: 'count_over_time({place="moon"} [5m])', + legendFormat: '{{place}}', + queryType: 'range', + refId: 'A', + }, + { + datasource: { type: 'loki', uid: 'abc' }, + editorMode: 'code', + expr: 'count_over_time({place="luna"} [5m])', + legendFormat: '{{place}}', + queryType: 'range', + refId: 'A', + }, + ], + }, + }) + ); + }), + }), + }; +}); + +describe('queriesOnInitDashboard', () => { + it('should report a grafana_loki_dashboard_loaded interaction ', () => { + // subscribeDashboardLoadedEvent(); + expect(reportInteraction).toHaveBeenCalledWith('grafana_loki_dashboard_loaded', { + builder_mode_queries_count: 3, + grafana_version: 'v9.0.0', + dashboard_id: 'dashboard123', + org_id: 1, + code_mode_queries_count: 2, + instant_queries_count: 1, + logs_queries_count: 2, + metric_queries_count: 3, + queries_count: 5, + queries_with_changed_legend_count: 3, + queries_with_changed_line_limit_count: 5, + queries_with_changed_resolution_count: 0, + queries_with_template_variables_count: 2, + range_queries_count: 4, + }); + }); +}); diff --git a/public/app/plugins/datasource/loki/module.ts b/public/app/plugins/datasource/loki/module.ts index dddfa023aed..10ee51588d3 100644 --- a/public/app/plugins/datasource/loki/module.ts +++ b/public/app/plugins/datasource/loki/module.ts @@ -1,11 +1,17 @@ -import { DataSourcePlugin } from '@grafana/data'; +import { DashboardLoadedEvent, DataSourcePlugin } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; import LokiCheatSheet from './components/LokiCheatSheet'; import LokiQueryEditorByApp from './components/LokiQueryEditorByApp'; import { ConfigEditor } from './configuration/ConfigEditor'; import { LokiDatasource } from './datasource'; +import { onDashboardLoadedHandler } from './tracking'; +import { LokiQuery } from './types'; export const plugin = new DataSourcePlugin(LokiDatasource) .setQueryEditor(LokiQueryEditorByApp) .setConfigEditor(ConfigEditor) .setQueryEditorHelp(LokiCheatSheet); + +// Subscribe to on dashboard loaded event so that we can track plugin adoption +getAppEvents().subscribe>(DashboardLoadedEvent, onDashboardLoadedHandler); diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts new file mode 100644 index 00000000000..99f32fa4a27 --- /dev/null +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -0,0 +1,118 @@ +import { DashboardLoadedEvent } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { variableRegex } from 'app/features/variables/utils'; + +import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; + +import pluginJson from './plugin.json'; +import { getNormalizedLokiQuery, isLogsQuery } from './queryUtils'; +import { LokiQuery, LokiQueryType } from './types'; + +type LokiOnDashboardLoadedTrackingEvent = { + grafana_version?: string; + dashboard_id?: string; + org_id?: number; + + /* The number of Loki queries present in the dashboard*/ + queries_count: number; + + /* The number of Loki logs queries present in the dashboard*/ + logs_queries_count: number; + + /* The number of Loki metric queries present in the dashboard*/ + metric_queries_count: number; + + /* The number of Loki instant queries present in the dashboard*/ + instant_queries_count: number; + + /* The number of Loki range queries present in the dashboard*/ + range_queries_count: number; + + /* The number of Loki queries created in builder mode present in the dashboard*/ + builder_mode_queries_count: number; + + /* The number of Loki queries created in code mode present in the dashboard*/ + code_mode_queries_count: number; + + /* The number of Loki queries with used template variables present in the dashboard*/ + queries_with_template_variables_count: number; + + /* The number of Loki queries with changed resolution present in the dashboard*/ + queries_with_changed_resolution_count: number; + + /* The number of Loki queries with changed line limit present in the dashboard*/ + queries_with_changed_line_limit_count: number; + + /* The number of Loki queries with changed legend present in the dashboard*/ + queries_with_changed_legend_count: number; +}; + +export const onDashboardLoadedHandler = ({ + payload: { dashboardId, orgId, grafanaVersion, queries }, +}: DashboardLoadedEvent) => { + try { + // We only want to track visible Loki queries + const lokiQueries = queries[pluginJson.id] + .filter((query) => !query.hide) + .map((query) => getNormalizedLokiQuery(query)); + + if (!lokiQueries?.length) { + return; + } + + const logsQueries = lokiQueries.filter((query) => isLogsQuery(query.expr)); + const metricQueries = lokiQueries.filter((query) => !isLogsQuery(query.expr)); + const instantQueries = lokiQueries.filter((query) => query.queryType === LokiQueryType.Instant); + const rangeQueries = lokiQueries.filter((query) => query.queryType === LokiQueryType.Range); + const builderModeQueries = lokiQueries.filter((query) => query.editorMode === QueryEditorMode.Builder); + const codeModeQueries = lokiQueries.filter((query) => query.editorMode === QueryEditorMode.Code); + const queriesWithTemplateVariables = lokiQueries.filter(isQueryWithTemplateVariables); + const queriesWithChangedResolution = lokiQueries.filter(isQueryWithChangedResolution); + const queriesWithChangedLineLimit = lokiQueries.filter(isQueryWithChangedLineLimit); + const queriesWithChangedLegend = lokiQueries.filter(isQueryWithChangedLegend); + + const event: LokiOnDashboardLoadedTrackingEvent = { + grafana_version: grafanaVersion, + dashboard_id: dashboardId, + org_id: orgId, + queries_count: lokiQueries.length, + logs_queries_count: logsQueries.length, + metric_queries_count: metricQueries.length, + instant_queries_count: instantQueries.length, + range_queries_count: rangeQueries.length, + builder_mode_queries_count: builderModeQueries.length, + code_mode_queries_count: codeModeQueries.length, + queries_with_template_variables_count: queriesWithTemplateVariables.length, + queries_with_changed_resolution_count: queriesWithChangedResolution.length, + queries_with_changed_line_limit_count: queriesWithChangedLineLimit.length, + queries_with_changed_legend_count: queriesWithChangedLegend.length, + }; + + reportInteraction('grafana_loki_dashboard_loaded', event); + } catch (error) { + console.error('error in loki tracking handler', error); + } +}; + +const isQueryWithTemplateVariables = (query: LokiQuery): boolean => { + return variableRegex.test(query.expr); +}; + +const isQueryWithChangedResolution = (query: LokiQuery): boolean => { + if (!query.resolution) { + return false; + } + // 1 is the default resolution + return query.resolution !== 1; +}; + +const isQueryWithChangedLineLimit = (query: LokiQuery): boolean => { + return query.maxLines !== null || query.maxLines !== undefined; +}; + +const isQueryWithChangedLegend = (query: LokiQuery): boolean => { + if (!query.legendFormat) { + return false; + } + return query.legendFormat !== ''; +}; From 43f40e6c7c12eabb277e447f08697d4be0acfc2a Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Tue, 6 Dec 2022 21:17:17 +0100 Subject: [PATCH 30/94] Chore: Replace yaml.v2 with yaml.v3 (#59897) * replace yaml.v2 with yaml.v3 * fix a few tests due to the yaml.v3 api changes * and another goconvey mistake in tests --- go.mod | 2 +- pkg/services/featuremgmt/settings.go | 2 +- .../ngalert/notifier/channels/util.go | 2 +- .../provisioning/alerting/config_reader.go | 2 +- .../alerting/contact_point_types_test.go | 2 +- .../notification_policy_types_test.go | 2 +- .../provisioning/dashboards/config_reader.go | 2 +- .../provisioning/datasources/config_reader.go | 2 +- .../provisioning/notifiers/config_reader.go | 2 +- .../provisioning/plugins/config_reader.go | 2 +- pkg/services/provisioning/values/values.go | 19 +++++++--------- .../provisioning/values/values_test.go | 22 +++++++++++++------ 12 files changed, 33 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index fc9778ffbe9..1fe70309c60 100644 --- a/go.mod +++ b/go.mod @@ -121,7 +121,7 @@ require ( gopkg.in/ldap.v3 v3.1.0 gopkg.in/mail.v2 v2.3.1 gopkg.in/square/go-jose.v2 v2.5.1 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 xorm.io/builder v0.3.6 xorm.io/core v0.7.3 diff --git a/pkg/services/featuremgmt/settings.go b/pkg/services/featuremgmt/settings.go index 3ff22a61cad..9b813a94a36 100644 --- a/pkg/services/featuremgmt/settings.go +++ b/pkg/services/featuremgmt/settings.go @@ -3,7 +3,7 @@ package featuremgmt import ( "os" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type configBody struct { diff --git a/pkg/services/ngalert/notifier/channels/util.go b/pkg/services/ngalert/notifier/channels/util.go index 244418df1c9..1149f3b40b7 100644 --- a/pkg/services/ngalert/notifier/channels/util.go +++ b/pkg/services/ngalert/notifier/channels/util.go @@ -20,7 +20,7 @@ import ( "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/provisioning/alerting/config_reader.go b/pkg/services/provisioning/alerting/config_reader.go index 66eda559df0..489e5b5776b 100644 --- a/pkg/services/provisioning/alerting/config_reader.go +++ b/pkg/services/provisioning/alerting/config_reader.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/log" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type rulesConfigReader struct { diff --git a/pkg/services/provisioning/alerting/contact_point_types_test.go b/pkg/services/provisioning/alerting/contact_point_types_test.go index 3a64bead6ac..34141f0949e 100644 --- a/pkg/services/provisioning/alerting/contact_point_types_test.go +++ b/pkg/services/provisioning/alerting/contact_point_types_test.go @@ -5,7 +5,7 @@ import ( "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) func TestReceivers(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/notification_policy_types_test.go b/pkg/services/provisioning/alerting/notification_policy_types_test.go index 3538ad23657..d281427e5a6 100644 --- a/pkg/services/provisioning/alerting/notification_policy_types_test.go +++ b/pkg/services/provisioning/alerting/notification_policy_types_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/stretchr/testify/require" ) diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 47547a6fa70..a8aa1a06b3b 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type configReader struct { diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 29345dcdd7a..8b8d73a4496 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go index e9f1f69354c..944527bfacc 100644 --- a/pkg/services/provisioning/notifiers/config_reader.go +++ b/pkg/services/provisioning/notifiers/config_reader.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type configReader struct { diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index f7e9551a775..13ea247da78 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type configReader interface { diff --git a/pkg/services/provisioning/values/values.go b/pkg/services/provisioning/values/values.go index 8d487dbb398..20c0de27b9a 100644 --- a/pkg/services/provisioning/values/values.go +++ b/pkg/services/provisioning/values/values.go @@ -210,8 +210,8 @@ func (val *JSONSliceValue) UnmarshalYAML(unmarshal func(interface{}) error) erro for _, v := range unmarshaled { i := make(map[string]interface{}) r := make(map[string]interface{}) - for key, val := range v.(map[interface{}]interface{}) { - i[key.(string)], r[key.(string)], err = transformInterface(val) + for key, val := range v.(map[string]interface{}) { + i[key], r[key], err = transformInterface(val) if err != nil { return err } @@ -245,7 +245,7 @@ func transformInterface(i interface{}) (interface{}, interface{}, error) { case reflect.Slice: return transformSlice(i.([]interface{})) case reflect.Map: - return transformMap(i.(map[interface{}]interface{})) + return transformMap(i.(map[string]interface{})) case reflect.String: return interpolateValue(i.(string)) default: @@ -268,17 +268,14 @@ func transformSlice(i []interface{}) (interface{}, interface{}, error) { return transformedSlice, rawSlice, nil } -func transformMap(i map[interface{}]interface{}) (interface{}, interface{}, error) { +func transformMap(i map[string]interface{}) (interface{}, interface{}, error) { transformed := make(map[string]interface{}) raw := make(map[string]interface{}) for key, val := range i { - stringKey, ok := key.(string) - if ok { - var err error - transformed[stringKey], raw[stringKey], err = transformInterface(val) - if err != nil { - return nil, nil, err - } + var err error + transformed[key], raw[key], err = transformInterface(val) + if err != nil { + return nil, nil, err } } return transformed, raw, nil diff --git a/pkg/services/provisioning/values/values_test.go b/pkg/services/provisioning/values/values_test.go index 8a3615f21c6..15954efd401 100644 --- a/pkg/services/provisioning/values/values_test.go +++ b/pkg/services/provisioning/values/values_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) func TestValues(t *testing.T) { @@ -39,21 +39,23 @@ func TestValues(t *testing.T) { type Data struct { Val IntValue `yaml:"val"` } - d := &Data{} - t.Run("Should unmarshal simple number", func(t *testing.T) { + d := &Data{} + unmarshalingTest(t, `val: 1`, d) require.Equal(t, d.Val.Value(), 1) require.Equal(t, d.Val.Raw, "1") }) t.Run("Should unmarshal env var", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: $INT`, d) require.Equal(t, d.Val.Value(), 1) require.Equal(t, d.Val.Raw, "$INT") }) t.Run("Should ignore empty value", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: `, d) require.Equal(t, d.Val.Value(), 0) require.Equal(t, d.Val.Raw, "") @@ -64,39 +66,43 @@ func TestValues(t *testing.T) { type Data struct { Val StringValue `yaml:"val"` } - d := &Data{} - t.Run("Should unmarshal simple string", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: test`, d) require.Equal(t, d.Val.Value(), "test") require.Equal(t, d.Val.Raw, "test") }) t.Run("Should unmarshal env var", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: $STRING`, d) require.Equal(t, d.Val.Value(), "test") require.Equal(t, d.Val.Raw, "$STRING") }) t.Run("Should ignore empty value", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: `, d) require.Equal(t, d.Val.Value(), "") require.Equal(t, d.Val.Raw, "") }) t.Run("empty var should have empty value", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: $EMPTYSTRING`, d) require.Equal(t, d.Val.Value(), "") require.Equal(t, d.Val.Raw, "$EMPTYSTRING") }) t.Run("$$ should be a literal $", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: $$`, d) require.Equal(t, d.Val.Value(), "$") require.Equal(t, d.Val.Raw, "$$") }) t.Run("$$ should be a literal $ and not expanded within a string", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: mY,Passwo$$rd`, d) require.Equal(t, d.Val.Value(), "mY,Passwo$rd") require.Equal(t, d.Val.Raw, "mY,Passwo$$rd") @@ -107,27 +113,29 @@ func TestValues(t *testing.T) { type Data struct { Val BoolValue `yaml:"val"` } - d := &Data{} - t.Run("Should unmarshal bool value", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: true`, d) require.True(t, d.Val.Value()) require.Equal(t, d.Val.Raw, "true") }) t.Run("Should unmarshal explicit string", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: "true"`, d) require.True(t, d.Val.Value()) require.Equal(t, d.Val.Raw, "true") }) t.Run("Should unmarshal env var", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: $BOOL`, d) require.True(t, d.Val.Value()) require.Equal(t, d.Val.Raw, "$BOOL") }) t.Run("Should ignore empty value", func(t *testing.T) { + d := &Data{} unmarshalingTest(t, `val: `, d) require.False(t, d.Val.Value()) require.Equal(t, d.Val.Raw, "") From d0eeff2fa0a65a1bcb4735f57f3d37291c978df4 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 6 Dec 2022 21:54:20 +0100 Subject: [PATCH 31/94] Loki: Add tracking of executed queries (#59887) * add query tracking * add app * add comment * use `reportInteraction` not `console.log` * add test to `queryUtils` * organize imports * add datasource tests * change `metrics` to `metric` Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * change `parseToArray` to `parseToNodeNamesArray` Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- .../datasource/loki/datasource.test.ts | 32 +++++++++++++++-- .../app/plugins/datasource/loki/datasource.ts | 29 ++++++++-------- .../datasource/loki/queryUtils.test.ts | 34 +++++++++++++++++++ .../app/plugins/datasource/loki/queryUtils.ts | 11 ++++++ .../app/plugins/datasource/loki/tracking.ts | 22 ++++++++++-- 5 files changed, 110 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 0b9046ce348..4598d5ac948 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -15,17 +15,32 @@ import { LogRowModel, MutableDataFrame, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse, setBackendSrv, getBackendSrv, BackendSrv } from '@grafana/runtime'; +import { + BackendSrv, + BackendSrvRequest, + FetchResponse, + getBackendSrv, + reportInteraction, + setBackendSrv, +} from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; import { CustomVariableModel } from '../../../features/variables/types'; import { LokiDatasource } from './datasource'; -import { createMetadataRequest, createLokiDatasource } from './mocks'; +import { createLokiDatasource, createMetadataRequest } from './mocks'; +import { parseToNodeNamesArray } from './queryUtils'; import { LokiOptions, LokiQuery, LokiQueryType, LokiVariableQueryType } from './types'; import { LokiVariableSupport } from './variables'; +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); + const templateSrvStub = { getAdhocFilters: jest.fn(() => [] as unknown[]), replace: jest.fn((a: string, ...rest: unknown[]) => a), @@ -112,6 +127,7 @@ describe('LokiDatasource', () => { afterEach(() => { setBackendSrv(origBackendSrv); + (reportInteraction as jest.Mock).mockClear(); }); describe('when doing logs queries with limits', () => { @@ -156,6 +172,18 @@ describe('LokiDatasource', () => { it('should use query max lines, if both exist, even if it is higher than ds max lines', async () => { await runTest(80, '40', 80); }); + + it('should report query interaction', async () => { + await runTest(80, '40', 80); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_loki_query_executed', + expect.objectContaining({ + query_type: 'logs', + line_limit: 80, + parsed_query: parseToNodeNamesArray('{a="b"}').join(','), + }) + ); + }); }); describe('When using adhoc filters', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index c323694fa5d..debd615c4b3 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1,8 +1,9 @@ import { cloneDeep, map as lodashMap } from 'lodash'; import { lastValueFrom, merge, Observable, of, throwError } from 'rxjs'; -import { catchError, map, switchMap } from 'rxjs/operators'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { + AbstractQuery, AnnotationEvent, AnnotationQueryRequest, CoreApp, @@ -19,21 +20,20 @@ import { dateMath, DateTime, FieldCache, - AbstractQuery, FieldType, + getDefaultTimeRange, Labels, LoadingState, LogLevel, LogRowModel, + QueryFixAction, + QueryHint, + rangeUtil, ScopedVars, TimeRange, - rangeUtil, toUtc, - QueryHint, - getDefaultTimeRange, - QueryFixAction, } from '@grafana/data'; -import { FetchError, config, DataSourceWithBackend } from '@grafana/runtime'; +import { config, DataSourceWithBackend, FetchError } from '@grafana/runtime'; import { queryLogsVolume } from 'app/core/logsModel'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -62,6 +62,7 @@ import { getQueryHints } from './queryHints'; import { getNormalizedLokiQuery, isLogsQuery, isValidQuery } from './queryUtils'; import { sortDataFrameByTime } from './sortDataFrame'; import { doLokiChannelStream } from './streaming'; +import { trackQuery } from './tracking'; import { LokiOptions, LokiQuery, @@ -191,13 +192,13 @@ export class LokiDatasource if (fixedRequest.liveStreaming) { return this.runLiveQueryThroughBackend(fixedRequest); } else { - return super - .query(fixedRequest) - .pipe( - map((response) => - transformBackendResult(response, fixedRequest.targets, this.instanceSettings.jsonData.derivedFields ?? []) - ) - ); + return super.query(fixedRequest).pipe( + // in case of an empty query, this is somehow run twice. `share()` is no workaround here as the observable is generated from `of()`. + map((response) => + transformBackendResult(response, fixedRequest.targets, this.instanceSettings.jsonData.derivedFields ?? []) + ), + tap((response) => trackQuery(response, fixedRequest.targets, fixedRequest.app)) + ); } } diff --git a/public/app/plugins/datasource/loki/queryUtils.test.ts b/public/app/plugins/datasource/loki/queryUtils.test.ts index 8650682cabd..449d30d5ae2 100644 --- a/public/app/plugins/datasource/loki/queryUtils.test.ts +++ b/public/app/plugins/datasource/loki/queryUtils.test.ts @@ -5,6 +5,7 @@ import { isQueryWithLabelFormat, isQueryWithParser, isValidQuery, + parseToNodeNamesArray, } from './queryUtils'; import { LokiQuery, LokiQueryType } from './types'; @@ -166,6 +167,39 @@ describe('isValidQuery', () => { }); }); +describe('parseToArray', () => { + it('returns on empty query', () => { + expect(parseToNodeNamesArray('{}')).toEqual(['LogQL', 'Expr', 'LogExpr', 'Selector', '⚠']); + }); + it('returns on invalid query', () => { + expect(parseToNodeNamesArray('{job="grafana"')).toEqual([ + 'LogQL', + 'Expr', + 'LogExpr', + 'Selector', + 'Matchers', + 'Matcher', + 'Identifier', + 'Eq', + 'String', + '⚠', + ]); + }); + it('returns on valid query', () => { + expect(parseToNodeNamesArray('{job="grafana"}')).toEqual([ + 'LogQL', + 'Expr', + 'LogExpr', + 'Selector', + 'Matchers', + 'Matcher', + 'Identifier', + 'Eq', + 'String', + ]); + }); +}); + describe('isLogsQuery', () => { it('returns false if metrics query', () => { expect(isLogsQuery('rate({job="grafana"}[5m])')).toBe(false); diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 93613f3f2df..6b5c69777f0 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -108,6 +108,17 @@ export function getNormalizedLokiQuery(query: LokiQuery): LokiQuery { return { ...rest, queryType: LokiQueryType.Range }; } +export function parseToNodeNamesArray(query: string): string[] { + const queryParts: string[] = []; + const tree = parser.parse(query); + tree.iterate({ + enter: ({ name }): false | void => { + queryParts.push(name); + }, + }); + return queryParts; +} + export function isValidQuery(query: string): boolean { let isValid = true; const tree = parser.parse(query); diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index 99f32fa4a27..18fdb94d1e4 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -1,11 +1,11 @@ -import { DashboardLoadedEvent } from '@grafana/data'; +import { DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { variableRegex } from 'app/features/variables/utils'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; import pluginJson from './plugin.json'; -import { getNormalizedLokiQuery, isLogsQuery } from './queryUtils'; +import { getNormalizedLokiQuery, isLogsQuery, parseToNodeNamesArray } from './queryUtils'; import { LokiQuery, LokiQueryType } from './types'; type LokiOnDashboardLoadedTrackingEvent = { @@ -116,3 +116,21 @@ const isQueryWithChangedLegend = (query: LokiQuery): boolean => { } return query.legendFormat !== ''; }; + +export function trackQuery(response: DataQueryResponse, queries: LokiQuery[], app: string): void { + for (const query of queries) { + reportInteraction('grafana_loki_query_executed', { + app, + editor_mode: query.editorMode, + has_data: response.data.some((frame) => frame.length > 0), + has_error: response.error !== undefined, + legend: query.legendFormat, + line_limit: query.maxLines, + parsed_query: parseToNodeNamesArray(query.expr).join(','), + query_type: isLogsQuery(query.expr) ? 'logs' : 'metric', + query_vector_type: query.queryType, + resolution: query.resolution, + simultaneously_sent_query_count: queries.length, + }); + } +} From 440d8a3d889196678ea6b7df4c6868559aabee22 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 7 Dec 2022 02:13:57 -0500 Subject: [PATCH 32/94] Build: Drone starlark file cleanup (#59919) * format drone starlark files with black * clean up unused params * more simplification * more cleanup * more cleanup --- .drone.star | 46 +- Makefile | 3 + scripts/drone/events/cron.star | 78 +- scripts/drone/events/main.star | 56 +- scripts/drone/events/pr.star | 76 +- scripts/drone/events/release.star | 731 ++++++++++-------- scripts/drone/pipelines/build.star | 197 +++-- scripts/drone/pipelines/docs.star | 14 +- .../drone/pipelines/integration_tests.star | 31 +- scripts/drone/pipelines/lint_backend.star | 18 +- scripts/drone/pipelines/lint_frontend.star | 15 +- scripts/drone/pipelines/publish_images.star | 53 +- scripts/drone/pipelines/shellcheck.star | 20 +- scripts/drone/pipelines/test_backend.star | 73 +- scripts/drone/pipelines/test_frontend.star | 58 +- .../drone/pipelines/trigger_downstream.star | 28 +- scripts/drone/pipelines/verify_drone.star | 8 +- scripts/drone/pipelines/windows.star | 133 +--- scripts/drone/services/services.star | 57 +- scripts/drone/steps/lib.star | 632 +++++++-------- scripts/drone/utils/utils.star | 61 +- scripts/drone/vault.star | 73 +- scripts/drone/version.star | 9 +- 23 files changed, 1449 insertions(+), 1021 deletions(-) diff --git a/.drone.star b/.drone.star index 2504f1a394c..1051ac1918c 100644 --- a/.drone.star +++ b/.drone.star @@ -7,17 +7,45 @@ load('scripts/drone/events/pr.star', 'pr_pipelines') load('scripts/drone/events/main.star', 'main_pipelines') load('scripts/drone/pipelines/docs.star', 'docs_pipelines') -load('scripts/drone/events/release.star', 'oss_pipelines', 'enterprise_pipelines', 'enterprise2_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline', 'artifacts_page_pipeline') -load('scripts/drone/pipelines/publish_images.star', 'publish_image_pipelines_public', 'publish_image_pipelines_security') +load( + 'scripts/drone/events/release.star', + 'oss_pipelines', + 'enterprise_pipelines', + 'enterprise2_pipelines', + 'publish_artifacts_pipelines', + 'publish_npm_pipelines', + 'publish_packages_pipeline', + 'artifacts_page_pipeline', +) +load( + 'scripts/drone/pipelines/publish_images.star', + 'publish_image_pipelines_public', + 'publish_image_pipelines_security', +) load('scripts/drone/version.star', 'version_branch_pipelines') load('scripts/drone/events/cron.star', 'cronjobs') load('scripts/drone/vault.star', 'secrets') + def main(ctx): - edition = 'oss' - return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + oss_pipelines() + enterprise_pipelines() + enterprise2_pipelines() + \ - enterprise2_pipelines(prefix='custom-', trigger = {'event': ['custom']},) + \ - publish_image_pipelines_public() + publish_image_pipelines_security() + \ - publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \ - publish_npm_pipelines('public') + publish_packages_pipeline() + artifacts_page_pipeline() + \ - version_branch_pipelines() + cronjobs(edition=edition) + secrets() + return ( + pr_pipelines() + + main_pipelines() + + oss_pipelines() + + enterprise_pipelines() + + enterprise2_pipelines() + + enterprise2_pipelines( + prefix='custom-', + trigger={'event': ['custom']}, + ) + + publish_image_pipelines_public() + + publish_image_pipelines_security() + + publish_artifacts_pipelines('security') + + publish_artifacts_pipelines('public') + + publish_npm_pipelines() + + publish_packages_pipeline() + + artifacts_page_pipeline() + + version_branch_pipelines() + + cronjobs() + + secrets() + ) diff --git a/Makefile b/Makefile index d780ee08657..5c3cf3f5256 100644 --- a/Makefile +++ b/Makefile @@ -233,5 +233,8 @@ drone: $(DRONE) $(DRONE) lint .drone.yml --trusted $(DRONE) --server https://drone.grafana.net sign --save grafana/grafana +format-drone: + black --include '\.star$$' -S scripts/drone/ .drone.star + help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/scripts/drone/events/cron.star b/scripts/drone/events/cron.star index 8684e4948ec..d311214ea72 100644 --- a/scripts/drone/events/cron.star +++ b/scripts/drone/events/cron.star @@ -1,22 +1,23 @@ -load('scripts/drone/vault.star', 'from_secret', 'pull_secret') -load('scripts/drone/steps/lib.star', 'publish_image', 'compile_build_cmd') +load('scripts/drone/vault.star', 'from_secret') +load( + 'scripts/drone/steps/lib.star', + 'publish_image', + 'compile_build_cmd', +) aquasec_trivy_image = 'aquasec/trivy:0.21.0' -def cronjobs(edition): - grafana_com_nightly_pipeline = cron_job_pipeline( - cronName='grafana-com-nightly', - name='grafana-com-nightly', - steps=[compile_build_cmd(),post_to_grafana_com_step()] - ) + +def cronjobs(): return [ - scan_docker_image_pipeline(edition, 'latest'), - scan_docker_image_pipeline(edition, 'main'), - scan_docker_image_pipeline(edition, 'latest-ubuntu'), - scan_docker_image_pipeline(edition, 'main-ubuntu'), - grafana_com_nightly_pipeline, + scan_docker_image_pipeline('latest'), + scan_docker_image_pipeline('main'), + scan_docker_image_pipeline('latest-ubuntu'), + scan_docker_image_pipeline('main-ubuntu'), + grafana_com_nightly_pipeline(), ] + def cron_job_pipeline(cronName, name, steps): return { 'kind': 'pipeline', @@ -36,13 +37,9 @@ def cron_job_pipeline(cronName, name, steps): 'steps': steps, } -def scan_docker_image_pipeline(edition, tag): - if edition != 'oss': - edition='grafana-enterprise' - else: - edition='grafana' - dockerImage='grafana/{}:{}'.format(edition, tag) +def scan_docker_image_pipeline(tag): + dockerImage = 'grafana/{}:{}'.format('grafana', tag) return cron_job_pipeline( cronName='nightly', @@ -51,7 +48,9 @@ def scan_docker_image_pipeline(edition, tag): scan_docker_image_unkown_low_medium_vulnerabilities_step(dockerImage), scan_docker_image_high_critical_vulnerabilities_step(dockerImage), slack_job_failed_step('grafana-backend-ops', dockerImage), - ]) + ], + ) + def scan_docker_image_unkown_low_medium_vulnerabilities_step(dockerImage): return { @@ -62,6 +61,7 @@ def scan_docker_image_unkown_low_medium_vulnerabilities_step(dockerImage): ], } + def scan_docker_image_high_critical_vulnerabilities_step(dockerImage): return { 'name': 'scan-high-critical-vulnerabilities', @@ -71,6 +71,7 @@ def scan_docker_image_high_critical_vulnerabilities_step(dockerImage): ], } + def slack_job_failed_step(channel, image): return { 'name': 'slack-notify-failure', @@ -78,22 +79,33 @@ def slack_job_failed_step(channel, image): 'settings': { 'webhook': from_secret('slack_webhook_backend'), 'channel': channel, - 'template': 'Nightly docker image scan job for ' + image + ' failed: {{build.link}}', + 'template': 'Nightly docker image scan job for ' + + image + + ' failed: {{build.link}}', }, - 'when': { - 'status': 'failure' - } + 'when': {'status': 'failure'}, } + def post_to_grafana_com_step(): return { - 'name': 'post-to-grafana-com', - 'image': publish_image, - 'environment': { - 'GRAFANA_COM_API_KEY': from_secret('grafana_api_key'), - 'GCP_KEY': from_secret('gcp_key'), - }, - 'depends_on': ['compile-build-cmd'], - 'commands': ['./bin/build publish grafana-com --edition oss'], - } + 'name': 'post-to-grafana-com', + 'image': publish_image, + 'environment': { + 'GRAFANA_COM_API_KEY': from_secret('grafana_api_key'), + 'GCP_KEY': from_secret('gcp_key'), + }, + 'depends_on': ['compile-build-cmd'], + 'commands': ['./bin/build publish grafana-com --edition oss'], + } + +def grafana_com_nightly_pipeline(): + return cron_job_pipeline( + cronName='grafana-com-nightly', + name='grafana-com-nightly', + steps=[ + compile_build_cmd(), + post_to_grafana_com_step(), + ], + ) diff --git a/scripts/drone/events/main.star b/scripts/drone/events/main.star index bfb0374ce6b..bc238ecf719 100644 --- a/scripts/drone/events/main.star +++ b/scripts/drone/events/main.star @@ -57,7 +57,9 @@ load('scripts/drone/vault.star', 'from_secret') ver_mode = 'main' trigger = { - 'event': ['push',], + 'event': [ + 'push', + ], 'branch': 'main', 'paths': { 'exclude': [ @@ -68,9 +70,12 @@ trigger = { }, } -def main_pipelines(edition): + +def main_pipelines(): drone_change_trigger = { - 'event': ['push',], + 'event': [ + 'push', + ], 'branch': 'main', 'repo': [ 'grafana/grafana', @@ -86,23 +91,36 @@ def main_pipelines(edition): } pipelines = [ - docs_pipelines(edition, ver_mode, trigger_docs_main()), - test_frontend(trigger, ver_mode), + docs_pipelines(ver_mode, trigger_docs_main()), + test_frontend(trigger, ver_mode, committish='${DRONE_COMMIT}'), lint_frontend_pipeline(trigger, ver_mode), - test_backend(trigger, ver_mode), + test_backend(trigger, ver_mode, committish='${DRONE_COMMIT}'), lint_backend_pipeline(trigger, ver_mode), - build_e2e(trigger, ver_mode, edition), - integration_tests(trigger, ver_mode, edition), - windows(trigger, edition, ver_mode), - notify_pipeline( - name='notify-drone-changes', slack_channel='slack-webhooks-test', trigger=drone_change_trigger, - template=drone_change_template, secret='drone-changes-webhook', - ), - enterprise_downstream_pipeline(edition, ver_mode), - notify_pipeline( - name='main-notify', slack_channel='grafana-ci-notifications', trigger=dict(trigger, status=['failure']), - depends_on=['main-test-frontend', 'main-test-backend', 'main-build-e2e-publish', 'main-integration-tests', 'main-windows'], - template=failure_template, secret='slack_webhook' - )] + build_e2e(trigger, ver_mode), + integration_tests(trigger, prefix=ver_mode), + windows(trigger, edition='oss', ver_mode=ver_mode), + notify_pipeline( + name='notify-drone-changes', + slack_channel='slack-webhooks-test', + trigger=drone_change_trigger, + template=drone_change_template, + secret='drone-changes-webhook', + ), + enterprise_downstream_pipeline(), + notify_pipeline( + name='main-notify', + slack_channel='grafana-ci-notifications', + trigger=dict(trigger, status=['failure']), + depends_on=[ + 'main-test-frontend', + 'main-test-backend', + 'main-build-e2e-publish', + 'main-integration-tests', + 'main-windows', + ], + template=failure_template, + secret='slack_webhook', + ), + ] return pipelines diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index c193cba8a33..3052ac83a3d 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -64,16 +64,73 @@ trigger = { } -def pr_pipelines(edition): +def pr_pipelines(): return [ - verify_drone(get_pr_trigger(include_paths=['scripts/drone/**', '.drone.yml', '.drone.star']), ver_mode), - test_frontend(get_pr_trigger(exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod']), ver_mode), - lint_frontend_pipeline(get_pr_trigger(exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod']), ver_mode), - test_backend(get_pr_trigger(include_paths=['pkg/**', 'packaging/**', '.drone.yml', 'conf/**', 'go.sum', 'go.mod', 'public/app/plugins/**/plugin.json', 'devenv/**']), ver_mode), - lint_backend_pipeline(get_pr_trigger(include_paths=['pkg/**', 'packaging/**', 'conf/**', 'go.sum', 'go.mod', 'public/app/plugins/**/plugin.json', 'devenv/**']), ver_mode), - build_e2e(trigger, ver_mode, edition), - integration_tests(get_pr_trigger(include_paths=['pkg/**', 'packaging/**', '.drone.yml', 'conf/**', 'go.sum', 'go.mod', 'public/app/plugins/**/plugin.json']), ver_mode, edition), - docs_pipelines(edition, ver_mode, trigger_docs_pr()), + verify_drone( + get_pr_trigger( + include_paths=['scripts/drone/**', '.drone.yml', '.drone.star'] + ), + ver_mode, + ), + test_frontend( + get_pr_trigger( + exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod'] + ), + ver_mode, + committish='${DRONE_COMMIT}', + ), + lint_frontend_pipeline( + get_pr_trigger( + exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod'] + ), + ver_mode, + ), + test_backend( + get_pr_trigger( + include_paths=[ + 'pkg/**', + 'packaging/**', + '.drone.yml', + 'conf/**', + 'go.sum', + 'go.mod', + 'public/app/plugins/**/plugin.json', + 'devenv/**', + ] + ), + ver_mode, + committish='${DRONE_COMMIT}', + ), + lint_backend_pipeline( + get_pr_trigger( + include_paths=[ + 'pkg/**', + 'packaging/**', + 'conf/**', + 'go.sum', + 'go.mod', + 'public/app/plugins/**/plugin.json', + 'devenv/**', + ] + ), + ver_mode, + ), + build_e2e(trigger, ver_mode), + integration_tests( + get_pr_trigger( + include_paths=[ + 'pkg/**', + 'packaging/**', + '.drone.yml', + 'conf/**', + 'go.sum', + 'go.mod', + 'public/app/plugins/**/plugin.json', + ] + ), + prefix=ver_mode, + ), + docs_pipelines(ver_mode, trigger_docs_pr()), shellcheck_pipeline(), ] @@ -96,4 +153,3 @@ def get_pr_trigger(include_paths=None, exclude_paths=None): 'include': paths_in, }, } - diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index 702e59dd7b6..560b2c59aa4 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -1,49 +1,48 @@ load( 'scripts/drone/steps/lib.star', - 'disable_tests', - 'clone_enterprise_step', - 'download_grabpl_step', - 'yarn_install_step', - 'wire_install_step', - 'init_enterprise_step', - 'lint_drone_step', - 'build_image', - 'identify_runner_step', - 'publish_image', - 'lint_backend_step', - 'lint_frontend_step', - 'test_backend_step', - 'test_backend_integration_step', - 'test_frontend_step', - 'build_backend_step', - 'build_frontend_step', - 'build_frontend_package_step', - 'build_plugins_step', - 'package_step', - 'grafana_server_step', - 'e2e_tests_step', - 'e2e_tests_artifacts', - 'build_storybook_step', - 'copy_packages_for_docker_step', - 'build_docker_images_step', - 'postgres_integration_tests_step', - 'mysql_integration_tests_step', - 'redis_integration_tests_step', - 'memcached_integration_tests_step', - 'get_windows_steps', + 'artifacts_page_step', 'benchmark_ldap_step', - 'store_storybook_step', - 'upload_packages_step', + 'build_backend_step', + 'build_docker_images_step', + 'build_frontend_package_step', + 'build_frontend_step', + 'build_image', + 'build_plugins_step', + 'build_storybook_step', + 'clone_enterprise_step', + 'compile_build_cmd', + 'copy_packages_for_docker_step', + 'download_grabpl_step', + 'e2e_tests_artifacts', + 'e2e_tests_step', + 'fetch_images_step', + 'get_windows_steps', + 'grafana_server_step', + 'identify_runner_step', + 'init_enterprise_step', + 'lint_backend_step', + 'lint_drone_step', + 'lint_frontend_step', + 'memcached_integration_tests_step', + 'mysql_integration_tests_step', + 'package_step', + 'postgres_integration_tests_step', 'publish_grafanacom_step', - 'upload_cdn_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', + 'publish_image', 'publish_images_step', 'publish_linux_packages_step', + 'redis_integration_tests_step', + 'store_storybook_step', + 'test_backend_integration_step', + 'test_backend_step', + 'test_frontend_step', 'trigger_oss', - 'artifacts_page_step', - 'fetch_images_step', - 'compile_build_cmd', + 'upload_cdn_step', + 'upload_packages_step', + 'verify_gen_cue_step', + 'verify_gen_jsonnet_step', + 'wire_install_step', + 'yarn_install_step', ) load( @@ -59,30 +58,38 @@ load( 'notify_pipeline', 'failure_template', 'drone_change_template', + 'with_deps', ) load( 'scripts/drone/pipelines/test_frontend.star', 'test_frontend', + 'test_frontend_enterprise', ) load( 'scripts/drone/pipelines/test_backend.star', 'test_backend', + 'test_backend_enterprise', ) -load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') +load( + 'scripts/drone/vault.star', + 'from_secret', + 'pull_secret', + 'drone_token', + 'prerelease_bucket', +) -ver_mode='release' +ver_mode = 'release' release_trigger = { - 'event': { - 'exclude': [ - 'promote' - ] - }, - 'ref': ['refs/tags/v*',], + 'event': {'exclude': ['promote']}, + 'ref': [ + 'refs/tags/v*', + ], } + def store_npm_packages_step(): return { 'name': 'store-npm-packages', @@ -92,13 +99,12 @@ def store_npm_packages_step(): ], 'environment': { 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) + 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), }, - 'commands': [ - './bin/grabpl artifacts npm store --tag ${DRONE_TAG}' - ], + 'commands': ['./bin/grabpl artifacts npm store --tag ${DRONE_TAG}'], } + def retrieve_npm_packages_step(): return { 'name': 'retrieve-npm-packages', @@ -109,13 +115,12 @@ def retrieve_npm_packages_step(): 'failure': 'ignore', 'environment': { 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) + 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), }, - 'commands': [ - './bin/grabpl artifacts npm retrieve --tag ${DRONE_TAG}' - ], + 'commands': ['./bin/grabpl artifacts npm retrieve --tag ${DRONE_TAG}'], } + def release_npm_packages_step(): return { 'name': 'release-npm-packages', @@ -127,308 +132,393 @@ def release_npm_packages_step(): 'environment': { 'NPM_TOKEN': from_secret('npm_token'), }, - 'commands': [ - './bin/grabpl artifacts npm release --tag ${DRONE_TAG}' - ], + 'commands': ['./bin/grabpl artifacts npm release --tag ${DRONE_TAG}'], } + def oss_pipelines(ver_mode=ver_mode, trigger=release_trigger): + if ver_mode == 'release': + committish = '${DRONE_TAG}' + elif ver_mode == 'release-branch': + committish = '${DRONE_BRANCH}' + else: + committish = '${DRONE_COMMIT}' + environment = {'EDITION': 'oss'} - edition = 'oss' - services = integration_test_services(edition=edition) + + services = integration_test_services(edition='oss') volumes = integration_test_services_volumes() - package_steps = [] - publish_steps = [] - should_publish = ver_mode == 'release' - should_upload = should_publish or ver_mode in ('release-branch',) + init_steps = [ identify_runner_step(), download_grabpl_step(), - verify_gen_cue_step(edition), + verify_gen_cue_step(), wire_install_step(), yarn_install_step(), compile_build_cmd(), ] 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, ver_mode=ver_mode), + build_backend_step(edition='oss', ver_mode=ver_mode), + build_frontend_step(edition='oss', ver_mode=ver_mode), + build_frontend_package_step(edition='oss', ver_mode=ver_mode), + build_plugins_step(edition='oss', ver_mode=ver_mode), + package_step(edition='oss', ver_mode=ver_mode), + copy_packages_for_docker_step(), + build_docker_images_step(edition='oss', ver_mode=ver_mode, publish=True), + build_docker_images_step( + edition='oss', ver_mode=ver_mode, publish=True, ubuntu=True + ), + grafana_server_step(edition='oss'), + e2e_tests_step('dashboards-suite', tries=3), + e2e_tests_step('smoke-tests-suite', tries=3), + e2e_tests_step('panels-suite', tries=3), + e2e_tests_step('various-suite', tries=3), + e2e_tests_artifacts(), + build_storybook_step(ver_mode=ver_mode), ] + publish_steps = [] + + if ver_mode in ( + 'release', + 'release-branch', + ): + publish_steps.extend( + [ + upload_cdn_step(edition='oss', ver_mode=ver_mode, trigger=trigger_oss), + upload_packages_step( + edition='oss', ver_mode=ver_mode, trigger=trigger_oss + ), + ] + ) + + if ver_mode in ('release',): + publish_steps.extend( + [ + store_storybook_step(ver_mode=ver_mode), + store_npm_packages_step(), + ] + ) + integration_test_steps = [ - postgres_integration_tests_step(edition=edition, ver_mode=ver_mode), - mysql_integration_tests_step(edition=edition, ver_mode=ver_mode), + postgres_integration_tests_step(), + mysql_integration_tests_step(), ] - # Insert remaining steps - build_steps.extend([ - package_step(edition=edition, ver_mode=ver_mode), - copy_packages_for_docker_step(), - build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=True), - build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=True), - grafana_server_step(edition=edition), - ]) - - if not disable_tests: - build_steps.extend([ - e2e_tests_step('dashboards-suite', edition=edition, tries=3), - e2e_tests_step('smoke-tests-suite', edition=edition, tries=3), - e2e_tests_step('panels-suite', edition=edition, tries=3), - e2e_tests_step('various-suite', edition=edition, tries=3), - e2e_tests_artifacts(edition=edition), - ]) - - build_storybook = build_storybook_step(edition=edition, ver_mode=ver_mode) - if build_storybook: - build_steps.append(build_storybook) - - if should_upload: - publish_steps.append(upload_cdn_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss)) - publish_steps.append(upload_packages_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss)) - if should_publish: - publish_step = store_storybook_step(edition=edition, ver_mode=ver_mode) - store_npm_step = store_npm_packages_step() - if publish_step: - publish_steps.append(publish_step) - if store_npm_step: - publish_steps.append(store_npm_step) - windows_package_steps = get_windows_steps(edition=edition, ver_mode=ver_mode) - windows_pipeline = pipeline( - name='{}-oss-windows'.format(ver_mode), edition=edition, trigger=trigger, - steps=[identify_runner_step('windows')] + windows_package_steps, - platform='windows', depends_on=[ - 'oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), - ], environment=environment, + name='{}-oss-windows'.format(ver_mode), + edition='oss', + trigger=trigger, + steps=get_windows_steps(edition='oss', ver_mode=ver_mode), + platform='windows', + depends_on=[ + # 'oss-build-e2e-publish-{}'.format(ver_mode), + '{}-oss-build-e2e-publish'.format(ver_mode), + '{}-oss-test-frontend'.format(ver_mode), + '{}-oss-test-backend'.format(ver_mode), + '{}-oss-integration-tests'.format(ver_mode), + ], + environment=environment, ) + pipelines = [ pipeline( - name='{}-oss-build{}-publish'.format(ver_mode, get_e2e_suffix()), edition=edition, trigger=trigger, services=[], - steps=init_steps + build_steps + package_steps + publish_steps, - environment=environment, volumes=volumes, + name='{}-oss-build-e2e-publish'.format(ver_mode), + edition='oss', + trigger=trigger, + services=[], + steps=init_steps + build_steps + publish_steps, + environment=environment, + volumes=volumes, ), - ] - if not disable_tests: - pipelines.extend([ - test_frontend(trigger, ver_mode), - test_backend(trigger, ver_mode), - pipeline( - name='{}-oss-integration-tests'.format(ver_mode), edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step(), identify_runner_step(), verify_gen_cue_step(edition), verify_gen_jsonnet_step(edition), wire_install_step(), ] + integration_test_steps, - environment=environment, volumes=volumes, - ) - ]) - deps = { - 'depends_on': [ - '{}-oss-build{}-publish'.format(ver_mode, get_e2e_suffix()), - '{}-oss-test-frontend'.format(ver_mode), - '{}-oss-test-backend'.format(ver_mode), - '{}-oss-integration-tests'.format(ver_mode) + test_frontend(trigger, ver_mode, committish=committish), + test_backend(trigger, ver_mode, committish=committish), + pipeline( + name='{}-oss-integration-tests'.format(ver_mode), + edition='oss', + trigger=trigger, + services=services, + steps=[ + download_grabpl_step(), + identify_runner_step(), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + wire_install_step(), ] - } - windows_pipeline.update(deps) + + integration_test_steps, + environment=environment, + volumes=volumes, + ), + windows_pipeline, + ] - pipelines.extend([windows_pipeline]) return pipelines + def enterprise_pipelines(ver_mode=ver_mode, trigger=release_trigger): + if ver_mode == 'release': + committish = '${DRONE_TAG}' + elif ver_mode == 'release-branch': + committish = '${DRONE_BRANCH}' + else: + committish = '${DRONE_COMMIT}' + environment = {'EDITION': 'enterprise'} - edition = 'enterprise' - services = integration_test_services(edition=edition) + + services = integration_test_services(edition='enterprise') volumes = integration_test_services_volumes() - package_steps = [] - publish_steps = [] - should_publish = ver_mode == 'release' - should_upload = should_publish or ver_mode in ('release-branch',) - include_enterprise = edition == 'enterprise' - edition2 = 'enterprise2' + init_steps = [ download_grabpl_step(), identify_runner_step(), - clone_enterprise_step(ver_mode), + clone_enterprise_step(committish=committish), init_enterprise_step(ver_mode), - compile_build_cmd(edition), - ] + compile_build_cmd('enterprise'), + ] + with_deps( + [ + wire_install_step(), + yarn_install_step(), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + ], + [ + 'init-enterprise', + ], + ) 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, ver_mode=ver_mode), + build_backend_step(edition='enterprise', ver_mode=ver_mode), + build_frontend_step(edition='enterprise', ver_mode=ver_mode), + build_frontend_package_step(edition='enterprise', ver_mode=ver_mode), + build_plugins_step(edition='enterprise', ver_mode=ver_mode), + build_backend_step( + edition='enterprise2', ver_mode=ver_mode, variants=['linux-amd64'] + ), + package_step( + edition='enterprise', + ver_mode=ver_mode, + include_enterprise2=True, + ), + copy_packages_for_docker_step(), + build_docker_images_step(edition='enterprise', ver_mode=ver_mode, publish=True), + build_docker_images_step( + edition='enterprise', ver_mode=ver_mode, publish=True, ubuntu=True + ), + grafana_server_step(edition='enterprise'), + e2e_tests_step('dashboards-suite', tries=3), + e2e_tests_step('smoke-tests-suite', tries=3), + e2e_tests_step('panels-suite', tries=3), + e2e_tests_step('various-suite', tries=3), + e2e_tests_artifacts(), ] + publish_steps = [] + + if ver_mode in ( + 'release', + 'release-branch', + ): + upload_packages_enterprise = upload_packages_step( + edition='enterprise', ver_mode=ver_mode, trigger=trigger_oss + ) + upload_packages_enterprise['depends_on'] = ['package'] + + upload_packages_enterprise2 = upload_packages_step( + edition='enterprise2', ver_mode=ver_mode + ) + upload_packages_enterprise2['depends_on'] = ['package-enterprise2'] + + publish_steps.extend( + [ + upload_cdn_step( + edition='enterprise', ver_mode=ver_mode, trigger=trigger_oss + ), + upload_packages_enterprise, + package_step( + edition='enterprise2', + ver_mode=ver_mode, + include_enterprise2=True, + variants=['linux-amd64'], + ), + upload_cdn_step(edition='enterprise2', ver_mode=ver_mode), + upload_packages_enterprise2, + ] + ) + integration_test_steps = [ - postgres_integration_tests_step(edition=edition, ver_mode=ver_mode), - mysql_integration_tests_step(edition=edition, ver_mode=ver_mode), + postgres_integration_tests_step(), + mysql_integration_tests_step(), ] - if include_enterprise: - build_steps.extend([ - build_backend_step(edition=edition2, ver_mode=ver_mode, variants=['linux-amd64']), - ]) - - # Insert remaining steps - build_steps.extend([ - package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise), - copy_packages_for_docker_step(), - build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=True), - build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=True), - grafana_server_step(edition=edition), - ]) - - if not disable_tests: - build_steps.extend([ - e2e_tests_step('dashboards-suite', edition=edition, tries=3), - e2e_tests_step('smoke-tests-suite', edition=edition, tries=3), - e2e_tests_step('panels-suite', edition=edition, tries=3), - e2e_tests_step('various-suite', edition=edition, tries=3), - e2e_tests_artifacts(edition=edition), - ]) - - build_storybook = build_storybook_step(edition=edition, ver_mode=ver_mode) - if build_storybook: - build_steps.append(build_storybook) - - if should_upload: - publish_steps.extend([ - upload_cdn_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss), - upload_packages_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss), - package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise, variants=['linux-amd64']), - upload_cdn_step(edition=edition2, ver_mode=ver_mode), - ]) - if should_publish: - publish_step = store_storybook_step(edition=edition, ver_mode=ver_mode) - if publish_step: - publish_steps.append(publish_step) - windows_package_steps = get_windows_steps(edition=edition, ver_mode=ver_mode) - - if should_upload: - step = upload_packages_step(edition=edition2, ver_mode=ver_mode) - if step: - publish_steps.append(step) - - deps_on_clone_enterprise_step = { - 'depends_on': [ - 'init-enterprise', - ] - } - - for step in [wire_install_step(), yarn_install_step(edition), verify_gen_cue_step(edition), verify_gen_jsonnet_step(edition)]: - step.update(deps_on_clone_enterprise_step) - init_steps.extend([step]) - windows_pipeline = pipeline( - name='{}-enterprise-windows'.format(ver_mode), edition=edition, trigger=trigger, - steps=[identify_runner_step('windows')] + windows_package_steps, - platform='windows', depends_on=[ - 'enterprise-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), - ], environment=environment, + name='{}-enterprise-windows'.format(ver_mode), + edition='enterprise', + trigger=trigger, + steps=get_windows_steps(edition='enterprise', ver_mode=ver_mode), + platform='windows', + depends_on=[ + # 'enterprise-build-e2e-publish-{}'.format(ver_mode), + '{}-enterprise-build-e2e-publish'.format(ver_mode), + '{}-enterprise-test-frontend'.format(ver_mode), + '{}-enterprise-test-backend'.format(ver_mode), + '{}-enterprise-integration-tests'.format(ver_mode), + ], + environment=environment, ) + pipelines = [ pipeline( - name='{}-enterprise-build{}-publish'.format(ver_mode, get_e2e_suffix()), edition=edition, trigger=trigger, services=[], - steps=init_steps + build_steps + package_steps + publish_steps, environment=environment, + name='{}-enterprise-build-e2e-publish'.format(ver_mode), + edition='enterprise', + trigger=trigger, + services=[], + steps=init_steps + build_steps + publish_steps, + environment=environment, volumes=volumes, ), - ] - if not disable_tests: - pipelines.extend([ - test_frontend(trigger, ver_mode, edition), - test_backend(trigger, ver_mode, edition), - pipeline( - name='{}-enterprise-integration-tests'.format(ver_mode), edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step(), identify_runner_step(), clone_enterprise_step(ver_mode), init_enterprise_step(ver_mode), verify_gen_cue_step(edition), verify_gen_jsonnet_step(edition), wire_install_step()] + integration_test_steps + [redis_integration_tests_step(), memcached_integration_tests_step()], - environment=environment, volumes=volumes, - ), - ]) - deps = { - 'depends_on': [ - '{}-enterprise-build{}-publish'.format(ver_mode, get_e2e_suffix()), - '{}-enterprise-test-frontend'.format(ver_mode), - '{}-enterprise-test-backend'.format(ver_mode), - '{}-enterprise-integration-tests'.format(ver_mode) + test_frontend_enterprise(trigger, ver_mode, committish=committish), + test_backend_enterprise(trigger, ver_mode, committish=committish), + pipeline( + name='{}-enterprise-integration-tests'.format(ver_mode), + edition='enterprise', + trigger=trigger, + services=services, + steps=[ + download_grabpl_step(), + identify_runner_step(), + clone_enterprise_step(committish=committish), + init_enterprise_step(ver_mode), ] - } - windows_pipeline.update(deps) - - pipelines.extend([windows_pipeline]) + + with_deps( + [ + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + ], + [ + 'init-enterprise', + ], + ) + + [ + wire_install_step(), + ] + + integration_test_steps + + [ + redis_integration_tests_step(), + memcached_integration_tests_step(), + ], + environment=environment, + volumes=volumes, + ), + windows_pipeline, + ] return pipelines + def enterprise2_pipelines(prefix='', ver_mode=ver_mode, trigger=release_trigger): + if ver_mode == 'release': + committish = '${DRONE_TAG}' + elif ver_mode == 'release-branch': + committish = '${DRONE_BRANCH}' + else: + committish = '${DRONE_COMMIT}' + environment = { 'EDITION': 'enterprise2', } - edition = 'enterprise' - services = integration_test_services(edition=edition) + + services = integration_test_services(edition='enterprise') volumes = integration_test_services_volumes() - package_steps = [] - publish_steps = [] - should_publish = ver_mode == 'release' - should_upload = should_publish or ver_mode in ('release-branch',) - include_enterprise = edition == 'enterprise' - edition2 = 'enterprise2' + init_steps = [ download_grabpl_step(), identify_runner_step(), - clone_enterprise_step(ver_mode), + clone_enterprise_step(committish=committish), init_enterprise_step(ver_mode), - compile_build_cmd(edition), - ] + compile_build_cmd('enterprise'), + ] + with_deps( + [ + wire_install_step(), + yarn_install_step(), + verify_gen_cue_step(), + ], + [ + 'init-enterprise', + ], + ) build_steps = [ - build_frontend_step(edition=edition, ver_mode=ver_mode), - build_frontend_package_step(edition=edition, ver_mode=ver_mode), - build_plugins_step(edition=edition, ver_mode=ver_mode), + build_frontend_step(edition='enterprise', ver_mode=ver_mode), + build_frontend_package_step(edition='enterprise', ver_mode=ver_mode), + build_plugins_step(edition='enterprise', ver_mode=ver_mode), + build_backend_step( + edition='enterprise2', ver_mode=ver_mode, variants=['linux-amd64'] + ), ] - if include_enterprise: - build_steps.extend([ - build_backend_step(edition=edition2, ver_mode=ver_mode, variants=['linux-amd64']), - ]) + fetch_images = fetch_images_step('enterprise2') + fetch_images.update( + {'depends_on': ['build-docker-images', 'build-docker-images-ubuntu']} + ) - fetch_images = fetch_images_step(edition2) - fetch_images.update({'depends_on': ['build-docker-images', 'build-docker-images-ubuntu']}) - upload_cdn = upload_cdn_step(edition=edition2, ver_mode=ver_mode) - upload_cdn['environment'].update({'ENTERPRISE2_CDN_PATH': from_secret('enterprise2-cdn-path')}) + upload_cdn = upload_cdn_step(edition='enterprise2', ver_mode=ver_mode) + upload_cdn['environment'].update( + {'ENTERPRISE2_CDN_PATH': from_secret('enterprise2-cdn-path')} + ) - build_steps.extend([ - package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise, variants=['linux-amd64']), - upload_cdn, - copy_packages_for_docker_step(edition=edition2), - build_docker_images_step(edition=edition2, ver_mode=ver_mode, publish=True), - build_docker_images_step(edition=edition2, ver_mode=ver_mode, ubuntu=True, publish=True), - fetch_images, - publish_images_step(edition2, 'release', mode=edition2, docker_repo='${{DOCKER_ENTERPRISE2_REPO}}'), - ]) - - if should_upload: - step = upload_packages_step(edition=edition2, ver_mode=ver_mode) - if step: - publish_steps.append(step) - - deps_on_clone_enterprise_step = { - 'depends_on': [ - 'init-enterprise', + build_steps.extend( + [ + package_step( + edition='enterprise2', + ver_mode=ver_mode, + include_enterprise2=True, + variants=['linux-amd64'], + ), + upload_cdn, + copy_packages_for_docker_step(edition='enterprise2'), + build_docker_images_step( + edition='enterprise2', ver_mode=ver_mode, publish=True + ), + build_docker_images_step( + edition='enterprise2', ver_mode=ver_mode, publish=True, ubuntu=True + ), + fetch_images, + publish_images_step( + 'enterprise2', + 'release', + mode='enterprise2', + docker_repo='${{DOCKER_ENTERPRISE2_REPO}}', + ), ] - } + ) - for step in [wire_install_step(), yarn_install_step(), verify_gen_cue_step(edition)]: - step.update(deps_on_clone_enterprise_step) - init_steps.extend([step]) + publish_steps = [] + + if ver_mode in ( + 'release', + 'release-branch', + ): + step = upload_packages_step(edition='enterprise2', ver_mode=ver_mode) + step['depends_on'] = ['package-enterprise2'] + + publish_steps.append(step) pipelines = [ pipeline( - name='{}{}-enterprise2-build{}-publish'.format(prefix, ver_mode, get_e2e_suffix()), edition=edition, trigger=trigger, services=[], - steps=init_steps + build_steps + package_steps + publish_steps, - volumes=volumes, environment=environment, + name='{}{}-enterprise2-build-e2e-publish'.format(prefix, ver_mode), + edition='enterprise', + trigger=trigger, + services=[], + steps=init_steps + build_steps + publish_steps, + volumes=volumes, + environment=environment, ), ] return pipelines + def publish_artifacts_step(mode): security = '' if mode == 'security': @@ -440,10 +530,15 @@ def publish_artifacts_step(mode): 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), }, - 'commands': ['./bin/grabpl artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}'.format(security)], + 'commands': [ + './bin/grabpl artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}'.format( + security + ) + ], 'depends_on': ['grabpl'], } + def publish_artifacts_pipelines(mode): trigger = { 'event': ['promote'], @@ -454,9 +549,16 @@ def publish_artifacts_pipelines(mode): publish_artifacts_step(mode), ] - return [pipeline( - name='publish-artifacts-{}'.format(mode), trigger=trigger, steps=steps, edition="all", environment = {'EDITION': 'all'} - )] + return [ + pipeline( + name='publish-artifacts-{}'.format(mode), + trigger=trigger, + steps=steps, + edition="all", + environment={'EDITION': 'all'}, + ) + ] + def publish_packages_pipeline(): trigger = { @@ -481,40 +583,63 @@ def publish_packages_pipeline(): deps = [ 'publish-artifacts-public', 'publish-docker-oss-public', - 'publish-docker-enterprise-public' + 'publish-docker-enterprise-public', ] - return [pipeline( - name='publish-packages-oss', trigger=trigger, steps=oss_steps, edition="all", depends_on=deps, environment = {'EDITION': 'oss'}, - ), pipeline( - name='publish-packages-enterprise', trigger=trigger, steps=enterprise_steps, edition="all", depends_on=deps, environment = {'EDITION': 'enterprise'} - )] + return [ + pipeline( + name='publish-packages-oss', + trigger=trigger, + steps=oss_steps, + edition="all", + depends_on=deps, + environment={'EDITION': 'oss'}, + ), + pipeline( + name='publish-packages-enterprise', + trigger=trigger, + steps=enterprise_steps, + edition="all", + depends_on=deps, + environment={'EDITION': 'enterprise'}, + ), + ] -def publish_npm_pipelines(mode): + +def publish_npm_pipelines(): trigger = { 'event': ['promote'], - 'target': [mode], + 'target': ['public'], } steps = [ download_grabpl_step(), yarn_install_step(), retrieve_npm_packages_step(), - release_npm_packages_step() + release_npm_packages_step(), + ] + + return [ + pipeline( + name='publish-npm-packages-public', + trigger=trigger, + steps=steps, + edition="all", + environment={'EDITION': 'all'}, + ) ] - return [pipeline( - name='publish-npm-packages-{}'.format(mode), trigger=trigger, steps = steps, edition="all", environment = {'EDITION': 'all'}, - )] def artifacts_page_pipeline(): trigger = { 'event': ['promote'], 'target': 'security', } - return [pipeline(name='publish-artifacts-page', trigger=trigger, steps = [download_grabpl_step(), artifacts_page_step()], edition="all", environment = {'EDITION': 'all'} - )] - -def get_e2e_suffix(): - if not disable_tests: - return '-e2e' - return '' + return [ + pipeline( + name='publish-artifacts-page', + trigger=trigger, + steps=[download_grabpl_step(), artifacts_page_step()], + edition="all", + environment={'EDITION': 'all'}, + ) + ] diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index 730efbd099b..2b4512daed8 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -1,42 +1,41 @@ load( 'scripts/drone/steps/lib.star', - 'download_grabpl_step', - 'build_image', - 'identify_runner_step', - 'wire_install_step', - 'yarn_install_step', - 'build_backend_step', - 'build_frontend_step', - 'build_frontend_package_step', - 'build_plugins_step', - 'package_step', - 'grafana_server_step', - 'e2e_tests_step', - 'e2e_tests_artifacts', - 'build_storybook_step', - 'copy_packages_for_docker_step', - 'build_docker_images_step', - 'publish_images_step', - 'postgres_integration_tests_step', - 'mysql_integration_tests_step', - 'redis_integration_tests_step', - 'memcached_integration_tests_step', - 'get_windows_steps', 'benchmark_ldap_step', + 'betterer_frontend_step', + 'build_backend_step', + 'build_docker_images_step', + 'build_frontend_package_step', + 'build_frontend_step', + 'build_image', + 'build_plugins_step', + 'build_storybook_step', + 'cloud_plugins_e2e_tests_step', + 'compile_build_cmd', + 'copy_packages_for_docker_step', + 'download_grabpl_step', + 'e2e_tests_artifacts', + 'e2e_tests_step', 'enterprise_downstream_step', 'frontend_metrics_step', - 'store_storybook_step', + 'grafana_server_step', + 'identify_runner_step', + 'memcached_integration_tests_step', + 'mysql_integration_tests_step', + 'package_step', + 'postgres_integration_tests_step', + 'publish_images_step', + 'redis_integration_tests_step', 'release_canary_npm_packages_step', - 'upload_packages_step', - 'upload_cdn_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', + 'store_storybook_step', 'test_a11y_frontend_step', 'trigger_oss', - 'betterer_frontend_step', 'trigger_test_release', - 'compile_build_cmd', - 'cloud_plugins_e2e_tests_step', + 'upload_cdn_step', + 'upload_packages_step', + 'verify_gen_cue_step', + 'verify_gen_jsonnet_step', + 'wire_install_step', + 'yarn_install_step', ) load( @@ -44,68 +43,118 @@ load( 'pipeline', ) -def build_e2e(trigger, ver_mode, edition): + +def build_e2e(trigger, ver_mode): + edition = 'oss' environment = {'EDITION': edition} - variants = ['linux-amd64', 'linux-amd64-musl', 'darwin-amd64', 'windows-amd64',] init_steps = [ identify_runner_step(), download_grabpl_step(), compile_build_cmd(), - verify_gen_cue_step(edition="oss"), - verify_gen_jsonnet_step(edition="oss"), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), wire_install_step(), yarn_install_step(), ] + build_steps = [] + variants = None + if ver_mode == 'pr': - build_steps.extend([trigger_test_release()]) - build_steps.extend([enterprise_downstream_step(edition=edition, ver_mode=ver_mode)]) - build_steps.extend([ - 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, ver_mode=ver_mode), - ]) - if ver_mode == 'main': - build_steps.extend([package_step(edition=edition, ver_mode=ver_mode)]) - elif ver_mode == 'pr': - build_steps.extend([package_step(edition=edition, ver_mode=ver_mode, variants=variants)]) + build_steps.extend( + [ + trigger_test_release(), + enterprise_downstream_step(ver_mode=ver_mode), + ] + ) - build_steps.extend([ - grafana_server_step(edition=edition), - e2e_tests_step('dashboards-suite', edition=edition), - e2e_tests_step('smoke-tests-suite', edition=edition), - e2e_tests_step('panels-suite', edition=edition), - e2e_tests_step('various-suite', edition=edition), - cloud_plugins_e2e_tests_step('cloud-plugins-suite', edition=edition, cloud='azure', trigger=trigger_oss), - e2e_tests_artifacts(edition=edition), - build_storybook_step(edition=edition, ver_mode=ver_mode), - copy_packages_for_docker_step(), - test_a11y_frontend_step(ver_mode=ver_mode, edition=edition), - ]) - if ver_mode == 'main': - build_steps.extend([ - store_storybook_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss), - frontend_metrics_step(edition=edition, trigger=trigger_oss) - ]) + variants = [ + 'linux-amd64', + 'linux-amd64-musl', + 'darwin-amd64', + 'windows-amd64', + ] + + build_steps.extend( + [ + 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, ver_mode=ver_mode), + package_step(edition=edition, ver_mode=ver_mode, variants=variants), + grafana_server_step(edition=edition), + e2e_tests_step('dashboards-suite'), + e2e_tests_step('smoke-tests-suite'), + e2e_tests_step('panels-suite'), + e2e_tests_step('various-suite'), + cloud_plugins_e2e_tests_step( + 'cloud-plugins-suite', + cloud='azure', + trigger=trigger_oss, + ), + e2e_tests_artifacts(), + build_storybook_step(ver_mode=ver_mode), + copy_packages_for_docker_step(), + test_a11y_frontend_step(ver_mode=ver_mode), + ] + ) if ver_mode == 'main': - build_steps.extend([ - build_docker_images_step(edition=edition, ver_mode=ver_mode, publish=False), - build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=False), - publish_images_step(edition=edition, ver_mode=ver_mode, mode='', docker_repo='grafana', trigger=trigger_oss), - publish_images_step(edition=edition, ver_mode=ver_mode, mode='', docker_repo='grafana-oss', trigger=trigger_oss), - release_canary_npm_packages_step(edition, trigger=trigger_oss), - upload_packages_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss), - upload_cdn_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss) - ]) + build_steps.extend( + [ + store_storybook_step(ver_mode=ver_mode, trigger=trigger_oss), + frontend_metrics_step(trigger=trigger_oss), + build_docker_images_step( + edition=edition, ver_mode=ver_mode, publish=False + ), + build_docker_images_step( + edition=edition, ver_mode=ver_mode, publish=False, ubuntu=True + ), + publish_images_step( + edition=edition, + ver_mode=ver_mode, + mode='', + docker_repo='grafana', + trigger=trigger_oss, + ), + publish_images_step( + edition=edition, + ver_mode=ver_mode, + mode='', + docker_repo='grafana-oss', + trigger=trigger_oss, + ), + release_canary_npm_packages_step(trigger=trigger_oss), + upload_packages_step( + edition=edition, ver_mode=ver_mode, trigger=trigger_oss + ), + upload_cdn_step( + edition=edition, ver_mode=ver_mode, trigger=trigger_oss + ), + ] + ) elif ver_mode == 'pr': - build_steps.extend([build_docker_images_step(edition=edition, ver_mode=ver_mode, archs=['amd64', ])]) + build_steps.extend( + [ + build_docker_images_step( + edition=edition, + ver_mode=ver_mode, + archs=[ + 'amd64', + ], + ) + ] + ) publish_suffix = '' if ver_mode == 'main': publish_suffix = '-publish' return pipeline( - name='{}-build-e2e{}'.format(ver_mode, publish_suffix), edition="oss", trigger=trigger, services=[], steps=init_steps + build_steps, environment=environment, + name='{}-build-e2e{}'.format(ver_mode, publish_suffix), + edition="oss", + trigger=trigger, + services=[], + steps=init_steps + build_steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/docs.star b/scripts/drone/pipelines/docs.star index 32f9eb97826..031efa946ce 100644 --- a/scripts/drone/pipelines/docs.star +++ b/scripts/drone/pipelines/docs.star @@ -31,8 +31,9 @@ docs_paths = { ], } -def docs_pipelines(edition, ver_mode, trigger): - environment = {'EDITION': edition} + +def docs_pipelines(ver_mode, trigger): + environment = {'EDITION': 'oss'} steps = [ download_grabpl_step(), identify_runner_step(), @@ -43,9 +44,15 @@ def docs_pipelines(edition, ver_mode, trigger): ] return pipeline( - name='{}-docs'.format(ver_mode), edition=edition, trigger=trigger, services=[], steps=steps, environment=environment, + name='{}-docs'.format(ver_mode), + edition='oss', + trigger=trigger, + services=[], + steps=steps, + environment=environment, ) + def lint_docs(): return { 'name': 'lint-docs', @@ -71,6 +78,7 @@ def trigger_docs_main(): 'paths': docs_paths, } + def trigger_docs_pr(): return { 'event': [ diff --git a/scripts/drone/pipelines/integration_tests.star b/scripts/drone/pipelines/integration_tests.star index 6994d00a2f0..efb810d58ab 100644 --- a/scripts/drone/pipelines/integration_tests.star +++ b/scripts/drone/pipelines/integration_tests.star @@ -1,13 +1,13 @@ load( 'scripts/drone/steps/lib.star', - 'identify_runner_step', + 'compile_build_cmd', 'download_grabpl_step', + 'identify_runner_step', 'verify_gen_cue_step', 'verify_gen_jsonnet_step', 'wire_install_step', 'postgres_integration_tests_step', 'mysql_integration_tests_step', - 'compile_build_cmd', ) load( @@ -22,24 +22,33 @@ load( 'pipeline', ) -def integration_tests(trigger, ver_mode, edition): - environment = {'EDITION': edition} - services = integration_test_services(edition) + +def integration_tests(trigger, prefix): + environment = {'EDITION': 'oss'} + + services = integration_test_services(edition="oss") volumes = integration_test_services_volumes() + init_steps = [ download_grabpl_step(), compile_build_cmd(), identify_runner_step(), - verify_gen_cue_step(edition="oss"), - verify_gen_jsonnet_step(edition="oss"), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), wire_install_step(), ] + test_steps = [ - postgres_integration_tests_step(edition=edition, ver_mode=ver_mode), - mysql_integration_tests_step(edition=edition, ver_mode=ver_mode), + postgres_integration_tests_step(), + mysql_integration_tests_step(), ] return pipeline( - name='{}-integration-tests'.format(ver_mode), edition="oss", trigger=trigger, services=services, steps=init_steps + test_steps, - environment=environment, volumes=volumes + name='{}-integration-tests'.format(prefix), + edition='oss', + trigger=trigger, + environment=environment, + services=services, + volumes=volumes, + steps=init_steps + test_steps, ) diff --git a/scripts/drone/pipelines/lint_backend.star b/scripts/drone/pipelines/lint_backend.star index 08112f28868..1efbc24fb7e 100644 --- a/scripts/drone/pipelines/lint_backend.star +++ b/scripts/drone/pipelines/lint_backend.star @@ -12,21 +12,31 @@ load( 'pipeline', ) + def lint_backend_pipeline(trigger, ver_mode): environment = {'EDITION': 'oss'} + wire_step = wire_install_step() - wire_step.update({ 'depends_on': [] }) + wire_step.update({'depends_on': []}) + init_steps = [ identify_runner_step(), compile_build_cmd(), wire_step, ] + test_steps = [ - lint_backend_step(edition="oss"), + lint_backend_step(), ] + if ver_mode == 'main': - test_steps.extend([lint_drone_step()]) + test_steps.append(lint_drone_step()) return pipeline( - name='{}-lint-backend'.format(ver_mode), edition="oss", trigger=trigger, services=[], steps=init_steps + test_steps, environment=environment, + name='{}-lint-backend'.format(ver_mode), + edition="oss", + trigger=trigger, + services=[], + steps=init_steps + test_steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/lint_frontend.star b/scripts/drone/pipelines/lint_frontend.star index bb99a9e0d21..dd3947aac8d 100644 --- a/scripts/drone/pipelines/lint_frontend.star +++ b/scripts/drone/pipelines/lint_frontend.star @@ -10,17 +10,24 @@ load( 'pipeline', ) + def lint_frontend_pipeline(trigger, ver_mode): environment = {'EDITION': 'oss'} - yarn_step = yarn_install_step() - yarn_step.update({ 'depends_on': [] }) + init_steps = [ identify_runner_step(), - yarn_step, + yarn_install_step(), ] + test_steps = [ lint_frontend_step(), ] + return pipeline( - name='{}-lint-frontend'.format(ver_mode), edition="oss", trigger=trigger, services=[], steps=init_steps + test_steps, environment=environment, + name='{}-lint-frontend'.format(ver_mode), + edition="oss", + trigger=trigger, + services=[], + steps=init_steps + test_steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/publish_images.star b/scripts/drone/pipelines/publish_images.star index 6281890a17c..f2b7cd28cd3 100644 --- a/scripts/drone/pipelines/publish_images.star +++ b/scripts/drone/pipelines/publish_images.star @@ -14,9 +14,6 @@ load( def publish_image_steps(edition, mode, docker_repo): - additional_docker_repo = "" - if edition == 'oss': - additional_docker_repo='grafana/grafana-oss' steps = [ identify_runner_step(), download_grabpl_step(), @@ -24,29 +21,55 @@ def publish_image_steps(edition, mode, docker_repo): fetch_images_step(edition), publish_images_step(edition, 'release', mode, docker_repo), ] - if additional_docker_repo != "": - steps.extend([publish_images_step(edition, 'release', mode, additional_docker_repo)]) + + if edition == 'oss': + steps.append( + publish_images_step(edition, 'release', mode, 'grafana/grafana-oss') + ) return steps + def publish_image_pipelines_public(): - mode='public' + mode = 'public' trigger = { 'event': ['promote'], 'target': [mode], } - return [pipeline( - name='publish-docker-oss-{}'.format(mode), trigger=trigger, steps=publish_image_steps(edition='oss', mode=mode, docker_repo='grafana'), edition="", environment = {'EDITION': 'oss'} - ), pipeline( - name='publish-docker-enterprise-{}'.format(mode), trigger=trigger, steps=publish_image_steps(edition='enterprise', mode=mode, docker_repo='grafana-enterprise'), edition="", environment = {'EDITION': 'enterprise'} - ),] + return [ + pipeline( + name='publish-docker-oss-{}'.format(mode), + trigger=trigger, + steps=publish_image_steps(edition='oss', mode=mode, docker_repo='grafana'), + edition="", + environment={'EDITION': 'oss'}, + ), + pipeline( + name='publish-docker-enterprise-{}'.format(mode), + trigger=trigger, + steps=publish_image_steps( + edition='enterprise', mode=mode, docker_repo='grafana-enterprise' + ), + edition="", + environment={'EDITION': 'enterprise'}, + ), + ] + def publish_image_pipelines_security(): - mode='security' + mode = 'security' trigger = { 'event': ['promote'], 'target': [mode], } - return [pipeline( - name='publish-docker-enterprise-{}'.format(mode), trigger=trigger, steps=publish_image_steps(edition='enterprise', mode=mode, docker_repo='grafana-enterprise'), edition="", environment = {'EDITION': 'enterprise'} - ),] + return [ + pipeline( + name='publish-docker-enterprise-{}'.format(mode), + trigger=trigger, + steps=publish_image_steps( + edition='enterprise', mode=mode, docker_repo='grafana-enterprise' + ), + edition="", + environment={'EDITION': 'enterprise'}, + ), + ] diff --git a/scripts/drone/pipelines/shellcheck.star b/scripts/drone/pipelines/shellcheck.star index 95bf84ac732..78a356adf8d 100644 --- a/scripts/drone/pipelines/shellcheck.star +++ b/scripts/drone/pipelines/shellcheck.star @@ -1,8 +1,4 @@ -load( - 'scripts/drone/steps/lib.star', - 'build_image', - 'compile_build_cmd' -) +load('scripts/drone/steps/lib.star', 'build_image', 'compile_build_cmd') load( 'scripts/drone/utils/utils.star', @@ -19,12 +15,11 @@ trigger = { 'docs/**', 'latest.json', ], - 'include': [ - 'scripts/**/*.sh' - ], + 'include': ['scripts/**/*.sh'], }, } + def shellcheck_step(): return { 'name': 'shellcheck', @@ -37,6 +32,7 @@ def shellcheck_step(): ], } + def shellcheck_pipeline(): environment = {'EDITION': 'oss'} steps = [ @@ -44,6 +40,10 @@ def shellcheck_pipeline(): shellcheck_step(), ] return pipeline( - name='pr-shellcheck', edition="oss", trigger=trigger, services=[], steps=steps, environment=environment, + name='pr-shellcheck', + edition="oss", + trigger=trigger, + services=[], + steps=steps, + environment=environment, ) - diff --git a/scripts/drone/pipelines/test_backend.star b/scripts/drone/pipelines/test_backend.star index db916f46382..aa5cdfd8834 100644 --- a/scripts/drone/pipelines/test_backend.star +++ b/scripts/drone/pipelines/test_backend.star @@ -15,28 +15,71 @@ load( load( 'scripts/drone/utils/utils.star', 'pipeline', + 'with_deps', ) -def test_backend(trigger, ver_mode, edition="oss"): - environment = {'EDITION': edition} - init_steps = [] - if edition != 'oss': - init_steps.extend([clone_enterprise_step(ver_mode), download_grabpl_step(), init_enterprise_step(ver_mode),]) - init_steps.extend([ + +def test_backend(trigger, ver_mode, committish): + environment = {'EDITION': 'oss'} + + steps = [ identify_runner_step(), - compile_build_cmd(edition), - verify_gen_cue_step(edition), - verify_gen_jsonnet_step(edition), + compile_build_cmd(edition='oss'), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), wire_install_step(), - ]) - test_steps = [ - test_backend_step(edition), - test_backend_integration_step(edition), + test_backend_step(), + test_backend_integration_step(), ] pipeline_name = '{}-test-backend'.format(ver_mode) if ver_mode in ("release-branch", "release"): - pipeline_name = '{}-{}-test-backend'.format(ver_mode, edition) + pipeline_name = '{}-{}-test-backend'.format(ver_mode, 'oss') + return pipeline( - name=pipeline_name, edition=edition, trigger=trigger, services=[], steps=init_steps + test_steps, environment=environment + name=pipeline_name, + edition='oss', + trigger=trigger, + steps=steps, + environment=environment, + ) + + +def test_backend_enterprise(trigger, ver_mode, committish, edition="enterprise"): + environment = {'EDITION': edition} + + steps = ( + [ + clone_enterprise_step(committish), + download_grabpl_step(), + init_enterprise_step(ver_mode), + identify_runner_step(), + compile_build_cmd(edition), + ] + + with_deps( + [ + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + ], + [ + 'init-enterprise', + ], + ) + + [ + wire_install_step(), + test_backend_step(), + test_backend_integration_step(), + ] + ) + + pipeline_name = '{}-test-backend'.format(ver_mode) + if ver_mode in ("release-branch", "release"): + pipeline_name = '{}-{}-test-backend'.format(ver_mode, edition) + + return pipeline( + name=pipeline_name, + edition=edition, + trigger=trigger, + steps=steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/test_frontend.star b/scripts/drone/pipelines/test_frontend.star index 6c72ccc8a00..0f3b7fd4655 100644 --- a/scripts/drone/pipelines/test_frontend.star +++ b/scripts/drone/pipelines/test_frontend.star @@ -12,25 +12,59 @@ load( load( 'scripts/drone/utils/utils.star', 'pipeline', + 'with_deps', ) -def test_frontend(trigger, ver_mode, edition="oss"): - environment = {'EDITION': edition} - init_steps = [] - if edition != 'oss': - init_steps.extend([clone_enterprise_step(ver_mode), init_enterprise_step(ver_mode),]) - init_steps.extend([ + +def test_frontend(trigger, ver_mode, committish): + environment = {'EDITION': 'oss'} + + steps = [ identify_runner_step(), download_grabpl_step(), - yarn_install_step(edition), - ]) - test_steps = [ - betterer_frontend_step(edition), - test_frontend_step(edition), + yarn_install_step(), + betterer_frontend_step(edition='oss'), + test_frontend_step(edition='oss'), ] + + pipeline_name = '{}-test-frontend'.format(ver_mode) + if ver_mode in ("release-branch", "release"): + pipeline_name = '{}-{}-test-frontend'.format(ver_mode, 'oss') + + return pipeline( + name=pipeline_name, + edition='oss', + trigger=trigger, + steps=steps, + environment=environment, + ) + + +def test_frontend_enterprise(trigger, ver_mode, committish, edition='enterprise'): + environment = {'EDITION': edition} + + steps = ( + [ + clone_enterprise_step(committish), + init_enterprise_step(ver_mode), + identify_runner_step(), + download_grabpl_step(), + ] + + with_deps([yarn_install_step()], ['init-enterprise']) + + [ + betterer_frontend_step(edition), + test_frontend_step(edition), + ] + ) + pipeline_name = '{}-test-frontend'.format(ver_mode) if ver_mode in ("release-branch", "release"): pipeline_name = '{}-{}-test-frontend'.format(ver_mode, edition) + return pipeline( - name=pipeline_name, edition=edition, trigger=trigger, services=[], steps=init_steps + test_steps, environment=environment, + name=pipeline_name, + edition=edition, + trigger=trigger, + steps=steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/trigger_downstream.star b/scripts/drone/pipelines/trigger_downstream.star index c1bc672905e..dafab45fe35 100644 --- a/scripts/drone/pipelines/trigger_downstream.star +++ b/scripts/drone/pipelines/trigger_downstream.star @@ -9,7 +9,9 @@ load( ) trigger = { - 'event': ['push',], + 'event': [ + 'push', + ], 'branch': 'main', 'paths': { 'exclude': [ @@ -20,10 +22,22 @@ trigger = { }, } -def enterprise_downstream_pipeline(edition, ver_mode): - environment = {'EDITION': edition} - steps = [enterprise_downstream_step(edition, ver_mode)] - deps = ['main-build-e2e-publish', 'main-integration-tests'] + +def enterprise_downstream_pipeline(): + environment = {'EDITION': 'oss'} + steps = [ + enterprise_downstream_step(ver_mode='main'), + ] + deps = [ + 'main-build-e2e-publish', + 'main-integration-tests', + ] return pipeline( - name='main-trigger-downstream', edition=edition, trigger=trigger, services=[], steps=steps, depends_on=deps, environment=environment, - ) + name='main-trigger-downstream', + edition='oss', + trigger=trigger, + services=[], + steps=steps, + depends_on=deps, + environment=environment, + ) diff --git a/scripts/drone/pipelines/verify_drone.star b/scripts/drone/pipelines/verify_drone.star index 6d55044d12c..4a2f3c91d45 100644 --- a/scripts/drone/pipelines/verify_drone.star +++ b/scripts/drone/pipelines/verify_drone.star @@ -11,6 +11,7 @@ load( 'pipeline', ) + def verify_drone(trigger, ver_mode): environment = {'EDITION': 'oss'} steps = [ @@ -20,5 +21,10 @@ def verify_drone(trigger, ver_mode): lint_drone_step(), ] return pipeline( - name='{}-verify-drone'.format(ver_mode), edition="oss", trigger=trigger, services=[], steps=steps, environment=environment, + name='{}-verify-drone'.format(ver_mode), + edition="oss", + trigger=trigger, + services=[], + steps=steps, + environment=environment, ) diff --git a/scripts/drone/pipelines/windows.star b/scripts/drone/pipelines/windows.star index 357346e8829..e3af9b99f2d 100644 --- a/scripts/drone/pipelines/windows.star +++ b/scripts/drone/pipelines/windows.star @@ -1,8 +1,6 @@ load( 'scripts/drone/steps/lib.star', - 'grabpl_version', - 'wix_image', - 'identify_runner_step', + 'get_windows_steps', ) load( @@ -10,126 +8,21 @@ load( 'pipeline', ) -load('scripts/drone/vault.star', 'from_secret', 'prerelease_bucket', 'github_token') def windows(trigger, edition, ver_mode): environment = {'EDITION': edition} - init_cmds = [] - sfx = '' - if edition in ('enterprise', 'enterprise2'): - sfx = '-{}'.format(edition) - else: - init_cmds.extend([ - '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version), - ]) - steps = [ - { - 'name': 'windows-init', - 'image': wix_image, - 'commands': init_cmds, - }, - ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( - 'release', 'release-branch', - ): - bucket = '%PRERELEASE_BUCKET%/artifacts/downloads' - if ver_mode == 'release': - ver_part = '${DRONE_TAG}' - dir = 'release' - else: - dir = 'main' - bucket = 'grafana-downloads' - build_no = 'DRONE_BUILD_NUMBER' - ver_part = '--build-id $$env:{}'.format(build_no) - installer_commands = [ - '$$gcpKey = $$env:GCP_KEY', - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) > gcpkey.json', - # gcloud fails to read the file unless converted with dos2unix - 'dos2unix gcpkey.json', - 'gcloud auth activate-service-account --key-file=gcpkey.json', - 'rm gcpkey.json', - 'cp C:\\App\\nssm-2.24.zip .', - ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( - 'release', - ): - installer_commands.extend([ - '.\\grabpl.exe windows-installer --edition {} {}'.format(edition, ver_part), - '$$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0]', - ]) - if ver_mode == 'main': - installer_commands.extend([ - 'gsutil cp $$fname gs://{}/{}/{}/'.format(bucket, edition, dir), - 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/'.format(bucket, edition, dir), - ]) - else: - installer_commands.extend([ - 'gsutil cp $$fname gs://{}/{}/{}/{}/'.format(bucket, ver_part, edition, dir), - 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/{}/'.format(bucket, ver_part, edition, dir), - ]) - steps.append({ - 'name': 'build-windows-installer', - 'image': wix_image, - 'depends_on': [ - 'windows-init', - ], - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), - 'GITHUB_TOKEN': from_secret('github_token') - }, - 'commands': installer_commands, - }) - - if edition in ('enterprise', 'enterprise2'): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '$$env:DRONE_BRANCH' - else: - committish = '$$env:DRONE_COMMIT' - # For enterprise, we have to clone both OSS and enterprise and merge the latter into the former - download_grabpl_step_cmds = [ - '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version), - ] - clone_cmds = [ - 'git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"', - ] - clone_cmds.extend([ - 'cd grafana-enterprise', - 'git checkout {}'.format(committish), - ]) - steps.insert(0, { - 'name': 'clone', - 'image': wix_image, - 'environment': { - 'GITHUB_TOKEN': from_secret(github_token), - }, - 'commands': download_grabpl_step_cmds + clone_cmds, - }) - steps[1]['depends_on'] = [ - 'clone', - ] - steps[1]['commands'].extend([ - # Need to move grafana-enterprise out of the way, so directory is empty and can be cloned into - 'cp -r grafana-enterprise C:\\App\\grafana-enterprise', - 'rm -r -force grafana-enterprise', - 'cp grabpl.exe C:\\App\\grabpl.exe', - 'rm -force grabpl.exe', - 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise', - 'cp C:\\App\\grabpl.exe grabpl.exe', - ]) - if 'environment' in steps[1]: - steps[1]['environment'] + {'GITHUB_TOKEN': from_secret(github_token)} - else: - steps[1]['environment'] = {'GITHUB_TOKEN': from_secret(github_token)} return pipeline( - name='main-windows', edition=edition, trigger=dict(trigger, repo=['grafana/grafana']), - steps=[identify_runner_step('windows')] + steps, - depends_on=['main-test-frontend', 'main-test-backend', 'main-build-e2e-publish', 'main-integration-tests'], platform='windows', environment=environment, + name='main-windows', + edition=edition, + trigger=dict(trigger, repo=['grafana/grafana']), + steps=get_windows_steps(edition, ver_mode), + depends_on=[ + 'main-test-frontend', + 'main-test-backend', + 'main-build-e2e-publish', + 'main-integration-tests', + ], + platform='windows', + environment=environment, ) diff --git a/scripts/drone/services/services.star b/scripts/drone/services/services.star index e81d0550077..b0d745b0b7a 100644 --- a/scripts/drone/services/services.star +++ b/scripts/drone/services/services.star @@ -1,8 +1,9 @@ def integration_test_services_volumes(): return [ - { 'name': 'postgres', 'temp': { 'medium': 'memory' } }, - { 'name': 'mysql', 'temp': { 'medium': 'memory' } - }] + {'name': 'postgres', 'temp': {'medium': 'memory'}}, + {'name': 'mysql', 'temp': {'medium': 'memory'}}, + ] + def integration_test_services(edition): services = [ @@ -10,15 +11,14 @@ def integration_test_services(edition): 'name': 'postgres', 'image': 'postgres:12.3-alpine', 'environment': { - 'POSTGRES_USER': 'grafanatest', - 'POSTGRES_PASSWORD': 'grafanatest', - 'POSTGRES_DB': 'grafanatest', - 'PGDATA': '/var/lib/postgresql/data/pgdata', + 'POSTGRES_USER': 'grafanatest', + 'POSTGRES_PASSWORD': 'grafanatest', + 'POSTGRES_DB': 'grafanatest', + 'PGDATA': '/var/lib/postgresql/data/pgdata', }, - 'volumes': [{ - 'name': 'postgres', - 'path': '/var/lib/postgresql/data/pgdata' - }], + 'volumes': [ + {'name': 'postgres', 'path': '/var/lib/postgresql/data/pgdata'} + ], }, { 'name': 'mysql', @@ -29,33 +29,36 @@ def integration_test_services(edition): 'MYSQL_USER': 'grafana', 'MYSQL_PASSWORD': 'password', }, - 'volumes': [{ - 'name': 'mysql', - 'path': '/var/lib/mysql' - }] + 'volumes': [{'name': 'mysql', 'path': '/var/lib/mysql'}], }, ] if edition in ('enterprise', 'enterprise2'): - services.extend([{ - 'name': 'redis', - 'image': 'redis:6.2.1-alpine', - 'environment': {}, - }, { - 'name': 'memcached', - 'image': 'memcached:1.6.9-alpine', - 'environment': {}, - }]) + services.extend( + [ + { + 'name': 'redis', + 'image': 'redis:6.2.1-alpine', + 'environment': {}, + }, + { + 'name': 'memcached', + 'image': 'memcached:1.6.9-alpine', + 'environment': {}, + }, + ] + ) return services + def ldap_service(): return { 'name': 'ldap', 'image': 'osixia/openldap:1.4.0', 'environment': { - 'LDAP_ADMIN_PASSWORD': 'grafana', - 'LDAP_DOMAIN': 'grafana.org', - 'SLAPD_ADDITIONAL_MODULES': 'memberof', + 'LDAP_ADMIN_PASSWORD': 'grafana', + 'LDAP_DOMAIN': 'grafana.org', + 'SLAPD_ADDITIONAL_MODULES': 'memberof', }, } diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index b2513e5633e..1a51ab3a3dc 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,4 +1,9 @@ -load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') +load( + 'scripts/drone/vault.star', + 'from_secret', + 'prerelease_bucket', + 'pull_secret', +) grabpl_version = 'v3.0.17' build_image = 'grafana/build-container:1.6.4' @@ -10,7 +15,6 @@ windows_image = 'mcr.microsoft.com/windows:1809' wix_image = 'grafana/ci-wix:0.1.1' go_image = 'golang:1.19.3' -disable_tests = False trigger_oss = { 'repo': [ 'grafana/grafana', @@ -29,17 +33,15 @@ def slack_step(channel, template, secret): }, } -def yarn_install_step(edition="oss"): - deps = [] - if edition == 'enterprise': - deps = ['init-enterprise'] + +def yarn_install_step(): return { 'name': 'yarn-install', 'image': build_image, 'commands': [ 'yarn install --immutable', ], - 'depends_on': deps, + 'depends_on': [], } @@ -57,15 +59,7 @@ def wire_install_step(): def identify_runner_step(platform='linux'): - if platform == 'windows': - return { - 'name': 'identify-runner', - 'image': windows_image, - 'commands': [ - 'echo $env:DRONE_RUNNER_NAME', - ], - } - else: + if platform == 'linux': return { 'name': 'identify-runner', 'image': alpine_image, @@ -73,20 +67,22 @@ def identify_runner_step(platform='linux'): 'echo $DRONE_RUNNER_NAME', ], } - - -def clone_enterprise_step(ver_mode): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '${DRONE_BRANCH}' else: - committish = '${DRONE_COMMIT}' + return { + 'name': 'identify-runner', + 'image': windows_image, + 'commands': [ + 'echo $env:DRONE_RUNNER_NAME', + ], + } + + +def clone_enterprise_step(committish='${DRONE_COMMIT}'): return { 'name': 'clone-enterprise', 'image': build_image, 'environment': { - 'GITHUB_TOKEN': from_secret(github_token), + 'GITHUB_TOKEN': from_secret('github_token'), }, 'commands': [ 'git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"', @@ -95,17 +91,18 @@ def clone_enterprise_step(ver_mode): ], } + def init_enterprise_step(ver_mode): source_commit = '' if ver_mode == 'release': source_commit = ' ${DRONE_TAG}' environment = { - 'GITHUB_TOKEN': from_secret(github_token), + 'GITHUB_TOKEN': from_secret('github_token'), } token = "--github-token $${GITHUB_TOKEN}" elif ver_mode == 'release-branch': environment = { - 'GITHUB_TOKEN': from_secret(github_token), + 'GITHUB_TOKEN': from_secret('github_token'), } token = "--github-token $${GITHUB_TOKEN}" else: @@ -122,10 +119,12 @@ def init_enterprise_step(ver_mode): 'mv bin/grabpl /tmp/', 'rmdir bin', 'mv grafana-enterprise /tmp/', - '/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format(token, source_commit), + '/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format( + token, source_commit + ).rstrip(), 'mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json', 'mkdir bin', - 'mv /tmp/grabpl bin/' + 'mv /tmp/grabpl bin/', ], } @@ -138,8 +137,9 @@ def download_grabpl_step(platform="linux"): 'commands': [ '$$ProgressPreference = "SilentlyContinue"', 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version), - ] + grabpl_version + ), + ], } return { @@ -151,7 +151,7 @@ def download_grabpl_step(platform="linux"): grabpl_version ), 'chmod +x bin/grabpl', - ] + ], } @@ -167,10 +167,8 @@ def lint_drone_step(): ], } -def enterprise_downstream_step(edition, ver_mode): - if edition in ('enterprise', 'enterprise2'): - return None +def enterprise_downstream_step(ver_mode): repo = 'grafana/grafana-enterprise@' if ver_mode == 'pr': repo += '${DRONE_SOURCE_BRANCH}' @@ -182,7 +180,7 @@ def enterprise_downstream_step(edition, ver_mode): 'image': 'grafana/drone-downstream', 'settings': { 'server': 'https://drone.grafana.net', - 'token': from_secret(drone_token), + 'token': from_secret('drone_token'), 'repositories': [ repo, ], @@ -194,15 +192,16 @@ def enterprise_downstream_step(edition, ver_mode): } if ver_mode == 'pr': - step.update({ 'failure': 'ignore' }) + step.update({'failure': 'ignore'}) step['settings']['params'].append('OSS_PULL_REQUEST=${DRONE_PULL_REQUEST}') return step -def lint_backend_step(edition): +def lint_backend_step(): return { 'name': 'lint-backend', + # TODO: build_image or go_image? 'image': go_image, 'environment': { # We need CGO because of go-sqlite3 @@ -233,10 +232,7 @@ def benchmark_ldap_step(): } -def build_storybook_step(edition, ver_mode): - if edition in ('enterprise', 'enterprise2'): - return None - +def build_storybook_step(ver_mode): return { 'name': 'build-storybook', 'image': build_image, @@ -256,28 +252,32 @@ def build_storybook_step(edition, ver_mode): } -def store_storybook_step(edition, ver_mode, trigger=None): - if edition in ('enterprise', 'enterprise2'): - return None - +def store_storybook_step(ver_mode, trigger=None): commands = [] if ver_mode == 'release': - commands.extend([ - './bin/build store-storybook --deployment latest', - './bin/build store-storybook --deployment ${DRONE_TAG}', - ]) + commands.extend( + [ + './bin/build store-storybook --deployment latest', + './bin/build store-storybook --deployment ${DRONE_TAG}', + ] + ) else: # main pipelines should deploy storybook to grafana-storybook/canary public bucket - commands = ['./bin/build store-storybook --deployment canary', ] + commands = [ + './bin/build store-storybook --deployment canary', + ] step = { 'name': 'store-storybook', 'image': publish_image, - 'depends_on': ['build-storybook', ] + end_to_end_tests_deps(), + 'depends_on': [ + 'build-storybook', + ] + + end_to_end_tests_deps(), 'environment': { 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) + 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), }, 'commands': commands, 'when': get_trigger_storybook(ver_mode), @@ -285,20 +285,22 @@ def store_storybook_step(edition, ver_mode, trigger=None): if trigger and ver_mode in ("release-branch", "main"): # no dict merge operation available, https://github.com/harness/drone-cli/pull/220 when_cond = { - 'repo': ['grafana/grafana',], + 'repo': [ + 'grafana/grafana', + ], 'paths': { 'include': [ 'packages/grafana-ui/**', ], - } + }, } step = dict(step, when=when_cond) return step -def e2e_tests_artifacts(edition): +def e2e_tests_artifacts(): return { - 'name': 'e2e-tests-artifacts-upload' + enterprise2_suffix(edition), + 'name': 'e2e-tests-artifacts-upload', 'image': 'google/cloud-sdk:406.0.0', 'depends_on': [ 'end-to-end-tests-dashboards-suite', @@ -328,8 +330,8 @@ def e2e_tests_artifacts(edition): 'gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip', 'export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip', 'echo "E2E Test artifacts uploaded to: $${E2E_ARTIFACTS_VIDEO_ZIP}"', - 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} -H "Authorization: token $${GITHUB_TOKEN}" -d ' + - '"{\\"state\\":\\"success\\",\\"target_url\\":\\"$${E2E_ARTIFACTS_VIDEO_ZIP}\\", \\"description\\": \\"Click on the details to download e2e recording videos\\", \\"context\\": \\"e2e_artifacts\\"}"', + 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} -H "Authorization: token $${GITHUB_TOKEN}" -d ' + + '"{\\"state\\":\\"success\\",\\"target_url\\":\\"$${E2E_ARTIFACTS_VIDEO_ZIP}\\", \\"description\\": \\"Click on the details to download e2e recording videos\\", \\"context\\": \\"e2e_artifacts\\"}"', ], } @@ -337,13 +339,17 @@ def e2e_tests_artifacts(edition): def upload_cdn_step(edition, ver_mode, trigger=None): deps = [] if edition in 'enterprise2': - deps.extend([ - 'package' + enterprise2_suffix(edition), - ]) + deps.extend( + [ + 'package' + enterprise2_suffix(edition), + ] + ) else: - deps.extend([ - 'grafana-server', - ]) + deps.extend( + [ + 'grafana-server', + ] + ) step = { 'name': 'upload-cdn-assets' + enterprise2_suffix(edition), @@ -351,7 +357,7 @@ def upload_cdn_step(edition, ver_mode, trigger=None): 'depends_on': deps, 'environment': { 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) + 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), }, 'commands': [ './bin/build upload-cdn --edition {}'.format(edition), @@ -378,7 +384,9 @@ def build_backend_step(edition, ver_mode, variants=None): build_no = '${DRONE_BUILD_NUMBER}' cmds = [ './bin/build build-backend --jobs 8 --edition {} --build-id {}{}'.format( - edition, build_no, variants_str, + edition, + build_no, + variants_str, ), ] @@ -399,13 +407,13 @@ def build_frontend_step(edition, ver_mode): # TODO: Use percentage for num jobs if ver_mode == 'release': cmds = [ - './bin/build build-frontend --jobs 8 ' + \ - '--edition {} ${{DRONE_TAG}}'.format(edition), + './bin/build build-frontend --jobs 8 ' + + '--edition {} ${{DRONE_TAG}}'.format(edition), ] else: cmds = [ - './bin/build build-frontend --jobs 8 --edition {} '.format(edition) + \ - '--build-id {}'.format(build_no), + './bin/build build-frontend --jobs 8 --edition {} '.format(edition) + + '--build-id {}'.format(build_no), ] return { @@ -428,13 +436,13 @@ def build_frontend_package_step(edition, ver_mode): # TODO: Use percentage for num jobs if ver_mode == 'release': cmds = [ - './bin/build build-frontend-packages --jobs 8 ' + \ - '--edition {} ${{DRONE_TAG}}'.format(edition), + './bin/build build-frontend-packages --jobs 8 ' + + '--edition {} ${{DRONE_TAG}}'.format(edition), ] else: cmds = [ - './bin/build build-frontend-packages --jobs 8 --edition {} '.format(edition) + \ - '--build-id {}'.format(build_no), + './bin/build build-frontend-packages --jobs 8 --edition {} '.format(edition) + + '--build-id {}'.format(build_no), ] return { @@ -452,7 +460,7 @@ def build_frontend_package_step(edition, ver_mode): def build_plugins_step(edition, ver_mode): - if ver_mode!='pr': + if ver_mode != 'pr': env = { 'GRAFANA_API_KEY': from_secret('grafana_api_key'), } @@ -473,7 +481,7 @@ def build_plugins_step(edition, ver_mode): } -def test_backend_step(edition): +def test_backend_step(): return { 'name': 'test-backend', 'image': build_image, @@ -486,8 +494,7 @@ def test_backend_step(edition): } - -def test_backend_integration_step(edition): +def test_backend_integration_step(): return { 'name': 'test-backend-integration', 'image': build_image, @@ -499,6 +506,7 @@ def test_backend_integration_step(edition): ], } + def betterer_frontend_step(edition="oss"): deps = [] if edition == "enterprise": @@ -514,7 +522,6 @@ def betterer_frontend_step(edition="oss"): } - def test_frontend_step(edition="oss"): deps = [] if edition == "enterprise": @@ -552,30 +559,35 @@ def lint_frontend_step(): } -def test_a11y_frontend_step(ver_mode, edition, port=3001): +def test_a11y_frontend_step(ver_mode, port=3001): commands = [ 'yarn wait-on http://$HOST:$PORT', ] failure = 'ignore' if ver_mode == 'pr': - commands.extend([ - 'pa11y-ci --config .pa11yci-pr.conf.js', - ]) + commands.extend( + [ + 'pa11y-ci --config .pa11yci-pr.conf.js', + ] + ) failure = 'always' else: - commands.extend([ - 'pa11y-ci --config .pa11yci.conf.js --json > pa11y-ci-results.json', - ]) + commands.extend( + [ + 'pa11y-ci --config .pa11yci.conf.js --json > pa11y-ci-results.json', + ] + ) return { - 'name': 'test-a11y-frontend' + enterprise2_suffix(edition), + 'name': 'test-a11y-frontend', + # TODO which image should be used? 'image': 'grafana/docker-puppeteer:1.1.0', 'depends_on': [ - 'grafana-server' + enterprise2_suffix(edition), + 'grafana-server', ], 'environment': { 'GRAFANA_MISC_STATS_API_KEY': from_secret('grafana_misc_stats_api_key'), - 'HOST': 'grafana-server' + enterprise2_suffix(edition), + 'HOST': 'grafana-server', 'PORT': port, }, 'failure': failure, @@ -583,15 +595,12 @@ def test_a11y_frontend_step(ver_mode, edition, port=3001): } -def frontend_metrics_step(edition, trigger=None): - if edition in ('enterprise', 'enterprise2'): - return None - +def frontend_metrics_step(trigger=None): step = { 'name': 'publish-frontend-metrics', 'image': build_image, 'depends_on': [ - 'test-a11y-frontend' + enterprise2_suffix(edition), + 'test-a11y-frontend', ], 'environment': { 'GRAFANA_MISC_STATS_API_KEY': from_secret('grafana_misc_stats_api_key'), @@ -643,21 +652,20 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None): else: sign_args = '' env = None + # TODO: env vars no longer needed by build if not signing test_args = '. scripts/build/gpg-test-vars.sh && ' # TODO: Use percentage for jobs if ver_mode == 'release': cmds = [ - '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) + \ - '{} ${{DRONE_TAG}}'.format( - sign_args - ), + '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) + + '{} ${{DRONE_TAG}}'.format(sign_args), ] else: build_no = '${DRONE_BUILD_NUMBER}' cmds = [ - '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) + \ - '--build-id {}{}{}'.format(build_no, variants_str, sign_args), + '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) + + '--build-id {}{}{}'.format(build_no, variants_str, sign_args), ] return { @@ -670,21 +678,12 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None): def grafana_server_step(edition, port=3001): - package_file_pfx = '' - if edition == 'enterprise2': - package_file_pfx = 'grafana' + enterprise2_suffix(edition) - elif edition == 'enterprise': - package_file_pfx = 'grafana-' + edition - - environment = { - 'PORT': port, - 'ARCH': 'linux-amd64' - } - if package_file_pfx: - environment['RUNDIR'] = 'scripts/grafana-server/tmp-{}'.format(package_file_pfx) + environment = {'PORT': port, 'ARCH': 'linux-amd64'} + if edition == 'enterprise': + environment['RUNDIR'] = 'scripts/grafana-server/tmp-grafana-enterprise' return { - 'name': 'grafana-server' + enterprise2_suffix(edition), + 'name': 'grafana-server', 'image': build_image, 'detach': True, 'depends_on': [ @@ -700,18 +699,18 @@ def grafana_server_step(edition, port=3001): } -def e2e_tests_step(suite, edition, port=3001, tries=None): +def e2e_tests_step(suite, port=3001, tries=None): cmd = './bin/build e2e-tests --port {} --suite {}'.format(port, suite) if tries: cmd += ' --tries {}'.format(tries) return { - 'name': 'end-to-end-tests-{}'.format(suite) + enterprise2_suffix(edition), + 'name': 'end-to-end-tests-{}'.format(suite), 'image': 'cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97', 'depends_on': [ 'grafana-server', ], 'environment': { - 'HOST': 'grafana-server' + enterprise2_suffix(edition), + 'HOST': 'grafana-server', }, 'commands': [ 'apt-get install -y netcat', @@ -719,7 +718,8 @@ def e2e_tests_step(suite, edition, port=3001, tries=None): ], } -def cloud_plugins_e2e_tests_step(suite, edition, cloud, port=3001, video="false", trigger=None): + +def cloud_plugins_e2e_tests_step(suite, cloud, port=3001, video="false", trigger=None): environment = {} when = {} if trigger: @@ -727,34 +727,35 @@ def cloud_plugins_e2e_tests_step(suite, edition, cloud, port=3001, video="false" if cloud == 'azure': environment = { 'CYPRESS_CI': 'true', - 'HOST': 'grafana-server' + enterprise2_suffix(edition), + 'HOST': 'grafana-server', 'GITHUB_TOKEN': from_secret('github_token_pr'), 'AZURE_SP_APP_ID': from_secret('azure_sp_app_id'), 'AZURE_SP_PASSWORD': from_secret('azure_sp_app_pw'), - 'AZURE_TENANT': from_secret('azure_tenant') + 'AZURE_TENANT': from_secret('azure_tenant'), } - when= dict(when, paths={ - 'include' : [ + when = dict( + when, + paths={ + 'include': [ 'pkg/tsdb/azuremonitor/**', - 'public/app/plugins/datasource/grafana-azure-monitor-datasource/**' + 'public/app/plugins/datasource/grafana-azure-monitor-datasource/**', ] - }) + }, + ) branch = "${DRONE_SOURCE_BRANCH}".replace("/", "-") step = { - 'name': 'end-to-end-tests-{}-{}'.format(suite, cloud) + enterprise2_suffix(edition), + 'name': 'end-to-end-tests-{}-{}'.format(suite, cloud), 'image': 'us-docker.pkg.dev/grafanalabs-dev/cloud-data-sources/e2e:latest', 'depends_on': [ 'grafana-server', ], 'environment': environment, - 'commands': [ - 'cd /', - './cpp-e2e/scripts/ci-run.sh {} {}'.format(cloud, branch) - ], + 'commands': ['cd /', './cpp-e2e/scripts/ci-run.sh {} {}'.format(cloud, branch)], } step = dict(step, when=when) return step + def build_docs_website_step(): return { 'name': 'build-docs-website', @@ -782,7 +783,9 @@ def copy_packages_for_docker_step(edition=None): } -def build_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publish=False): +def build_docker_images_step( + edition, ver_mode, archs=None, ubuntu=False, publish=False +): cmd = './bin/build build-docker --edition {}'.format(edition) if publish: cmd += ' --shouldSave' @@ -800,25 +803,23 @@ def build_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publis } if edition == 'enterprise2': - environment.update({'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo')}) + environment.update( + {'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo')} + ) return { 'name': 'build-docker-images' + ubuntu_sfx, 'image': 'google/cloud-sdk', 'depends_on': [ - 'copy-packages-for-docker', - 'compile-build-cmd', + 'copy-packages-for-docker', + 'compile-build-cmd', ], - 'commands': [ - cmd - ], - 'volumes': [{ - 'name': 'docker', - 'path': '/var/run/docker.sock' - }], - 'environment': environment + 'commands': [cmd], + 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], + 'environment': environment, } + def fetch_images_step(edition): return { 'name': 'fetch-images-{}'.format(edition), @@ -831,10 +832,7 @@ def fetch_images_step(edition): }, 'commands': ['./bin/build artifacts docker fetch --edition {}'.format(edition)], 'depends_on': ['compile-build-cmd'], - 'volumes': [{ - 'name': 'docker', - 'path': '/var/run/docker.sock' - }], + 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], } @@ -852,19 +850,27 @@ def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): 'DOCKER_PASSWORD': from_secret('docker_password'), } - cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {}'.format(mode, docker_repo) + cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {}'.format( + mode, docker_repo + ) + deps = ['build-docker-images', 'build-docker-images-ubuntu'] if ver_mode == 'release': deps = ['fetch-images-{}'.format(edition)] cmd += ' --version-tag ${DRONE_TAG}' - else: - deps = ['build-docker-images', 'build-docker-images-ubuntu'] if edition == 'enterprise2': name = edition docker_repo = '$${DOCKER_ENTERPRISE2_REPO}' - environment.update({'GCP_KEY': from_secret('gcp_key_hg'), 'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo')}) - cmd = './bin/build artifacts docker publish-enterprise2 --dockerhub-repo {}'.format(docker_repo) + environment.update( + { + 'GCP_KEY': from_secret('gcp_key_hg'), + 'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo'), + } + ) + cmd = './bin/build artifacts docker publish-enterprise2 --dockerhub-repo {}'.format( + docker_repo + ) step = { 'name': 'publish-images-{}'.format(name), @@ -872,10 +878,7 @@ def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): 'environment': environment, 'commands': [cmd], 'depends_on': deps, - 'volumes': [{ - 'name': 'docker', - 'path': '/var/run/docker.sock' - }], + 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], } if trigger and ver_mode in ("release-branch", "main"): step = dict(step, when=trigger) @@ -883,17 +886,17 @@ def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): return step -def postgres_integration_tests_step(edition, ver_mode): +def postgres_integration_tests_step(): cmds = [ - 'apt-get update', - 'apt-get install -yq postgresql-client', - 'dockerize -wait tcp://postgres:5432 -timeout 120s', - 'psql -p 5432 -h postgres -U grafanatest -d grafanatest -f ' + - 'devenv/docker/blocks/postgres_tests/setup.sql', - # Make sure that we don't use cached results for another database - 'go clean -testcache', - "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", - ] + 'apt-get update', + 'apt-get install -yq postgresql-client', + 'dockerize -wait tcp://postgres:5432 -timeout 120s', + 'psql -p 5432 -h postgres -U grafanatest -d grafanatest -f ' + + 'devenv/docker/blocks/postgres_tests/setup.sql', + # Make sure that we don't use cached results for another database + 'go clean -testcache', + "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", + ] return { 'name': 'postgres-integration-tests', 'image': build_image, @@ -907,16 +910,16 @@ def postgres_integration_tests_step(edition, ver_mode): } -def mysql_integration_tests_step(edition, ver_mode): +def mysql_integration_tests_step(): cmds = [ - 'apt-get update', - 'apt-get install -yq default-mysql-client', - 'dockerize -wait tcp://mysql:3306 -timeout 120s', - 'cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass', - # Make sure that we don't use cached results for another database - 'go clean -testcache', - "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", - ] + 'apt-get update', + 'apt-get install -yq default-mysql-client', + 'dockerize -wait tcp://mysql:3306 -timeout 120s', + 'cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass', + # Make sure that we don't use cached results for another database + 'go clean -testcache', + "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", + ] return { 'name': 'mysql-integration-tests', 'image': build_image, @@ -959,10 +962,7 @@ def memcached_integration_tests_step(): } -def release_canary_npm_packages_step(edition, trigger=None): - if edition in ('enterprise', 'enterprise2'): - return None - +def release_canary_npm_packages_step(trigger=None): step = { 'name': 'release-canary-npm-packages', 'image': build_image, @@ -986,26 +986,17 @@ def enterprise2_suffix(edition): def upload_packages_step(edition, ver_mode, trigger=None): - if ver_mode == 'main' and edition in ('enterprise', 'enterprise2'): - return None - - deps = [] - if edition in 'enterprise2' or not end_to_end_tests_deps(): - deps.extend([ - 'package' + enterprise2_suffix(edition), - ]) - else: - deps.extend(end_to_end_tests_deps()) - step = { 'name': 'upload-packages' + enterprise2_suffix(edition), 'image': publish_image, - 'depends_on': deps, + 'depends_on': end_to_end_tests_deps(), 'environment': { 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), }, - 'commands': ['./bin/build upload-packages --edition {}'.format(edition),], + 'commands': [ + './bin/build upload-packages --edition {}'.format(edition), + ], } if trigger and ver_mode in ("release-branch", "main"): step = dict(step, when=trigger) @@ -1020,7 +1011,8 @@ def publish_grafanacom_step(edition, ver_mode): elif ver_mode == 'main': build_no = '${DRONE_BUILD_NUMBER}' cmd = './bin/build publish grafana-com --edition {} --build-id {}'.format( - edition, build_no, + edition, + build_no, ) else: fail('Unexpected version mode {}'.format(ver_mode)) @@ -1041,14 +1033,13 @@ def publish_grafanacom_step(edition, ver_mode): ], } + def publish_linux_packages_step(edition, package_manager='deb'): return { 'name': 'publish-linux-packages-{}'.format(package_manager), # See https://github.com/grafana/deployment_tools/blob/master/docker/package-publish/README.md for docs on that image 'image': 'us.gcr.io/kubernetes-dev/package-publish:latest', - 'depends_on': [ - 'grabpl' - ], + 'depends_on': ['grabpl'], 'privileged': True, 'settings': { 'access_key_id': from_secret('packages_access_key_id'), @@ -1059,31 +1050,92 @@ def publish_linux_packages_step(edition, package_manager='deb'): 'gpg_passphrase': from_secret('packages_gpg_passphrase'), 'gpg_public_key': from_secret('packages_gpg_public_key'), 'gpg_private_key': from_secret('packages_gpg_private_key'), - 'package_path': 'gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/{}/**.{}'.format(edition, package_manager) - } + 'package_path': 'gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/{}/**.{}'.format( + edition, package_manager + ), + }, } def get_windows_steps(edition, ver_mode): - init_cmds = [] - sfx = '' + steps = [ + identify_runner_step('windows'), + ] + if edition in ('enterprise', 'enterprise2'): - sfx = '-{}'.format(edition) - else: - init_cmds.extend([ + if ver_mode == 'release': + committish = '${DRONE_TAG}' + elif ver_mode == 'release-branch': + committish = '$$env:DRONE_BRANCH' + else: + committish = '$$env:DRONE_COMMIT' + + # For enterprise, we have to clone both OSS and enterprise and merge the latter into the former + download_grabpl_cmds = [ '$$ProgressPreference = "SilentlyContinue"', 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version), - ]) - steps = [ - { - 'name': 'windows-init', - 'image': wix_image, - 'commands': init_cmds, - }, - ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( - 'release', 'release-branch', + grabpl_version + ), + ] + + clone_cmds = [ + 'git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"', + 'cd grafana-enterprise', + 'git checkout {}'.format(committish), + ] + + init_cmds = [ + # Need to move grafana-enterprise out of the way, so directory is empty and can be cloned into + 'cp -r grafana-enterprise C:\\App\\grafana-enterprise', + 'rm -r -force grafana-enterprise', + 'cp grabpl.exe C:\\App\\grabpl.exe', + 'rm -force grabpl.exe', + 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise', + 'cp C:\\App\\grabpl.exe grabpl.exe', + ] + + steps.extend( + [ + { + 'name': 'clone', + 'image': wix_image, + 'environment': { + 'GITHUB_TOKEN': from_secret('github_token'), + }, + 'commands': download_grabpl_cmds + clone_cmds, + }, + { + 'name': 'windows-init', + 'image': wix_image, + 'commands': init_cmds, + 'depends_on': ['clone'], + 'environment': {'GITHUB_TOKEN': from_secret('github_token')}, + }, + ] + ) + else: + init_cmds = [ + '$$ProgressPreference = "SilentlyContinue"', + 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( + grabpl_version + ), + ] + + steps.extend( + [ + { + 'name': 'windows-init', + 'image': wix_image, + 'commands': init_cmds, + }, + ] + ) + + if ( + ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2')) + ) or ver_mode in ( + 'release', + 'release-branch', ): bucket = '%PRERELEASE_BUCKET%/artifacts/downloads' if ver_mode == 'release': @@ -1103,92 +1155,61 @@ def get_windows_steps(edition, ver_mode): 'rm gcpkey.json', 'cp C:\\App\\nssm-2.24.zip .', ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( - 'release', - ): - installer_commands.extend([ - '.\\grabpl.exe windows-installer --edition {} {}'.format(edition, ver_part), - '$$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0]', - ]) + if ( + ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2')) + ) or ver_mode in ('release',): + installer_commands.extend( + [ + '.\\grabpl.exe windows-installer --edition {} {}'.format( + edition, ver_part + ), + '$$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0]', + ] + ) if ver_mode == 'main': - installer_commands.extend([ - 'gsutil cp $$fname gs://{}/{}/{}/'.format(bucket, edition, dir), - 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/'.format(bucket, edition, dir), - ]) + installer_commands.extend( + [ + 'gsutil cp $$fname gs://{}/{}/{}/'.format(bucket, edition, dir), + 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/'.format( + bucket, edition, dir + ), + ] + ) else: - installer_commands.extend([ - 'gsutil cp $$fname gs://{}/{}/{}/{}/'.format(bucket, ver_part, edition, dir), - 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/{}/'.format(bucket, ver_part, edition, dir), - ]) - steps.append({ - 'name': 'build-windows-installer', - 'image': wix_image, - 'depends_on': [ - 'windows-init', - ], - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), - 'GITHUB_TOKEN': from_secret('github_token') - }, - 'commands': installer_commands, - }) - - if edition in ('enterprise', 'enterprise2'): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '$$env:DRONE_BRANCH' - else: - committish = '$$env:DRONE_COMMIT' - # For enterprise, we have to clone both OSS and enterprise and merge the latter into the former - download_grabpl_step_cmds = [ - '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version), - ] - clone_cmds = [ - 'git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"', - ] - clone_cmds.extend([ - 'cd grafana-enterprise', - 'git checkout {}'.format(committish), - ]) - steps.insert(0, { - 'name': 'clone', - 'image': wix_image, - 'environment': { - 'GITHUB_TOKEN': from_secret(github_token), - }, - 'commands': download_grabpl_step_cmds + clone_cmds, - }) - steps[1]['depends_on'] = [ - 'clone', - ] - steps[1]['commands'].extend([ - # Need to move grafana-enterprise out of the way, so directory is empty and can be cloned into - 'cp -r grafana-enterprise C:\\App\\grafana-enterprise', - 'rm -r -force grafana-enterprise', - 'cp grabpl.exe C:\\App\\grabpl.exe', - 'rm -force grabpl.exe', - 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise', - 'cp C:\\App\\grabpl.exe grabpl.exe', - ]) - if 'environment' in steps[1]: - steps[1]['environment'] + {'GITHUB_TOKEN': from_secret(github_token)} - else: - steps[1]['environment'] = {'GITHUB_TOKEN': from_secret(github_token)} + installer_commands.extend( + [ + 'gsutil cp $$fname gs://{}/{}/{}/{}/'.format( + bucket, ver_part, edition, dir + ), + 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/{}/'.format( + bucket, ver_part, edition, dir + ), + ] + ) + steps.append( + { + 'name': 'build-windows-installer', + 'image': wix_image, + 'depends_on': [ + 'windows-init', + ], + 'environment': { + 'GCP_KEY': from_secret('gcp_key'), + 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), + 'GITHUB_TOKEN': from_secret('github_token'), + }, + 'commands': installer_commands, + } + ) return steps -def verify_gen_cue_step(edition): - deps = [] - if edition in ('enterprise', 'enterprise2'): - deps.extend(['init-enterprise']) + +def verify_gen_cue_step(): return { 'name': 'verify-gen-cue', 'image': build_image, - 'depends_on': deps, + 'depends_on': [], 'commands': [ '# It is required that code generated from Thema/CUE be committed and in sync with its inputs.', '# The following command will fail if running code generators produces any diff in output.', @@ -1196,14 +1217,12 @@ def verify_gen_cue_step(edition): ], } -def verify_gen_jsonnet_step(edition): - deps = [] - if edition in ('enterprise', 'enterprise2'): - deps.extend(['init-enterprise']) + +def verify_gen_jsonnet_step(): return { 'name': 'verify-gen-jsonnet', 'image': build_image, - 'depends_on': deps, + 'depends_on': [], 'commands': [ '# It is required that generated jsonnet is committed and in sync with its inputs.', '# The following command will fail if running code generators produces any diff in output.', @@ -1211,6 +1230,7 @@ def verify_gen_jsonnet_step(edition): ], } + def trigger_test_release(): return { 'name': 'trigger-test-release', @@ -1241,10 +1261,11 @@ def trigger_test_release(): }, 'repo': [ 'grafana/grafana', - ] - } + ], + }, } + def artifacts_page_step(): return { 'name': 'artifacts-page', @@ -1260,9 +1281,8 @@ def artifacts_page_step(): ], } + def end_to_end_tests_deps(): - if disable_tests: - return [] return [ 'end-to-end-tests-dashboards-suite', 'end-to-end-tests-panels-suite', @@ -1270,10 +1290,13 @@ def end_to_end_tests_deps(): 'end-to-end-tests-various-suite', ] + def compile_build_cmd(edition='oss'): dependencies = [] if edition in ('enterprise', 'enterprise2'): - dependencies = ['init-enterprise',] + dependencies = [ + 'init-enterprise', + ] return { 'name': 'compile-build-cmd', 'image': go_image, @@ -1283,15 +1306,14 @@ def compile_build_cmd(edition='oss'): 'depends_on': dependencies, 'environment': { 'CGO_ENABLED': 0, - }, -} + }, + } + def get_trigger_storybook(ver_mode): trigger_storybook = '' if ver_mode == 'release': - trigger_storybook = { - 'event': ['tag'] - } + trigger_storybook = {'event': ['tag']} else: trigger_storybook = { 'paths': { diff --git a/scripts/drone/utils/utils.star b/scripts/drone/utils/utils.star index eef792b9207..4178dae8a99 100644 --- a/scripts/drone/utils/utils.star +++ b/scripts/drone/utils/utils.star @@ -4,25 +4,33 @@ load( 'slack_step', ) -load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token') +load( + 'scripts/drone/vault.star', + 'from_secret', + 'pull_secret', +) failure_template = 'Build {{build.number}} failed for commit: : {{build.link}}\nBranch: \nAuthor: {{build.author}}' drone_change_template = '`.drone.yml` and `starlark` files have been changed on the OSS repo, by: {{build.author}}. \nBranch: \nCommit hash: ' + def pipeline( - name, edition, trigger, steps, services=[], platform='linux', depends_on=[], environment=None, volumes=[], - ): + name, + edition, + trigger, + steps, + services=[], + platform='linux', + depends_on=[], + environment=None, + volumes=[], +): if platform != 'windows': platform_conf = { - 'platform': { - 'os': 'linux', - 'arch': 'amd64' - }, + 'platform': {'os': 'linux', 'arch': 'amd64'}, # A shared cache is used on the host # To avoid issues with parallel builds, we run this repo on single build agents - 'node': { - 'type': 'no-parallel' - } + 'node': {'type': 'no-parallel'}, } else: platform_conf = { @@ -43,19 +51,23 @@ def pipeline( 'clone': { 'retries': 3, }, - 'volumes': [{ - 'name': 'docker', - 'host': { - 'path': '/var/run/docker.sock', - }, - }], + 'volumes': [ + { + 'name': 'docker', + 'host': { + 'path': '/var/run/docker.sock', + }, + } + ], 'depends_on': depends_on, 'image_pull_secrets': [pull_secret], } if environment: - pipeline.update({ - 'environment': environment, - }) + pipeline.update( + { + 'environment': environment, + } + ) pipeline['volumes'].extend(volumes) pipeline.update(platform_conf) @@ -68,7 +80,10 @@ def pipeline( return pipeline -def notify_pipeline(name, slack_channel, trigger, depends_on=[], template=None, secret=None): + +def notify_pipeline( + name, slack_channel, trigger, depends_on=[], template=None, secret=None +): trigger = dict(trigger) return { 'kind': 'pipeline', @@ -89,3 +104,9 @@ def notify_pipeline(name, slack_channel, trigger, depends_on=[], template=None, } +# TODO: this overrides any existing dependencies because we're following the existing logic +# it should append to any existing dependencies +def with_deps(steps, deps=[]): + for step in steps: + step['depends_on'] = deps + return steps diff --git a/scripts/drone/vault.star b/scripts/drone/vault.star index c1a4e25c5bb..e2804f0d879 100644 --- a/scripts/drone/vault.star +++ b/scripts/drone/vault.star @@ -1,5 +1,4 @@ pull_secret = 'dockerconfigjson' -github_token = 'github_token' drone_token = 'drone_token' prerelease_bucket = 'prerelease_bucket' gcp_upload_artifacts_key = 'gcp_upload_artifacts_key' @@ -7,10 +6,10 @@ azure_sp_app_id = 'azure_sp_app_id' azure_sp_app_pw = 'azure_sp_app_pw' azure_tenant = 'azure_tenant' + def from_secret(secret): - return { - 'from_secret': secret - } + return {'from_secret': secret} + def vault_secret(name, path, key): return { @@ -19,25 +18,65 @@ def vault_secret(name, path, key): 'get': { 'path': path, 'name': key, - } + }, } + def secrets(): return [ vault_secret(pull_secret, 'secret/data/common/gcr', '.dockerconfigjson'), - vault_secret(github_token, 'infra/data/ci/github/grafanabot', 'pat'), + vault_secret('github_token', 'infra/data/ci/github/grafanabot', 'pat'), vault_secret(drone_token, 'infra/data/ci/drone', 'machine-user-token'), vault_secret(prerelease_bucket, 'infra/data/ci/grafana/prerelease', 'bucket'), - vault_secret(gcp_upload_artifacts_key, 'infra/data/ci/grafana/releng/artifacts-uploader-service-account', 'credentials.json'), - vault_secret(azure_sp_app_id, 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', 'application_id'), - vault_secret(azure_sp_app_pw, 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', 'application_secret'), - vault_secret(azure_tenant, 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', 'tenant_id'), - + vault_secret( + gcp_upload_artifacts_key, + 'infra/data/ci/grafana/releng/artifacts-uploader-service-account', + 'credentials.json', + ), + vault_secret( + azure_sp_app_id, + 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', + 'application_id', + ), + vault_secret( + azure_sp_app_pw, + 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', + 'application_secret', + ), + vault_secret( + azure_tenant, + 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', + 'tenant_id', + ), # Package publishing - vault_secret('packages_gpg_public_key', 'infra/data/ci/packages-publish/gpg', 'public-key'), - vault_secret('packages_gpg_private_key', 'infra/data/ci/packages-publish/gpg', 'private-key'), - vault_secret('packages_gpg_passphrase', 'infra/data/ci/packages-publish/gpg', 'passphrase'), - vault_secret('packages_service_account', 'infra/data/ci/packages-publish/service-account', 'credentials.json'), - vault_secret('packages_access_key_id', 'infra/data/ci/packages-publish/bucket-credentials', 'AccessID'), - vault_secret('packages_secret_access_key', 'infra/data/ci/packages-publish/bucket-credentials', 'Secret'), + vault_secret( + 'packages_gpg_public_key', + 'infra/data/ci/packages-publish/gpg', + 'public-key', + ), + vault_secret( + 'packages_gpg_private_key', + 'infra/data/ci/packages-publish/gpg', + 'private-key', + ), + vault_secret( + 'packages_gpg_passphrase', + 'infra/data/ci/packages-publish/gpg', + 'passphrase', + ), + vault_secret( + 'packages_service_account', + 'infra/data/ci/packages-publish/service-account', + 'credentials.json', + ), + vault_secret( + 'packages_access_key_id', + 'infra/data/ci/packages-publish/bucket-credentials', + 'AccessID', + ), + vault_secret( + 'packages_secret_access_key', + 'infra/data/ci/packages-publish/bucket-credentials', + 'Secret', + ), ] diff --git a/scripts/drone/version.star b/scripts/drone/version.star index 651a557ec75..dfdc2da5323 100644 --- a/scripts/drone/version.star +++ b/scripts/drone/version.star @@ -6,7 +6,12 @@ load( ) ver_mode = 'release-branch' -trigger={'ref': ['refs/heads/v[0-9]*']} +trigger = {'ref': ['refs/heads/v[0-9]*']} + def version_branch_pipelines(): - return oss_pipelines(ver_mode=ver_mode, trigger=trigger) + enterprise_pipelines(ver_mode=ver_mode, trigger=trigger) + enterprise2_pipelines(ver_mode=ver_mode, trigger=trigger) + return ( + oss_pipelines(ver_mode=ver_mode, trigger=trigger) + + enterprise_pipelines(ver_mode=ver_mode, trigger=trigger) + + enterprise2_pipelines(ver_mode=ver_mode, trigger=trigger) + ) From 0801fce23c92c4870c53bf4a0d70f8426d4c123d Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 7 Dec 2022 09:56:15 +0200 Subject: [PATCH 33/94] CI: Move `verify-storybook` command from grabpl (#59928) * Move verify-storybook from grabpl * Remove zerolog --- .drone.yml | 10 +++++----- pkg/build/cmd/main.go | 7 ++++++- pkg/build/cmd/verifystorybook.go | 32 ++++++++++++++++++++++++++++++++ scripts/drone/steps/lib.star | 2 +- 4 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 pkg/build/cmd/verifystorybook.go diff --git a/.drone.yml b/.drone.yml index 4d427ed0cfa..b8ebfe2ace7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -575,7 +575,7 @@ steps: - failure - commands: - yarn storybook:build - - ./bin/grabpl verify-storybook + - ./bin/build verify-storybook depends_on: - build-frontend - build-frontend-packages @@ -1430,7 +1430,7 @@ steps: - failure - commands: - yarn storybook:build - - ./bin/grabpl verify-storybook + - ./bin/build verify-storybook depends_on: - build-frontend - build-frontend-packages @@ -2155,7 +2155,7 @@ steps: - failure - commands: - yarn storybook:build - - ./bin/grabpl verify-storybook + - ./bin/build verify-storybook depends_on: - build-frontend - build-frontend-packages @@ -4699,7 +4699,7 @@ steps: - failure - commands: - yarn storybook:build - - ./bin/grabpl verify-storybook + - ./bin/build verify-storybook depends_on: - build-frontend - build-frontend-packages @@ -6318,6 +6318,6 @@ kind: secret name: packages_secret_access_key --- kind: signature -hmac: efffc2eb55bbfa1cebe950585df1f3f81d2f2a38311e2854f3ce08d1eec16fe3 +hmac: dcf24226fae30872050cdc031430374d811e6bbe13158ce0fbf234c90c1d83f9 ... diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index bf60c54e7e2..bd507dee212 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -152,7 +152,7 @@ func main() { }, { Name: "store-storybook", - Usage: "Integrity check for storybook build", + Usage: "Stores storybook to GCS buckets", Action: StoreStorybook, Flags: []cli.Flag{ &cli.StringFlag{ @@ -161,6 +161,11 @@ func main() { }, }, }, + { + Name: "verify-storybook", + Usage: "Integrity check for storybook build", + Action: VerifyStorybook, + }, { Name: "upload-packages", Usage: "Upload Grafana packages", diff --git a/pkg/build/cmd/verifystorybook.go b/pkg/build/cmd/verifystorybook.go new file mode 100644 index 00000000000..052206f1660 --- /dev/null +++ b/pkg/build/cmd/verifystorybook.go @@ -0,0 +1,32 @@ +// Package verifystorybook contains the sub-command "verify-storybook". +package main + +import ( + "fmt" + "log" + "path/filepath" + + "github.com/grafana/grafana/pkg/infra/fs" + "github.com/urfave/cli/v2" +) + +// VerifyStorybook Action implements the sub-command "verify-storybook". +func VerifyStorybook(c *cli.Context) error { + const grafanaDir = "." + + paths := []string{ + "packages/grafana-ui/dist/storybook/index.html", + "packages/grafana-ui/dist/storybook/iframe.html"} + for _, p := range paths { + exists, err := fs.Exists(filepath.Join(grafanaDir, p)) + if err != nil { + return cli.NewExitError(fmt.Sprintf("failed to verify Storybook build: %s", err), 1) + } + if !exists { + return fmt.Errorf("failed to verify Storybook build, missing %q", p) + } + } + + log.Printf("Successfully verified Storybook integrity") + return nil +} diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 1a51ab3a3dc..96ab84129ca 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -246,7 +246,7 @@ def build_storybook_step(ver_mode): }, 'commands': [ 'yarn storybook:build', - './bin/grabpl verify-storybook', + './bin/build verify-storybook', ], 'when': get_trigger_storybook(ver_mode), } From dce7f50d5954fd16b5703af96637b6f831e47f2a Mon Sep 17 00:00:00 2001 From: Conor Evans <43791257+conorevans@users.noreply.github.com> Date: Wed, 7 Dec 2022 08:42:04 +0000 Subject: [PATCH 34/94] CloudWatch: Fix - make sure dimensions are propagated to alert query editor (#58281) * fix(unified-alerting/cloudwatch): propagate dimensions to alert Signed-off-by: Conor Evans * add unit test Signed-off-by: Conor Evans Co-authored-by: Erik Sundell --- .../components/Dimensions/Dimensions.test.tsx | 18 ++++++++++ .../components/Dimensions/Dimensions.tsx | 33 ++++++++++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx index 7286356811b..1829732b5d8 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx @@ -55,6 +55,24 @@ describe('Dimensions', () => { }); }); + describe('when rendered with two existing dimensions and values are represented as arrays', () => { + it('should render two filter items', async () => { + props.query.dimensions = { + InstanceId: ['*'], + InstanceGroup: ['Group1'], + }; + render(); + const filterItems = screen.getAllByTestId('cloudwatch-dimensions-filter-item'); + expect(filterItems.length).toBe(2); + + expect(within(filterItems[0]).getByText('InstanceId')).toBeInTheDocument(); + expect(within(filterItems[0]).getByText('*')).toBeInTheDocument(); + + expect(within(filterItems[1]).getByText('InstanceGroup')).toBeInTheDocument(); + expect(within(filterItems[1]).getByText('Group1')).toBeInTheDocument(); + }); + }); + describe('when adding a new filter item', () => { it('it should add the new item but not call onChange', async () => { props.query.dimensions = {}; diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx index 4dc13d44744..c95365b3430 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx @@ -25,15 +25,32 @@ export interface DimensionFilterCondition { const dimensionsToFilterConditions = (dimensions: DimensionsType | undefined) => Object.entries(dimensions ?? {}).reduce((acc, [key, value]) => { - if (value && typeof value === 'string') { - const filter = { - key, - value, - operator: '=', - }; - return [...acc, filter]; + if (!value) { + return acc; } - return acc; + + // Previously, we only appended to the `acc`umulated dimensions if the value was a string. + // However, Cloudwatch can present dimensions with single-value arrays, e.g. + // k: FunctionName + // v: ['MyLambdaFunction'] + // in which case we grab the single-value from the Array and use that as the value. + let v = ''; + if (typeof value === 'string') { + v = value; + } else if (Array.isArray(value) && typeof value[0] === 'string') { + v = value[0]; + } + + if (!v) { + return acc; + } + + const filter = { + key: key, + value: v, + operator: '=', + }; + return [...acc, filter]; }, []); const filterConditionsToDimensions = (filters: DimensionFilterCondition[]) => { From 13acddd605ca054d791fee11891e91d14952a922 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 7 Dec 2022 09:47:31 +0100 Subject: [PATCH 35/94] Elasticsearch: Fix adding correct min_doc_count to all aggregation types in backend implementation (#59713) * Elasticsearch: Fix adding correct min_doc_count to all aggregation types in backend implementation * Update snapshot tests * Fix linting --- .../testdata_request/metric_complex.request.line1.json | 1 + .../testdata_request/metric_simple.request.line1.json | 1 + pkg/tsdb/elasticsearch/time_series_query.go | 6 +----- pkg/tsdb/elasticsearch/time_series_query_test.go | 6 +++--- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/tsdb/elasticsearch/testdata_request/metric_complex.request.line1.json b/pkg/tsdb/elasticsearch/testdata_request/metric_complex.request.line1.json index 17904da9b67..2b57b03f27b 100644 --- a/pkg/tsdb/elasticsearch/testdata_request/metric_complex.request.line1.json +++ b/pkg/tsdb/elasticsearch/testdata_request/metric_complex.request.line1.json @@ -29,6 +29,7 @@ }, "terms": { "field": "label", + "min_doc_count": 1, "order": { "_key": "desc" }, diff --git a/pkg/tsdb/elasticsearch/testdata_request/metric_simple.request.line1.json b/pkg/tsdb/elasticsearch/testdata_request/metric_simple.request.line1.json index f088a768ac3..c1830002a2a 100644 --- a/pkg/tsdb/elasticsearch/testdata_request/metric_simple.request.line1.json +++ b/pkg/tsdb/elasticsearch/testdata_request/metric_simple.request.line1.json @@ -17,6 +17,7 @@ }, "terms": { "field": "label", + "min_doc_count": 1, "order": { "_key": "desc" }, diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index a8c496a0615..649f167b891 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -227,11 +227,7 @@ func (metricAggregation MetricAgg) generateSettingsForDSL() map[string]interface } func (bucketAgg BucketAgg) generateSettingsForDSL() map[string]interface{} { - // TODO: This might also need to be applied to other bucket aggregations and other fields. - switch bucketAgg.Type { - case "date_histogram": - setIntPath(bucketAgg.Settings, "min_doc_count") - } + setIntPath(bucketAgg.Settings, "min_doc_count") return bucketAgg.Settings.MustMap() } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index f6c3e61815d..40880e99a82 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -310,9 +310,9 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { sr := c.multisearchRequests[0].Requests[0] firstLevel := sr.Aggs[0] require.Equal(t, firstLevel.Key, "2") - // FIXME: This is a bug in the current implementation. The min_doc_count is not set. - // termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) - // require.Equal(t, termsAgg.MinDocCount, "1") + termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) + expectedMinDocCount := 1 + require.Equal(t, termsAgg.MinDocCount, &expectedMinDocCount) }) t.Run("With metric percentiles", func(t *testing.T) { From df4f0343e587695f37fa7a6de04cc884af4df284 Mon Sep 17 00:00:00 2001 From: Jo Date: Wed, 7 Dec 2022 09:55:48 +0000 Subject: [PATCH 36/94] Auth: Session cache [main] (#59935) * Auth: Session cache [v9.2.x] (#59907) * add cache wrapper only cache token if not to rotate Co-authored-by: Kalle Persson anticipate next rotation Co-authored-by: ievaVasiljeva Co-authored-by: Kalle Persson Co-authored-by: Gabriel MABILLE (cherry picked from commit 07a4b2343d59cb593ad128355e1d9217ba493700) * FeatureToggle: for storing sessions in a Remote Cache Co-authored-by: ievaVasiljeva (cherry picked from commit b8a8c15148c53a76f14f76da1b0fa4cec9be9216) * use feature flag for session cache * ensure ttl is minimum 1 second Co-authored-by: ievaVasiljeva Co-authored-by: Kalle Persson Co-authored-by: Gabriel MABILLE * ensure 2 ttl window to prevent caching of tokens near rotation Co-authored-by: Kalle Persson * fix description of toggle Co-authored-by: gamab Co-authored-by: ievaVasiljeva Co-authored-by: Kalle Persson Co-authored-by: Gabriel MABILLE (cherry picked from commit 2919588a82d0a11c6400453d9b8ecf3b94e670bf) * fix broken quota test --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/auth/authimpl/auth_token.go | 66 ++++++++++++++++++- pkg/services/featuremgmt/registry.go | 5 ++ pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/quota/quotaimpl/quota_test.go | 2 +- 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index bd44bd27723..14a668c0378 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -91,6 +91,7 @@ Alpha features might be changed or removed without prior notice. | `elasticsearchBackendMigration` | Use Elasticsearch as backend data source | | `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources | | `authnService` | Use new auth service to perform authentication | +| `sessionRemoteCache` | Enable using remote cache for user sessions | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 518bf5e1c87..d6bcdf70bb7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -83,4 +83,5 @@ export interface FeatureToggles { elasticsearchBackendMigration?: boolean; secureSocksDatasourceProxy?: boolean; authnService?: boolean; + sessionRemoteCache?: boolean; } diff --git a/pkg/services/auth/authimpl/auth_token.go b/pkg/services/auth/authimpl/auth_token.go index 7bf1d8bf332..991143f538a 100644 --- a/pkg/services/auth/authimpl/auth_token.go +++ b/pkg/services/auth/authimpl/auth_token.go @@ -4,30 +4,43 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "net" "strings" "time" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -const urgentRotateTime = 1 * time.Minute +const ( + ttl = 15 * time.Second + urgentRotateTime = 1 * time.Minute +) var getTime = time.Now -func ProvideUserAuthTokenService(sqlStore db.DB, cfg *setting.Cfg, serverLockService *serverlock.ServerLockService, quotaService quota.Service) (*UserAuthTokenService, error) { +func ProvideUserAuthTokenService(sqlStore db.DB, + serverLockService *serverlock.ServerLockService, + remoteCache *remotecache.RemoteCache, + features *featuremgmt.FeatureManager, + quotaService quota.Service, + cfg *setting.Cfg) (*UserAuthTokenService, error) { s := &UserAuthTokenService{ sqlStore: sqlStore, serverLockService: serverLockService, cfg: cfg, log: log.New("auth"), + remoteCache: remoteCache, + features: features, } defaultLimits, err := readQuotaConfig(cfg) @@ -43,6 +56,8 @@ func ProvideUserAuthTokenService(sqlStore db.DB, cfg *setting.Cfg, serverLockSer return s, err } + remotecache.Register(auth.UserToken{}) + return s, nil } @@ -51,6 +66,8 @@ type UserAuthTokenService struct { serverLockService *serverlock.ServerLockService cfg *setting.Cfg log log.Logger + remoteCache *remotecache.RemoteCache + features *featuremgmt.FeatureManager } func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { @@ -101,7 +118,52 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, return &userToken, err } +func (s *UserAuthTokenService) lookupTokenWithCache(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + hashedToken := hashToken(unhashedToken) + cacheKey := "auth_token:" + hashedToken + + session, errCache := s.remoteCache.Get(ctx, cacheKey) + if errCache == nil { + token := session.(auth.UserToken) + return &token, nil + } else { + if errors.Is(errCache, remotecache.ErrCacheItemNotFound) { + s.log.Debug("user auth token not found in cache", + "cacheKey", cacheKey) + } else { + s.log.Warn("failed to get user auth token from cache", + "cacheKey", cacheKey, "error", errCache) + } + } + + token, err := s.lookupToken(ctx, unhashedToken) + if err != nil { + return nil, err + } + + // only cache tokens until their near rotation time + // Near rotation time = tokens last rotation plus the rotation interval minus 2 ttl (=30s by default) + nextRotation := time.Unix(token.RotatedAt, 0). + Add(-2 * ttl). // subtract 2 ttl to make sure we don't cache tokens that are about to expire + Add(time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute) + if now := getTime(); now.Before(nextRotation) { + if err := s.remoteCache.Set(ctx, cacheKey, *token, ttl); err != nil { + s.log.Warn("could not cache token", "error", err, "cacheKey", cacheKey, "userId", token.UserId) + } + } + + return token, nil +} + func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + if s.features != nil && s.features.IsEnabled(featuremgmt.FlagSessionRemoteCache) { + return s.lookupTokenWithCache(ctx, unhashedToken) + } + + return s.lookupToken(ctx, unhashedToken) +} + +func (s *UserAuthTokenService) lookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { hashedToken := hashToken(unhashedToken) var model userAuthToken var exists bool diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 10035d8db75..5f6d9ddbf76 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -378,5 +378,10 @@ var ( Description: "Use new auth service to perform authentication", State: FeatureStateAlpha, }, + { + Name: "sessionRemoteCache", + Description: "Enable using remote cache for user sessions", + State: FeatureStateAlpha, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 31c794f62a2..36e3e91e96d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -274,4 +274,8 @@ const ( // FlagAuthnService // Use new auth service to perform authentication FlagAuthnService = "authnService" + + // FlagSessionRemoteCache + // Enable using remote cache for user sessions + FlagSessionRemoteCache = "sessionRemoteCache" ) diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 54d5c127cd1..d8ffa2432ae 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -465,7 +465,7 @@ func getQuotaBySrvTargetScope(t *testing.T, quotaService quota.Service, srv quot func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService quota.Service) { _, err := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) require.NoError(t, err) - _, err = authimpl.ProvideUserAuthTokenService(sqlStore, sqlStore.Cfg, nil, quotaService) + _, err = authimpl.ProvideUserAuthTokenService(sqlStore, nil, nil, featuremgmt.WithFeatures(), quotaService, sqlStore.Cfg) require.NoError(t, err) _, err = dashboardStore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) From dd406e887bcf0b51b626c0bd7c6a88f67a4d4c1e Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Wed, 7 Dec 2022 10:01:20 +0000 Subject: [PATCH 37/94] Chore: Refactor `packages/jaeger-ui-components/src/TraceTimelineViewer/ListView` to TS (#59873) --- .betterer.results | 4 +- .../{Positions.test.js => Positions.test.ts} | 5 +- ...index.test.js.snap => index.test.tsx.snap} | 0 .../{index.test.js => index.test.tsx} | 61 +++++++++++++------ .../TraceTimelineViewer/ListView/index.tsx | 2 +- 5 files changed, 50 insertions(+), 22 deletions(-) rename packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/{Positions.test.js => Positions.test.ts} (99%) rename packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/__snapshots__/{index.test.js.snap => index.test.tsx.snap} (100%) rename packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/{index.test.js => index.test.tsx} (82%) diff --git a/.betterer.results b/.betterer.results index 08df5db3e94..8cf81d8d286 100644 --- a/.betterer.results +++ b/.betterer.results @@ -14,8 +14,8 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx:596989456": [ [14, 35, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [ - [14, 26, 13, "RegExp match", "2409514259"] + "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [ + [14, 56, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js:989353473": [ [15, 17, 13, "RegExp match", "2409514259"] diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.js rename to packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.ts index e000b7e18c0..82f0ae3b2dc 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/Positions.test.ts @@ -16,8 +16,9 @@ import Positions from './Positions'; describe('Positions', () => { const bufferLen = 1; - const getHeight = (i) => i * 2 + 2; - let ps; + const getHeight = (i: number) => i * 2 + 2; + + let ps: Positions; beforeEach(() => { ps = new Positions(bufferLen); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/__snapshots__/index.test.js.snap b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/__snapshots__/index.test.tsx.snap similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/__snapshots__/index.test.js.snap rename to packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/__snapshots__/index.test.tsx.snap diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx similarity index 82% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js rename to packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx index 55fa6848dfa..3a55a264c83 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx @@ -12,18 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { mount, shallow } from 'enzyme'; +import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; +import { TNil } from '../../types'; import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; -import ListView from './index'; +import ListView, { TListViewProps } from './index'; // Util to get list of all callbacks added to an event emitter by event type. // jest adds "error" event listeners to window, this util makes it easier to // ignore those calls. -function getListenersByType(mockFn) { - const rv = {}; +function getListenersByType( + mockFn: jest.MockContext< + void, + [ + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions | undefined + ] + > +) { + const rv: { + [eventType: string]: EventListenerOrEventListenerObject[]; + } = {}; mockFn.calls.forEach(([eventType, callback]) => { if (!rv[eventType]) { rv[eventType] = [callback]; @@ -40,25 +52,24 @@ describe('', () => { const DATA_LENGTH = 40; - function getHeight(index) { + function getHeight(index: number) { return index * 2 + 2; } - function Item(props) { + function Item(props: React.HTMLProps) { const { children, ...rest } = props; return
{children}
; } - function renderItem(itemKey, styles, itemIndex, attrs) { + const renderItem: TListViewProps['itemRenderer'] = (itemKey, styles, itemIndex, attrs) => { return ( {itemIndex} ); - } + }; - let wrapper; - let instance; + let instance: ListView; const props = { dataLength: DATA_LENGTH, @@ -74,6 +85,7 @@ describe('', () => { }; describe('shallow tests', () => { + let wrapper: ShallowWrapper; beforeEach(() => { wrapper = shallow(); }); @@ -92,10 +104,10 @@ describe('', () => { it('sets the height of the items according to the height func', () => { const items = wrapper.find(Item); - const expectedHeights = []; + const expectedHeights: number[] = []; const heights = items.map((node, i) => { expectedHeights.push(getHeight(i)); - return node.prop('style').height; + return node.prop('style')?.height; }); expect(heights.length).toBe(props.initialDraw); expect(heights).toEqual(expectedHeights); @@ -109,12 +121,13 @@ describe('', () => { }); describe('mount tests', () => { + let wrapper: ReactWrapper; describe('accessor functions', () => { const clientHeight = 2; const scrollTop = 3; - let oldRender; - let oldInitWrapper; + let oldRender: () => JSX.Element; + let oldInitWrapper: (elm: HTMLElement | TNil) => void; const initWrapperMock = jest.fn((elm) => { if (elm != null) { // jsDom requires `defineProperties` instead of just setting the props @@ -173,14 +186,28 @@ describe('', () => { }); describe('windowScroller', () => { - let windowAddListenerSpy; - let windowRmListenerSpy; + let windowAddListenerSpy: jest.SpyInstance< + void, + [ + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions | undefined + ] + >; + let windowRmListenerSpy: jest.SpyInstance< + void, + [ + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions | undefined + ] + >; beforeEach(() => { windowAddListenerSpy = jest.spyOn(window, 'addEventListener'); windowRmListenerSpy = jest.spyOn(window, 'removeEventListener'); const wsProps = { ...props, windowScroller: true }; - wrapper = mount(); + wrapper = mount(); instance = wrapper.instance(); }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx index e7587f0a563..86b93470428 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx @@ -27,7 +27,7 @@ type TWrapperProps = { /** * @typedef */ -type TListViewProps = { +export type TListViewProps = { /** * Number of elements in the list. */ From 2b3c86d201ddfbaf09137ca511d0b86246f4bb00 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 7 Dec 2022 10:10:59 +0000 Subject: [PATCH 38/94] Chore: Remove unused/deprecated @types definitions (#59888) * remove @types/rc-tree, @types/classnames * remove @types/rc-time-picker * remove @types/reselect * remove @types/testing-library__react-hooks * only include known package.json's --- .github/renovate.json5 | 1 + package.json | 5 -- packages/grafana-data/package.json | 1 - packages/grafana-ui/package.json | 2 - packages/jaeger-ui-components/package.json | 2 - yarn.lock | 97 ++-------------------- 6 files changed, 10 insertions(+), 98 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 235131251d8..a5396c0faa9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -30,6 +30,7 @@ "react-dom", "react-test-renderer" ], + "includePaths": ["package.json", "packages/*"], "ignorePaths": ["packages/grafana-toolkit/package.json", "emails/**", "plugins-bundled/**", "**/mocks/**"], "labels": ["area/frontend", "dependencies", "no-backport", "no-changelog"], "packageRules": [ diff --git a/package.json b/package.json index d9a207d5128..f82549dbcb6 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "@testing-library/user-event": "14.4.3", "@types/angular": "1.8.4", "@types/angular-route": "1.7.2", - "@types/classnames": "2.3.0", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", "@types/d3-force": "^2.1.0", @@ -137,7 +136,6 @@ "@types/papaparse": "5.3.5", "@types/pluralize": "^0.0.29", "@types/prismjs": "1.26.0", - "@types/rc-time-picker": "3.4.1", "@types/react": "17.0.42", "@types/react-beautiful-dnd": "13.1.2", "@types/react-dom": "17.0.14", @@ -152,13 +150,11 @@ "@types/react-window": "1.8.5", "@types/react-window-infinite-loader": "^1", "@types/redux-mock-store": "1.0.3", - "@types/reselect": "2.2.0", "@types/semver": "7.3.13", "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.2", "@types/slate-react": "0.22.9", "@types/testing-library__jest-dom": "5.14.5", - "@types/testing-library__react-hooks": "^3.2.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "5.42.0", @@ -283,7 +279,6 @@ "@sentry/browser": "6.19.7", "@sentry/types": "6.19.7", "@sentry/utils": "6.19.7", - "@types/rc-tree": "^3.0.0", "@types/react-resizable": "3.0.3", "@types/webpack-env": "1.18.0", "@visx/event": "2.6.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index d3145ef5b35..6f0db335307 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -78,7 +78,6 @@ "@types/react-dom": "17.0.14", "@types/sinon": "10.0.13", "@types/testing-library__jest-dom": "5.14.5", - "@types/testing-library__react-hooks": "^3.2.0", "@types/tinycolor2": "1.4.3", "esbuild": "0.15.12", "react": "17.0.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index dd46eaef2fa..352c70eb73e 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -140,7 +140,6 @@ "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", - "@types/classnames": "2.3.0", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", "@types/enzyme": "3.10.12", @@ -168,7 +167,6 @@ "@types/slate-plain-serializer": "0.7.2", "@types/slate-react": "0.22.9", "@types/testing-library__jest-dom": "5.14.5", - "@types/testing-library__react-hooks": "^3.2.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", "@wojtekmaj/enzyme-adapter-react-17": "0.7.0", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index fd0c72d3aa7..63438c1c2a8 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -13,7 +13,6 @@ "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/user-event": "14.4.3", - "@types/classnames": "^2.2.7", "@types/deep-freeze": "^0.1.1", "@types/enzyme": "3.10.12", "@types/hoist-non-react-statics": "^3.3.1", @@ -22,7 +21,6 @@ "@types/prop-types": "15.7.5", "@types/react": "17.0.42", "@types/react-icons": "2.2.7", - "@types/reselect": "2.2.0", "@types/slate-react": "0.22.9", "@types/testing-library__jest-dom": "5.14.5", "@types/tinycolor2": "1.4.3", diff --git a/yarn.lock b/yarn.lock index 28f495ade83..84ee48674ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4326,7 +4326,6 @@ __metadata: "@types/react-dom": 17.0.14 "@types/sinon": 10.0.13 "@types/testing-library__jest-dom": 5.14.5 - "@types/testing-library__react-hooks": ^3.2.0 "@types/tinycolor2": 1.4.3 d3-interpolate: 1.4.0 date-fns: 2.29.3 @@ -4719,7 +4718,6 @@ __metadata: "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 "@testing-library/user-event": 14.4.3 - "@types/classnames": 2.3.0 "@types/common-tags": ^1.8.0 "@types/d3": 7.4.0 "@types/enzyme": 3.10.12 @@ -4747,7 +4745,6 @@ __metadata: "@types/slate-plain-serializer": 0.7.2 "@types/slate-react": 0.22.9 "@types/testing-library__jest-dom": 5.14.5 - "@types/testing-library__react-hooks": ^3.2.0 "@types/tinycolor2": 1.4.3 "@types/uuid": 8.3.4 "@wojtekmaj/enzyme-adapter-react-17": 0.7.0 @@ -4964,7 +4961,6 @@ __metadata: "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.4 "@testing-library/user-event": 14.4.3 - "@types/classnames": ^2.2.7 "@types/deep-freeze": ^0.1.1 "@types/enzyme": 3.10.12 "@types/hoist-non-react-statics": ^3.3.1 @@ -4973,7 +4969,6 @@ __metadata: "@types/prop-types": 15.7.5 "@types/react": 17.0.42 "@types/react-icons": 2.2.7 - "@types/reselect": 2.2.0 "@types/slate-react": 0.22.9 "@types/testing-library__jest-dom": 5.14.5 "@types/tinycolor2": 1.4.3 @@ -10207,15 +10202,6 @@ __metadata: languageName: node linkType: hard -"@types/classnames@npm:2.3.0, @types/classnames@npm:^2.2.7": - version: 2.3.0 - resolution: "@types/classnames@npm:2.3.0" - dependencies: - classnames: "*" - checksum: e1bd6de60fc5e4ead601838078b10f378d6a66ad21112922856de59a1f450b1361f597aa3ca34f2cc33bc030f950112872fddfde43c5189bec89737a8b237f24 - languageName: node - linkType: hard - "@types/clean-css@npm:*": version: 4.2.5 resolution: "@types/clean-css@npm:4.2.5" @@ -11333,25 +11319,6 @@ __metadata: languageName: node linkType: hard -"@types/rc-time-picker@npm:3.4.1": - version: 3.4.1 - resolution: "@types/rc-time-picker@npm:3.4.1" - dependencies: - "@types/react": "*" - moment: ^2.19.4 - checksum: 26d43beb8c5fdbd1932996bee63d3f854ca8e931915463106cbb382d98efcc81a1140f7fda004b6652ca12e87b211b1b70302b854c5ce41e3f87e68537181c06 - languageName: node - linkType: hard - -"@types/rc-tree@npm:^3.0.0": - version: 3.0.0 - resolution: "@types/rc-tree@npm:3.0.0" - dependencies: - rc-tree: "*" - checksum: bf8e599c5a62c8f8d91fbecdc210119c4c2ad07bdfb8adecb4d1cce9cb967fd6856ad2940431b8fdc9993cd261f127c33588f2988bfc7bc6438993de1c6eeff3 - languageName: node - linkType: hard - "@types/react-beautiful-dnd@npm:13.1.2": version: 13.1.2 resolution: "@types/react-beautiful-dnd@npm:13.1.2" @@ -11511,7 +11478,7 @@ __metadata: languageName: node linkType: hard -"@types/react-test-renderer@npm:*, @types/react-test-renderer@npm:17.0.1": +"@types/react-test-renderer@npm:17.0.1": version: 17.0.1 resolution: "@types/react-test-renderer@npm:17.0.1" dependencies: @@ -11624,15 +11591,6 @@ __metadata: languageName: node linkType: hard -"@types/reselect@npm:2.2.0": - version: 2.2.0 - resolution: "@types/reselect@npm:2.2.0" - dependencies: - reselect: "*" - checksum: 034b5796c16316b60561fca01503111ecddde638178efc20c7293f07ef7d9cbc2bffce267899381f630fa9ffef3bfed54384fe87756e97132b4dc2881f585e19 - languageName: node - linkType: hard - "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -11821,15 +11779,6 @@ __metadata: languageName: node linkType: hard -"@types/testing-library__react-hooks@npm:^3.2.0": - version: 3.4.1 - resolution: "@types/testing-library__react-hooks@npm:3.4.1" - dependencies: - "@types/react-test-renderer": "*" - checksum: f32a5659330a7c2e555bffa74807f7331b1d2fcdd5fe4c08cc9130a0a12826f5f1c5bdf3210e7b2b4c9b774bddecc283171a347f78dc98d28836b0f773583a03 - languageName: node - linkType: hard - "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -15486,13 +15435,6 @@ __metadata: languageName: node linkType: hard -"classnames@npm:*, classnames@npm:2.x, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1": - version: 2.3.1 - resolution: "classnames@npm:2.3.1" - checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960 - languageName: node - linkType: hard - "classnames@npm:2.3.2, classnames@npm:^2.3.2": version: 2.3.2 resolution: "classnames@npm:2.3.2" @@ -15500,6 +15442,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:2.x, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1": + version: 2.3.1 + resolution: "classnames@npm:2.3.1" + checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960 + languageName: node + linkType: hard + "clean-css@npm:5.2.0": version: 5.2.0 resolution: "clean-css@npm:5.2.0" @@ -21560,7 +21509,6 @@ __metadata: "@testing-library/user-event": 14.4.3 "@types/angular": 1.8.4 "@types/angular-route": 1.7.2 - "@types/classnames": 2.3.0 "@types/common-tags": ^1.8.0 "@types/d3": 7.4.0 "@types/d3-force": ^2.1.0 @@ -21586,8 +21534,6 @@ __metadata: "@types/papaparse": 5.3.5 "@types/pluralize": ^0.0.29 "@types/prismjs": 1.26.0 - "@types/rc-time-picker": 3.4.1 - "@types/rc-tree": ^3.0.0 "@types/react": 17.0.42 "@types/react-beautiful-dnd": 13.1.2 "@types/react-dom": 17.0.14 @@ -21603,13 +21549,11 @@ __metadata: "@types/react-window": 1.8.5 "@types/react-window-infinite-loader": ^1 "@types/redux-mock-store": 1.0.3 - "@types/reselect": 2.2.0 "@types/semver": 7.3.13 "@types/slate": 0.47.11 "@types/slate-plain-serializer": 0.7.2 "@types/slate-react": 0.22.9 "@types/testing-library__jest-dom": 5.14.5 - "@types/testing-library__react-hooks": ^3.2.0 "@types/tinycolor2": 1.4.3 "@types/uuid": 8.3.4 "@types/webpack-env": 1.18.0 @@ -27864,7 +27808,7 @@ __metadata: languageName: node linkType: hard -"moment@npm:2.29.4, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.19.4, moment@npm:^2.20.1, moment@npm:^2.29.4": +"moment@npm:2.29.4, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.20.1, moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e @@ -31814,22 +31758,6 @@ __metadata: languageName: node linkType: hard -"rc-tree@npm:*": - version: 5.6.5 - resolution: "rc-tree@npm:5.6.5" - dependencies: - "@babel/runtime": ^7.10.1 - classnames: 2.x - rc-motion: ^2.0.1 - rc-util: ^5.16.1 - rc-virtual-list: ^3.4.8 - peerDependencies: - react: "*" - react-dom: "*" - checksum: 107efb7dbf6d44faf11072c4c53277f234ecd8d0c5a5cb387d5d9ac18b96098fa9cef3312f52589930c5562fd708f4da50f3b7880b27b2405c6ac558dce9f037 - languageName: node - linkType: hard - "rc-tree@npm:5.7.0, rc-tree@npm:~5.7.0": version: 5.7.0 resolution: "rc-tree@npm:5.7.0" @@ -33660,13 +33588,6 @@ __metadata: languageName: node linkType: hard -"reselect@npm:*": - version: 4.1.0 - resolution: "reselect@npm:4.1.0" - checksum: ef150c3cccf8c43f7e5f9f35b0f826f82287e84fb5219928b1c7a4c8e99297f004ecd27e4888334115d35c82ab20bc2c95987244d12253480d256a1241495ad2 - languageName: node - linkType: hard - "reselect@npm:4.1.6": version: 4.1.6 resolution: "reselect@npm:4.1.6" From 5d0029c6470d7e137fdb4352114817cfd60f2dcb Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 7 Dec 2022 10:16:55 +0000 Subject: [PATCH 39/94] Navigation: handle case where navIndex is undefined (#59826) handle case where navIndex is undefined --- public/app/core/components/Page/usePageTitle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Page/usePageTitle.ts b/public/app/core/components/Page/usePageTitle.ts index 7a9e0e025a5..c2e53b23251 100644 --- a/public/app/core/components/Page/usePageTitle.ts +++ b/public/app/core/components/Page/usePageTitle.ts @@ -8,7 +8,7 @@ import { Branding } from '../Branding/Branding'; import { buildBreadcrumbs } from '../Breadcrumbs/utils'; export function usePageTitle(navModel?: NavModel, pageNav?: NavModelItem) { - const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID]; + const homeNav = useSelector((state) => state.navIndex)?.[HOME_NAV_ID]; useEffect(() => { const sectionNav = (navModel?.node !== navModel?.main ? navModel?.node : navModel?.main) ?? { text: 'Grafana' }; const parts: string[] = buildBreadcrumbs(sectionNav, pageNav, homeNav) From 6359dab0409b5660e965d917d16e1c46c766d071 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Wed, 7 Dec 2022 10:45:56 +0000 Subject: [PATCH 40/94] Alerting: Change resultError in preparation for supporting ForError duration (#59894) --- pkg/services/ngalert/state/manager.go | 17 ++- pkg/services/ngalert/state/manager_test.go | 2 +- pkg/services/ngalert/state/state.go | 149 +++++++++------------ 3 files changed, 80 insertions(+), 88 deletions(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 9e687647135..1ffc99b6c1e 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -205,17 +205,24 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu oldState := currentState.State oldReason := currentState.StateReason - logger.Debug("Setting alert state") + // Add the instance to the log context to help correlate log lines for a state + logger = logger.New("instance", result.Instance) + switch result.State { case eval.Normal: - currentState.resultNormal(alertRule, result) + logger.Debug("Setting next state", "handler", "resultNormal") + resultNormal(currentState, alertRule, result, logger) case eval.Alerting: - currentState.resultAlerting(alertRule, result) + logger.Debug("Setting next state", "handler", "resultAlerting") + resultAlerting(currentState, alertRule, result, logger) case eval.Error: - currentState.resultError(alertRule, result) + logger.Debug("Setting next state", "handler", "resultError") + resultError(currentState, alertRule, result, logger) case eval.NoData: - currentState.resultNoData(alertRule, result) + logger.Debug("Setting next state", "handler", "resultNoData") + resultNoData(currentState, alertRule, result, logger) case eval.Pending: // we do not emit results with this state + logger.Debug("Ignoring set next state as result is pending") } // Set reason iff: result is different than state, reason is not Alerting or Normal diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 8ee29ab0efd..c4c67b1ddee 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -1937,7 +1937,7 @@ func TestProcessEvalResults(t *testing.T) { "instance_label": "test", }, Values: make(map[string]float64), - State: eval.Alerting, + State: eval.Pending, Results: []state.Evaluation{ { EvaluationTime: evaluationTime.Add(30 * time.Second), diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 65ba71c72e2..3bc95340e59 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/screenshot" @@ -127,118 +128,102 @@ func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float return result } -func (a *State) resultNormal(_ *models.AlertRule, result eval.Result) { - a.Error = nil // should be nil since state is not error - if a.State != eval.Normal { - a.EndsAt = result.EvaluatedAt - a.StartsAt = result.EvaluatedAt +func resultNormal(state *State, _ *models.AlertRule, result eval.Result, logger log.Logger) { + state.Error = nil // should be nil since state is not error + + if state.State != eval.Normal { + logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Normal) + state.State = eval.Normal + state.StartsAt = result.EvaluatedAt + state.EndsAt = result.EvaluatedAt } - a.State = eval.Normal } -func (a *State) resultAlerting(alertRule *models.AlertRule, result eval.Result) { - a.Error = result.Error // should be nil since the state is not an error +func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) { + state.Error = result.Error - switch a.State { + switch state.State { case eval.Alerting: - a.setEndsAt(alertRule, result) + // If the previous state is Alerting then update the expiration time + state.setEndsAt(rule, result) case eval.Pending: - if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For { - a.State = eval.Alerting - a.StartsAt = result.EvaluatedAt - a.setEndsAt(alertRule, result) + // If the previous state is Pending then check if the For duration has been observed + if result.EvaluatedAt.Sub(state.StartsAt) >= rule.For { + logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Alerting) + state.State = eval.Alerting + state.StartsAt = result.EvaluatedAt + state.setEndsAt(rule, result) } default: - a.StartsAt = result.EvaluatedAt - a.setEndsAt(alertRule, result) - if !(alertRule.For > 0) { - // If For is 0, immediately set Alerting - a.State = eval.Alerting + if rule.For > 0 { + // If the alert rule has a For duration that should be observed then the state should be set to Pending + logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Pending) + state.State = eval.Pending } else { - a.State = eval.Pending + logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Alerting) + state.State = eval.Alerting } + state.StartsAt = result.EvaluatedAt + state.setEndsAt(rule, result) } } -func (a *State) resultError(alertRule *models.AlertRule, result eval.Result) { - a.Error = result.Error - - execErrState := eval.Error - switch alertRule.ExecErrState { +func resultError(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) { + switch rule.ExecErrState { case models.AlertingErrState: - execErrState = eval.Alerting + resultAlerting(state, rule, result, logger) case models.ErrorErrState: - // If the evaluation failed because a query returned an error then - // update the state with the Datasource UID as a label and the error - // message as an annotation so other code can use this metadata to - // add context to alerts - var queryError expr.QueryError - if errors.As(a.Error, &queryError) { - for _, next := range alertRule.Data { - if next.RefID == queryError.RefID { - a.Labels["ref_id"] = next.RefID - a.Labels["datasource_uid"] = next.DatasourceUID - break + state.Error = result.Error + if result.Error != nil { + // If the evaluation failed because a query returned an error then add the Ref ID and + // Datasource UID as labels + var queryError expr.QueryError + if errors.As(state.Error, &queryError) { + for _, next := range rule.Data { + if next.RefID == queryError.RefID { + state.Labels["ref_id"] = next.RefID + state.Labels["datasource_uid"] = next.DatasourceUID + break + } } + state.Annotations["Error"] = queryError.Error() } - a.Annotations["Error"] = queryError.Error() } - execErrState = eval.Error - case models.OkErrState: - a.resultNormal(alertRule, result) - return - default: - a.Error = fmt.Errorf("cannot map error to a state because option [%s] is not supported. evaluation error: %w", alertRule.ExecErrState, a.Error) - } - switch a.State { - case eval.Alerting, eval.Error: - // We must set the state here as the state can change both from Alerting - // to Error and from Error to Alerting. This can happen when the datasource - // is unavailable or queries against the datasource returns errors, and is - // then resolved as soon as the datasource is available and queries return - // without error - if a.State != execErrState { - // Set the start time if the state changes from Alerting to Error or from - // Error to Alerting - a.StartsAt = result.EvaluatedAt - } - a.State = execErrState - a.setEndsAt(alertRule, result) - case eval.Pending: - if result.EvaluatedAt.Sub(a.StartsAt) >= alertRule.For { - a.State = execErrState - a.StartsAt = result.EvaluatedAt - a.setEndsAt(alertRule, result) - } - default: - // For is observed when Alerting is chosen for the alert state - // if execution error or timeout. - if execErrState == eval.Alerting && alertRule.For > 0 { - a.State = eval.Pending + if state.State == eval.Error { + // If the previous state is Error then update the expiration time + state.setEndsAt(rule, result) } else { - a.State = execErrState + // This is the first occurrence of an error + logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Error) + state.State = eval.Error + state.StartsAt = result.EvaluatedAt + state.setEndsAt(rule, result) } - a.StartsAt = result.EvaluatedAt - a.setEndsAt(alertRule, result) + case models.OkErrState: + resultNormal(state, rule, result, logger) + default: + state.State = eval.Error + state.Error = fmt.Errorf("unsupported execution error state: %s", rule.ExecErrState) + state.Annotations["Error"] = state.Error.Error() } } -func (a *State) resultNoData(alertRule *models.AlertRule, result eval.Result) { - a.Error = result.Error +func resultNoData(state *State, rule *models.AlertRule, result eval.Result, _ log.Logger) { + state.Error = result.Error - if a.StartsAt.IsZero() { - a.StartsAt = result.EvaluatedAt + if state.StartsAt.IsZero() { + state.StartsAt = result.EvaluatedAt } - a.setEndsAt(alertRule, result) + state.setEndsAt(rule, result) - switch alertRule.NoDataState { + switch rule.NoDataState { case models.Alerting: - a.State = eval.Alerting + state.State = eval.Alerting case models.NoData: - a.State = eval.NoData + state.State = eval.NoData case models.OK: - a.State = eval.Normal + state.State = eval.Normal } } From db1e19fe8682219e248e7a91424e4a897af22f00 Mon Sep 17 00:00:00 2001 From: Polina Boneva <13227501+polibb@users.noreply.github.com> Date: Wed, 7 Dec 2022 12:46:10 +0200 Subject: [PATCH 41/94] Dashboard: Making a dashboard editable does not allow resizing of panels (#59255) * DashboardGrid should rerender ReactGridLayout when mutable dashboard updates * add missing prop to test --- .../app/features/dashboard/containers/DashboardPage.tsx | 7 ++++++- .../features/dashboard/dashgrid/DashboardGrid.test.tsx | 1 + public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1783d7d34bd..717f79579de 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -392,7 +392,12 @@ export class UnthemedDashboardPage extends PureComponent { )} - + {inspectPanel && } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 05af4a5831e..111583e7745 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -57,6 +57,7 @@ describe('DashboardGrid', () => { const props: Props = { editPanel: null, viewPanel: null, + isEditable: true, dashboard: getTestDashboard(), }; expect(() => render()).not.toThrow(); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 74f11cfea55..02a2204fbb7 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -17,6 +17,7 @@ import { DashboardPanel } from './DashboardPanel'; export interface Props { dashboard: DashboardModel; + isEditable: boolean; editPanel: PanelModel | null; viewPanel: PanelModel | null; } @@ -200,7 +201,7 @@ export class DashboardGrid extends PureComponent { } render() { - const { dashboard } = this.props; + const { isEditable } = this.props; /** * We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer @@ -215,14 +216,13 @@ export class DashboardGrid extends PureComponent { return null; } - const draggable = width <= 769 ? false : dashboard.meta.canEdit; + const draggable = width <= 769 ? false : isEditable; /* Disable draggable if mobile device, solving an issue with unintentionally moving panels. https://github.com/grafana/grafana/issues/18497 theme.breakpoints.md = 769 */ - return ( /** * The children is using a width of 100% so we need to guarantee that it is wrapped @@ -233,7 +233,7 @@ export class DashboardGrid extends PureComponent { Date: Wed, 7 Dec 2022 11:52:35 +0100 Subject: [PATCH 42/94] Scenes: Fix flaky scenes time range test (#59900) * Scenes: Fix flaky scenes time range test * Update public/app/features/scenes/core/SceneTimeRange.test.tsx --- public/app/features/scenes/core/SceneTimeRange.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/scenes/core/SceneTimeRange.test.tsx b/public/app/features/scenes/core/SceneTimeRange.test.tsx index 76baeb651d1..aba98e42832 100644 --- a/public/app/features/scenes/core/SceneTimeRange.test.tsx +++ b/public/app/features/scenes/core/SceneTimeRange.test.tsx @@ -13,7 +13,6 @@ describe('SceneTimeRange', () => { timeRange.onRefresh(); const diff = timeRange.state.value.from.valueOf() - startTime; expect(diff).toBeGreaterThan(1); - expect(diff).toBeLessThan(100); }); it('toUrlValues with relative range', () => { From 1b676d0d49460572352af4639274a4e7fe03fc9b Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 7 Dec 2022 13:14:53 +0100 Subject: [PATCH 43/94] Contexthandler: Add uname as response header (#59930) Signed-off-by: bergquist --- .../setup-grafana/configure-grafana/feature-toggles/index.md | 1 + packages/grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/services/contexthandler/contexthandler.go | 5 +++++ pkg/services/featuremgmt/registry.go | 5 +++++ pkg/services/featuremgmt/toggles_gen.go | 4 ++++ 5 files changed, 16 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 14a668c0378..d9531c63b80 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -53,6 +53,7 @@ Alpha features might be changed or removed without prior notice. | Feature toggle name | Description | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `returnUnameHeader` | Return user login as header for authenticated requests | | `alertingBigTransactions` | Use big transactions for alerting database writes | | `dashboardPreviews` | Create and show thumbnails for dashboard search results | | `live-config` | Save Grafana Live configuration in SQL tables | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index d6bcdf70bb7..8664082f5a0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -16,6 +16,7 @@ export interface FeatureToggles { [name: string]: boolean | undefined; // support any string value + returnUnameHeader?: boolean; alertingBigTransactions?: boolean; promQueryBuilder?: boolean; trimDefaults?: boolean; diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index a894eee743b..ed12223d8ab 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -186,6 +186,11 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { } } + // this can be used by proxies to identify certain users + if h.features.IsEnabled(featuremgmt.FlagReturnUnameHeader) { + w.Header().Add("grafana-uname", reqContext.Login) + } + next.ServeHTTP(w, r) }) } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5f6d9ddbf76..f0d32770ee5 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -9,6 +9,11 @@ package featuremgmt var ( // Register each toggle here standardFeatureFlags = []FeatureFlag{ + { + Name: "returnUnameHeader", + Description: "Return user login as header for authenticated requests", + State: FeatureStateAlpha, + }, { Name: "alertingBigTransactions", Description: "Use big transactions for alerting database writes", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 36e3e91e96d..708787cd608 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -7,6 +7,10 @@ package featuremgmt const ( + // FlagReturnUnameHeader + // Return user login as header for authenticated requests + FlagReturnUnameHeader = "returnUnameHeader" + // FlagAlertingBigTransactions // Use big transactions for alerting database writes FlagAlertingBigTransactions = "alertingBigTransactions" From a5ace56be8644c725803e63798554a35eba27e79 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 7 Dec 2022 13:15:42 +0100 Subject: [PATCH 44/94] Plugins: Add username to datasource plugin logging (#59893) Co-authored-by: Marcus Efraimsson Signed-off-by: bergquist --- pkg/infra/log/requestTiming.go | 28 +++++++++++++++++++ pkg/middleware/logger.go | 2 ++ .../instrumentation/instrumentation.go | 7 +++++ 3 files changed, 37 insertions(+) create mode 100644 pkg/infra/log/requestTiming.go diff --git a/pkg/infra/log/requestTiming.go b/pkg/infra/log/requestTiming.go new file mode 100644 index 00000000000..a6b9c0bb5a1 --- /dev/null +++ b/pkg/infra/log/requestTiming.go @@ -0,0 +1,28 @@ +package log + +import ( + "context" + "time" +) + +type requestStartTimeContextKey struct{} + +var requestStartTime = requestStartTimeContextKey{} + +// InitCounter creates a pointer on the context that can be incremented later +func InitstartTime(ctx context.Context, now time.Time) context.Context { + return context.WithValue(ctx, requestStartTime, now) +} + +// TimeSinceStart returns time spend since the request started in grafana +func TimeSinceStart(ctx context.Context, now time.Time) time.Duration { + val := ctx.Value(requestStartTime) + if val != nil { + startTime, ok := val.(time.Time) + if ok { + return now.Sub(startTime) + } + } + + return 0 +} diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 321daa210d0..4366ca45e45 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -35,6 +35,8 @@ func Logger(cfg *setting.Cfg) web.Middleware { // we have to init the context with the counter here to update the request r = r.WithContext(log.InitCounter(r.Context())) + // put the start time on context so we can measure it later. + r = r.WithContext(log.InitstartTime(r.Context(), time.Now())) rw := web.Rw(w, r) next.ServeHTTP(rw, r) diff --git a/pkg/plugins/backendplugin/instrumentation/instrumentation.go b/pkg/plugins/backendplugin/instrumentation/instrumentation.go index 5a42ef9a1fc..b9c12016272 100644 --- a/pkg/plugins/backendplugin/instrumentation/instrumentation.go +++ b/pkg/plugins/backendplugin/instrumentation/instrumentation.go @@ -51,6 +51,13 @@ func instrumentPluginRequest(ctx context.Context, cfg *config.Cfg, pluginCtx *ba "duration", elapsed, "pluginId", pluginCtx.PluginID, "endpoint", endpoint, + "eventName", "grafana-data-egress", + "insight_logs", true, + "since_grafana_request_started", log.TimeSinceStart(ctx, time.Now()), + } + + if pluginCtx.User != nil { + logParams = append(logParams, "uname", pluginCtx.User.Login) } traceID := tracing.TraceIDFromContext(ctx, false) From d833031726ce64a085483d02ce3c3f30e5d03b5e Mon Sep 17 00:00:00 2001 From: Jo Date: Wed, 7 Dec 2022 13:49:28 +0000 Subject: [PATCH 45/94] ServiceAccounts: Do not display warnings about secret scanner when scanner is disabled (#59952) ServiceAccounts: Fix warnings about secret scanner when scanner is disabled --- pkg/services/serviceaccounts/manager/service.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 4365fcd52af..635e467df8d 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -55,10 +55,9 @@ func ProvideServiceAccountsService( serviceaccountsAPI.RegisterAPIEndpoints() s.secretScanEnabled = cfg.SectionWithEnvOverrides("secretscan").Key("enabled").MustBool(false) + s.secretScanInterval = cfg.SectionWithEnvOverrides("secretscan"). + Key("interval").MustDuration(defaultSecretScanInterval) if s.secretScanEnabled { - s.secretScanInterval = cfg.SectionWithEnvOverrides("secretscan"). - Key("interval").MustDuration(defaultSecretScanInterval) - s.secretScanService = secretscan.NewService(s.store, cfg) } @@ -76,7 +75,7 @@ func (sa *ServiceAccountsService) Run(ctx context.Context) error { defer updateStatsTicker.Stop() // Enforce a minimum interval of 1 minute. - if sa.secretScanInterval < time.Minute { + if sa.secretScanEnabled && sa.secretScanInterval < time.Minute { sa.backgroundLog.Warn("secret scan interval is too low, increasing to " + defaultSecretScanInterval.String()) From 55055d0b9ba0c4ef81d871282b76dbab9e38c0de Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:15:33 +0100 Subject: [PATCH 46/94] Alerting: Fix RuleEditor flaky test for existing grafana managed alert (#59780) * Fix RuleEditor flaky test for existing grafana managed alert * Fix RuleEditor flaky test for creating a new grafana managed alert * Fix tests for cloud rules * Split tests for different rule types * Move common clickSelectOption method to helpers * Remove from RuleEditorCloudRules.test.tsx a repeated cloud rule test * Move existing grafana rule test case to a new file * Split RuleEditorCloudRules test: move checking only allowed data sources test case to a new test file * Re-use common ui const * Re-use renderRuleEditor method * Create getDiscoverFeaturesMock to reduce code --- .betterer.results | 3 - .../alerting/unified/RuleEditor.test.tsx | 687 ------------------ .../RuleEditorCloudOnlyAllowed.test.tsx | 183 +++++ .../unified/RuleEditorCloudRules.test.tsx | 153 ++++ .../unified/RuleEditorExisting.test.tsx | 219 ++++++ .../unified/RuleEditorGrafanaRules.test.tsx | 169 +++++ .../unified/RuleEditorRecordingRule.test.tsx | 160 ++++ public/test/helpers/alertingRuleEditor.tsx | 50 ++ public/test/helpers/selectOptionInTest.ts | 13 +- 9 files changed, 946 insertions(+), 691 deletions(-) delete mode 100644 public/app/features/alerting/unified/RuleEditor.test.tsx create mode 100644 public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx create mode 100644 public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx create mode 100644 public/app/features/alerting/unified/RuleEditorExisting.test.tsx create mode 100644 public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx create mode 100644 public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx create mode 100644 public/test/helpers/alertingRuleEditor.tsx diff --git a/.betterer.results b/.betterer.results index 8cf81d8d286..b3c1494e10e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2907,9 +2907,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], - "public/app/features/alerting/unified/RuleEditor.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/alerting/unified/RuleList.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/alerting/unified/RuleEditor.test.tsx b/public/app/features/alerting/unified/RuleEditor.test.tsx deleted file mode 100644 index bad0f3cc75d..00000000000 --- a/public/app/features/alerting/unified/RuleEditor.test.tsx +++ /dev/null @@ -1,687 +0,0 @@ -import { Matcher, render, waitFor, screen, within } from '@testing-library/react'; -import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { Route, Router } from 'react-router-dom'; -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; -import { byRole, byTestId, byText } from 'testing-library-selector'; - -import { DataSourceInstanceSettings } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { locationService, setDataSourceSrv } from '@grafana/runtime'; -import { ADD_NEW_FOLER_OPTION } from 'app/core/components/Select/FolderPicker'; -import { contextSrv } from 'app/core/services/context_srv'; -import { DashboardSearchHit } from 'app/features/search/types'; -import { configureStore } from 'app/store/configureStore'; -import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto'; - -import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions'; -import { backendSrv } from '../../../core/services/backend_srv'; -import { AccessControlAction } from '../../../types'; - -import RuleEditor from './RuleEditor'; -import { discoverFeatures } from './api/buildInfo'; -import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { disableRBAC, mockDataSource, MockDataSourceSrv, mockFolder } from './mocks'; -import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; -import * as config from './utils/config'; -import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; -import { getDefaultQueries } from './utils/rule-form'; - -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ - // eslint-disable-next-line react/display-name - ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( - onChange(e.target.value)} /> - ), -})); - -jest.mock('./api/buildInfo'); -jest.mock('./api/ruler'); -jest.mock('../../../../app/features/manage-dashboards/state/actions'); - -// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. -// lets just skip it -jest.mock('app/features/query/components/QueryEditorRow', () => ({ - // eslint-disable-next-line react/display-name - QueryEditorRow: () =>

hi

, -})); - -jest.spyOn(config, 'getAllDataSources'); - -const mocks = { - getAllDataSources: jest.mocked(config.getAllDataSources), - searchFolders: jest.mocked(searchFolders), - api: { - discoverFeatures: jest.mocked(discoverFeatures), - fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), - setRulerRuleGroup: jest.mocked(setRulerRuleGroup), - fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), - fetchRulerRules: jest.mocked(fetchRulerRules), - fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), - }, -}; - -function renderRuleEditor(identifier?: string) { - const store = configureStore(); - - locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`); - - return render( - - - - - - ); -} - -const ui = { - inputs: { - name: byRole('textbox', { name: /rule name name for the alert rule\./i }), - alertType: byTestId('alert-type-picker'), - dataSource: byTestId('datasource-picker'), - folder: byTestId('folder-picker'), - folderContainer: byTestId(selectors.components.FolderPicker.containerV2), - namespace: byTestId('namespace-picker'), - group: byTestId('group-picker'), - annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`), - annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), - labelKey: (idx: number) => byTestId(`label-key-${idx}`), - labelValue: (idx: number) => byTestId(`label-value-${idx}`), - expr: byTestId('expr'), - }, - buttons: { - save: byRole('button', { name: 'Save' }), - addAnnotation: byRole('button', { name: /Add info/ }), - addLabel: byRole('button', { name: /Add label/ }), - // alert type buttons - grafanaManagedAlert: byRole('button', { name: /Grafana managed/ }), - lotexAlert: byRole('button', { name: /Mimir or Loki alert/ }), - lotexRecordingRule: byRole('button', { name: /Mimir or Loki recording rule/ }), - }, -}; - -const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); - -// Until flakiness is fixed -// https://github.com/grafana/grafana/issues/58747 -describe.skip('RuleEditor', () => { - beforeEach(() => { - jest.clearAllMocks(); - contextSrv.isEditor = true; - contextSrv.hasEditPermissionInFolders = true; - }); - - disableRBAC(); - - it('can create a new cloud alert', async () => { - const dataSources = { - default: mockDataSource( - { - type: 'prometheus', - name: 'Prom', - isDefault: true, - }, - { alerting: true } - ), - }; - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.api.setRulerRuleGroup.mockResolvedValue(); - mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - mocks.api.fetchRulerRulesGroup.mockResolvedValue({ - name: 'group2', - rules: [], - }); - mocks.api.fetchRulerRules.mockResolvedValue({ - namespace1: [ - { - name: 'group1', - rules: [], - }, - ], - namespace2: [ - { - name: 'group2', - rules: [], - }, - ], - }); - mocks.searchFolders.mockResolvedValue([]); - - mocks.api.discoverFeatures.mockResolvedValue({ - application: PromApplication.Cortex, - features: { - rulerApiEnabled: true, - }, - }); - - await renderRuleEditor(); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled()); - - await userEvent.click(await ui.buttons.lotexAlert.find()); - - const dataSourceSelect = ui.inputs.dataSource.get(); - await userEvent.click(byRole('combobox').get(dataSourceSelect)); - await clickSelectOption(dataSourceSelect, 'Prom (default)'); - await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled()); - - await userEvent.type(await ui.inputs.expr.find(), 'up == 1'); - - await userEvent.type(ui.inputs.name.get(), 'my great new rule'); - await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); - await clickSelectOption(ui.inputs.group.get(), 'group2'); - - await userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary'); - await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); - - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - - await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}'); - - // save and check what was sent to backend - await userEvent.click(ui.buttons.save.get()); - await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); - expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( - { dataSourceName: 'Prom', apiVersion: 'legacy' }, - 'namespace2', - { - name: 'group2', - rules: [ - { - alert: 'my great new rule', - annotations: { description: 'some description', summary: 'some summary' }, - labels: { severity: 'warn', team: 'the a-team' }, - expr: 'up == 1', - for: '1m', - }, - ], - } - ); - }); - - it('can create new grafana managed alert', async () => { - const dataSources = { - default: mockDataSource( - { - type: 'prometheus', - name: 'Prom', - isDefault: true, - }, - { alerting: true } - ), - }; - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.api.setRulerRuleGroup.mockResolvedValue(); - mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - mocks.api.fetchRulerRulesGroup.mockResolvedValue({ - name: 'group2', - rules: [], - }); - mocks.api.fetchRulerRules.mockResolvedValue({ - 'Folder A': [ - { - name: 'group1', - rules: [], - }, - ], - namespace2: [ - { - name: 'group2', - rules: [], - }, - ], - }); - mocks.searchFolders.mockResolvedValue([ - { - title: 'Folder A', - id: 1, - }, - { - title: 'Folder B', - id: 2, - }, - { - title: 'Folder / with slash', - id: 2, - }, - ] as DashboardSearchHit[]); - - mocks.api.discoverFeatures.mockResolvedValue({ - application: PromApplication.Prometheus, - features: { - rulerApiEnabled: false, - }, - }); - - // fill out the form - await renderRuleEditor(); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled()); - - await userEvent.type(await ui.inputs.name.find(), 'my great new rule'); - - const folderInput = await ui.inputs.folder.find(); - await clickSelectOption(folderInput, 'Folder A'); - const groupInput = await ui.inputs.group.find(); - await userEvent.click(byRole('combobox').get(groupInput)); - await clickSelectOption(groupInput, 'group1 (1m)'); - - await userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary'); - await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); - - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - - await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}'); - - // save and check what was sent to backend - await userEvent.click(ui.buttons.save.get()); - await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); - expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( - { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, - 'Folder A', - { - interval: '1m', - name: 'group1', - rules: [ - { - annotations: { description: 'some description', summary: 'some summary' }, - labels: { severity: 'warn', team: 'the a-team' }, - for: '5m', - grafana_alert: { - condition: 'C', - data: getDefaultQueries(), - exec_err_state: GrafanaAlertStateDecision.Error, - no_data_state: 'NoData', - title: 'my great new rule', - }, - }, - ], - } - ); - }); - - it('can create a new cloud recording rule', async () => { - const dataSources = { - default: mockDataSource( - { - type: 'prometheus', - name: 'Prom', - isDefault: true, - }, - { alerting: true } - ), - }; - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.api.setRulerRuleGroup.mockResolvedValue(); - mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - mocks.api.fetchRulerRulesGroup.mockResolvedValue({ - name: 'group2', - rules: [], - }); - mocks.api.fetchRulerRules.mockResolvedValue({ - namespace1: [ - { - name: 'group1', - rules: [], - }, - ], - namespace2: [ - { - name: 'group2', - rules: [], - }, - ], - }); - mocks.searchFolders.mockResolvedValue([]); - - mocks.api.discoverFeatures.mockResolvedValue({ - application: PromApplication.Cortex, - features: { - rulerApiEnabled: true, - }, - }); - - await renderRuleEditor(); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled()); - await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule'); - await userEvent.click(await ui.buttons.lotexRecordingRule.get()); - - const dataSourceSelect = ui.inputs.dataSource.get(); - await userEvent.click(byRole('combobox').get(dataSourceSelect)); - - await clickSelectOption(dataSourceSelect, 'Prom (default)'); - await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled()); - await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); - await clickSelectOption(ui.inputs.group.get(), 'group2'); - - await userEvent.type(await ui.inputs.expr.find(), 'up == 1'); - - // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed - await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); - - await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}'); - - // try to save, find out that recording rule name is invalid - await userEvent.click(ui.buttons.save.get()); - await waitFor(() => - expect( - byText( - 'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.' - ).get() - ).toBeInTheDocument() - ); - expect(mocks.api.setRulerRuleGroup).not.toBeCalled(); - - // fix name and re-submit - await userEvent.clear(await ui.inputs.name.find()); - await userEvent.type(await ui.inputs.name.find(), 'my:great:new:recording:rule'); - - // save and check what was sent to backend - await userEvent.click(ui.buttons.save.get()); - await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); - expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( - { dataSourceName: 'Prom', apiVersion: 'legacy' }, - 'namespace2', - { - name: 'group2', - rules: [ - { - record: 'my:great:new:recording:rule', - labels: { team: 'the a-team' }, - expr: 'up == 1', - }, - ], - } - ); - }); - - it('can edit grafana managed rule', async () => { - const uid = 'FOOBAR123'; - const folder = { - title: 'Folder A', - uid: 'abcd', - id: 1, - }; - - const slashedFolder = { - title: 'Folder with /', - uid: 'abcde', - id: 2, - }; - - const dataSources = { - default: mockDataSource( - { - type: 'prometheus', - name: 'Prom', - isDefault: true, - }, - { alerting: true } - ), - }; - - jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({ - ...mockFolder(), - accessControl: { - [AccessControlAction.AlertingRuleUpdate]: true, - }, - }); - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.api.setRulerRuleGroup.mockResolvedValue(); - mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({ - [folder.title]: [ - { - interval: '1m', - name: 'my great new rule', - rules: [ - { - annotations: { description: 'some description', summary: 'some summary' }, - labels: { severity: 'warn', team: 'the a-team' }, - for: '5m', - grafana_alert: { - uid, - namespace_uid: 'abcd', - namespace_id: 1, - condition: 'B', - data: getDefaultQueries(), - exec_err_state: GrafanaAlertStateDecision.Error, - no_data_state: GrafanaAlertStateDecision.NoData, - title: 'my great new rule', - }, - }, - ], - }, - ], - }); - mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]); - - await renderRuleEditor(uid); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled()); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - - // check that it's filled in - const nameInput = await ui.inputs.name.find(); - expect(nameInput).toHaveValue('my great new rule'); - //check that folder is in the list - expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title)); - expect(ui.inputs.annotationValue(0).get()).toHaveValue('some description'); - expect(ui.inputs.annotationValue(1).get()).toHaveValue('some summary'); - - //check that slashed folders are not in the list - expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title)); - expect(ui.inputs.folder.get()).not.toHaveTextContent(new RegExp(slashedFolder.title)); - - //check that slashes warning is only shown once user search slashes - const folderInput = await ui.inputs.folderContainer.find(); - expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument(); - await userEvent.type(within(folderInput).getByRole('combobox'), 'new slashed //'); - expect(within(folderInput).getByText("Folders with '/' character are not allowed.")).toBeInTheDocument(); - await userEvent.keyboard('{backspace} {backspace}{backspace}'); - expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument(); - - // add an annotation - await clickSelectOption(ui.inputs.annotationKey(2).get(), /Add new/); - await userEvent.type(byRole('textbox').get(ui.inputs.annotationKey(2).get()), 'custom'); - await userEvent.type(ui.inputs.annotationValue(2).get(), 'value'); - - //add a label - await userEvent.type(getLabelInput(ui.inputs.labelKey(2).get()), 'custom{enter}'); - await userEvent.type(getLabelInput(ui.inputs.labelValue(2).get()), 'value{enter}'); - - // save and check what was sent to backend - await userEvent.click(ui.buttons.save.get()); - await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); - - //check that '+ Add new' option is in folders drop down even if we don't have values - const emptyFolderInput = await ui.inputs.folderContainer.find(); - mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]); - await renderRuleEditor(uid); - await userEvent.click(within(emptyFolderInput).getByRole('combobox')); - expect(screen.getByText(ADD_NEW_FOLER_OPTION)).toBeInTheDocument(); - - expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( - { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, - 'Folder A', - { - interval: '1m', - name: 'my great new rule', - rules: [ - { - annotations: { description: 'some description', summary: 'some summary', custom: 'value' }, - labels: { severity: 'warn', team: 'the a-team', custom: 'value' }, - for: '5m', - grafana_alert: { - uid, - condition: 'B', - data: getDefaultQueries(), - exec_err_state: GrafanaAlertStateDecision.Error, - no_data_state: 'NoData', - title: 'my great new rule', - }, - }, - ], - } - ); - }); - - it('for cloud alerts, should only allow to select editable rules sources', async () => { - const dataSources: Record> = { - // can edit rules - loki: mockDataSource( - { - type: DataSourceType.Loki, - name: 'loki with ruler', - }, - { alerting: true } - ), - loki_disabled: mockDataSource( - { - type: DataSourceType.Loki, - name: 'loki disabled for alerting', - jsonData: { - manageAlerts: false, - }, - }, - { alerting: true } - ), - // can edit rules - prom: mockDataSource( - { - type: DataSourceType.Prometheus, - name: 'cortex with ruler', - }, - { alerting: true } - ), - // cannot edit rules - loki_local_rule_store: mockDataSource( - { - type: DataSourceType.Loki, - name: 'loki with local rule store', - }, - { alerting: true } - ), - // cannot edit rules - prom_no_ruler_api: mockDataSource( - { - type: DataSourceType.Loki, - name: 'cortex without ruler api', - }, - { alerting: true } - ), - // not a supported datasource type - splunk: mockDataSource( - { - type: 'splunk', - name: 'splunk', - }, - { alerting: true } - ), - }; - - mocks.api.discoverFeatures.mockImplementation(async (dataSourceName) => { - if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') { - return { - application: PromApplication.Cortex, - features: { - rulerApiEnabled: true, - alertManagerConfigApi: false, - federatedRules: false, - querySharding: false, - }, - }; - } - if (dataSourceName === 'loki with local rule store') { - return { - application: PromApplication.Cortex, - features: { - rulerApiEnabled: false, - alertManagerConfigApi: false, - federatedRules: false, - querySharding: false, - }, - }; - } - if (dataSourceName === 'cortex without ruler api') { - return { - application: PromApplication.Cortex, - features: { - rulerApiEnabled: false, - alertManagerConfigApi: false, - federatedRules: false, - querySharding: false, - }, - }; - } - - throw new Error(`${dataSourceName} not handled`); - }); - - mocks.api.fetchRulerRulesGroup.mockImplementation(async ({ dataSourceName }) => { - if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') { - return null; - } - if (dataSourceName === 'loki with local rule store') { - throw { - status: 400, - data: { - message: 'GetRuleGroup unsupported in rule local store', - }, - }; - } - if (dataSourceName === 'cortex without ruler api') { - throw new Error('404 from rules config endpoint. Perhaps ruler API is not enabled?'); - } - return null; - }); - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); - mocks.searchFolders.mockResolvedValue([]); - - // render rule editor, select mimir/loki managed alerts - await renderRuleEditor(); - await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled()); - await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); - - await ui.inputs.name.find(); - await userEvent.click(await ui.buttons.lotexAlert.get()); - - // check that only rules sources that have ruler available are there - const dataSourceSelect = ui.inputs.dataSource.get(); - await userEvent.click(byRole('combobox').get(dataSourceSelect)); - expect(await byText('loki with ruler').query()).toBeInTheDocument(); - expect(byText('cortex with ruler').query()).toBeInTheDocument(); - expect(byText('loki with local rule store').query()).not.toBeInTheDocument(); - expect(byText('prom without ruler api').query()).not.toBeInTheDocument(); - expect(byText('splunk').query()).not.toBeInTheDocument(); - expect(byText('loki disabled for alerting').query()).not.toBeInTheDocument(); - }); -}); - -const clickSelectOption = async (selectElement: HTMLElement, optionText: Matcher): Promise => { - await userEvent.click(byRole('combobox').get(selectElement)); - await selectOptionInTest(selectElement, optionText as string); -}; diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx new file mode 100644 index 00000000000..01c66b8cf27 --- /dev/null +++ b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx @@ -0,0 +1,183 @@ +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; +import { byRole, byText } from 'testing-library-selector'; + +import { setDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto'; + +import { searchFolders } from '../../manage-dashboards/state/actions'; + +import { discoverFeatures } from './api/buildInfo'; +import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; +import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks'; +import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; +import * as config from './utils/config'; +import { DataSourceType } from './utils/datasource'; + +jest.mock('./components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('./api/buildInfo'); +jest.mock('./api/ruler'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +jest.spyOn(config, 'getAllDataSources'); + +const mocks = { + getAllDataSources: jest.mocked(config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +function getDiscoverFeaturesMock(application: PromApplication, features?: Partial) { + return { + application: application, + features: { + rulerApiEnabled: false, + alertManagerConfigApi: false, + federatedRules: false, + querySharding: false, + ...features, + }, + }; +} + +describe('RuleEditor cloud: checking editable data sources', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + }); + + disableRBAC(); + + it('for cloud alerts, should only allow to select editable rules sources', async () => { + const dataSources = { + // can edit rules + loki: mockDataSource( + { + type: DataSourceType.Loki, + name: 'loki with ruler', + }, + { alerting: true } + ), + loki_disabled: mockDataSource( + { + type: DataSourceType.Loki, + name: 'loki disabled for alerting', + jsonData: { + manageAlerts: false, + }, + }, + { alerting: true } + ), + // can edit rules + prom: mockDataSource( + { + type: DataSourceType.Prometheus, + name: 'cortex with ruler', + }, + { alerting: true } + ), + // cannot edit rules + loki_local_rule_store: mockDataSource( + { + type: DataSourceType.Loki, + name: 'loki with local rule store', + }, + { alerting: true } + ), + // cannot edit rules + prom_no_ruler_api: mockDataSource( + { + type: DataSourceType.Loki, + name: 'cortex without ruler api', + }, + { alerting: true } + ), + // not a supported datasource type + splunk: mockDataSource( + { + type: 'splunk', + name: 'splunk', + }, + { alerting: true } + ), + }; + + mocks.api.discoverFeatures.mockImplementation(async (dataSourceName) => { + if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') { + return getDiscoverFeaturesMock(PromApplication.Cortex, { rulerApiEnabled: true }); + } + if (dataSourceName === 'loki with local rule store') { + return getDiscoverFeaturesMock(PromApplication.Cortex); + } + if (dataSourceName === 'cortex without ruler api') { + return getDiscoverFeaturesMock(PromApplication.Cortex); + } + + throw new Error(`${dataSourceName} not handled`); + }); + + mocks.api.fetchRulerRulesGroup.mockImplementation(async ({ dataSourceName }) => { + if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') { + return null; + } + if (dataSourceName === 'loki with local rule store') { + throw { + status: 400, + data: { + message: 'GetRuleGroup unsupported in rule local store', + }, + }; + } + if (dataSourceName === 'cortex without ruler api') { + throw new Error('404 from rules config endpoint. Perhaps ruler API is not enabled?'); + } + return null; + }); + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.searchFolders.mockResolvedValue([]); + + // render rule editor, select mimir/loki managed alerts + renderRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + + await ui.inputs.name.find(); + await userEvent.click(await ui.buttons.lotexAlert.get()); + + // check that only rules sources that have ruler available are there + const dataSourceSelect = ui.inputs.dataSource.get(); + await userEvent.click(byRole('combobox').get(dataSourceSelect)); + expect(await byText('loki with ruler').query()).toBeInTheDocument(); + expect(byText('cortex with ruler').query()).toBeInTheDocument(); + expect(byText('loki with local rule store').query()).not.toBeInTheDocument(); + expect(byText('prom without ruler api').query()).not.toBeInTheDocument(); + expect(byText('splunk').query()).not.toBeInTheDocument(); + expect(byText('loki disabled for alerting').query()).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx new file mode 100644 index 00000000000..3443e9b1422 --- /dev/null +++ b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx @@ -0,0 +1,153 @@ +import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; +import React from 'react'; +import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; +import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { byRole } from 'testing-library-selector'; + +import { setDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import { PromApplication } from 'app/types/unified-alerting-dto'; + +import { searchFolders } from '../../manage-dashboards/state/actions'; + +import { discoverFeatures } from './api/buildInfo'; +import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; +import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks'; +import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; +import * as config from './utils/config'; + +jest.mock('./components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('./api/buildInfo'); +jest.mock('./api/ruler'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +jest.spyOn(config, 'getAllDataSources'); + +const mocks = { + getAllDataSources: jest.mocked(config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); + +describe('RuleEditor cloud', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + }); + + disableRBAC(); + + it('can create a new cloud alert', async () => { + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: true } + ), + }; + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + namespace1: [ + { + name: 'group1', + rules: [], + }, + ], + namespace2: [ + { + name: 'group2', + rules: [], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([]); + + mocks.api.discoverFeatures.mockResolvedValue({ + application: PromApplication.Cortex, + features: { + rulerApiEnabled: true, + }, + }); + + renderRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + + await userEvent.click(await ui.buttons.lotexAlert.find()); + + const dataSourceSelect = ui.inputs.dataSource.get(); + await userEvent.click(byRole('combobox').get(dataSourceSelect)); + await clickSelectOption(dataSourceSelect, 'Prom (default)'); + await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled()); + + await userEvent.type(await ui.inputs.expr.find(), 'up == 1'); + + await userEvent.type(ui.inputs.name.get(), 'my great new rule'); + await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); + await clickSelectOption(ui.inputs.group.get(), 'group2'); + + await userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary'); + await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); + + // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed + await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); + + await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); + await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); + + // save and check what was sent to backend + await userEvent.click(ui.buttons.save.get()); + await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); + expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( + { dataSourceName: 'Prom', apiVersion: 'legacy' }, + 'namespace2', + { + name: 'group2', + rules: [ + { + alert: 'my great new rule', + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn' }, + expr: 'up == 1', + for: '1m', + }, + ], + } + ); + }); +}); diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx new file mode 100644 index 00000000000..1debcd414ab --- /dev/null +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -0,0 +1,219 @@ +import { render, waitFor, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Route, Router } from 'react-router-dom'; +import { ui } from 'test/helpers/alertingRuleEditor'; +import { clickSelectOptionMatch } from 'test/helpers/selectOptionInTest'; +import { byRole } from 'testing-library-selector'; + +import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { ADD_NEW_FOLER_OPTION } from 'app/core/components/Select/FolderPicker'; +import { contextSrv } from 'app/core/services/context_srv'; +import { DashboardSearchHit } from 'app/features/search/types'; +import { configureStore } from 'app/store/configureStore'; +import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto'; + +import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions'; +import { backendSrv } from '../../../core/services/backend_srv'; +import { AccessControlAction } from '../../../types'; + +import RuleEditor from './RuleEditor'; +import { discoverFeatures } from './api/buildInfo'; +import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; +import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { disableRBAC, mockDataSource, MockDataSourceSrv, mockFolder } from './mocks'; +import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; +import * as config from './utils/config'; +import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { getDefaultQueries } from './utils/rule-form'; + +jest.mock('./components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('./api/buildInfo'); +jest.mock('./api/ruler'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +jest.spyOn(config, 'getAllDataSources'); + +const mocks = { + getAllDataSources: jest.mocked(config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +function renderRuleEditor(identifier?: string) { + const store = configureStore(); + + locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`); + + return render( + + + + + + ); +} + +const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); +describe('RuleEditor grafana managed rules', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + }); + + disableRBAC(); + + it('can edit grafana managed rule', async () => { + const uid = 'FOOBAR123'; + const folder = { + title: 'Folder A', + uid: 'abcd', + id: 1, + }; + + const slashedFolder = { + title: 'Folder with /', + uid: 'abcde', + id: 2, + }; + + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: false } + ), + }; + + jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({ + ...mockFolder(), + accessControl: { + [AccessControlAction.AlertingRuleUpdate]: true, + }, + }); + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({ + [folder.title]: [ + { + interval: '1m', + name: 'my great new rule', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn', team: 'the a-team' }, + for: '5m', + grafana_alert: { + uid, + namespace_uid: 'abcd', + namespace_id: 1, + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: GrafanaAlertStateDecision.NoData, + title: 'my great new rule', + }, + }, + ], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]); + + renderRuleEditor(uid); + + // check that it's filled in + const nameInput = await ui.inputs.name.find(); + expect(nameInput).toHaveValue('my great new rule'); + //check that folder is in the list + expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title)); + expect(ui.inputs.annotationValue(0).get()).toHaveValue('some description'); + expect(ui.inputs.annotationValue(1).get()).toHaveValue('some summary'); + + //check that slashed folders are not in the list + expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title)); + expect(ui.inputs.folder.get()).not.toHaveTextContent(new RegExp(slashedFolder.title)); + + //check that slashes warning is only shown once user search slashes + //todo: move this test to a unit test in FolderAndGroup unit test + // const folderInput = await ui.inputs.folderContainer.find(); + // expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument(); + // await userEvent.type(within(folderInput).getByRole('combobox'), 'new slashed //'); + // expect(within(folderInput).getByText("Folders with '/' character are not allowed.")).toBeInTheDocument(); + // await userEvent.keyboard('{backspace} {backspace}{backspace}'); + // expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument(); + + // add an annotation + await clickSelectOptionMatch(ui.inputs.annotationKey(2).get(), /Add new/); + await userEvent.type(byRole('textbox').get(ui.inputs.annotationKey(2).get()), 'custom'); + await userEvent.type(ui.inputs.annotationValue(2).get(), 'value'); + + //add a label + await userEvent.type(getLabelInput(ui.inputs.labelKey(2).get()), 'custom{enter}'); + await userEvent.type(getLabelInput(ui.inputs.labelValue(2).get()), 'value{enter}'); + + // save and check what was sent to backend + await userEvent.click(ui.buttons.save.get()); + await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); + + //check that '+ Add new' option is in folders drop down even if we don't have values + const emptyFolderInput = await ui.inputs.folderContainer.find(); + mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]); + await renderRuleEditor(uid); + await userEvent.click(within(emptyFolderInput).getByRole('combobox')); + expect(screen.getByText(ADD_NEW_FOLER_OPTION)).toBeInTheDocument(); + + expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( + { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, + 'Folder A', + { + interval: '1m', + name: 'my great new rule', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary', custom: 'value' }, + labels: { severity: 'warn', team: 'the a-team', custom: 'value' }, + for: '5m', + grafana_alert: { + uid, + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: 'NoData', + title: 'my great new rule', + }, + }, + ], + } + ); + }); +}); diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx new file mode 100644 index 00000000000..83022dd54f8 --- /dev/null +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx @@ -0,0 +1,169 @@ +import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; +import React from 'react'; +import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; +import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { byRole } from 'testing-library-selector'; + +import { setDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import { DashboardSearchHit } from 'app/features/search/types'; +import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto'; + +import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions'; + +import { discoverFeatures } from './api/buildInfo'; +import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; +import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks'; +import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; +import * as config from './utils/config'; +import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { getDefaultQueries } from './utils/rule-form'; + +jest.mock('./components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('./api/buildInfo'); +jest.mock('./api/ruler'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +jest.spyOn(config, 'getAllDataSources'); + +const mocks = { + getAllDataSources: jest.mocked(config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); +describe('RuleEditor grafana managed rules', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + }); + + disableRBAC(); + + it('can create new grafana managed alert', async () => { + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: false } + ), + }; + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + 'Folder A': [ + { + name: 'group1', + rules: [], + }, + ], + namespace2: [ + { + name: 'group2', + rules: [], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([ + { + title: 'Folder A', + id: 1, + }, + { + title: 'Folder B', + id: 2, + }, + { + title: 'Folder / with slash', + id: 2, + }, + ] as DashboardSearchHit[]); + + mocks.api.discoverFeatures.mockResolvedValue({ + application: PromApplication.Prometheus, + features: { + rulerApiEnabled: false, + }, + }); + renderRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + + await userEvent.type(await ui.inputs.name.find(), 'my great new rule'); + + const folderInput = await ui.inputs.folder.find(); + await clickSelectOption(folderInput, 'Folder A'); + const groupInput = await ui.inputs.group.find(); + await userEvent.click(byRole('combobox').get(groupInput)); + await clickSelectOption(groupInput, 'group1 (1m)'); + await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); + + // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed + await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); + + await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}'); + await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}'); + //8 segons + + // save and check what was sent to backend + await userEvent.click(ui.buttons.save.get()); + // 9seg + await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); + // 9seg + expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( + { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, + 'Folder A', + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: { description: 'some description' }, + labels: { severity: 'warn' }, + for: '5m', + grafana_alert: { + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: 'NoData', + title: 'my great new rule', + }, + }, + ], + } + ); + }); +}); diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx new file mode 100644 index 00000000000..73aa134df89 --- /dev/null +++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx @@ -0,0 +1,160 @@ +import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; +import React from 'react'; +import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; +import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { byRole, byText } from 'testing-library-selector'; + +import { setDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import { PromApplication } from 'app/types/unified-alerting-dto'; + +import { searchFolders } from '../../manage-dashboards/state/actions'; + +import { discoverFeatures } from './api/buildInfo'; +import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler'; +import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; +import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks'; +import { fetchRulerRulesIfNotFetchedYet } from './state/actions'; +import * as config from './utils/config'; + +jest.mock('./components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('./api/buildInfo'); +jest.mock('./api/ruler'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +jest.spyOn(config, 'getAllDataSources'); + +const mocks = { + getAllDataSources: jest.mocked(config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox'); + +describe('RuleEditor recording rules', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + }); + + disableRBAC(); + it('can create a new cloud recording rule', async () => { + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: true } + ), + }; + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + namespace1: [ + { + name: 'group1', + rules: [], + }, + ], + namespace2: [ + { + name: 'group2', + rules: [], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([]); + + mocks.api.discoverFeatures.mockResolvedValue({ + application: PromApplication.Cortex, + features: { + rulerApiEnabled: true, + }, + }); + + renderRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule'); + await userEvent.click(await ui.buttons.lotexRecordingRule.get()); + + const dataSourceSelect = ui.inputs.dataSource.get(); + await userEvent.click(byRole('combobox').get(dataSourceSelect)); + + await clickSelectOption(dataSourceSelect, 'Prom (default)'); + await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); + await clickSelectOption(ui.inputs.group.get(), 'group2'); + + await userEvent.type(await ui.inputs.expr.find(), 'up == 1'); + + // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed + await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); + + await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}'); + await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}'); + + // try to save, find out that recording rule name is invalid + await userEvent.click(ui.buttons.save.get()); + await waitFor(() => + expect( + byText( + 'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.' + ).get() + ).toBeInTheDocument() + ); + expect(mocks.api.setRulerRuleGroup).not.toBeCalled(); + + // fix name and re-submit + await userEvent.clear(await ui.inputs.name.find()); + await userEvent.type(await ui.inputs.name.find(), 'my:great:new:recording:rule'); + + // save and check what was sent to backend + await userEvent.click(ui.buttons.save.get()); + await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); + expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( + { dataSourceName: 'Prom', apiVersion: 'legacy' }, + 'namespace2', + { + name: 'group2', + rules: [ + { + record: 'my:great:new:recording:rule', + labels: { team: 'the a-team' }, + expr: 'up == 1', + }, + ], + } + ); + }); +}); diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx new file mode 100644 index 00000000000..eac7a02fc57 --- /dev/null +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -0,0 +1,50 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router, Route } from 'react-router-dom'; +import { byRole, byTestId } from 'testing-library-selector'; + +import { selectors } from '@grafana/e2e-selectors'; +import { locationService } from '@grafana/runtime'; +import RuleEditor from 'app/features/alerting/unified/RuleEditor'; +import { configureStore } from 'app/store/configureStore'; + +export const ui = { + inputs: { + name: byRole('textbox', { name: /rule name name for the alert rule\./i }), + alertType: byTestId('alert-type-picker'), + dataSource: byTestId('datasource-picker'), + folder: byTestId('folder-picker'), + folderContainer: byTestId(selectors.components.FolderPicker.containerV2), + namespace: byTestId('namespace-picker'), + group: byTestId('group-picker'), + annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`), + annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), + labelKey: (idx: number) => byTestId(`label-key-${idx}`), + labelValue: (idx: number) => byTestId(`label-value-${idx}`), + expr: byTestId('expr'), + }, + buttons: { + save: byRole('button', { name: 'Save' }), + addAnnotation: byRole('button', { name: /Add info/ }), + addLabel: byRole('button', { name: /Add label/ }), + // alert type buttons + grafanaManagedAlert: byRole('button', { name: /Grafana managed/ }), + lotexAlert: byRole('button', { name: /Mimir or Loki alert/ }), + lotexRecordingRule: byRole('button', { name: /Mimir or Loki recording rule/ }), + }, +}; + +export function renderRuleEditor(identifier?: string) { + const store = configureStore(); + + locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`); + + return render( + + + + + + ); +} diff --git a/public/test/helpers/selectOptionInTest.ts b/public/test/helpers/selectOptionInTest.ts index 2e3ec5c3e73..c1ff432dacc 100644 --- a/public/test/helpers/selectOptionInTest.ts +++ b/public/test/helpers/selectOptionInTest.ts @@ -1,5 +1,7 @@ -import { waitFor } from '@testing-library/react'; +import { Matcher, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { select } from 'react-select-event'; +import { byRole } from 'testing-library-selector'; // Used to select an option or options from a Select in unit tests export const selectOptionInTest = async ( @@ -10,3 +12,12 @@ export const selectOptionInTest = async ( // Finds the parent of the Select so you can assert if it has a value export const getSelectParent = (input: HTMLElement) => input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement; + +export const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => { + await userEvent.click(byRole('combobox').get(selectElement)); + await selectOptionInTest(selectElement, optionText); +}; +export const clickSelectOptionMatch = async (selectElement: HTMLElement, optionText: Matcher): Promise => { + await userEvent.click(byRole('combobox').get(selectElement)); + await selectOptionInTest(selectElement, optionText as string); +}; From 1aa94165d90e2e9e916a6676b4de643e948d371d Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:24:22 +0100 Subject: [PATCH 47/94] Loki: Do not track samples (#59960) --- public/app/plugins/datasource/loki/datasource.test.ts | 4 ++-- public/app/plugins/datasource/loki/datasource.ts | 5 +++-- public/app/plugins/datasource/loki/tracking.ts | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 4598d5ac948..c79f9327d75 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -28,7 +28,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; import { CustomVariableModel } from '../../../features/variables/types'; -import { LokiDatasource } from './datasource'; +import { LokiDatasource, REF_ID_DATA_SAMPLES } from './datasource'; import { createLokiDatasource, createMetadataRequest } from './mocks'; import { parseToNodeNamesArray } from './queryUtils'; import { LokiOptions, LokiQuery, LokiQueryType, LokiVariableQueryType } from './types'; @@ -946,7 +946,7 @@ describe('LokiDatasource', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ hideFromInspector: true, - requestId: 'log-samples', + requestId: REF_ID_DATA_SAMPLES, }) ); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index debd615c4b3..bb83acbb583 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -76,6 +76,7 @@ import { LokiVariableSupport } from './variables'; export type RangeQueryOptions = DataQueryRequest | AnnotationQueryRequest; export const DEFAULT_MAX_LINES = 1000; export const LOKI_ENDPOINT = '/loki/api/v1'; +export const REF_ID_DATA_SAMPLES = 'loki-data-samples'; const NS_IN_MS = 1000000; function makeRequest( @@ -419,13 +420,13 @@ export class LokiDatasource const lokiLogsQuery: LokiQuery = { expr: query.expr, queryType: LokiQueryType.Range, - refId: 'log-samples', + refId: REF_ID_DATA_SAMPLES, maxLines: 10, }; // For samples, we use defaultTimeRange (now-6h/now) and limit od 10 lines so queries are small and fast const timeRange = getDefaultTimeRange(); - const request = makeRequest(lokiLogsQuery, timeRange, CoreApp.Explore, 'log-samples', true); + const request = makeRequest(lokiLogsQuery, timeRange, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true); return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data)))); } diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index 18fdb94d1e4..ec21afc83f6 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -4,6 +4,7 @@ import { variableRegex } from 'app/features/variables/utils'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; +import { REF_ID_DATA_SAMPLES } from './datasource'; import pluginJson from './plugin.json'; import { getNormalizedLokiQuery, isLogsQuery, parseToNodeNamesArray } from './queryUtils'; import { LokiQuery, LokiQueryType } from './types'; @@ -119,6 +120,9 @@ const isQueryWithChangedLegend = (query: LokiQuery): boolean => { export function trackQuery(response: DataQueryResponse, queries: LokiQuery[], app: string): void { for (const query of queries) { + if (query.refId === REF_ID_DATA_SAMPLES) { + return; + } reportInteraction('grafana_loki_query_executed', { app, editor_mode: query.editorMode, From 43afb850561bd8c711d88524cddad1dfdbc7168a Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 7 Dec 2022 15:32:54 +0000 Subject: [PATCH 48/94] CloudMonitor: Improve detail of MQL series labels (#59747) * Improve consistency between MQL and builder queries * Review * Review and fix test --- .../cloudmonitoring/time_series_filter.go | 53 +--------- pkg/tsdb/cloudmonitoring/time_series_query.go | 25 +---- .../cloudmonitoring/time_series_query_test.go | 6 +- pkg/tsdb/cloudmonitoring/types.go | 96 ++++++++++++++++++- 4 files changed, 97 insertions(+), 83 deletions(-) diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter.go b/pkg/tsdb/cloudmonitoring/time_series_filter.go index a3c11572efb..d394156ec11 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/url" - "strconv" "strings" "time" @@ -20,62 +19,12 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesList) run(ctx context.Context, return runTimeSeriesRequest(ctx, timeSeriesFilter.logger, req, s, dsInfo, tracer, timeSeriesFilter.parameters.ProjectName, timeSeriesFilter.params, nil) } -func extractTimeSeriesLabels(series timeSeries, groupBys []string) (data.Labels, string) { - seriesLabels := data.Labels{} - defaultMetricName := series.Metric.Type - seriesLabels["resource.type"] = series.Resource.Type - groupBysMap := make(map[string]bool) - for _, groupBy := range groupBys { - groupBysMap[groupBy] = true - } - - for key, value := range series.Metric.Labels { - seriesLabels["metric.label."+key] = value - - if len(groupBys) == 0 || groupBysMap["metric.label."+key] { - defaultMetricName += " " + value - } - } - - for key, value := range series.Resource.Labels { - seriesLabels["resource.label."+key] = value - - if groupBysMap["resource.label."+key] { - defaultMetricName += " " + value - } - } - - for labelType, labelTypeValues := range series.MetaData { - for labelKey, labelValue := range labelTypeValues { - key := xstrings.ToSnakeCase(fmt.Sprintf("metadata.%s.%s", labelType, labelKey)) - - switch v := labelValue.(type) { - case string: - seriesLabels[key] = v - case bool: - strVal := strconv.FormatBool(v) - seriesLabels[key] = strVal - case []interface{}: - for _, v := range v { - strVal := v.(string) - if len(seriesLabels[key]) > 0 { - strVal = fmt.Sprintf("%s, %s", seriesLabels[key], strVal) - } - seriesLabels[key] = strVal - } - } - } - } - - return seriesLabels, defaultMetricName -} - func parseTimeSeriesResponse(queryRes *backend.DataResponse, response cloudMonitoringResponse, executedQueryString string, query cloudMonitoringQueryExecutor, params url.Values, groupBys []string) error { frames := data.Frames{} for _, series := range response.TimeSeries { - seriesLabels, defaultMetricName := extractTimeSeriesLabels(series, groupBys) + seriesLabels, defaultMetricName := series.getLabels(groupBys) frame := data.NewFrameOfFieldTypes("", len(series.Points), data.FieldTypeTime, data.FieldTypeFloat64) frame.RefID = query.getRefID() frame.Meta = &data.FrameMeta{ diff --git a/pkg/tsdb/cloudmonitoring/time_series_query.go b/pkg/tsdb/cloudmonitoring/time_series_query.go index 541d55067de..20965ea5145 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query.go @@ -3,13 +3,11 @@ package cloudmonitoring import ( "context" "fmt" - "strconv" "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/huandu/xstrings" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/tsdb/intervalv2" @@ -42,26 +40,6 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) run(ctx context.Context, return runTimeSeriesRequest(ctx, timeSeriesQuery.logger, req, s, dsInfo, tracer, timeSeriesQuery.parameters.ProjectName, nil, requestBody) } -func extractTimeSeriesDataLabels(response cloudMonitoringResponse, series timeSeriesData) map[string]string { - seriesLabels := make(map[string]string) - for n, d := range response.TimeSeriesDescriptor.LabelDescriptors { - key := xstrings.ToSnakeCase(d.Key) - key = strings.Replace(key, ".", ".label.", 1) - - labelValue := series.LabelValues[n] - switch d.ValueType { - case "BOOL": - strVal := strconv.FormatBool(labelValue.BoolValue) - seriesLabels[key] = strVal - case "INT64": - seriesLabels[key] = labelValue.Int64Value - default: - seriesLabels[key] = labelValue.StringValue - } - } - return seriesLabels -} - func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *backend.DataResponse, response cloudMonitoringResponse, executedQueryString string) error { frames := data.Frames{} @@ -69,7 +47,7 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *b for _, series := range response.TimeSeriesData { frame := data.NewFrameOfFieldTypes("", len(series.PointData), data.FieldTypeTime, data.FieldTypeFloat64) frame.RefID = timeSeriesQuery.refID - seriesLabels := extractTimeSeriesDataLabels(response, series) + seriesLabels, defaultMetricName := series.getLabels(response.TimeSeriesDescriptor.LabelDescriptors) for n, d := range response.TimeSeriesDescriptor.PointDescriptors { // If more than 1 pointdescriptor was returned, three aggregations are returned per time series - min, mean and max. @@ -81,7 +59,6 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *b } seriesLabels["metric.name"] = d.Key - defaultMetricName := d.Key customFrameMeta := map[string]interface{}{} customFrameMeta["labels"] = seriesLabels diff --git a/pkg/tsdb/cloudmonitoring/time_series_query_test.go b/pkg/tsdb/cloudmonitoring/time_series_query_test.go index 025e8cde225..27d4ed79b9a 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query_test.go @@ -5,6 +5,8 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + gdata "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -32,7 +34,7 @@ func TestTimeSeriesQuery(t *testing.T) { } err = query.parseResponse(res, data, "") frames := res.Frames - assert.Equal(t, "value.usage.mean", frames[0].Fields[1].Name) + assert.Equal(t, "grafana-prod asia-northeast1-c 6724404429462225363 200", frames[0].Fields[1].Name) assert.Equal(t, 843302441.9, frames[0].Fields[1].At(0)) }) @@ -105,7 +107,7 @@ func TestTimeSeriesQuery(t *testing.T) { frames := res.Frames custom, ok := frames[0].Meta.Custom.(map[string]interface{}) require.True(t, ok) - labels, ok := custom["labels"].(map[string]string) + labels, ok := custom["labels"].(gdata.Labels) require.True(t, ok) assert.Equal(t, "6724404429462225363", labels["resource.label.instance_id"]) }) diff --git a/pkg/tsdb/cloudmonitoring/types.go b/pkg/tsdb/cloudmonitoring/types.go index 421de8d2746..d42613788a9 100644 --- a/pkg/tsdb/cloudmonitoring/types.go +++ b/pkg/tsdb/cloudmonitoring/types.go @@ -2,10 +2,15 @@ package cloudmonitoring import ( "context" + "fmt" "net/url" + "strconv" + "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/huandu/xstrings" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -139,14 +144,16 @@ type point interface { } type timeSeriesDescriptor struct { - LabelDescriptors []struct { - Key string `json:"key"` - ValueType string `json:"valueType"` - Description string `json:"description"` - } `json:"labelDescriptors"` + LabelDescriptors []LabelDescriptor `json:"labelDescriptors"` PointDescriptors []timeSeriesPointDescriptor `json:"pointDescriptors"` } +type LabelDescriptor struct { + Key string `json:"key"` + ValueType string `json:"valueType"` + Description string `json:"description"` +} + type timeSeriesPointDescriptor struct { Key string `json:"key"` ValueType string `json:"valueType"` @@ -178,6 +185,35 @@ func (ts timeSeriesData) getPoint(index int) point { return &ts.PointData[index] } +func (ts timeSeriesData) getLabels(labelDescriptors []LabelDescriptor) (data.Labels, string) { + seriesLabels := make(map[string]string) + defaultMetricName := "" + + for n, d := range labelDescriptors { + key := xstrings.ToSnakeCase(d.Key) + key = strings.Replace(key, ".", ".label.", 1) + + labelValue := ts.LabelValues[n] + switch d.ValueType { + case "BOOL": + strVal := strconv.FormatBool(labelValue.BoolValue) + seriesLabels[key] = strVal + case "INT64": + seriesLabels[key] = labelValue.Int64Value + default: + seriesLabels[key] = labelValue.StringValue + } + + if strings.Contains(key, "metric.label") || strings.Contains(key, "resource.label") { + defaultMetricName += seriesLabels[key] + " " + } + } + + defaultMetricName = strings.Trim(defaultMetricName, " ") + + return seriesLabels, defaultMetricName +} + type timeSeriesDataIterator struct { timeSeriesData timeSeriesPointDescriptor @@ -250,6 +286,56 @@ func (ts timeSeries) valueType() string { return ts.ValueType } +func (ts timeSeries) getLabels(groupBys []string) (data.Labels, string) { + seriesLabels := data.Labels{} + defaultMetricName := ts.Metric.Type + seriesLabels["resource.type"] = ts.Resource.Type + groupBysMap := make(map[string]bool) + for _, groupBy := range groupBys { + groupBysMap[groupBy] = true + } + + for key, value := range ts.Metric.Labels { + seriesLabels["metric.label."+key] = value + + if len(groupBys) == 0 || groupBysMap["metric.label."+key] { + defaultMetricName += " " + value + } + } + + for key, value := range ts.Resource.Labels { + seriesLabels["resource.label."+key] = value + + if groupBysMap["resource.label."+key] { + defaultMetricName += " " + value + } + } + + for labelType, labelTypeValues := range ts.MetaData { + for labelKey, labelValue := range labelTypeValues { + key := xstrings.ToSnakeCase(fmt.Sprintf("metadata.%s.%s", labelType, labelKey)) + + switch v := labelValue.(type) { + case string: + seriesLabels[key] = v + case bool: + strVal := strconv.FormatBool(v) + seriesLabels[key] = strVal + case []interface{}: + for _, v := range v { + strVal := v.(string) + if len(seriesLabels[key]) > 0 { + strVal = fmt.Sprintf("%s, %s", seriesLabels[key], strVal) + } + seriesLabels[key] = strVal + } + } + } + } + + return seriesLabels, defaultMetricName +} + type timeSeriesPoint struct { Interval struct { StartTime time.Time `json:"startTime"` From e8ed79873a8ee6a072a398fda6467ce4c10d76ad Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:37:35 +0100 Subject: [PATCH 49/94] CloudWatch: return silently in case of missing log groups or incomplete query (#59701) * Remove defaultLogGroups check in filterQuery --- .../datasource/cloudwatch/datasource.test.ts | 92 +++++++++++++++---- .../CloudWatchLogsQueryRunner.test.ts | 40 +------- .../query-runner/CloudWatchLogsQueryRunner.ts | 37 ++++---- .../plugins/datasource/cloudwatch/types.ts | 3 +- 4 files changed, 96 insertions(+), 76 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index e15fb3da10d..42e1f6cdf62 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -19,23 +19,83 @@ describe('datasource', () => { jest.clearAllMocks(); }); describe('query', () => { - it('should return error if log query and log groups is not specified', async () => { - const { datasource } = setupMockedDataSource(); - const observable = datasource.query({ - targets: [{ queryMode: 'Logs', id: '', refId: '', region: '' }], - requestId: '', - interval: '', - intervalMs: 0, - range: timeRange, - scopedVars: {}, - timezone: '', - app: '', - startTime: 0, - }); + it('should not run a query if log groups is not specified', async () => { + const { datasource, fetchMock } = setupMockedDataSource(); + await lastValueFrom( + datasource.query({ + targets: [ + { + queryMode: 'Logs', + id: '', + refId: '', + region: '', + expression: 'some query string', // missing logGroups and logGroupNames, this query will be not be run + }, + { + queryMode: 'Logs', + id: '', + refId: '', + region: '', + logGroupNames: ['/some/group'], + expression: 'some query string', + }, + ], + requestId: '', + interval: '', + intervalMs: 0, + range: timeRange, + scopedVars: {}, + timezone: '', + app: '', + startTime: 0, + }) + ); - await expect(observable).toEmitValuesWith((received) => { - const response = received[0]; - expect(response.error?.message).toBe('Log group is required'); + expect(fetchMock.mock.calls[0][0].data.queries).toHaveLength(1); + expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({ + queryString: 'some query string', + logGroupNames: ['/some/group'], + region: 'us-west-1', + }); + }); + + it('should not run a query if query expression is not specified', async () => { + const { datasource, fetchMock } = setupMockedDataSource(); + await lastValueFrom( + datasource.query({ + targets: [ + { + queryMode: 'Logs', + id: '', + refId: '', + region: '', + logGroupNames: ['/some/group'], // missing query expression, this query will be not be run + }, + { + queryMode: 'Logs', + id: '', + refId: '', + region: '', + logGroupNames: ['/some/group'], + expression: 'some query string', + }, + ], + requestId: '', + interval: '', + intervalMs: 0, + range: timeRange, + scopedVars: {}, + timezone: '', + app: '', + startTime: 0, + }) + ); + + expect(fetchMock.mock.calls[0][0].data.queries).toHaveLength(1); + expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({ + queryString: 'some query string', + logGroupNames: ['/some/group'], + region: 'us-west-1', }); }); diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts index 73660dd5a08..80c8e3d59e7 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts @@ -1,18 +1,10 @@ import { interval, lastValueFrom, of } from 'rxjs'; -import { - dataFrameToJSON, - DataQueryErrorType, - DataQueryRequest, - FieldType, - LogLevel, - LogRowModel, - MutableDataFrame, -} from '@grafana/data'; +import { dataFrameToJSON, DataQueryErrorType, FieldType, LogLevel, LogRowModel, MutableDataFrame } from '@grafana/data'; import { genMockFrames, setupMockedLogsQueryRunner } from '../__mocks__/LogsQueryRunner'; import { validLogsQuery } from '../__mocks__/queries'; -import { CloudWatchQuery, LogAction } from '../types'; +import { LogAction } from '../types'; import * as rxjsUtils from '../utils/rxjs/increasingInterval'; import { LOG_IDENTIFIER_INTERNAL, LOGSTREAM_IDENTIFIER_INTERNAL } from './CloudWatchLogsQueryRunner'; @@ -221,32 +213,4 @@ describe('CloudWatchLogsQueryRunner', () => { expect(i).toBe(3); }); }); - - describe('handleLogQueries', () => { - it('should return error message when missing query string', async () => { - const { runner } = setupMockedLogsQueryRunner(); - const response = await lastValueFrom( - runner.handleLogQueries( - [ - { - datasource: { type: 'cloudwatch', uid: 'Zne6OZIVk' }, - id: '', - logGroups: [{ label: '', text: '', value: '' }], - queryMode: 'Logs', - refId: 'A', - region: 'default', - }, - ], - { scopedVars: {} } as DataQueryRequest - ) - ); - - expect(response).toEqual({ - data: [], - error: { - message: 'Query is required', - }, - }); - }); - }); }); diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index 254f2e32ac2..2516cf85b73 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -8,7 +8,6 @@ import { map, mergeMap, Observable, - of, repeat, scan, share, @@ -85,7 +84,9 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { logQueries: CloudWatchLogsQuery[], options: DataQueryRequest ): Observable => { - const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({ + const validLogQueries = logQueries.filter(this.filterQuery); + + const startQueryRequests: StartQueryRequest[] = validLogQueries.map((target: CloudWatchLogsQuery) => ({ queryString: target.expression || '', refId: target.refId, logGroupNames: target.logGroupNames || this.defaultLogGroups, @@ -98,24 +99,6 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { ), })); - const hasQueryWithMissingLogGroupSelection = queryParams.some((qp) => { - const missingLogGroupNames = qp.logGroupNames.length === 0; - const missingLogGroups = qp.logGroups.length === 0; - return missingLogGroupNames && missingLogGroups; - }); - - if (hasQueryWithMissingLogGroupSelection) { - return of({ data: [], error: { message: 'Log group is required' } }); - } - - const hasQueryWithMissingQueryString = queryParams.some((qp) => { - return qp.queryString.length === 0; - }); - - if (hasQueryWithMissingQueryString) { - return of({ data: [], error: { message: 'Query is required' } }); - } - const startTime = new Date(); const timeoutFunc = () => { return Date.now() >= startTime.valueOf() + rangeUtil.intervalToMs(this.logsTimeout); @@ -129,7 +112,7 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { skipCache: true, }); }, - queryParams, + startQueryRequests, timeoutFunc ).pipe( mergeMap(({ frames, error }: { frames: DataFrame[]; error?: DataQueryError }) => @@ -445,6 +428,18 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { return getLogGroupFieldsResponse; } + + private filterQuery(query: CloudWatchLogsQuery) { + const hasMissingLegacyLogGroupNames = !query.logGroupNames?.length; + const hasMissingLogGroups = !query.logGroups?.length; + const hasMissingQueryString = !query.expression?.length; + + if ((hasMissingLogGroups && hasMissingLegacyLogGroupNames) || hasMissingQueryString) { + return false; + } + + return true; + } } function withTeardown(observable: Observable, onUnsubscribe: () => void): Observable { diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index d656913493d..3f17b651ead 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -331,7 +331,8 @@ export interface StartQueryRequest { /** * The list of log groups to be queried. You can include up to 20 log groups. A StartQuery operation must include a logGroupNames or a logGroupName parameter, but not both. */ - logGroupNames?: string[]; + logGroupNames?: string[] /* not quite deprecated yet, but will be soon */; + logGroups?: SelectableResourceValue[]; /** * The query string to use. For more information, see CloudWatch Logs Insights Query Syntax. */ From d036225f7b715601165c9ecdb49ea176da3c9f71 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 7 Dec 2022 07:38:23 -0800 Subject: [PATCH 50/94] Chore: get user from appcontext rather than store utility (#59926) user from appcontext --- pkg/services/export/commit_helper.go | 4 +-- pkg/services/export/entity_store.go | 4 +-- pkg/services/export/service.go | 4 +-- .../playlist/playlistimpl/entity_store.go | 4 +-- pkg/services/store/auth.go | 33 ------------------- .../entity/sqlstash/sql_storage_server.go | 16 +++++++-- pkg/services/store/entity/tests/common.go | 4 +-- pkg/services/store/resolver/ds_cache.go | 9 +++-- pkg/services/store/resolver/service_test.go | 4 +-- 9 files changed, 31 insertions(+), 51 deletions(-) diff --git a/pkg/services/export/commit_helper.go b/pkg/services/export/commit_helper.go index 221912c7738..0cff51fcf76 100644 --- a/pkg/services/export/commit_helper.go +++ b/pkg/services/export/commit_helper.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" jsoniter "github.com/json-iterator/go" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" ) @@ -77,7 +77,7 @@ func (ch *commitHelper) initOrg(ctx context.Context, sql db.DB, orgID int64) err OrgID: orgID, // gets filled in from each row UserID: 0, } - ch.ctx = store.ContextWithUser(context.Background(), rowUser) + ch.ctx = appcontext.WithUser(context.Background(), rowUser) return err }) } diff --git a/pkg/services/export/entity_store.go b/pkg/services/export/entity_store.go index 630e683cec9..53ed63f9e5d 100644 --- a/pkg/services/export/entity_store.go +++ b/pkg/services/export/entity_store.go @@ -6,13 +6,13 @@ import ( "sync" "time" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/sqlstore/session" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind/snapshot" "github.com/grafana/grafana/pkg/services/user" @@ -101,7 +101,7 @@ func (e *entityStoreJob) start(ctx context.Context) { OrgID: 0, // gets filled in from each row UserID: 0, } - ctx = store.ContextWithUser(ctx, rowUser) + ctx = appcontext.WithUser(ctx, rowUser) what := models.StandardKindDashboard e.status.Count[what] = 0 diff --git a/pkg/services/export/service.go b/pkg/services/export/service.go index a1e7950e784..3e7f03e14d1 100644 --- a/pkg/services/export/service.go +++ b/pkg/services/export/service.go @@ -11,6 +11,7 @@ import ( "time" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -20,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/playlist" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" ) @@ -225,7 +225,7 @@ func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Res return response.Error(http.StatusLocked, "export already running", nil) } - ctx := store.ContextWithUser(context.Background(), c.SignedInUser) + ctx := appcontext.WithUser(context.Background(), c.SignedInUser) var job Job broadcast := func(s ExportStatus) { ex.broadcastStatus(c.OrgID, s) diff --git a/pkg/services/playlist/playlistimpl/entity_store.go b/pkg/services/playlist/playlistimpl/entity_store.go index 4d44ad85e2a..068302975dc 100644 --- a/pkg/services/playlist/playlistimpl/entity_store.go +++ b/pkg/services/playlist/playlistimpl/entity_store.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/sqlstore/session" - objectstore "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" ) @@ -44,7 +44,7 @@ func (s *entityStoreImpl) sync() { UserID: 0, // Admin user IsGrafanaAdmin: true, } - ctx := objectstore.ContextWithUser(context.Background(), rowUser) + ctx := appcontext.WithUser(context.Background(), rowUser) for _, info := range results { dto, err := s.sqlimpl.Get(ctx, &playlist.GetPlaylistByUidQuery{ OrgId: info.OrgID, diff --git a/pkg/services/store/auth.go b/pkg/services/store/auth.go index b9d7cb8bf9b..6ef47fe94f9 100644 --- a/pkg/services/store/auth.go +++ b/pkg/services/store/auth.go @@ -1,44 +1,11 @@ package store import ( - "context" "fmt" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) -type testUserKey struct{} - -func ContextWithUser(ctx context.Context, data *user.SignedInUser) context.Context { - return context.WithValue(ctx, testUserKey{}, data) -} - -// UserFromContext ** Experimental ** -// TODO: move to global infra package / new auth service -func UserFromContext(ctx context.Context) *user.SignedInUser { - grpcCtx := grpccontext.FromContext(ctx) - if grpcCtx != nil { - return grpcCtx.SignedInUser - } - - // Explicitly set in context - u, ok := ctx.Value(testUserKey{}).(*user.SignedInUser) - if ok && u != nil { - return u - } - - // From the HTTP request - c, ok := ctxkey.Get(ctx).(*models.ReqContext) - if !ok || c == nil || c.SignedInUser == nil { - return nil - } - - return c.SignedInUser -} - // Really just spitballing here :) this should hook into a system that can give better display info func GetUserIDString(user *user.SignedInUser) string { if user == nil { diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 3dc4d90e7a5..132f47c3428 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" @@ -109,7 +110,10 @@ func (s *sqlEntityServer) validateGRN(ctx context.Context, grn *entity.GRN) (*en if grn == nil { return nil, fmt.Errorf("missing GRN") } - user := store.UserFromContext(ctx) + user, err := appcontext.User(ctx) + if err != nil { + return nil, err + } if grn.TenantId == 0 { grn.TenantId = user.OrgID } else if grn.TenantId != user.OrgID { @@ -281,7 +285,10 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn updatedAt := r.UpdatedAt updatedBy := r.UpdatedBy if updatedBy == "" { - modifier := store.UserFromContext(ctx) + modifier, err := appcontext.User(ctx) + if err != nil { + return nil, err + } if modifier == nil { return nil, fmt.Errorf("can not find user in context") } @@ -618,7 +625,10 @@ func (s *sqlEntityServer) History(ctx context.Context, r *entity.EntityHistoryRe } func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequest) (*entity.EntitySearchResponse, error) { - user := store.UserFromContext(ctx) + user, err := appcontext.User(ctx) + if err != nil { + return nil, err + } if user == nil { return nil, fmt.Errorf("missing user in context") } diff --git a/pkg/services/store/entity/tests/common.go b/pkg/services/store/entity/tests/common.go index 78628eccbaf..b40122f9cef 100644 --- a/pkg/services/store/entity/tests/common.go +++ b/pkg/services/store/entity/tests/common.go @@ -5,12 +5,12 @@ import ( "testing" apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" saAPI "github.com/grafana/grafana/pkg/services/serviceaccounts/api" saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -86,6 +86,6 @@ func createTestContext(t *testing.T) testContext { authToken: authToken, client: client, user: serviceAccountUser, - ctx: store.ContextWithUser(context.Background(), serviceAccountUser), + ctx: appcontext.WithUser(context.Background(), serviceAccountUser), } } diff --git a/pkg/services/store/resolver/ds_cache.go b/pkg/services/store/resolver/ds_cache.go index 64081d057a7..93c648ebc8f 100644 --- a/pkg/services/store/resolver/ds_cache.go +++ b/pkg/services/store/resolver/ds_cache.go @@ -6,9 +6,9 @@ import ( "sync" "time" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/tsdb/grafanads" ) @@ -122,9 +122,12 @@ func (c *dsCache) getDS(ctx context.Context, uid string) (*dsVal, error) { } } - orgID := store.UserFromContext(ctx).OrgID + usr, err := appcontext.User(ctx) + if err != nil { + return nil, nil // no user + } - v, ok := c.cache[orgID] + v, ok := c.cache[usr.OrgID] if !ok { return nil, nil // org not found } diff --git a/pkg/services/store/resolver/service_test.go b/pkg/services/store/resolver/service_test.go index 92d305761cc..b420b27a508 100644 --- a/pkg/services/store/resolver/service_test.go +++ b/pkg/services/store/resolver/service_test.go @@ -4,17 +4,17 @@ import ( "context" "testing" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" - "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) func TestResolver(t *testing.T) { - ctxOrg1 := store.ContextWithUser(context.Background(), &user.SignedInUser{OrgID: 1}) + ctxOrg1 := appcontext.WithUser(context.Background(), &user.SignedInUser{OrgID: 1}) ds := &fakeDatasources.FakeDataSourceService{ DataSources: []*datasources.DataSource{ From 70fbf47022650bdf75c071314732ef226a055722 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 7 Dec 2022 17:03:22 +0100 Subject: [PATCH 51/94] chore: remove CreateUser from sqlstore & replace with userService.CreateUserForTests (#59910) --- pkg/api/org_users_test.go | 23 +- pkg/api/team_members_test.go | 24 +- pkg/api/user_test.go | 16 +- .../commands/conflict_user_command_test.go | 42 +++- .../resourcepermissions/api_test.go | 20 +- .../resourcepermissions/service_test.go | 13 +- .../resourcepermissions/store_bench_test.go | 11 +- .../resourcepermissions/store_test.go | 6 +- pkg/services/dashboards/database/acl_test.go | 8 +- .../libraryelements/libraryelements_test.go | 9 +- .../librarypanels/librarypanels_test.go | 11 +- .../login/authinfoservice/user_auth_test.go | 47 +++- pkg/services/org/orgimpl/org.go | 15 +- pkg/services/org/orgimpl/store_test.go | 57 +++-- pkg/services/org/orgtest/fake.go | 2 +- .../queryhistory/queryhistory_test.go | 11 +- pkg/services/serviceaccounts/api/api_test.go | 74 +++--- .../serviceaccounts/api/token_test.go | 28 +-- .../serviceaccounts/database/database.go | 7 +- .../serviceaccounts/database/database_test.go | 5 +- pkg/services/serviceaccounts/tests/common.go | 11 +- pkg/services/sqlstore/health_test.go | 3 +- pkg/services/sqlstore/org.go | 70 ------ pkg/services/sqlstore/org_test.go | 211 ----------------- pkg/services/sqlstore/org_users.go | 68 ------ pkg/services/sqlstore/org_users_test.go | 66 ------ pkg/services/sqlstore/sqlbuilder_test.go | 139 +++++++++-- pkg/services/sqlstore/sqlstore.go | 5 +- pkg/services/sqlstore/store.go | 2 - pkg/services/sqlstore/user.go | 201 ++++++++-------- pkg/services/stats/statsimpl/stats_test.go | 9 +- pkg/services/team/teamimpl/store_test.go | 28 ++- pkg/services/user/user.go | 4 + pkg/services/user/userimpl/store.go | 47 +++- pkg/services/user/userimpl/store_test.go | 99 +++++--- pkg/services/user/userimpl/time.go | 16 ++ pkg/services/user/userimpl/user.go | 217 ++++++++++++++++-- pkg/services/user/userimpl/user_test.go | 6 +- pkg/services/user/usertest/fake.go | 8 + .../api/alerting/api_alertmanager_test.go | 11 +- pkg/tests/api/correlations/common_test.go | 18 +- .../api/dashboards/api_dashboards_test.go | 11 +- pkg/tests/api/plugins/api_plugins_test.go | 11 +- pkg/tests/testinfra/testinfra.go | 11 +- 44 files changed, 943 insertions(+), 758 deletions(-) delete mode 100644 pkg/services/sqlstore/org.go delete mode 100644 pkg/services/sqlstore/org_test.go delete mode 100644 pkg/services/sqlstore/org_users.go delete mode 100644 pkg/services/sqlstore/org_users_test.go create mode 100644 pkg/services/user/userimpl/time.go diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 7a418591664..b86adfc5b29 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgtest" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" @@ -37,11 +38,17 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) { sqlStore.Cfg.AutoAssignOrg = true sqlStore.Cfg.AutoAssignOrgId = int(testOrgID) - _, err := sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "testUser@grafana.com", Login: testUserLogin}) + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) require.NoError(t, err) - _, err = sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "user1@grafana.com", Login: "user1"}) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService) require.NoError(t, err) - _, err = sqlStore.CreateUser(context.Background(), user.CreateUserCommand{Email: "user2@grafana.com", Login: "user2"}) + + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "testUser@grafana.com", Login: testUserLogin}) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "user1@grafana.com", Login: "user1"}) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: "user2@grafana.com", Login: "user2"}) require.NoError(t, err) } @@ -331,13 +338,15 @@ var ( func setupOrgUsersDBForAccessControlTests(t *testing.T, db *sqlstore.SQLStore, orgService org.Service) { t.Helper() - var err error + quotaService := quotaimpl.ProvideService(db, db.Cfg) + usrSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, nil, nil, quotaService) + require.NoError(t, err) - _, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testServerAdminViewer.Email, SkipOrgSetup: true, Login: testServerAdminViewer.Login}) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testServerAdminViewer.Email, SkipOrgSetup: true, Login: testServerAdminViewer.Login}) require.NoError(t, err) - _, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testAdminOrg2.Email, SkipOrgSetup: true, Login: testAdminOrg2.Login}) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testAdminOrg2.Email, SkipOrgSetup: true, Login: testAdminOrg2.Login}) require.NoError(t, err) - _, err = db.CreateUser(context.Background(), user.CreateUserCommand{Email: testEditorOrg1.Email, SkipOrgSetup: true, Login: testEditorOrg1.Login}) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Email: testEditorOrg1.Email, SkipOrgSetup: true, Login: testEditorOrg1.Login}) require.NoError(t, err) // Create both orgs with server admin diff --git a/pkg/api/team_members_test.go b/pkg/api/team_members_test.go index 5336cdb0768..3fb5aaff768 100644 --- a/pkg/api/team_members_test.go +++ b/pkg/api/team_members_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" @@ -25,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/services/teamguardian/database" "github.com/grafana/grafana/pkg/services/teamguardian/manager" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -46,6 +48,12 @@ func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) { teamSvc := teamimpl.ProvideService(sqlStore, setting.NewCfg()) team, err := teamSvc.CreateTeam("group1 name", "test1@test.com", testOrgID) require.NoError(t, err) + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService) + require.NoError(t, err) + for i := 0; i < 3; i++ { userCmd = user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), @@ -53,7 +61,7 @@ func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) { Login: fmt.Sprint("loginuser", i), } // user - user, err := sqlStore.CreateUser(context.Background(), userCmd) + user, err := usrSvc.CreateUserForTests(context.Background(), &userCmd) require.NoError(t, err) err = teamSvc.AddTeamMember(user.ID, testOrgID, team.Id, false, 1) require.NoError(t, err) @@ -115,7 +123,13 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) { } func createUser(db sqlstore.Store, orgId int64, t *testing.T) int64 { - user, err := db.CreateUser(context.Background(), user.CreateUserCommand{ + quotaService := quotaimpl.ProvideService(db, setting.NewCfg()) + orgService, err := orgimpl.ProvideService(db, setting.NewCfg(), quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(db, orgService, setting.NewCfg(), nil, nil, quotaService) + require.NoError(t, err) + + user, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: fmt.Sprintf("TestUser%d", rand.Int()), OrgID: orgId, Password: "password", @@ -127,7 +141,11 @@ func createUser(db sqlstore.Store, orgId int64, t *testing.T) int64 { func setupTeamTestScenario(userCount int, db *sqlstore.SQLStore, orgService org.Service, t *testing.T) int64 { teamService := teamimpl.ProvideService(db, setting.NewCfg()) // FIXME - user, err := db.CreateUser(context.Background(), user.CreateUserCommand{SkipOrgSetup: true, Login: testUserLogin}) + quotaService := quotaimpl.ProvideService(db, db.Cfg) + usrSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, teamService, nil, quotaService) + require.NoError(t, err) + + user, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{SkipOrgSetup: true, Login: testUserLogin}) require.NoError(t, err) cmd := &org.CreateOrgCommand{Name: "TestOrg", UserID: user.ID} testOrg, err := orgService.CreateWithMember(context.Background(), cmd) diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index e810cecc6c3..7f733b7dc9a 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/login/authinfoservice" authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/login/logintest" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" @@ -63,6 +64,11 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { &usagestats.UsageStatsMock{}, ) hs.authInfoService = srv + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil)) + require.NoError(t, err) + hs.userService = userSvc createUserCmd := user.CreateUserCommand{ Email: fmt.Sprint("user", "@test.com"), @@ -70,9 +76,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { Login: "loginuser", IsAdmin: true, } - user, err := sqlStore.CreateUser(context.Background(), createUserCmd) - require.Nil(t, err) - hs.userService, err = userimpl.ProvideService(sqlStore, nil, sc.cfg, nil, nil, quotatest.New(false, nil)) + user, err := userSvc.CreateUserForTests(context.Background(), &createUserCmd) require.NoError(t, err) sc.handlerFunc = hs.GetUserByID @@ -128,7 +132,11 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { Login: "admin", IsAdmin: true, } - _, err := sqlStore.CreateUser(context.Background(), createUserCmd) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sc.cfg, nil, nil, quotatest.New(false, nil)) + require.NoError(t, err) + _, err = userSvc.Create(context.Background(), &createUserCmd) require.Nil(t, err) sc.handlerFunc = hs.GetUserByLoginOrEmail diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go index 194edfeac4b..deb397a0bf2 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go @@ -12,9 +12,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" ) @@ -102,7 +106,7 @@ func TestBuildConflictBlock(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) - + usrSvc := setupTestUserService(t, sqlStore) if sqlStore.GetDialect().DriverName() != ignoredDatabase { for _, u := range tc.users { cmd := user.CreateUserCommand{ @@ -111,7 +115,7 @@ func TestBuildConflictBlock(t *testing.T) { Login: u.Login, OrgID: int64(testOrgID), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore) @@ -207,7 +211,7 @@ conflict: test2 t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) - + usrSvc := setupTestUserService(t, sqlStore) if sqlStore.GetDialect().DriverName() != ignoredDatabase { for _, u := range tc.users { cmd := user.CreateUserCommand{ @@ -216,7 +220,7 @@ conflict: test2 Login: u.Login, OrgID: int64(testOrgID), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } @@ -385,6 +389,7 @@ func TestGetConflictingUsers(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) + usrSvc := setupTestUserService(t, sqlStore) if sqlStore.GetDialect().DriverName() != ignoredDatabase { for _, u := range tc.users { cmd := user.CreateUserCommand{ @@ -394,7 +399,7 @@ func TestGetConflictingUsers(t *testing.T) { OrgID: int64(testOrgID), IsServiceAccount: u.IsServiceAccount, } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore) @@ -493,6 +498,7 @@ func TestGenerateConflictingUsersFile(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) + usrSvc := setupTestUserService(t, sqlStore) if sqlStore.GetDialect().DriverName() != ignoredDatabase { for _, u := range tc.users { cmd := user.CreateUserCommand{ @@ -501,7 +507,7 @@ func TestGenerateConflictingUsersFile(t *testing.T) { Login: u.Login, OrgID: int64(testOrgID), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } m, err := GetUsersWithConflictingEmailsOrLogins(&cli.Context{Context: context.Background()}, sqlStore) @@ -543,6 +549,8 @@ func TestRunValidateConflictUserFile(t *testing.T) { t.Run("should validate file thats gets created", func(t *testing.T) { // Restore after destructive operation sqlStore := db.InitTestDB(t) + usrSvc := setupTestUserService(t, sqlStore) + const testOrgID int64 = 1 if sqlStore.GetDialect().DriverName() != ignoredDatabase { // add additional user with conflicting login where DOMAIN is upper case @@ -551,14 +559,14 @@ func TestRunValidateConflictUserFile(t *testing.T) { Login: "user_duplicate_test_1_login", OrgID: testOrgID, } - _, err := sqlStore.CreateUser(context.Background(), dupUserLogincmd) + _, err := usrSvc.Create(context.Background(), &dupUserLogincmd) require.NoError(t, err) dupUserEmailcmd := user.CreateUserCommand{ Email: "USERDUPLICATETEST1@TEST.COM", Login: "USER_DUPLICATE_TEST_1_LOGIN", OrgID: testOrgID, } - _, err = sqlStore.CreateUser(context.Background(), dupUserEmailcmd) + _, err = usrSvc.Create(context.Background(), &dupUserEmailcmd) require.NoError(t, err) // get users @@ -589,6 +597,7 @@ func TestIntegrationMergeUser(t *testing.T) { teamSvc := teamimpl.ProvideService(sqlStore, setting.NewCfg()) team1, err := teamSvc.CreateTeam("team1 name", "", 1) require.Nil(t, err) + usrSvc := setupTestUserService(t, sqlStore) const testOrgID int64 = 1 if sqlStore.GetDialect().DriverName() != ignoredDatabase { @@ -601,7 +610,7 @@ func TestIntegrationMergeUser(t *testing.T) { Login: "user_duplicate_test_1_login", OrgID: testOrgID, } - _, err := sqlStore.CreateUser(context.Background(), dupUserLogincmd) + _, err := usrSvc.Create(context.Background(), &dupUserLogincmd) require.NoError(t, err) dupUserEmailcmd := user.CreateUserCommand{ Email: "USERDUPLICATETEST1@TEST.COM", @@ -609,7 +618,7 @@ func TestIntegrationMergeUser(t *testing.T) { Login: "USER_DUPLICATE_TEST_1_LOGIN", OrgID: testOrgID, } - userWithUpperCase, err := sqlStore.CreateUser(context.Background(), dupUserEmailcmd) + userWithUpperCase, err := usrSvc.Create(context.Background(), &dupUserEmailcmd) require.NoError(t, err) // this is the user we want to update to another team err = teamSvc.AddTeamMember(userWithUpperCase.ID, testOrgID, team1.Id, false, 0) @@ -746,6 +755,7 @@ conflict: test2 for _, tc := range testCases { // Restore after destructive operation sqlStore := db.InitTestDB(t) + usrSvc := setupTestUserService(t, sqlStore) if sqlStore.GetDialect().DriverName() != ignoredDatabase { for _, u := range tc.users { cmd := user.CreateUserCommand{ @@ -754,7 +764,7 @@ conflict: test2 Login: u.Login, OrgID: int64(testOrgID), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } // add additional user with conflicting login where DOMAIN is upper case @@ -840,3 +850,13 @@ func TestMarshalConflictUser(t *testing.T) { }) } } + +func setupTestUserService(t *testing.T, sqlStore *sqlstore.SQLStore) user.Service { + t.Helper() + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, "atest.FakeQuotaService{}) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, "atest.FakeQuotaService{}) + require.NoError(t, err) + + return usrSvc +} diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index d1d918098de..ddeee01766b 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -18,9 +18,12 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -399,10 +402,17 @@ 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, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service) + server := setupTestServer(t, &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}, + }, service) // seed user - _, err := sql.CreateUser(context.Background(), user.CreateUserCommand{Login: "test", OrgID: 1}) + orgSvc, err := orgimpl.ProvideService(sql, sql.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sql, orgSvc, sql.Cfg, nil, nil, "atest.FakeQuotaService{}) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "users", strconv.Itoa(int(tt.userID))) @@ -505,7 +515,11 @@ func seedPermissions(t *testing.T, resourceID string, sql *sqlstore.SQLStore, se _, err = service.SetTeamPermission(context.Background(), team.OrgId, team.Id, resourceID, "Edit") require.NoError(t, err) // seed user 1 with "View" permission on dashboard 1 - u, err := sql.CreateUser(context.Background(), user.CreateUserCommand{Login: "test", OrgID: 1}) + orgSvc, err := orgimpl.ProvideService(sql, sql.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sql, orgSvc, sql.Cfg, nil, nil, "atest.FakeQuotaService{}) + require.NoError(t, err) + u, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) _, err = service.SetUserPermission(context.Background(), u.OrgID, accesscontrol.User{ID: u.ID}, resourceID, "View") require.NoError(t, err) diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index c1352b89f9b..605db098460 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/team" @@ -47,7 +48,11 @@ func TestService_SetUserPermission(t *testing.T) { }) // seed user - user, err := sql.CreateUser(context.Background(), user.CreateUserCommand{Login: "test", OrgID: 1}) + orgSvc, err := orgimpl.ProvideService(sql, sql.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sql, orgSvc, sql.Cfg, nil, nil, "atest.FakeQuotaService{}) + require.NoError(t, err) + user, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1}) require.NoError(t, err) var hookCalled bool @@ -204,7 +209,11 @@ func TestService_SetPermissions(t *testing.T) { service, sql, teamSvc := setupTestEnvironment(t, []accesscontrol.Permission{}, tt.options) // seed user - _, err := sql.CreateUser(context.Background(), user.CreateUserCommand{Login: "user", OrgID: 1}) + orgSvc, err := orgimpl.ProvideService(sql, sql.Cfg, quotatest.New(false, nil)) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sql, orgSvc, sql.Cfg, nil, nil, "atest.FakeQuotaService{}) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "user", OrgID: 1}) require.NoError(t, err) _, err = teamSvc.CreateTeam("team", "", 1) require.NoError(t, err) diff --git a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go index e6fff4cacb8..0e782a7cd20 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go @@ -14,9 +14,12 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) const ( @@ -137,7 +140,11 @@ func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]in teamSvc := teamimpl.ProvideService(db, db.Cfg) numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam)) globalUserId := 0 - + qs := quotatest.New(false, nil) + orgSvc, err := orgimpl.ProvideService(db, db.Cfg, qs) + require.NoError(b, err) + usrSvc, err := userimpl.ProvideService(db, orgSvc, db.Cfg, nil, nil, qs) + require.NoError(b, err) userIds := make([]int64, 0) teamIds := make([]int64, 0) for i := 0; i < numberOfTeams; i++ { @@ -155,7 +162,7 @@ func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]in userEmail := fmt.Sprintf("%s@example.org", userName) createUserCmd := user.CreateUserCommand{Email: userEmail, Name: userName, Login: userName, OrgID: 1} - user, err := db.CreateUser(context.Background(), createUserCmd) + user, err := usrSvc.Create(context.Background(), &createUserCmd) require.NoError(b, err) userId := user.ID globalUserId++ diff --git a/pkg/services/accesscontrol/resourcepermissions/store_test.go b/pkg/services/accesscontrol/resourcepermissions/store_test.go index 81affa327ab..0598e8c9190 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) type setUserResourcePermissionTest struct { @@ -488,6 +489,9 @@ func TestIntegrationStore_GetResourcePermissions(t *testing.T) { func seedResourcePermissions(t *testing.T, store *store, sql *sqlstore.SQLStore, orgService org.Service, actions []string, resource, resourceID, resourceAttribute string, numUsers int) { t.Helper() var orgModel *org.Org + usrSvc, err := userimpl.ProvideService(sql, orgService, sql.Cfg, nil, nil, quotatest.New(false, nil)) + require.NoError(t, err) + for i := 0; i < numUsers; i++ { if orgModel == nil { cmd := &org.CreateOrgCommand{Name: "test", UserID: int64(i)} @@ -496,7 +500,7 @@ func seedResourcePermissions(t *testing.T, store *store, sql *sqlstore.SQLStore, orgModel = addedOrg } - u, err := sql.CreateUser(context.Background(), user.CreateUserCommand{ + u, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{ Login: fmt.Sprintf("user:%s%d", resourceID, i), OrgID: orgModel.ID, }) diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index ae097469e5d..4e67fe4446b 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) func TestIntegrationDashboardACLDataAccess(t *testing.T) { @@ -274,11 +275,14 @@ func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role str sqlStore.Cfg.AutoAssignOrgId = 1 sqlStore.Cfg.AutoAssignOrgRole = role - orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaimpl.ProvideService(sqlStore, sqlStore.Cfg)) + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, qs) require.NoError(t, err) currentUserCmd := user.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} - currentUser, err := sqlStore.CreateUser(context.Background(), currentUserCmd) + currentUser, err := usrSvc.CreateUserForTests(context.Background(), ¤tUserCmd) require.NoError(t, err) orgs, err := orgService.GetUserOrgList(context.Background(), &org.GetUserOrgListQuery{UserID: currentUser.ID}) require.NoError(t, err) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 49e02f71790..180188d6454 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -30,10 +30,12 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -456,8 +458,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Name: "User In DB", Login: userInDbName, } - - _, err = sqlStore.CreateUser(context.Background(), cmd) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) sc := scenarioContext{ diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 356ea61e045..c326e415c5b 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -28,10 +28,12 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -856,12 +858,13 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Name: "User In DB", Login: userInDbName, } - ctx := appcontext.WithUser(context.Background(), usr) - - _, err = sqlStore.CreateUser(ctx, cmd) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) - sc := scenarioContext{ user: usr, ctx: ctx, diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index 5e8181e9528..f22814020a5 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -15,7 +15,10 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) //nolint:goconst @@ -29,13 +32,19 @@ func TestUserAuth(t *testing.T) { ) t.Run("Given 5 users", func(t *testing.T) { + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, qs) + require.NoError(t, err) + for i := 0; i < 5; i++ { cmd := user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.Create(context.Background(), &cmd) require.Nil(t, err) } @@ -204,6 +213,11 @@ func TestUserAuth(t *testing.T) { t.Run("Always return the most recently used auth_module", func(t *testing.T) { // Restore after destructive operation sqlStore = db.InitTestDB(t) + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, qs) + require.NoError(t, err) for i := 0; i < 5; i++ { cmd := user.CreateUserCommand{ @@ -211,8 +225,8 @@ func TestUserAuth(t *testing.T) { Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - _, err := sqlStore.CreateUser(context.Background(), cmd) - require.Nil(t, err) + _, err = usrSvc.Create(context.Background(), &cmd) + require.NoError(t, err) } // Find a user to set tokens on @@ -272,6 +286,11 @@ func TestUserAuth(t *testing.T) { t.Run("Keeps track of last used auth_module when not using oauth", func(t *testing.T) { // Restore after destructive operation sqlStore = db.InitTestDB(t) + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, qs) + require.NoError(t, err) for i := 0; i < 5; i++ { cmd := user.CreateUserCommand{ @@ -279,7 +298,7 @@ func TestUserAuth(t *testing.T) { Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.Create(context.Background(), &cmd) require.Nil(t, err) } @@ -406,6 +425,11 @@ func TestUserAuth(t *testing.T) { // Restore after destructive operation sqlStore = db.InitTestDB(t) + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, qs) + require.NoError(t, err) for i := 0; i < 5; i++ { cmd := user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), @@ -413,17 +437,22 @@ func TestUserAuth(t *testing.T) { Login: fmt.Sprint("loginuser", i), OrgID: 1, } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.Create(context.Background(), &cmd) require.Nil(t, err) } - _, err := srv.authInfoStore.GetLoginStats(context.Background()) + _, err = srv.authInfoStore.GetLoginStats(context.Background()) require.Nil(t, err) }) t.Run("calculate metrics on duplicate userstats", func(t *testing.T) { // Restore after destructive operation sqlStore = db.InitTestDB(t) + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, qs) + require.NoError(t, err) for i := 0; i < 5; i++ { cmd := user.CreateUserCommand{ @@ -432,7 +461,7 @@ func TestUserAuth(t *testing.T) { Login: fmt.Sprint("loginuser", i), OrgID: 1, } - _, err := sqlStore.CreateUser(context.Background(), cmd) + _, err := usrSvc.Create(context.Background(), &cmd) require.Nil(t, err) } @@ -443,7 +472,7 @@ func TestUserAuth(t *testing.T) { Name: "user name 1", Login: "USER_DUPLICATE_TEST_1_LOGIN", } - _, err := sqlStore.CreateUser(context.Background(), dupUserEmailcmd) + _, err := usrSvc.Create(context.Background(), &dupUserEmailcmd) require.NoError(t, err) // add additional user with duplicate login where DOMAIN is upper case @@ -452,7 +481,7 @@ func TestUserAuth(t *testing.T) { Name: "user name 1", Login: "user_duplicate_test_1_login", } - _, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd) + _, err = usrSvc.Create(context.Background(), &dupUserLogincmd) require.NoError(t, err) authInfoStore.ExpectedUser = &user.User{ Email: "userduplicatetest1@test.com", diff --git a/pkg/services/org/orgimpl/org.go b/pkg/services/org/orgimpl/org.go index ed6c3bc506a..030a3476d70 100644 --- a/pkg/services/org/orgimpl/org.go +++ b/pkg/services/org/orgimpl/org.go @@ -2,6 +2,7 @@ package orgimpl import ( "context" + "errors" "fmt" "time" @@ -65,10 +66,12 @@ func (s *Service) GetIDForNewUser(ctx context.Context, cmd org.GetOrgIDForNewUse return cmd.OrgID, nil } - orgName := cmd.OrgName + var orgName string + orgName = cmd.OrgName if len(orgName) == 0 { orgName = util.StringsFallback2(cmd.Email, cmd.Login) } + orga.Name = orgName if setting.AutoAssignOrg { orga, err := s.store.Get(ctx, int64(s.cfg.AutoAssignOrgId)) @@ -142,12 +145,14 @@ func (s *Service) Delete(ctx context.Context, cmd *org.DeleteOrgCommand) error { } func (s *Service) GetOrCreate(ctx context.Context, orgName string) (int64, error) { - var orga *org.Org + var orga = &org.Org{} var err error if s.cfg.AutoAssignOrg { - orga, err = s.store.Get(ctx, int64(s.cfg.AutoAssignOrgId)) - if err != nil { + got, err := s.store.Get(ctx, int64(s.cfg.AutoAssignOrgId)) + if err != nil && !errors.Is(err, org.ErrOrgNotFound) { return 0, err + } else if err == nil { + return got.ID, nil } if s.cfg.AutoAssignOrgId != 1 { @@ -156,11 +161,9 @@ func (s *Service) GetOrCreate(ctx context.Context, orgName string) (int64, error return 0, fmt.Errorf("could not create user: organization ID %d does not exist", s.cfg.AutoAssignOrgId) } - orga.Name = MainOrgName orga.ID = int64(s.cfg.AutoAssignOrgId) } else { - orga = &org.Org{} orga.Name = orgName } diff --git a/pkg/services/org/orgimpl/store_test.go b/pkg/services/org/orgimpl/store_test.go index 60461b8698c..06a75d217ef 100644 --- a/pkg/services/org/orgimpl/store_test.go +++ b/pkg/services/org/orgimpl/store_test.go @@ -14,8 +14,10 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -304,11 +306,12 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { }) t.Run("GetOrgUsers and UpdateOrgUsers", func(t *testing.T) { ss := db.InitTestDB(t) - ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} - ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} - ac1, err := ss.CreateUser(context.Background(), ac1cmd) + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + ac1cmd := &user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} + ac2cmd := &user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} + ac1, err := usrSvc.CreateUserForTests(context.Background(), ac1cmd) require.NoError(t, err) - ac2, err := ss.CreateUser(context.Background(), ac2cmd) + ac2, err := usrSvc.CreateUserForTests(context.Background(), ac2cmd) require.NoError(t, err) cmd := org.AddOrgUserCommand{ OrgID: ac1.OrgID, @@ -412,6 +415,8 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { t.Run("Given single org and 2 users inserted", func(t *testing.T) { ss = db.InitTestDB(t) + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + testUser := &user.SignedInUser{ Permissions: map[int64]map[string][]string{ 1: {accesscontrol.ActionOrgUsersRead: []string{accesscontrol.ScopeUsersAll}}, @@ -421,13 +426,13 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) { ss.Cfg.AutoAssignOrgId = 1 ss.Cfg.AutoAssignOrgRole = "Viewer" - ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} - ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} + ac1cmd := &user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} + ac2cmd := &user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} - ac1, err := ss.CreateUser(context.Background(), ac1cmd) + ac1, err := usrSvc.CreateUserForTests(context.Background(), ac1cmd) testUser.OrgID = ac1.OrgID require.NoError(t, err) - _, err = ss.CreateUser(context.Background(), ac2cmd) + _, err = usrSvc.Create(context.Background(), ac2cmd) require.NoError(t, err) t.Run("Can get organization users paginated with query", func(t *testing.T) { @@ -470,20 +475,20 @@ func TestIntegrationSQLStore_AddOrgUser(t *testing.T) { dialect: store.GetDialect(), cfg: setting.NewCfg(), } + _, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) // create org and admin - u, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + u, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: "admin", }) require.NoError(t, err) // create a service account with no org - sa, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + sa, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: "sa-no-org", IsServiceAccount: true, SkipOrgSetup: true, }) - require.NoError(t, err) require.Equal(t, int64(-1), sa.OrgID) @@ -599,9 +604,11 @@ func TestIntegration_SQLStore_GetOrgUsers(t *testing.T) { func seedOrgUsers(t *testing.T, orgUserStore store, store *sqlstore.SQLStore, numUsers int) { t.Helper() + _, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) + // Seed users for i := 1; i <= numUsers; i++ { - user, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + user, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: fmt.Sprintf("user-%d", i), OrgID: 1, }) @@ -633,8 +640,8 @@ func TestIntegration_SQLStore_GetOrgUsers_PopulatesCorrectly(t *testing.T) { } // The millisecond part is not stored in the DB constNow := time.Date(2022, 8, 17, 20, 34, 58, 0, time.UTC) - sqlstore.MockTimeNow(constNow) - defer sqlstore.ResetTimeNow() + userimpl.MockTimeNow(constNow) + defer userimpl.ResetTimeNow() store := db.InitTestDB(t, sqlstore.InitTestDBOpt{}) orgUserStore := sqlStore{ @@ -642,6 +649,7 @@ func TestIntegration_SQLStore_GetOrgUsers_PopulatesCorrectly(t *testing.T) { dialect: store.GetDialect(), cfg: setting.NewCfg(), } + _, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) id, err := orgUserStore.Insert(context.Background(), &org.Org{ @@ -651,7 +659,7 @@ func TestIntegration_SQLStore_GetOrgUsers_PopulatesCorrectly(t *testing.T) { }) require.NoError(t, err) - newUser, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + newUser, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: "Viewer", Email: "viewer@localhost", OrgID: id, @@ -752,8 +760,6 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { result, err := orgUserStore.SearchOrgUsers(context.Background(), tt.query) - fmt.Println("users:", result) - require.NoError(t, err) assert.Len(t, result.OrgUsers, tt.expectedNumUsers) @@ -776,15 +782,16 @@ func TestIntegration_SQLStore_RemoveOrgUser(t *testing.T) { dialect: store.GetDialect(), cfg: setting.NewCfg(), } + _, usrSvc := createOrgAndUserSvc(t, store, store.Cfg) // create org and admin - _, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + _, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{ Login: "admin", OrgID: 1, }) require.NoError(t, err) // create a user with no org - _, err = store.CreateUser(context.Background(), user.CreateUserCommand{ + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{ Login: "user", OrgID: 1, SkipOrgSetup: true, @@ -807,3 +814,15 @@ func TestIntegration_SQLStore_RemoveOrgUser(t *testing.T) { }) require.NoError(t, err) } + +func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Service, user.Service) { + t.Helper() + + quotaService := quotaimpl.ProvideService(store, cfg) + orgService, err := ProvideService(store, cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, cfg, nil, nil, quotaService) + require.NoError(t, err) + + return orgService, usrSvc +} diff --git a/pkg/services/org/orgtest/fake.go b/pkg/services/org/orgtest/fake.go index d359d03fb34..3ac6ac57eab 100644 --- a/pkg/services/org/orgtest/fake.go +++ b/pkg/services/org/orgtest/fake.go @@ -75,7 +75,7 @@ func (f *FakeOrgService) Delete(ctx context.Context, cmd *org.DeleteOrgCommand) } func (f *FakeOrgService) GetOrCreate(ctx context.Context, orgName string) (int64, error) { - return 0, f.ExpectedError + return f.ExpectedOrg.ID, f.ExpectedError } func (f *FakeOrgService) AddOrgUser(ctx context.Context, cmd *org.AddOrgUserCommand) error { diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index 56f71f218a1..2976281b6d8 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -17,7 +17,10 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -51,8 +54,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Cfg: setting.NewCfg(), store: sqlStore, } - service.Cfg.QueryHistoryEnabled = true + quotaService := quotatest.New(false, nil) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, nil, nil, quotaService) + require.NoError(t, err) usr := user.SignedInUser{ UserID: testUserID, @@ -64,7 +71,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo LastSeenAt: time.Now(), } - _, err := sqlStore.CreateUser(context.Background(), user.CreateUserCommand{ + _, err = usrSvc.Create(context.Background(), &user.CreateUserCommand{ Email: "signed.in.user@test.com", Name: "Signed In User", Login: "signed_in_user", diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index 92130e02611..54f8ac7a0ef 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" "github.com/grafana/grafana/pkg/services/licensing" @@ -32,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" @@ -46,14 +48,7 @@ var ( func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { store := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - kvStore := kvstore.ProvideService(store) - orgService, err := orgimpl.ProvideService(store, setting.NewCfg(), quotaService) - require.NoError(t, err) - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, orgService) - svcmock := tests.ServiceAccountMock{} + services := setupTestServices(t, store) autoAssignOrg := store.Cfg.AutoAssignOrg store.Cfg.AutoAssignOrg = true @@ -62,7 +57,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { }() orgCmd := &org.CreateOrgCommand{Name: "Some Test Org"} - _, err = orgService.CreateWithMember(context.Background(), orgCmd) + _, err := services.OrgService.CreateWithMember(context.Background(), orgCmd) require.Nil(t, err) type testCreateSATestCase struct { @@ -167,7 +162,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { serviceAccountRequestScenario(t, http.MethodPost, serviceAccountPath, testUser, func(httpmethod string, endpoint string, usr *tests.TestUser) { - server, api := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, api := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), tc.acmock, store, services.SAStore) marshalled, err := json.Marshal(tc.body) require.NoError(t, err) @@ -216,12 +211,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { // with permissions and without permissions func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { store := db.InitTestDB(t) - kvStore := kvstore.ProvideService(store) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil) - svcmock := tests.ServiceAccountMock{} + services := setupTestServices(t, store) var requestResponse = func(server *web.Mux, httpMethod, requestpath string) *httptest.ResponseRecorder { req, err := http.NewRequest(httpMethod, requestpath, nil) @@ -249,7 +239,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { } serviceAccountRequestScenario(t, http.MethodDelete, serviceAccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) { createduser := tests.SetupUserServiceAccount(t, store, testcase.user) - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, saStore) + server, _ := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), testcase.acmock, store, services.SAStore) actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.ID))).Code require.Equal(t, testcase.expectedCode, actual) }) @@ -273,7 +263,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { } serviceAccountRequestScenario(t, http.MethodDelete, serviceAccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) { createduser := tests.SetupUserServiceAccount(t, store, testcase.user) - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, saStore) + server, _ := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), testcase.acmock, store, services.SAStore) actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, createduser.ID)).Code require.Equal(t, testcase.expectedCode, actual) }) @@ -326,12 +316,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { store := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - kvStore := kvstore.ProvideService(store) - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil) - svcmock := tests.ServiceAccountMock{} + services := setupTestServices(t, store) type testRetrieveSATestCase struct { desc string user *tests.TestUser @@ -395,7 +380,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { createdUser := tests.SetupUserServiceAccount(t, store, *tc.user) scopeID = int(createdUser.ID) } - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, _ := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), tc.acmock, store, services.SAStore) actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, scopeID)) @@ -420,12 +405,7 @@ func newString(s string) *string { func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { store := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - kvStore := kvstore.ProvideService(store) - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil) - svcmock := tests.ServiceAccountMock{} + services := setupTestServices(t, store) type testUpdateSATestCase struct { desc string user *tests.TestUser @@ -518,7 +498,7 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - server, saAPI := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, saAPI := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), tc.acmock, store, services.SAStore) scopeID := tc.Id if tc.user != nil { createdUser := tests.SetupUserServiceAccount(t, store, *tc.user) @@ -556,3 +536,33 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { }) } } + +type services struct { + OrgService org.Service + UserService user.Service + SAStore serviceaccounts.Store + SAService tests.ServiceAccountMock + APIKeyService apikey.Service +} + +func setupTestServices(t *testing.T, db *sqlstore.SQLStore) services { + kvStore := kvstore.ProvideService(db) + quotaService := quotatest.New(false, nil) + apiKeyService, err := apikeyimpl.ProvideService(db, db.Cfg, quotaService) + require.NoError(t, err) + + orgService, err := orgimpl.ProvideService(db, setting.NewCfg(), quotaService) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, nil, nil, quotaService) + require.NoError(t, err) + saStore := database.ProvideServiceAccountsStore(db, apiKeyService, kvStore, userSvc, orgService) + svcmock := tests.ServiceAccountMock{} + + return services{ + OrgService: orgService, + UserService: userSvc, + SAStore: saStore, + SAService: svcmock, + APIKeyService: apiKeyService, + } +} diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 90b234d24d2..ca5fda55914 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -18,14 +18,10 @@ import ( "github.com/grafana/grafana/pkg/components/apikeygen" apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/accesscontrol" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" - "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/serviceaccounts" - "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" @@ -55,12 +51,7 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string, func TestServiceAccountsAPI_CreateToken(t *testing.T) { store := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - kvStore := kvstore.ProvideService(store) - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil) - svcmock := tests.ServiceAccountMock{} + services := setupTestServices(t, store) sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) type testCreateSAToken struct { @@ -140,7 +131,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { bodyString = string(b) } - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, _ := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), tc.acmock, store, services.SAStore) actual := requestResponse(server, http.MethodPost, endpoint, strings.NewReader(bodyString)) actualCode := actual.Code @@ -154,7 +145,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { assert.Equal(t, tc.body["name"], actualBody["name"]) query := apikey.GetByNameQuery{KeyName: tc.body["name"].(string), OrgId: sa.OrgID} - err = apiKeyService.GetApiKeyByName(context.Background(), &query) + err = services.APIKeyService.GetApiKeyByName(context.Background(), &query) require.NoError(t, err) assert.Equal(t, sa.ID, *query.Result.ServiceAccountId) @@ -174,12 +165,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { func TestServiceAccountsAPI_DeleteToken(t *testing.T) { store := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService) - require.NoError(t, err) - kvStore := kvstore.ProvideService(store) - svcMock := &tests.ServiceAccountMock{} - saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil) + services := setupTestServices(t, store) sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) type testCreateSAToken struct { @@ -239,11 +225,11 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - token := createTokenforSA(t, saStore, tc.keyName, sa.OrgID, sa.ID, 1) + token := createTokenforSA(t, services.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, saStore) + server, _ := setupTestServer(t, &services.SAService, routing.NewRouteRegister(), tc.acmock, store, services.SAStore) actual := requestResponse(server, http.MethodDelete, endpoint, strings.NewReader(bodyString)) actualCode := actual.Code @@ -253,7 +239,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { require.Equal(t, tc.expectedCode, actualCode, endpoint, actualBody) query := apikey.GetByNameQuery{KeyName: tc.keyName, OrgId: sa.OrgID} - err := apiKeyService.GetApiKeyByName(context.Background(), &query) + err := services.APIKeyService.GetApiKeyByName(context.Background(), &query) if actualCode == http.StatusOK { require.Error(t, err) } else { diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 19265645f15..20982a29f42 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -30,13 +30,14 @@ type ServiceAccountsStoreImpl struct { } func ProvideServiceAccountsStore(store *sqlstore.SQLStore, apiKeyService apikey.Service, - kvStore kvstore.KVStore, orgService org.Service) *ServiceAccountsStoreImpl { + kvStore kvstore.KVStore, userService user.Service, orgService org.Service) *ServiceAccountsStoreImpl { return &ServiceAccountsStoreImpl{ sqlStore: store, apiKeyService: apiKeyService, kvStore: kvStore, log: log.New("serviceaccounts.store"), orgService: orgService, + userService: userService, } } @@ -55,7 +56,7 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, org var newSA *user.User createErr := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) (err error) { var errUser error - newSA, errUser = s.sqlStore.CreateUser(ctx, user.CreateUserCommand{ + newSA, errUser = s.userService.CreateServiceAccount(ctx, &user.CreateUserCommand{ Login: generatedLogin, OrgID: orgId, Name: saForm.Name, @@ -461,7 +462,7 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Co } return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - newSA, errCreateSA := s.sqlStore.CreateUser(ctx, cmd) + newSA, errCreateSA := s.userService.CreateServiceAccount(ctx, &cmd) if errCreateSA != nil { return fmt.Errorf("failed to create service account: %w", errCreateSA) } diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index 268f6a678c0..41084066d4c 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -118,7 +119,9 @@ func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreI kvStore := kvstore.ProvideService(db) orgService, err := orgimpl.ProvideService(db, setting.NewCfg(), quotaService) require.NoError(t, err) - return db, ProvideServiceAccountsStore(db, apiKeyService, kvStore, orgService) + userSvc, err := userimpl.ProvideService(db, orgService, db.Cfg, nil, nil, quotaService) + require.NoError(t, err) + return db, ProvideServiceAccountsStore(db, apiKeyService, kvStore, userSvc, orgService) } func TestStore_RetrieveServiceAccount(t *testing.T) { diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index d8b5dea247b..232cfc8da8b 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -12,10 +12,13 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) type TestUser struct { @@ -41,7 +44,13 @@ func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser role = testUser.Role } - u1, err := sqlStore.CreateUser(context.Background(), user.CreateUserCommand{ + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + u1, err := usrSvc.CreateUserForTests(context.Background(), &user.CreateUserCommand{ Login: testUser.Login, IsServiceAccount: testUser.IsServiceAccount, DefaultOrgRole: role, diff --git a/pkg/services/sqlstore/health_test.go b/pkg/services/sqlstore/health_test.go index 4873ec37fbf..2865a4302b8 100644 --- a/pkg/services/sqlstore/health_test.go +++ b/pkg/services/sqlstore/health_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" ) func TestIntegrationGetDBHealthQuery(t *testing.T) { diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go deleted file mode 100644 index 52b970f9128..00000000000 --- a/pkg/services/sqlstore/org.go +++ /dev/null @@ -1,70 +0,0 @@ -// DO NOT ADD METHODS TO THIS FILES. SQLSTORE IS DEPRECATED AND WILL BE REMOVED. -package sqlstore - -import ( - "fmt" - "time" - - "github.com/grafana/grafana/pkg/events" - "github.com/grafana/grafana/pkg/models" -) - -const mainOrgName = "Main Org." - -func verifyExistingOrg(sess *DBSession, orgId int64) error { - var org models.Org - has, err := sess.Where("id=?", orgId).Get(&org) - if err != nil { - return err - } - if !has { - return models.ErrOrgNotFound - } - return nil -} - -func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { - var org models.Org - if ss.Cfg.AutoAssignOrg { - has, err := sess.Where("id=?", ss.Cfg.AutoAssignOrgId).Get(&org) - if err != nil { - return 0, err - } - if has { - return org.Id, nil - } - - if ss.Cfg.AutoAssignOrgId != 1 { - ss.log.Error("Could not create user: organization ID does not exist", "orgID", - ss.Cfg.AutoAssignOrgId) - return 0, fmt.Errorf("could not create user: organization ID %d does not exist", - ss.Cfg.AutoAssignOrgId) - } - - org.Name = mainOrgName - org.Id = int64(ss.Cfg.AutoAssignOrgId) - } else { - org.Name = orgName - } - - org.Created = time.Now() - org.Updated = time.Now() - - if org.Id != 0 { - if _, err := sess.InsertId(&org); err != nil { - return 0, err - } - } else { - if _, err := sess.InsertOne(&org); err != nil { - return 0, err - } - } - - sess.publishAfterCommit(&events.OrgCreated{ - Timestamp: org.Created, - Id: org.Id, - Name: org.Name, - }) - - return org.Id, nil -} diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go deleted file mode 100644 index c348e404fb2..00000000000 --- a/pkg/services/sqlstore/org_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package sqlstore - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - dashver "github.com/grafana/grafana/pkg/services/dashboardversion" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/util" -) - -func TestIntegrationAccountDataAccess(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - t.Run("Testing Account DB Access", func(t *testing.T) { - sqlStore := InitTestDB(t) - - t.Run("Given single org mode", func(t *testing.T) { - sqlStore.Cfg.AutoAssignOrg = true - sqlStore.Cfg.AutoAssignOrgId = 1 - sqlStore.Cfg.AutoAssignOrgRole = "Viewer" - - t.Run("Users should be added to default organization", func(t *testing.T) { - ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} - ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} - - ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd) - require.NoError(t, err) - ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd) - require.NoError(t, err) - - q1 := models.GetUserOrgListQuery{UserId: ac1.ID} - q2 := models.GetUserOrgListQuery{UserId: ac2.ID} - err = sqlStore.getUserOrgList(context.Background(), &q1) - require.NoError(t, err) - err = sqlStore.getUserOrgList(context.Background(), &q2) - require.NoError(t, err) - - require.Equal(t, q1.Result[0].OrgId, q2.Result[0].OrgId) - require.Equal(t, string(q1.Result[0].Role), "Viewer") - }) - }) - - t.Run("Given two saved users", func(t *testing.T) { - sqlStore = InitTestDB(t) - sqlStore.Cfg.AutoAssignOrg = false - - ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} - ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} - serviceaccountcmd := user.CreateUserCommand{Login: "serviceaccount", Email: "service@test.com", Name: "serviceaccount name", IsAdmin: true, IsServiceAccount: true} - - ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd) - require.NoError(t, err) - ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd) - require.NoError(t, err) - // user only used for making sure we filter out the service accounts - _, err = sqlStore.CreateUser(context.Background(), serviceaccountcmd) - require.NoError(t, err) - - t.Run("Given an added org user", func(t *testing.T) { - cmd := models.AddOrgUserCommand{ - OrgId: ac1.OrgID, - UserId: ac2.ID, - Role: org.RoleViewer, - } - - err := sqlStore.addOrgUser(context.Background(), &cmd) - t.Run("Should have been saved without error", func(t *testing.T) { - require.NoError(t, err) - }) - - t.Run("Can get user organizations", func(t *testing.T) { - query := models.GetUserOrgListQuery{UserId: ac2.ID} - err := sqlStore.getUserOrgList(context.Background(), &query) - - require.NoError(t, err) - require.Equal(t, len(query.Result), 2) - }) - - t.Run("Given an org user with dashboard permissions", func(t *testing.T) { - ac3cmd := user.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false} - ac3, err := sqlStore.CreateUser(context.Background(), ac3cmd) - require.NoError(t, err) - - orgUserCmd := models.AddOrgUserCommand{ - OrgId: ac1.OrgID, - UserId: ac3.ID, - Role: org.RoleViewer, - } - - err = sqlStore.addOrgUser(context.Background(), &orgUserCmd) - require.NoError(t, err) - - dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgID, 0, false, "prod", "webapp") - dash2 := insertTestDashboard(t, sqlStore, "2 test dash", ac3.OrgID, 0, false, "prod", "webapp") - - err = updateDashboardACL(t, sqlStore, dash1.Id, &models.DashboardACL{ - DashboardID: dash1.Id, OrgID: ac1.OrgID, UserID: ac3.ID, Permission: models.PERMISSION_EDIT, - }) - require.NoError(t, err) - - err = updateDashboardACL(t, sqlStore, dash2.Id, &models.DashboardACL{ - DashboardID: dash2.Id, OrgID: ac3.OrgID, UserID: ac3.ID, Permission: models.PERMISSION_EDIT, - }) - require.NoError(t, err) - }) - }) - }) - }) -} - -// TODO: Use FakeDashboardStore when org has its own service -func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { - t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, - IsFolder: isFolder, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": title, - "tags": tags, - }), - } - - var dash *models.Dashboard - err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { - dash = cmd.GetDashboardModel() - dash.SetVersion(1) - dash.Created = time.Now() - dash.Updated = time.Now() - dash.Uid = util.GenerateShortUID() - _, err := sess.Insert(dash) - return err - }) - - require.NoError(t, err) - require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) - - err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { - dashVersion := &dashver.DashboardVersion{ - DashboardID: dash.Id, - ParentVersion: dash.Version, - RestoredFrom: cmd.RestoredFrom, - Version: dash.Version, - Created: time.Now(), - CreatedBy: dash.UpdatedBy, - Message: cmd.Message, - Data: dash.Data, - } - require.NoError(t, err) - - if affectedRows, err := sess.Insert(dashVersion); err != nil { - return err - } else if affectedRows == 0 { - return dashboards.ErrDashboardNotFound - } - - return nil - }) - require.NoError(t, err) - - return dash -} - -// TODO: Use FakeDashboardStore when org has its own service -func updateDashboardACL(t *testing.T, sqlStore *SQLStore, dashboardID int64, items ...*models.DashboardACL) error { - t.Helper() - - err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { - _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) - if err != nil { - return fmt.Errorf("deleting from dashboard_acl failed: %w", err) - } - - for _, item := range items { - item.Created = time.Now() - item.Updated = time.Now() - if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing - } - - if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty - } - - sess.Nullable("user_id", "team_id") - if _, err := sess.Insert(item); err != nil { - return err - } - } - - // Update dashboard HasACL flag - dashboard := models.Dashboard{HasACL: true} - _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) - return err - }) - return err -} diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go deleted file mode 100644 index d15870fefb8..00000000000 --- a/pkg/services/sqlstore/org_users.go +++ /dev/null @@ -1,68 +0,0 @@ -package sqlstore - -import ( - "context" - "time" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/user" -) - -func (ss *SQLStore) addOrgUser(ctx context.Context, cmd *models.AddOrgUserCommand) error { - return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { - // check if user exists - var usr user.User - session := sess.ID(cmd.UserId) - if !cmd.AllowAddingServiceAccount { - session = session.Where(notServiceAccountFilter(ss)) - } - - if exists, err := session.Get(&usr); err != nil { - return err - } else if !exists { - return user.ErrUserNotFound - } - - if res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and user_id=?", cmd.OrgId, usr.ID); err != nil { - return err - } else if len(res) == 1 { - return models.ErrOrgUserAlreadyAdded - } - - if res, err := sess.Query("SELECT 1 from org WHERE id=?", cmd.OrgId); err != nil { - return err - } else if len(res) != 1 { - return models.ErrOrgNotFound - } - - entity := models.OrgUser{ - OrgId: cmd.OrgId, - UserId: cmd.UserId, - Role: cmd.Role, - Created: time.Now(), - Updated: time.Now(), - } - - _, err := sess.Insert(&entity) - if err != nil { - return err - } - - var userOrgs []*models.UserOrgDTO - sess.Table("org_user") - sess.Join("INNER", "org", "org_user.org_id=org.id") - sess.Where("org_user.user_id=? AND org_user.org_id=?", usr.ID, usr.OrgID) - sess.Cols("org.name", "org_user.role", "org_user.org_id") - err = sess.Find(&userOrgs) - - if err != nil { - return err - } - - if len(userOrgs) == 0 { - return setUsingOrgInTransaction(sess, usr.ID, cmd.OrgId) - } - - return nil - }) -} diff --git a/pkg/services/sqlstore/org_users_test.go b/pkg/services/sqlstore/org_users_test.go deleted file mode 100644 index 7528e679805..00000000000 --- a/pkg/services/sqlstore/org_users_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package sqlstore - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/user" -) - -func TestSQLStore_AddOrgUser(t *testing.T) { - var orgID int64 = 1 - store := InitTestDB(t) - - // create org and admin - _, err := store.CreateUser(context.Background(), user.CreateUserCommand{ - Login: "admin", - OrgID: orgID, - }) - require.NoError(t, err) - - // create a service account with no org - sa, err := store.CreateUser(context.Background(), user.CreateUserCommand{ - Login: "sa-no-org", - IsServiceAccount: true, - SkipOrgSetup: true, - }) - - require.NoError(t, err) - require.Equal(t, int64(-1), sa.OrgID) - - // assign the sa to the org but without the override. should fail - err = store.addOrgUser(context.Background(), &models.AddOrgUserCommand{ - Role: "Viewer", - OrgId: orgID, - UserId: sa.ID, - }) - require.Error(t, err) - - // assign the sa to the org with the override. should succeed - err = store.addOrgUser(context.Background(), &models.AddOrgUserCommand{ - Role: "Viewer", - OrgId: orgID, - UserId: sa.ID, - AllowAddingServiceAccount: true, - }) - - require.NoError(t, err) - - // assert the org has been correctly set - saFound := new(user.User) - err = store.WithDbSession(context.Background(), func(sess *DBSession) error { - has, err := sess.ID(sa.ID).Get(saFound) - if err != nil { - return err - } else if !has { - return user.ErrUserNotFound - } - return nil - }) - - require.NoError(t, err) - require.Equal(t, saFound.OrgID, orgID) -} diff --git a/pkg/services/sqlstore/sqlbuilder_test.go b/pkg/services/sqlstore/sqlbuilder_test.go index d9a56aab581..8d75c369cf7 100644 --- a/pkg/services/sqlstore/sqlbuilder_test.go +++ b/pkg/services/sqlstore/sqlbuilder_test.go @@ -2,17 +2,22 @@ package sqlstore import ( "context" + "fmt" "math/rand" "strconv" "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util" ) func TestIntegrationSQLBuilder(t *testing.T) { @@ -196,22 +201,28 @@ func createDummyUser(t *testing.T, sqlStore *SQLStore) *user.User { t.Helper() uid := strconv.Itoa(rand.Intn(9999999)) - createUserCmd := user.CreateUserCommand{ - Email: uid + "@example.com", - Login: uid, - Name: uid, - Company: "", - OrgName: "", - Password: uid, - EmailVerified: true, - IsAdmin: false, - SkipOrgSetup: false, - DefaultOrgRole: string(org.RoleViewer), + usr := &user.User{ + Email: uid + "@example.com", + Login: uid, + Name: uid, + Company: "", + Password: uid, + EmailVerified: true, + IsAdmin: false, + Created: time.Now(), + Updated: time.Now(), } - user, err := sqlStore.CreateUser(context.Background(), createUserCmd) - require.NoError(t, err) - return user + var id int64 + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + sess.UseBool("is_admin") + var err error + id, err = sess.Insert(usr) + return err + }) + require.NoError(t, err) + usr.ID = id + return usr } func createDummyDashboard(t *testing.T, sqlStore *SQLStore, dashboardProps DashboardProps) *models.Dashboard { @@ -317,3 +328,95 @@ func getDashboards(t *testing.T, sqlStore *SQLStore, search Search, aclUserID in require.NoError(t, err) return res } + +// TODO: Use FakeDashboardStore when org has its own service +func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + + var dash *models.Dashboard + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + dash = cmd.GetDashboardModel() + dash.SetVersion(1) + dash.Created = time.Now() + dash.Updated = time.Now() + dash.Uid = util.GenerateShortUID() + _, err := sess.Insert(dash) + return err + }) + + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + + err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, + ParentVersion: dash.Version, + RestoredFrom: cmd.RestoredFrom, + Version: dash.Version, + Created: time.Now(), + CreatedBy: dash.UpdatedBy, + Message: cmd.Message, + Data: dash.Data, + } + require.NoError(t, err) + + if affectedRows, err := sess.Insert(dashVersion); err != nil { + return err + } else if affectedRows == 0 { + return dashboards.ErrDashboardNotFound + } + + return nil + }) + require.NoError(t, err) + + return dash +} + +// TODO: Use FakeDashboardStore when org has its own service +func updateDashboardACL(t *testing.T, sqlStore *SQLStore, dashboardID int64, items ...*models.DashboardACL) error { + t.Helper() + + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return models.ErrDashboardACLInfoMissing + } + + if item.DashboardID == 0 { + return models.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasACL flag + dashboard := models.Dashboard{HasACL: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) + return err +} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 4dc3291164f..bbe0a39a8c7 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -198,7 +198,6 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error { if _, err := sess.SQL(rawSQL).Get(&stats); err != nil { return fmt.Errorf("could not determine if admin user exists: %w", err) } - if stats.Count > 0 { return nil } @@ -206,6 +205,7 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error { // ensure admin user if !ss.Cfg.DisableInitAdminCreation { ss.log.Debug("Creating default admin user") + if _, err := ss.createUser(ctx, sess, user.CreateUserCommand{ Login: ss.Cfg.AdminUser, Email: ss.Cfg.AdminEmail, @@ -216,9 +216,6 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error { } ss.log.Info("Created default admin", "user", ss.Cfg.AdminUser) - // Why should we return and not create the default org in this case? - // Returning here breaks tests using anonymous access - // return nil } ss.log.Debug("Creating default org", "name", mainOrgName) diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 77dfe576391..747af3a8aa7 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -8,13 +8,11 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/session" - "github.com/grafana/grafana/pkg/services/user" ) type Store interface { GetDialect() migrator.Dialect GetDBType() core.DbType - CreateUser(ctx context.Context, cmd user.CreateUserCommand) (*user.User, error) WithDbSession(ctx context.Context, callback DBTransactionFunc) error WithNewDbSession(ctx context.Context, callback DBTransactionFunc) error WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b30766fd6c8..b11cdb37558 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -4,8 +4,8 @@ package sqlstore import ( "context" "fmt" - "sort" "strings" + "time" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/models" @@ -14,6 +14,8 @@ import ( "github.com/grafana/grafana/pkg/util" ) +const mainOrgName = "Main Org." + func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args user.CreateUserCommand) (int64, error) { if ss.Cfg.AutoAssignOrg && args.OrgID != 0 { if err := verifyExistingOrg(sess, args.OrgID); err != nil { @@ -30,21 +32,19 @@ func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args user.CreateUserComm return ss.getOrCreateOrg(sess, orgName) } -// createUser creates a user in the database -// if autoAssignOrg is enabled then args.OrgID will be used -// to add to an existing Org with id=args.OrgID -// if autoAssignOrg is disabled then args.OrgName will be used -// to create a new Org with name=args.OrgName. -// If a org already exists with that name, it will error +// createUser creates a user in the database. It will also create a default +// organization, if none exists. This should only be used by the sqlstore +// Reset() function. +// +// If AutoAssignOrg is enabled then args.OrgID will be used to add to an +// existing Org with id=args.OrgID. If AutoAssignOrg is disabled then +// args.OrgName will be used to create a new Org with name=args.OrgName. If an +// org already exists with that name, it will error. func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.CreateUserCommand) (user.User, error) { var usr user.User - var orgID int64 = -1 - if !args.SkipOrgSetup { - var err error - orgID, err = ss.getOrgIDForNewUser(sess, args) - if err != nil { - return usr, err - } + orgID, err := ss.getOrgIDForNewUser(sess, args) + if err != nil { + return usr, err } if args.Email == "" { @@ -68,18 +68,13 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.C // create user usr = user.User{ - Email: args.Email, - Name: args.Name, - Login: args.Login, - Company: args.Company, - IsAdmin: args.IsAdmin, - IsDisabled: args.IsDisabled, - OrgID: orgID, - EmailVerified: args.EmailVerified, - Created: TimeNow(), - Updated: TimeNow(), - LastSeenAt: TimeNow().AddDate(-10, 0, 0), - IsServiceAccount: args.IsServiceAccount, + Email: args.Email, + Login: args.Login, + IsAdmin: args.IsAdmin, + OrgID: orgID, + Created: TimeNow(), + Updated: TimeNow(), + LastSeenAt: TimeNow().AddDate(-10, 0, 0), } salt, err := util.GetRandomString(10) @@ -115,95 +110,29 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.C Email: usr.Email, }) - // create org user link - if !args.SkipOrgSetup { - orgUser := models.OrgUser{ - OrgId: orgID, - UserId: usr.ID, - Role: org.RoleAdmin, - Created: TimeNow(), - Updated: TimeNow(), - } + orgUser := models.OrgUser{ + OrgId: orgID, + UserId: usr.ID, + Role: org.RoleAdmin, + Created: TimeNow(), + Updated: TimeNow(), + } - if ss.Cfg.AutoAssignOrg && !usr.IsAdmin { - if len(args.DefaultOrgRole) > 0 { - orgUser.Role = org.RoleType(args.DefaultOrgRole) - } else { - orgUser.Role = org.RoleType(ss.Cfg.AutoAssignOrgRole) - } + if ss.Cfg.AutoAssignOrg && !usr.IsAdmin { + if len(args.DefaultOrgRole) > 0 { + orgUser.Role = org.RoleType(args.DefaultOrgRole) + } else { + orgUser.Role = org.RoleType(ss.Cfg.AutoAssignOrgRole) } + } - if _, err = sess.Insert(&orgUser); err != nil { - return usr, err - } + if _, err = sess.Insert(&orgUser); err != nil { + return usr, err } return usr, nil } -// deprecated method, use only for tests -func (ss *SQLStore) CreateUser(ctx context.Context, cmd user.CreateUserCommand) (*user.User, error) { - var user user.User - createErr := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) (err error) { - user, err = ss.createUser(ctx, sess, cmd) - return - }) - return &user, createErr -} - -func notServiceAccountFilter(ss *SQLStore) string { - return fmt.Sprintf("%s.is_service_account = %s", - ss.Dialect.Quote("user"), - ss.Dialect.BooleanStr(false)) -} - -func setUsingOrgInTransaction(sess *DBSession, userID int64, orgID int64) error { - user := user.User{ - ID: userID, - OrgID: orgID, - } - - _, err := sess.ID(userID).Update(&user) - return err -} - -type byOrgName []*models.UserOrgDTO - -// Len returns the length of an array of organisations. -func (o byOrgName) Len() int { - return len(o) -} - -// Swap swaps two indices of an array of organizations. -func (o byOrgName) Swap(i, j int) { - o[i], o[j] = o[j], o[i] -} - -// Less returns whether element i of an array of organizations is less than element j. -func (o byOrgName) Less(i, j int) bool { - if strings.ToLower(o[i].Name) < strings.ToLower(o[j].Name) { - return true - } - - return o[i].Name < o[j].Name -} - -func (ss *SQLStore) getUserOrgList(ctx context.Context, query *models.GetUserOrgListQuery) error { - return ss.WithDbSession(ctx, func(dbSess *DBSession) error { - query.Result = make([]*models.UserOrgDTO, 0) - sess := dbSess.Table("org_user") - sess.Join("INNER", "org", "org_user.org_id=org.id") - sess.Join("INNER", ss.Dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", ss.Dialect.Quote("user"))) - sess.Where("org_user.user_id=?", query.UserId) - sess.Where(notServiceAccountFilter(ss)) - sess.Cols("org.name", "org_user.role", "org_user.org_id") - sess.OrderBy("org.name") - err := sess.Find(&query.Result) - sort.Sort(byOrgName(query.Result)) - return err - }) -} - func UserDeletions() []string { deletes := []string{ "DELETE FROM star WHERE user_id = ?", @@ -218,3 +147,61 @@ func UserDeletions() []string { } return deletes } + +func verifyExistingOrg(sess *DBSession, orgId int64) error { + var org models.Org + has, err := sess.Where("id=?", orgId).Get(&org) + if err != nil { + return err + } + if !has { + return models.ErrOrgNotFound + } + return nil +} + +func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { + var org models.Org + if ss.Cfg.AutoAssignOrg { + has, err := sess.Where("id=?", ss.Cfg.AutoAssignOrgId).Get(&org) + if err != nil { + return 0, err + } + if has { + return org.Id, nil + } + + if ss.Cfg.AutoAssignOrgId != 1 { + ss.log.Error("Could not create user: organization ID does not exist", "orgID", + ss.Cfg.AutoAssignOrgId) + return 0, fmt.Errorf("could not create user: organization ID %d does not exist", + ss.Cfg.AutoAssignOrgId) + } + + org.Name = mainOrgName + org.Id = int64(ss.Cfg.AutoAssignOrgId) + } else { + org.Name = orgName + } + + org.Created = time.Now() + org.Updated = time.Now() + + if org.Id != 0 { + if _, err := sess.InsertId(&org); err != nil { + return 0, err + } + } else { + if _, err := sess.InsertOne(&org); err != nil { + return 0, err + } + } + + sess.publishAfterCommit(&events.OrgCreated{ + Timestamp: org.Created, + Id: org.Id, + Name: org.Name, + }) + + return org.Id, nil +} diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index 1017f870b35..938bf16992d 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -5,14 +5,16 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) func TestIntegrationStatsDataAccess(t *testing.T) { @@ -71,6 +73,7 @@ func populateDB(t *testing.T, sqlStore *sqlstore.SQLStore) { t.Helper() orgService, _ := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + userSvc, _ := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, "atest.FakeQuotaService{}) users := make([]user.User, 3) for i := range users { @@ -80,7 +83,7 @@ func populateDB(t *testing.T, sqlStore *sqlstore.SQLStore) { Login: fmt.Sprintf("user_test_%v_login", i), OrgName: fmt.Sprintf("Org #%v", i), } - user, err := sqlStore.CreateUser(context.Background(), cmd) + user, err := userSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) users[i] = *user } diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index 17ec4f6d08b..047beacd9c9 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -13,16 +13,19 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" ) func TestIntegrationTeamCommandsAndQueries(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - t.Run("Testing Team commands & queries", func(t *testing.T) { + t.Run("Testing Team commands and queries", func(t *testing.T) { sqlStore := db.InitTestDB(t) teamSvc := ProvideService(sqlStore, sqlStore.Cfg) testUser := &user.SignedInUser{ @@ -35,6 +38,11 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }, }, } + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, teamSvc, nil, quotaService) + require.NoError(t, err) t.Run("Given saved users and two teams", func(t *testing.T) { var userIds []int64 @@ -51,7 +59,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - usr, err = sqlStore.CreateUser(context.Background(), userCmd) + usr, err = userSvc.Create(context.Background(), &userCmd) require.NoError(t, err) userIds = append(userIds, usr.ID) } @@ -82,7 +90,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { q1 := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id, SignedInUser: testUser} err = teamSvc.GetTeamMembers(context.Background(), q1) require.NoError(t, err) - require.Equal(t, len(q1.Result), 2) + require.Equal(t, 2, len(q1.Result)) require.Equal(t, q1.Result[0].TeamId, team1.Id) require.Equal(t, q1.Result[0].Login, "loginuser0") require.Equal(t, q1.Result[0].OrgId, testOrgID) @@ -373,6 +381,11 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { t.Run("Should be able to exclude service accounts from teamembers", func(t *testing.T) { sqlStore = db.InitTestDB(t) + quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgSvc, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(sqlStore, orgSvc, sqlStore.Cfg, teamSvc, nil, quotaService) + require.NoError(t, err) setup() userCmd = user.CreateUserCommand{ Email: fmt.Sprint("sa", 1, "@test.com"), @@ -380,7 +393,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { Login: fmt.Sprint("login-sa", 1), IsServiceAccount: true, } - serviceAccount, err := sqlStore.CreateUser(context.Background(), userCmd) + serviceAccount, err := userSvc.CreateUserForTests(context.Background(), &userCmd) require.NoError(t, err) groupId := team2.Id @@ -497,6 +510,11 @@ func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) { require.NoError(t, errCreateTeam) team2, errCreateTeam := teamSvc.CreateTeam("group2 name", "test2@example.org", testOrgID) require.NoError(t, errCreateTeam) + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgSvc, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService(store, orgSvc, store.Cfg, teamSvc, nil, quotaService) + require.NoError(t, err) for i := 0; i < 4; i++ { userCmd := user.CreateUserCommand{ @@ -504,7 +522,7 @@ func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) { Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - user, errCreateUser := store.CreateUser(context.Background(), userCmd) + user, errCreateUser := userSvc.Create(context.Background(), &userCmd) require.NoError(t, errCreateUser) userIds[i] = user.ID } diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index b66962c48f9..2852ef4afd4 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -6,6 +6,7 @@ import ( type Service interface { Create(context.Context, *CreateUserCommand) (*User, error) + CreateServiceAccount(context.Context, *CreateUserCommand) (*User, error) Delete(context.Context, *DeleteUserCommand) error GetByID(context.Context, *GetUserByIDQuery) (*User, error) GetByLogin(context.Context, *GetUserByLoginQuery) (*User, error) @@ -23,4 +24,7 @@ type Service interface { UpdatePermissions(context.Context, int64, bool) error SetUserHelpFlag(context.Context, *SetUserHelpFlagCommand) error GetProfile(context.Context, *GetUserProfileQuery) (*UserProfileDTO, error) + + // TEST ONLY METHOD + CreateUserForTests(context.Context, *CreateUserCommand) (*User, error) } diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index 53deaf17c41..2d1710bbe92 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -24,6 +23,7 @@ type store interface { GetByID(context.Context, int64) (*user.User, error) GetNotServiceAccount(context.Context, int64) (*user.User, error) Delete(context.Context, int64) error + LoginConflict(ctx context.Context, login, email string, caseInsensitive bool) error CaseInsensitiveLoginConflict(context.Context, string, string) error GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) @@ -59,12 +59,11 @@ func ProvideStore(db db.DB, cfg *setting.Cfg) sqlStore { } func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { - var userID int64 var err error err = ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { sess.UseBool("is_admin") - if userID, err = sess.Insert(cmd); err != nil { + if _, err = sess.Insert(cmd); err != nil { return err } sess.PublishAfterCommit(&events.UserCreated{ @@ -79,7 +78,7 @@ func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { if err != nil { return 0, err } - return userID, nil + return cmd.ID, nil } func (ss *sqlStore) Get(ctx context.Context, usr *user.User) (*user.User, error) { @@ -185,7 +184,6 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu where = "LOWER(email)=LOWER(?)" } has, err = sess.Where(ss.notServiceAccountFilter()).Where(where, query.LoginOrEmail).Get(usr) - if err != nil { return err } @@ -264,6 +262,43 @@ func (ss *sqlStore) userCaseInsensitiveLoginConflict(ctx context.Context, sess * return nil } +// LoginConflict returns an error if the provided email or login are already +// associated with a user. If caseInsensitive is true the search is not case +// sensitive. +func (ss *sqlStore) LoginConflict(ctx context.Context, login, email string, caseInsensitive bool) error { + err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + return ss.loginConflict(ctx, sess, login, email, caseInsensitive) + }) + return err +} + +func (ss *sqlStore) loginConflict(ctx context.Context, sess *db.Session, login, email string, caseInsensitive bool) error { + users := make([]user.User, 0) + where := "email=? OR login=?" + if caseInsensitive { + where = "LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)" + login = strings.ToLower(login) + email = strings.ToLower(email) + } + + exists, err := sess.Where(where, email, login).Get(&user.User{}) + if err != nil { + return err + } + if exists { + return user.ErrUserAlreadyExists + } + if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)", + email, login).Find(&users); err != nil { + return err + } + + if len(users) > 1 { + return &user.ErrCaseInsensitiveLoginConflict{Users: users} + } + return nil +} + func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { if ss.cfg.CaseInsensitiveLogin { cmd.Login = strings.ToLower(cmd.Login) @@ -470,7 +505,7 @@ func (ss *sqlStore) Count(ctx context.Context) (int64, error) { } r := result{} - err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s WHERE is_service_account=%s", ss.db.GetDialect().Quote("user"), ss.db.GetDialect().BooleanStr(false)) if _, err := sess.SQL(rawSQL).Get(&r); err != nil { return err diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 48e2d2de23b..2a58935c035 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -31,6 +30,8 @@ func TestIntegrationUserDataAccess(t *testing.T) { orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService) require.NoError(t, err) userStore := ProvideStore(ss, setting.NewCfg()) + usrSvc, err := ProvideService(ss, orgService, ss.Cfg, nil, nil, quotaService) + require.NoError(t, err) usr := &user.SignedInUser{ OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"global.users:*"}}}, @@ -73,12 +74,14 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - creates and loads user", func(t *testing.T) { ss := db.InitTestDB(t) + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + cmd := user.CreateUserCommand{ Email: "usertest@test.com", Name: "user name", Login: "user_test_login", } - usr, err := ss.CreateUser(context.Background(), cmd) + usr, err := usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) result, err := userStore.GetByID(context.Background(), usr.ID) @@ -147,7 +150,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { Login: "user_email_conflict", } // userEmailConflict - _, err := ss.CreateUser(context.Background(), cmd) + _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) cmd = user.CreateUserCommand{ @@ -155,7 +158,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { Name: "user name", Login: "user_email_conflict_two", } - _, err = ss.CreateUser(context.Background(), cmd) + _, err := usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) cmd = user.CreateUserCommand{ @@ -164,7 +167,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { Login: "user_test_login_conflict", } // userLoginConflict - _, err = ss.CreateUser(context.Background(), cmd) + _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) cmd = user.CreateUserCommand{ @@ -172,7 +175,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { Name: "user name", Login: "user_test_login_CONFLICT", } - _, err = ss.CreateUser(context.Background(), cmd) + _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) ss.Cfg.CaseInsensitiveLogin = true @@ -262,7 +265,9 @@ func TestIntegrationUserDataAccess(t *testing.T) { }) t.Run("get signed in user", func(t *testing.T) { - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + ss := db.InitTestDB(t) + orgService, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -300,26 +305,24 @@ func TestIntegrationUserDataAccess(t *testing.T) { }) t.Run("Testing DB - grafana admin users", func(t *testing.T) { - ss = db.InitTestDB(t) - + ss := db.InitTestDB(t) + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) createUserCmd := user.CreateUserCommand{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", Login: "admin", IsAdmin: true, } - usr, err := ss.CreateUser(context.Background(), createUserCmd) + usr, err := usrSvc.Create(context.Background(), &createUserCmd) require.Nil(t, err) // Cannot make themselves a non-admin updatePermsError := userStore.UpdatePermissions(context.Background(), usr.ID, false) - require.Equal(t, user.ErrLastGrafanaAdmin, updatePermsError) query := user.GetUserByIDQuery{ID: usr.ID} queryResult, getUserError := userStore.GetByID(context.Background(), query.ID) require.Nil(t, getUserError) - require.True(t, queryResult.IsAdmin) // One user @@ -330,7 +333,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { Name: "user", Login: username, } - _, err = ss.CreateUser(context.Background(), createUserCmd) + _, err = usrSvc.Create(context.Background(), &createUserCmd) require.Nil(t, err) // When trying to create a new user with the same email, an error is returned @@ -340,8 +343,8 @@ func TestIntegrationUserDataAccess(t *testing.T) { Login: "user2", SkipOrgSetup: true, } - _, err = ss.CreateUser(context.Background(), createUserCmd) - require.Equal(t, err, user.ErrUserAlreadyExists) + _, err = usrSvc.Create(context.Background(), &createUserCmd) + require.Equal(t, user.ErrUserAlreadyExists, err) // When trying to create a new user with the same login, an error is returned createUserCmd = user.CreateUserCommand{ @@ -350,8 +353,8 @@ func TestIntegrationUserDataAccess(t *testing.T) { Login: username, SkipOrgSetup: true, } - _, err = ss.CreateUser(context.Background(), createUserCmd) - require.Equal(t, err, user.ErrUserAlreadyExists) + _, err = usrSvc.Create(context.Background(), &createUserCmd) + require.Equal(t, user.ErrUserAlreadyExists, err) }) t.Run("GetProfile", func(t *testing.T) { @@ -366,7 +369,10 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - return list users based on their is_disabled flag", func(t *testing.T) { ss = db.InitTestDB(t) - createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) + userStore := ProvideStore(ss, ss.Cfg) + + createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -379,7 +385,6 @@ func TestIntegrationUserDataAccess(t *testing.T) { query := user.SearchUsersQuery{IsDisabled: &isDisabled, SignedInUser: usr} result, err := userStore.Search(context.Background(), &query) require.Nil(t, err) - require.Len(t, result.Users, 2) first, third := false, false @@ -397,8 +402,10 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.True(t, third) // Re-init DB - ss = db.InitTestDB(t) - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + ss := db.InitTestDB(t) + orgService, usrSvc = createOrgAndUserSvc(t, ss, ss.Cfg) + + users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -432,7 +439,8 @@ func TestIntegrationUserDataAccess(t *testing.T) { // A user is an org member and has been assigned permissions // Re-init DB ss = db.InitTestDB(t) - users = createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + orgService, usrSvc = createOrgAndUserSvc(t, ss, ss.Cfg) + users = createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -487,7 +495,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - return list of users that the SignedInUser has permission to read", func(t *testing.T) { ss := db.InitTestDB(t) - createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := ProvideService(ss, orgService, ss.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -508,7 +521,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { ss = db.InitTestDB(t) t.Run("Testing DB - enable all users", func(t *testing.T) { - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -541,12 +554,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} serviceaccountcmd := user.CreateUserCommand{Login: "serviceaccount", Email: "service@test.com", Name: "serviceaccount name", IsAdmin: true, IsServiceAccount: true} - _, err := ss.CreateUser(context.Background(), ac1cmd) + _, err := usrSvc.Create(context.Background(), &ac1cmd) require.NoError(t, err) - _, err = ss.CreateUser(context.Background(), ac2cmd) + _, err = usrSvc.Create(context.Background(), &ac2cmd) require.NoError(t, err) // user only used for making sure we filter out the service accounts - _, err = ss.CreateUser(context.Background(), serviceaccountcmd) + _, err = usrSvc.Create(context.Background(), &serviceaccountcmd) require.NoError(t, err) query := user.SearchUsersQuery{Query: "", SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -564,7 +577,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { ss = db.InitTestDB(t) t.Run("Testing DB - disable only specific users", func(t *testing.T) { - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -594,7 +607,6 @@ func TestIntegrationUserDataAccess(t *testing.T) { // Check if user id is in the userIdsToDisable list for _, disabledUserId := range userIdsToDisable { - fmt.Println(user.ID, disabledUserId) if user.ID == disabledUserId { require.True(t, user.IsDisabled) shouldBeDisabled = true @@ -612,7 +624,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - search users", func(t *testing.T) { // Since previous tests were destructive - createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -636,7 +648,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { t.Run("Testing DB - multiple users", func(t *testing.T) { ss = db.InitTestDB(t) - createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -729,8 +741,9 @@ func TestIntegrationUserUpdate(t *testing.T) { ss := db.InitTestDB(t) userStore := ProvideStore(ss, setting.NewCfg()) + _, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg) - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + users := createFiveTestUsers(t, usrSvc, func(i int) *user.CreateUserCommand { return &user.CreateUserCommand{ Email: fmt.Sprint("USER", i, "@test.com"), Name: fmt.Sprint("USER", i), @@ -789,17 +802,15 @@ func TestIntegrationUserUpdate(t *testing.T) { ss.Cfg.CaseInsensitiveLogin = false } -func createFiveTestUsers(t *testing.T, sqlStore *sqlstore.SQLStore, fn func(i int) *user.CreateUserCommand) []user.User { +func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.CreateUserCommand) []user.User { t.Helper() - users := []user.User{} + users := make([]user.User, 5) for i := 0; i < 5; i++ { cmd := fn(i) - - user, err := sqlStore.CreateUser(context.Background(), *cmd) - users = append(users, *user) - + user, err := svc.CreateUserForTests(context.Background(), cmd) require.Nil(t, err) + users[i] = *user } return users @@ -924,3 +935,15 @@ func (ss *sqlStore) getDashboardACLInfoList(query *models.GetDashboardACLInfoLis return nil } + +func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Service, user.Service) { + t.Helper() + + quotaService := quotaimpl.ProvideService(store, cfg) + orgService, err := orgimpl.ProvideService(store, cfg, quotaService) + require.NoError(t, err) + usrSvc, err := ProvideService(store, orgService, cfg, nil, nil, quotaService) + require.NoError(t, err) + + return orgService, usrSvc +} diff --git a/pkg/services/user/userimpl/time.go b/pkg/services/user/userimpl/time.go new file mode 100644 index 00000000000..87ded6cd9c5 --- /dev/null +++ b/pkg/services/user/userimpl/time.go @@ -0,0 +1,16 @@ +package userimpl + +import "time" + +// timeNow wraps time.Now so it can be mocked in tests. +var timeNow = time.Now + +func MockTimeNow(constTime time.Time) { + timeNow = func() time.Time { + return constTime + } +} + +func ResetTimeNow() { + timeNow = time.Now +} diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index 132f556a1c2..4f275baa598 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -82,32 +82,27 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use SkipOrgSetup: cmd.SkipOrgSetup, } orgID, err := s.orgService.GetIDForNewUser(ctx, cmdOrg) - cmd.OrgID = orgID if err != nil { return nil, err } - if cmd.Email == "" { cmd.Email = cmd.Login } - usr := &user.User{ - Login: cmd.Login, - Email: cmd.Email, - } - usr, err = s.store.Get(ctx, usr) - if err != nil && !errors.Is(err, user.ErrUserNotFound) { - return usr, err + + err = s.store.LoginConflict(ctx, cmd.Login, cmd.Email, s.cfg.CaseInsensitiveLogin) + if err != nil { + return nil, user.ErrUserAlreadyExists } // create user - usr = &user.User{ + usr := &user.User{ Email: cmd.Email, Name: cmd.Name, Login: cmd.Login, Company: cmd.Company, IsAdmin: cmd.IsAdmin, IsDisabled: cmd.IsDisabled, - OrgID: cmd.OrgID, + OrgID: orgID, EmailVerified: cmd.EmailVerified, Created: time.Now(), Updated: time.Now(), @@ -134,7 +129,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use usr.Password = encodedPassword } - userID, err := s.store.Insert(ctx, usr) + _, err = s.store.Insert(ctx, usr) if err != nil { return nil, err } @@ -158,11 +153,10 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use } _, err = s.orgService.InsertOrgUser(ctx, &orgUser) if err != nil { - err := s.store.Delete(ctx, userID) + err := s.store.Delete(ctx, usr.ID) return usr, err } } - return usr, nil } @@ -354,3 +348,198 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { limits.Set(globalQuotaTag, cfg.Quota.Global.User) return limits, nil } + +// CreateUserForTests creates a test user and optionally an organization. Unlike +// Create, `cmd.SkipOrgSetup` toggles whether or not to create an org for the +// test user if there isn't already an existing org. This must only be used in tests. +func (s *Service) CreateUserForTests(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + var orgID int64 = -1 + var err error + if !cmd.SkipOrgSetup { + orgID, err = s.getOrgIDForNewUser(ctx, cmd) + if err != nil { + return nil, err + } + } + + if cmd.Email == "" { + cmd.Email = cmd.Login + } + + usr, err := s.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: cmd.Login}) + if err != nil && !errors.Is(err, user.ErrUserNotFound) { + return usr, err + } else if err == nil { // user exists + return usr, err + } + + // create user + usr = &user.User{ + Email: cmd.Email, + Name: cmd.Name, + Login: cmd.Login, + Company: cmd.Company, + IsAdmin: cmd.IsAdmin, + IsDisabled: cmd.IsDisabled, + OrgID: orgID, + EmailVerified: cmd.EmailVerified, + Created: timeNow(), + Updated: timeNow(), + LastSeenAt: timeNow().AddDate(-10, 0, 0), + IsServiceAccount: cmd.IsServiceAccount, + } + + salt, err := util.GetRandomString(10) + if err != nil { + return usr, err + } + usr.Salt = salt + rands, err := util.GetRandomString(10) + if err != nil { + return usr, err + } + usr.Rands = rands + + if len(cmd.Password) > 0 { + encodedPassword, err := util.EncodePassword(cmd.Password, usr.Salt) + if err != nil { + return usr, err + } + usr.Password = encodedPassword + } + + _, err = s.store.Insert(ctx, usr) + if err != nil { + return usr, err + } + + // create org user link + if !cmd.SkipOrgSetup { + orgCmd := &org.AddOrgUserCommand{ + OrgID: orgID, + UserID: usr.ID, + Role: org.RoleAdmin, + AllowAddingServiceAccount: true, + } + + if s.cfg.AutoAssignOrg && !usr.IsAdmin { + if len(cmd.DefaultOrgRole) > 0 { + orgCmd.Role = org.RoleType(cmd.DefaultOrgRole) + } else { + orgCmd.Role = org.RoleType(s.cfg.AutoAssignOrgRole) + } + } + + if err = s.orgService.AddOrgUser(ctx, orgCmd); err != nil { + return nil, err + } + } + + return usr, nil +} + +func (s *Service) getOrgIDForNewUser(ctx context.Context, cmd *user.CreateUserCommand) (int64, error) { + if s.cfg.AutoAssignOrg && cmd.OrgID != 0 { + if _, err := s.orgService.GetByID(ctx, &org.GetOrgByIdQuery{ID: cmd.OrgID}); err != nil { + return -1, err + } + return cmd.OrgID, nil + } + + orgName := cmd.OrgName + if orgName == "" { + orgName = util.StringsFallback2(cmd.Email, cmd.Login) + } + + orgID, err := s.orgService.GetOrCreate(ctx, orgName) + if err != nil { + return 0, err + } + return orgID, err +} + +// CreateServiceAccount is a copy of Create with a single difference; it will create the OrgUser service account. +func (s *Service) CreateServiceAccount(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + cmdOrg := org.GetOrgIDForNewUserCommand{ + Email: cmd.Email, + Login: cmd.Login, + OrgID: cmd.OrgID, + OrgName: cmd.OrgName, + SkipOrgSetup: cmd.SkipOrgSetup, + } + orgID, err := s.orgService.GetIDForNewUser(ctx, cmdOrg) + if err != nil { + return nil, err + } + if cmd.Email == "" { + cmd.Email = cmd.Login + } + + err = s.store.LoginConflict(ctx, cmd.Login, cmd.Email, s.cfg.CaseInsensitiveLogin) + if err != nil { + return nil, user.ErrUserAlreadyExists + } + + // create user + usr := &user.User{ + Email: cmd.Email, + Name: cmd.Name, + Login: cmd.Login, + Company: cmd.Company, + IsAdmin: cmd.IsAdmin, + IsDisabled: cmd.IsDisabled, + OrgID: cmd.OrgID, + EmailVerified: cmd.EmailVerified, + Created: time.Now(), + Updated: time.Now(), + LastSeenAt: time.Now().AddDate(-10, 0, 0), + IsServiceAccount: cmd.IsServiceAccount, + } + + salt, err := util.GetRandomString(10) + if err != nil { + return nil, err + } + usr.Salt = salt + rands, err := util.GetRandomString(10) + if err != nil { + return nil, err + } + usr.Rands = rands + + if len(cmd.Password) > 0 { + encodedPassword, err := util.EncodePassword(cmd.Password, usr.Salt) + if err != nil { + return nil, err + } + usr.Password = encodedPassword + } + + _, err = s.store.Insert(ctx, usr) + if err != nil { + return nil, err + } + + // create org user link + if !cmd.SkipOrgSetup { + orgCmd := &org.AddOrgUserCommand{ + OrgID: orgID, + UserID: usr.ID, + Role: org.RoleAdmin, + AllowAddingServiceAccount: true, + } + + if s.cfg.AutoAssignOrg && !usr.IsAdmin { + if len(cmd.DefaultOrgRole) > 0 { + orgCmd.Role = org.RoleType(cmd.DefaultOrgRole) + } else { + orgCmd.Role = org.RoleType(s.cfg.AutoAssignOrgRole) + } + } + + if err = s.orgService.AddOrgUser(ctx, orgCmd); err != nil { + return nil, err + } + } + return usr, nil +} diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index d491bd4f848..53f63e72940 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -26,6 +26,7 @@ func TestUserService(t *testing.T) { cacheService: localcache.ProvideService(), teamService: &teamtest.FakeService{}, } + userService.cfg = setting.NewCfg() t.Run("create user", func(t *testing.T) { _, err := userService.Create(context.Background(), &user.CreateUserCommand{ @@ -44,7 +45,6 @@ func TestUserService(t *testing.T) { require.NoError(t, err) require.Equal(t, "login", u.Login) require.Equal(t, "name", u.Name) - require.Equal(t, "email", u.Email) }) @@ -229,6 +229,10 @@ func (f *FakeUserStore) CaseInsensitiveLoginConflict(context.Context, string, st return f.ExpectedError } +func (f *FakeUserStore) LoginConflict(context.Context, string, string, bool) error { + return f.ExpectedError +} + func (f *FakeUserStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index 0041dd9e8d3..aa26ee05314 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -28,6 +28,14 @@ func (f *FakeUserService) Create(ctx context.Context, cmd *user.CreateUserComman return f.ExpectedUser, f.ExpectedError } +func (f *FakeUserService) CreateUserForTests(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + +func (f *FakeUserService) CreateServiceAccount(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + func (f *FakeUserService) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error { return f.ExpectedError } diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index ca79eb54301..b6815a3278f 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -20,8 +20,11 @@ import ( ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testinfra" ) @@ -2534,7 +2537,13 @@ func createUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserComma store.Cfg.AutoAssignOrg = true store.Cfg.AutoAssignOrgId = 1 - u, err := store.CreateUser(context.Background(), cmd) + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + u, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) return u.ID } diff --git a/pkg/tests/api/correlations/common_test.go b/pkg/tests/api/correlations/common_test.go index 1f8ee1c4736..aabdb761929 100644 --- a/pkg/tests/api/correlations/common_test.go +++ b/pkg/tests/api/correlations/common_test.go @@ -7,12 +7,16 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) type errorResponseBody struct { @@ -130,11 +134,17 @@ func (c TestContext) getURL(url string, user User) string { func (c TestContext) createUser(cmd user.CreateUserCommand) { c.t.Helper() + store := c.env.SQLStore + store.Cfg.AutoAssignOrg = true + store.Cfg.AutoAssignOrgId = 1 - c.env.SQLStore.Cfg.AutoAssignOrg = true - c.env.SQLStore.Cfg.AutoAssignOrgId = 1 + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(c.t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService) + require.NoError(c.t, err) - _, err := c.env.SQLStore.CreateUser(context.Background(), cmd) + _, err = usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(c.t, err) } diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 7e8841dedb0..5eefc5c15f5 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -20,9 +20,12 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/plugindashboards" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" ) @@ -100,7 +103,13 @@ func createUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserComma store.Cfg.AutoAssignOrg = true store.Cfg.AutoAssignOrgId = 1 - u, err := store.CreateUser(context.Background(), cmd) + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + u, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) return u.ID } diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 0d62275c4b7..0ebffe901b3 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -11,8 +11,11 @@ import ( "path/filepath" "testing" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/stretchr/testify/assert" @@ -115,7 +118,13 @@ func createUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserComma store.Cfg.AutoAssignOrg = true store.Cfg.AutoAssignOrgId = 1 - _, err := store.CreateUser(context.Background(), cmd) + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + _, err = usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) } diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index d5aeef74c52..eb565baa74b 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -22,8 +22,11 @@ import ( "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -370,7 +373,13 @@ func CreateUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserComma store.Cfg.AutoAssignOrg = true store.Cfg.AutoAssignOrgId = 1 - u, err := store.CreateUser(context.Background(), cmd) + quotaService := quotaimpl.ProvideService(store, store.Cfg) + orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService) + require.NoError(t, err) + + u, err := usrSvc.CreateUserForTests(context.Background(), &cmd) require.NoError(t, err) return u.ID } From a4c884e808fc90362c6ba9fcb60e66643fb4c762 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Wed, 7 Dec 2022 10:37:08 -0600 Subject: [PATCH 52/94] Docs: corrects relref (#59970) correct relref --- docs/sources/administration/data-source-management/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/administration/data-source-management/index.md b/docs/sources/administration/data-source-management/index.md index 42d56c8fda3..9aaaa39a771 100644 --- a/docs/sources/administration/data-source-management/index.md +++ b/docs/sources/administration/data-source-management/index.md @@ -195,7 +195,7 @@ To view available data source plugins, go to the [plugin catalog](/grafana/plugi For details about the plugin catalog, refer to [Plugin management]({{< relref "../../administration/plugin-management/" >}}). You can further filter the plugin catalog's results for data sources provided by the Grafana community, Grafana Labs, and partners. -If you use [Grafana Enterprise]{{< relref "../../enterprise/" >}}, you can also filter by Enterprise-supported plugins. +If you use [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}), you can also filter by Enterprise-supported plugins. For more documentation on a specific data source plugin's features, including its query language and editor, refer to its plugin catalog page. From 5c08f03512f1ef96527ba7653c6df02cb7f85434 Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Wed, 7 Dec 2022 17:40:15 +0100 Subject: [PATCH 53/94] CloudWatch: Fix deeplinks to still be able to pass log group names (#59809) Co-authored-by: Erik Sundell --- .../datasource/cloudwatch/utils/datalinks.test.ts | 3 +++ .../datasource/cloudwatch/utils/datalinks.ts | 15 ++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts index f83c4829c5b..9f146fd90a7 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts @@ -35,6 +35,7 @@ describe('addDataLinksToLogsResponse', () => { refId: 'A', expression: 'stats count(@message) by bin(1h)', logGroupNames: ['fake-log-group-one', 'fake-log-group-two'], + logGroups: [{}], // empty log groups should be ignored and fall back to logGroupNames region: 'us-east-1', }, ], @@ -115,6 +116,7 @@ describe('addDataLinksToLogsResponse', () => { { refId: 'A', expression: 'stats count(@message) by bin(1h)', + logGroupNames: [''], logGroups: [ { value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test:*' }, { value: 'arn:aws:logs:us-east-2:222222222222:log-group:/ecs/prometheus:*' }, @@ -174,6 +176,7 @@ describe('addDataLinksToLogsResponse', () => { { refId: 'A', expression: 'stats count(@message) by bin(1h)', + logGroupNames: [''], logGroups: [{ value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test' }], region: 'us-east-1', } as CloudWatchQuery, diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts index 3cfcb7cd7d4..8d29e0338a6 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts @@ -70,16 +70,13 @@ function createAwsConsoleLink( replace: (target: string, fieldName?: string) => string, getVariableValue: (value: string) => string[] ) { - const arns = target.logGroups?.flatMap((group) => { - if (group.value === undefined) { - return []; - } - return [group.value.replace(/:\*$/, '')]; // remove `:*` from end of arn - }); - const logGroupNames = target.logGroupNames; - const sources = arns ?? logGroupNames; + const arns = (target.logGroups ?? []) + .filter((group) => group?.value) + .map((group) => (group.value ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn + const logGroupNames = target.logGroupNames ?? []; + const sources = arns?.length ? arns : logGroupNames; const interpolatedExpression = target.expression ? replace(target.expression) : ''; - const interpolatedGroups = sources?.flatMap(getVariableValue) ?? []; + const interpolatedGroups = sources?.flatMap(getVariableValue); const urlProps: AwsUrl = { end: range.to.toISOString(), From ceb3d8d2952939bc2ab682f2909c64b8ec73dcf6 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 7 Dec 2022 18:33:40 +0100 Subject: [PATCH 54/94] feature (user service): Add a validation check after the user record is created in the sqlstore. (#59968) Add a validation check after the user record is created in the sqlstore. --- pkg/services/user/userimpl/store.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index 2d1710bbe92..09d6b5b48d7 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -78,6 +78,12 @@ func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { if err != nil { return 0, err } + + // verify that user was created and cmd.ID was updated with the actual new userID + _, err = ss.getAnyUserType(ctx, cmd.ID) + if err != nil { + return 0, err + } return cmd.ID, nil } @@ -683,3 +689,19 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* }) return &result, err } + +// getAnyUserType searches for a user record by ID. The user account may be a service account. +func (ss *sqlStore) getAnyUserType(ctx context.Context, userID int64) (*user.User, error) { + usr := user.User{ID: userID} + err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + has, err := sess.Get(&usr) + if err != nil { + return err + } + if !has { + return user.ErrUserNotFound + } + return nil + }) + return &usr, err +} From 69ffce4c424a67daecd1fe0411fcaaaa62b824aa Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 7 Dec 2022 12:52:16 -0600 Subject: [PATCH 55/94] CI: add a quick test, set enterprise2 when the enterprise2 flag is added (#59967) * add a quick test, set enterprise2 when the enterprise2 flag is added Co-authored-by: Dave Henderson --- pkg/build/cmd/uploadpackages.go | 20 ++++++++++++++------ pkg/build/packaging/grafana_test.go | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 pkg/build/packaging/grafana_test.go diff --git a/pkg/build/cmd/uploadpackages.go b/pkg/build/cmd/uploadpackages.go index 581af791ebc..a677ec807f5 100644 --- a/pkg/build/cmd/uploadpackages.go +++ b/pkg/build/cmd/uploadpackages.go @@ -69,9 +69,17 @@ func UploadPackages(c *cli.Context) error { return cli.NewExitError(err.Error(), 1) } - edition, ok := os.LookupEnv("EDITION") - if !ok { - return fmt.Errorf("EDITION envvar is missing, exitting") + var edition config.Edition + if e, ok := os.LookupEnv("EDITION"); ok { + edition = config.Edition(e) + } + + if c.Bool("enterprise2") { + edition = config.EditionEnterprise2 + } + + if edition == "" { + return fmt.Errorf("both EDITION envvar and '--enterprise2' flag are missing. At least one of those is required") } // TODO: Verify config values @@ -80,15 +88,15 @@ func UploadPackages(c *cli.Context) error { Version: version, Bucket: releaseModeConfig.Buckets.Artifacts, }, - edition: config.Edition(edition), + edition: edition, versionMode: releaseMode.Mode, gcpKey: gcpKey, distDir: distDir, } - if cfg.edition == config.EditionEnterprise2 || c.Bool("enterprise2") { + if cfg.edition == config.EditionEnterprise2 { if releaseModeConfig.Buckets.ArtifactsEnterprise2 != "" { - cfg.Config.Bucket = releaseModeConfig.Buckets.ArtifactsEnterprise2 + cfg.Bucket = releaseModeConfig.Buckets.ArtifactsEnterprise2 } else { return fmt.Errorf("enterprise2 bucket var doesn't exist") } diff --git a/pkg/build/packaging/grafana_test.go b/pkg/build/packaging/grafana_test.go new file mode 100644 index 00000000000..8f143893085 --- /dev/null +++ b/pkg/build/packaging/grafana_test.go @@ -0,0 +1,22 @@ +package packaging_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/build/config" + "github.com/grafana/grafana/pkg/build/packaging" + "github.com/stretchr/testify/assert" +) + +func TestPackageRegexp(t *testing.T) { + t.Run("It should match enterprise2 packages", func(t *testing.T) { + rgx := packaging.PackageRegexp(config.EditionEnterprise2) + matches := []string{ + "grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz", + "grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz.sha256", + } + for _, v := range matches { + assert.Truef(t, rgx.MatchString(v), "'%s' should match regex '%s'", v, rgx.String()) + } + }) +} From 74167b4d44119862c1246caa3367e4f488efd15e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 7 Dec 2022 21:19:35 +0100 Subject: [PATCH 56/94] UsageInsights: Record events for Explore queries (#59931) * usageinsights: record events for Explore queries * usageinsights: make the source field optional It is not logical to have it for an event like the dashboard-view * usageinsights: add comment to Explore test Explain why we are reversing a previous decision --- .../configure-security/export-logs.md | 1 + .../grafana-runtime/src/analytics/types.ts | 3 + .../query/state/queryAnalytics.test.ts | 65 ++++++++++++++++-- .../features/query/state/queryAnalytics.ts | 68 +++++++++++-------- 4 files changed, 106 insertions(+), 31 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/export-logs.md b/docs/sources/setup-grafana/configure-security/export-logs.md index 89baf385275..6d44b982dbb 100644 --- a/docs/sources/setup-grafana/configure-security/export-logs.md +++ b/docs/sources/setup-grafana/configure-security/export-logs.md @@ -44,6 +44,7 @@ Logs of usage insights contain the following fields, where the fields followed b | `panelName` | string | Name of the panel of the query. | | `error` | string | Error returned by the query. | | `duration` | number | Duration of the query. | +| `source` | string | Source of the query. For example, `dashboard` or `explore`. | | `orgId`\* | number | ID of the user’s organization. | | `orgName`\* | string | Name of the user’s organization. | | `timestamp`\* | string | The date and time that the request was made, in Coordinated Universal Time (UTC) in [RFC3339](https://tools.ietf.org/html/rfc3339#section-5.6) format. | diff --git a/packages/grafana-runtime/src/analytics/types.ts b/packages/grafana-runtime/src/analytics/types.ts index 2a7ef01504e..5f6812ead7e 100644 --- a/packages/grafana-runtime/src/analytics/types.ts +++ b/packages/grafana-runtime/src/analytics/types.ts @@ -1,3 +1,5 @@ +import { CoreApp } from '@grafana/data'; + import { EchoEvent, EchoEventType } from '../services/EchoSrv'; /** @@ -20,6 +22,7 @@ export interface DashboardInfo { * @public */ export interface DataRequestInfo extends Partial { + source?: CoreApp | string; datasourceName: string; datasourceId: number; datasourceUid: string; diff --git a/public/app/features/query/state/queryAnalytics.test.ts b/public/app/features/query/state/queryAnalytics.test.ts index 6c847338bc0..d23a537ac6d 100644 --- a/public/app/features/query/state/queryAnalytics.test.ts +++ b/public/app/features/query/state/queryAnalytics.test.ts @@ -1,4 +1,13 @@ -import { CoreApp, DataFrame, DataQueryRequest, DataSourceApi, dateTime, LoadingState, PanelData } from '@grafana/data'; +import { + CoreApp, + DataFrame, + DataQueryError, + DataQueryRequest, + DataSourceApi, + dateTime, + LoadingState, + PanelData, +} from '@grafana/data'; import { MetaAnalyticsEventName, reportMetaAnalytics } from '@grafana/runtime'; import { DashboardModel } from '../../dashboard/state'; @@ -12,6 +21,7 @@ beforeEach(() => { const datasource = { name: 'test', id: 1, + uid: 'test', } as DataSourceApi; const dashboardModel = new DashboardModel( @@ -94,6 +104,28 @@ function getTestData(requestApp: string, series: DataFrame[] = []): PanelData { }; } +function getTestDataForExplore(requestApp: string, series: DataFrame[] = []): PanelData { + const now = dateTime(); + const error: DataQueryError = { message: 'test error' }; + + return { + request: { + app: requestApp, + dashboardId: 0, + startTime: now.unix(), + endTime: now.add(1, 's').unix(), + } as DataQueryRequest, + series, + state: LoadingState.Done, + timeRange: { + from: dateTime(), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + error: error, + }; +} + describe('emitDataRequestEvent - from a dashboard panel', () => { it('Should report meta analytics', () => { const data = getTestData(CoreApp.Dashboard); @@ -185,10 +217,35 @@ describe('emitDataRequestEvent - from a dashboard panel', () => { }); }); +// Previously we filtered out Explore events due to too many errors being generated while a user is building a query +// This tests that we send an event for Explore queries but do not record errors describe('emitDataRequestEvent - from Explore', () => { - const data = getTestData(CoreApp.Explore); - it('Should not report meta analytics', () => { + it('Should report meta analytics', () => { + const data = getTestDataForExplore(CoreApp.Explore); emitDataRequestEvent(datasource)(data); - expect(reportMetaAnalytics).not.toBeCalled(); + + expect(reportMetaAnalytics).toBeCalledTimes(1); + expect(reportMetaAnalytics).toBeCalledWith( + expect.objectContaining({ + eventName: MetaAnalyticsEventName.DataRequest, + source: 'explore', + datasourceName: 'test', + datasourceId: 1, + datasourceUid: 'test', + dataSize: 0, + duration: 1, + totalQueries: 0, + }) + ); + }); + + describe('emitDataRequestEvent - from Explore', () => { + it('Should not report errors', () => { + const data = getTestDataForExplore(CoreApp.Explore); + emitDataRequestEvent(datasource)(data); + + expect(reportMetaAnalytics).toBeCalledTimes(1); + expect(reportMetaAnalytics).toBeCalledWith(expect.not.objectContaining({ error: 'test error' })); + }); }); }); diff --git a/public/app/features/query/state/queryAnalytics.ts b/public/app/features/query/state/queryAnalytics.ts index ae70999ff61..8d977555b78 100644 --- a/public/app/features/query/state/queryAnalytics.ts +++ b/public/app/features/query/state/queryAnalytics.ts @@ -8,7 +8,7 @@ export function emitDataRequestEvent(datasource: DataSourceApi) { let done = false; return (data: PanelData) => { - if (!data.request || done || data.request.app === CoreApp.Explore) { + if (!data.request || done) { return; } @@ -21,6 +21,41 @@ export function emitDataRequestEvent(datasource: DataSourceApi) { return; } + const eventData: DataRequestEventPayload = { + eventName: MetaAnalyticsEventName.DataRequest, + source: data.request.app, + datasourceName: datasource.name, + datasourceId: datasource.id, + datasourceUid: datasource.uid, + datasourceType: datasource.type, + dataSize: 0, + duration: data.request.endTime! - data.request.startTime, + }; + + if (data.request.app === CoreApp.Explore) { + enrichWithExploreInfo(eventData, data); + } else { + enrichWithDashboardInfo(eventData, data); + } + + if (data.series && data.series.length > 0) { + // estimate size + eventData.dataSize = data.series.length; + } + + reportMetaAnalytics(eventData); + + // this done check is to make sure we do not double emit events in case + // there are multiple responses with done state + done = true; + }; + + function enrichWithExploreInfo(eventData: DataRequestEventPayload, data: PanelData) { + const totalQueries = Object.keys(data.series).length; + eventData.totalQueries = totalQueries; + } + + function enrichWithDashboardInfo(eventData: DataRequestEventPayload, data: PanelData) { const queryCacheStatus: { [key: string]: boolean } = {}; for (let i = 0; i < data.series.length; i++) { const refId = data.series[i].refId; @@ -31,21 +66,11 @@ export function emitDataRequestEvent(datasource: DataSourceApi) { const totalQueries = Object.keys(queryCacheStatus).length; const cachedQueries = Object.values(queryCacheStatus).filter((val) => val === true).length; - const eventData: DataRequestEventPayload = { - eventName: MetaAnalyticsEventName.DataRequest, - datasourceName: datasource.name, - datasourceId: datasource.id, - datasourceUid: datasource.uid, - datasourceType: datasource.type, - panelId: data.request.panelId, - dashboardId: data.request.dashboardId, - dataSize: 0, - duration: data.request.endTime! - data.request.startTime, - totalQueries, - cachedQueries, - }; + eventData.panelId = data.request!.panelId; + eventData.dashboardId = data.request!.dashboardId; + eventData.totalQueries = totalQueries; + eventData.cachedQueries = cachedQueries; - // enrich with dashboard info const dashboard = getDashboardSrv().getCurrent(); if (dashboard) { eventData.dashboardId = dashboard.id; @@ -58,19 +83,8 @@ export function emitDataRequestEvent(datasource: DataSourceApi) { } } - if (data.series && data.series.length > 0) { - // estimate size - eventData.dataSize = data.series.length; - } - if (data.error) { eventData.error = data.error.message; } - - reportMetaAnalytics(eventData); - - // this done check is to make sure we do not double emit events in case - // there are multiple responses with done state - done = true; - }; + } } From 955bf55c6a6a710a2fd7ebdbdac2d9a826858eaf Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 7 Dec 2022 23:29:38 +0000 Subject: [PATCH 57/94] Datasource Onboarding: Create initial data source onboarding page (#58795) --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 5 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../containers/DatasourceOnboarding.tsx | 209 ++++++++++++++++++ .../dashboard/containers/NewDashboardPage.tsx | 30 +++ public/app/routes/routes.tsx | 2 +- 6 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard/containers/DatasourceOnboarding.tsx create mode 100644 public/app/features/dashboard/containers/NewDashboardPage.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8664082f5a0..eedcf76298e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -82,6 +82,7 @@ export interface FeatureToggles { nestedFolders?: boolean; accessTokenExpirationCheck?: boolean; elasticsearchBackendMigration?: boolean; + datasourceOnboarding?: boolean; secureSocksDatasourceProxy?: boolean; authnService?: boolean; sessionRemoteCache?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f0d32770ee5..d430d06fb3e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -373,6 +373,11 @@ var ( Description: "Use Elasticsearch as backend data source", State: FeatureStateAlpha, }, + { + Name: "datasourceOnboarding", + Description: "Enable data source onboarding page", + State: FeatureStateAlpha, + }, { Name: "secureSocksDatasourceProxy", Description: "Enable secure socks tunneling for supported core datasources", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 708787cd608..d27beaaca21 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -271,6 +271,10 @@ const ( // Use Elasticsearch as backend data source FlagElasticsearchBackendMigration = "elasticsearchBackendMigration" + // FlagDatasourceOnboarding + // Enable data source onboarding page + FlagDatasourceOnboarding = "datasourceOnboarding" + // FlagSecureSocksDatasourceProxy // Enable secure socks tunneling for supported core datasources FlagSecureSocksDatasourceProxy = "secureSocksDatasourceProxy" diff --git a/public/app/features/dashboard/containers/DatasourceOnboarding.tsx b/public/app/features/dashboard/containers/DatasourceOnboarding.tsx new file mode 100644 index 00000000000..36abb698e6c --- /dev/null +++ b/public/app/features/dashboard/containers/DatasourceOnboarding.tsx @@ -0,0 +1,209 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { useAsync } from 'react-use'; + +import { DataSourcePluginMeta, GrafanaTheme2, PageLayoutType } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { Icon, useStyles2 } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; +import { contextSrv } from 'app/core/core'; +import { t } from 'app/core/internationalization'; +import { useAddDatasource } from 'app/features/datasources/state'; + +const topDatasources = [ + 'prometheus', + 'mysql', + 'elasticsearch', + 'influxdb', + 'graphite', + 'stackdriver', + 'cloudwatch', + 'grafana-azure-monitor-datasource', +]; + +export function DatasourceOnboarding({ + onNewDashboard, + loading = false, +}: { + onNewDashboard?: () => void; + loading?: boolean; +}) { + const styles = useStyles2(getStyles); + const { value: datasources, loading: loadingDatasources } = useAsync(async () => { + const datasourceMeta: DataSourcePluginMeta[] = await getBackendSrv().get('/api/plugins', { + enabled: 1, + type: 'datasource', + }); + + const byId = datasourceMeta.reduce>((prev, cur) => { + prev[cur.id] = cur; + return prev; + }, {}); + + return topDatasources.map((d) => byId[d]); + }, []); + + const onAddDatasource = useAddDatasource(); + + if (loading) { + return null; + } + + return ( + +
+

{t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')}

+
+

+ {t('datasource-onboarding.explanation', "To visualize your data, you'll need to connect it first.")} +

+
+ {contextSrv.hasRole('Admin') ? ( + <> +

+ {t('datasource-onboarding.preferred', 'Connect your preferred data source:')} +

+ {!loadingDatasources && datasources !== undefined && ( + + )} + + ) : ( +

+ {t('datasource-onboarding.contact-admin', 'Please contact your administrator to configure data sources.')} +

+ )} + +
+
+ ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + wrapper: css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + alignItems: 'center', + justifyContent: 'center', + }), + title: css({ + textAlign: 'center', + fontSize: theme.typography.pxToRem(32), + fontWeight: theme.typography.fontWeightBold, + }), + description: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: theme.spacing(1), + maxWidth: '50vw', + marginBottom: theme.spacing(8), + color: theme.colors.text.secondary, + }), + explanation: css({ + marginBottom: '0px', + textAlign: 'center', + fontSize: theme.typography.pxToRem(19), + }), + preferredDataSource: css({ + marginBottom: theme.spacing(3), + fontSize: theme.typography.pxToRem(19), + fontWeight: theme.typography.fontWeightRegular, + }), + datasources: css({ + display: 'grid', + gridTemplateColumns: `repeat(auto-fit, minmax(${theme.spacing(28)}, 1fr))`, + gap: theme.spacing(2), + listStyle: 'none', + width: '100%', + maxWidth: theme.spacing(88), + + '> li': { + display: 'flex', + alignItems: 'center', + '> button': { + display: 'flex', + alignItems: 'center', + width: '100%', + height: theme.spacing(7), + gap: theme.spacing(2), + margin: '0px', + padding: `calc(${theme.spacing(2)} - 1px)`, + lineHeight: theme.spacing(3), + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.borderRadius(1), + background: theme.colors.background.primary, + fontSize: theme.typography.pxToRem(19), + color: 'inherit', + }, + }, + }), + logo: css({ + width: theme.spacing(2), + height: theme.spacing(2), + objectFit: 'contain', + }), + datasourceName: css({ + marginRight: 'auto', + }), + arrowIcon: css({ + color: theme.colors.text.link, + }), + viewAll: css({ + display: 'flex', + flexGrow: 1, + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(2), + lineHeight: theme.spacing(3), + fontSize: theme.typography.pxToRem(19), + color: theme.colors.text.link, + textDecoration: 'underline', + textUnderlinePosition: 'under', + }), + createNew: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + margin: '0px', + marginTop: theme.spacing(8), + padding: theme.spacing(0), + border: 'none', + background: 'inherit', + fontSize: theme.typography.h6.fontSize, + color: theme.colors.text.secondary, + }), + }; +} diff --git a/public/app/features/dashboard/containers/NewDashboardPage.tsx b/public/app/features/dashboard/containers/NewDashboardPage.tsx new file mode 100644 index 00000000000..2e1575d8e3b --- /dev/null +++ b/public/app/features/dashboard/containers/NewDashboardPage.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import { useEffectOnce } from 'react-use'; + +import { config } from '@grafana/runtime'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; +import { loadDataSources } from 'app/features/datasources/state'; +import { useDispatch, useSelector } from 'app/types'; + +import DashboardPage from './DashboardPage'; +import { DatasourceOnboarding } from './DatasourceOnboarding'; + +export default function NewDashboardPage(props: GrafanaRouteComponentProps) { + const dispatch = useDispatch(); + useEffectOnce(() => { + dispatch(loadDataSources()); + }); + + const { hasDatasource, loading } = useSelector((state) => ({ + hasDatasource: state.dataSources.dataSourcesCount > 0, + loading: !state.dataSources.hasFetched, + })); + const [createDashboard, setCreateDashboard] = useState(false); + const showDashboardPage = hasDatasource || createDashboard || !config.featureToggles.datasourceOnboarding; + + return showDashboardPage ? ( + + ) : ( + setCreateDashboard(true)} loading={loading} /> + ); +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 0e5e8a4b2a8..a75042077fe 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -88,7 +88,7 @@ export function getAppRoutes(): RouteDescriptor[] { pageClass: 'page-dashboard', routeName: DashboardRoutes.New, component: SafeDynamicImport( - () => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage') + () => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/NewDashboardPage') ), }, { From 601df109afaca68cb0a5d83972dbb649e7f085c6 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:05:11 +0100 Subject: [PATCH 58/94] Loki: Remove annotation, context and volume tracking (#59962) * Loki: Remove annotation and context tracking * Implement log volume --- .../app/plugins/datasource/loki/datasource.ts | 10 +++++++--- public/app/plugins/datasource/loki/tracking.ts | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index bb83acbb583..6510fae6b2c 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -77,6 +77,9 @@ export type RangeQueryOptions = DataQueryRequest | AnnotationQueryReq export const DEFAULT_MAX_LINES = 1000; export const LOKI_ENDPOINT = '/loki/api/v1'; export const REF_ID_DATA_SAMPLES = 'loki-data-samples'; +export const REF_ID_STARTER_ANNOTATION = 'annotation-'; +export const REF_ID_STARTER_LOG_ROW_CONTEXT = 'log-row-context-query-'; +export const REF_ID_STARTER_LOG_VOLUME = 'log-volume-'; const NS_IN_MS = 1000000; function makeRequest( @@ -148,6 +151,7 @@ export class LokiDatasource const query = removeCommentsFromQuery(target.expr); return { ...target, + refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`, instant: false, volumeQuery: true, expr: `sum by (level) (count_over_time(${query}[$__interval]))`, @@ -557,7 +561,7 @@ export class LokiDatasource const app = CoreApp.Explore; return lastValueFrom( - this.query(makeRequest(query, range, app, `log-row-context-query-${direction}`)).pipe( + this.query(makeRequest(query, range, app, `${REF_ID_STARTER_LOG_ROW_CONTEXT}${direction}`)).pipe( catchError((err) => { const error: DataQueryError = { message: 'Error during context query. Please check JS console logs.', @@ -598,7 +602,7 @@ export class LokiDatasource const query: LokiQuery = { expr: `{${expr}}`, queryType: LokiQueryType.Range, - refId: row.dataFrame.refId ?? '', + refId: `${REF_ID_STARTER_LOG_ROW_CONTEXT}${row.dataFrame.refId || ''}`, maxLines: limit, direction: queryDirection, }; @@ -676,7 +680,7 @@ export class LokiDatasource return []; } - const id = `annotation-${options.annotation.name}`; + const id = `${REF_ID_STARTER_ANNOTATION}${options.annotation.name}`; const query: LokiQuery = { refId: id, diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index ec21afc83f6..82165597465 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -4,7 +4,12 @@ import { variableRegex } from 'app/features/variables/utils'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; -import { REF_ID_DATA_SAMPLES } from './datasource'; +import { + REF_ID_STARTER_ANNOTATION, + REF_ID_DATA_SAMPLES, + REF_ID_STARTER_LOG_ROW_CONTEXT, + REF_ID_STARTER_LOG_VOLUME, +} from './datasource'; import pluginJson from './plugin.json'; import { getNormalizedLokiQuery, isLogsQuery, parseToNodeNamesArray } from './queryUtils'; import { LokiQuery, LokiQueryType } from './types'; @@ -118,9 +123,18 @@ const isQueryWithChangedLegend = (query: LokiQuery): boolean => { return query.legendFormat !== ''; }; +const shouldNotReportBasedOnRefId = (refId: string): boolean => { + const starters = [REF_ID_STARTER_ANNOTATION, REF_ID_STARTER_LOG_ROW_CONTEXT, REF_ID_STARTER_LOG_VOLUME]; + + if (refId === REF_ID_DATA_SAMPLES || starters.some((starter) => refId.startsWith(starter))) { + return true; + } + return false; +}; + export function trackQuery(response: DataQueryResponse, queries: LokiQuery[], app: string): void { for (const query of queries) { - if (query.refId === REF_ID_DATA_SAMPLES) { + if (shouldNotReportBasedOnRefId(query.refId)) { return; } reportInteraction('grafana_loki_query_executed', { From 75cceec62c3e99ba15b0da4958c28e97eea31ea1 Mon Sep 17 00:00:00 2001 From: Andriy Date: Thu, 8 Dec 2022 10:11:31 +0100 Subject: [PATCH 59/94] chore: update go version to 1.19.4 in plugin ci images (#59978) --- .../grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh | 2 +- .../docker/grafana-plugin-ci-alpine/scripts/deploy.sh | 4 ++-- .../grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh | 2 +- .../docker/grafana-plugin-ci-e2e/scripts/deploy.sh | 4 ++-- packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh | 2 +- .../docker/grafana-plugin-ci/scripts/deploy.sh | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh index 75d9038304a..8a99e1e649b 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh @@ -5,4 +5,4 @@ ## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags?page=1&name=alpine ## -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.0-alpine" +DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1-alpine" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh index 29325b5c380..94345950078 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh @@ -18,8 +18,8 @@ apk add --no-cache curl npm yarn build-base openssh git-lfs perl-utils coreutils # apk add --no-cache xvfb glib nss nspr gdk-pixbuf "gtk+3.0" pango atk cairo dbus-libs libxcomposite libxrender libxi libxtst libxrandr libxscrnsaver alsa-lib at-spi2-atk at-spi2-core cups-libs gcompat libc6-compat # Install Go -filename="go1.19.3.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba" +filename="go1.19.4.linux-amd64.tar.gz" +get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" untar_file "/tmp/$filename" # Install golangci-lint diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh index c1f4fad2b7f..29714cd5a84 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh @@ -6,5 +6,5 @@ ## DOCKER_IMAGE_BASE_NAME="grafana/grafana-plugin-ci-e2e" -DOCKER_IMAGE_VERSION="1.6.0" +DOCKER_IMAGE_VERSION="1.6.1" DOCKER_IMAGE_NAME="${DOCKER_IMAGE_BASE_NAME}:${DOCKER_IMAGE_VERSION}" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh index 16ec89b7560..99e711aee87 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh @@ -22,8 +22,8 @@ source "/etc/profile" npm i -g yarn # Install Go -filename="go1.19.3.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba" +filename="go1.19.4.linux-amd64.tar.gz" +get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" untar_file "/tmp/$filename" # Install golangci-lint diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh index f0143a2a393..47d2b068d8d 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh @@ -5,4 +5,4 @@ ## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags ## -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.0" +DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh index 1ad7002b8cb..3ee0bff1e16 100755 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh +++ b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh @@ -2,8 +2,8 @@ source "./deploy-common.sh" # Install Go -filename="go1.19.3.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "74b9640724fd4e6bb0ed2a1bc44ae813a03f1e72a4c76253e2d5c015494430ba" +filename="go1.19.4.linux-amd64.tar.gz" +get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" untar_file "/tmp/$filename" # Install golangci-lint From c5ee4e4ae1336d993bb171a50ed850db108d20c9 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 8 Dec 2022 04:44:02 -0500 Subject: [PATCH 60/94] Alerting: Improve rule validation to check if rule uses backend datasources (#58986) * validate if rule uses backend datasources * add backend datasource to test * fix tests * another forgotten import * remove unused var --- pkg/services/ngalert/eval/eval.go | 29 +++- pkg/services/ngalert/eval/eval_test.go | 145 ++++++++++++++---- pkg/services/ngalert/ngalert.go | 8 +- .../ngalert/schedule/schedule_unit_test.go | 3 +- pkg/services/ngalert/tests/util.go | 3 +- pkg/services/quota/quotaimpl/quota_test.go | 10 +- 6 files changed, 160 insertions(+), 38 deletions(-) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 5dcbbb030f6..726d47f6224 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" @@ -83,16 +84,20 @@ type evaluatorImpl struct { evaluationTimeout time.Duration dataSourceCache datasources.CacheService expressionService *expr.Service + pluginsStore plugins.Store } func NewEvaluatorFactory( cfg setting.UnifiedAlertingSettings, datasourceCache datasources.CacheService, - expressionService *expr.Service) EvaluatorFactory { + expressionService *expr.Service, + pluginsStore plugins.Store, +) EvaluatorFactory { return &evaluatorImpl{ evaluationTimeout: cfg.EvaluationTimeout, dataSourceCache: datasourceCache, expressionService: expressionService, + pluginsStore: pluginsStore, } } @@ -591,7 +596,23 @@ func (evalResults Results) AsDataFrame() data.Frame { } func (e *evaluatorImpl) Validate(ctx EvaluationContext, condition models.Condition) error { - _, err := e.Create(ctx, condition) + req, err := getExprRequest(ctx, condition.Data, e.dataSourceCache) + if err != nil { + return err + } + for _, query := range req.Queries { + if query.DataSource == nil || expr.IsDataSource(query.DataSource.Uid) { + continue + } + p, found := e.pluginsStore.Plugin(ctx.Ctx, query.DataSource.Type) + if !found { // technically this should fail earlier during datasource resolution phase. + return fmt.Errorf("datasource refID %s could not be found: %w", query.RefID, plugins.ErrPluginUnavailable) + } + if !p.Backend { + return fmt.Errorf("datasource refID %s is not a backend datasource", query.RefID) + } + } + _, err = e.create(condition, req) return err } @@ -606,6 +627,10 @@ func (e *evaluatorImpl) Create(ctx EvaluationContext, condition models.Condition if err != nil { return nil, err } + return e.create(condition, req) +} + +func (e *evaluatorImpl) create(condition models.Condition, req *expr.Request) (ConditionEvaluator, error) { pipeline, err := e.expressionService.BuildPipeline(req) if err != nil { return nil, err diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 1b77efc04c3..188999b3c69 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -12,11 +12,13 @@ import ( ptr "github.com/xorcare/pointer" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" fakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) func TestEvaluateExecutionResult(t *testing.T) { @@ -351,15 +353,20 @@ func TestEvaluateExecutionResultsNoData(t *testing.T) { } func TestValidate(t *testing.T) { + type services struct { + cache *fakes.FakeCacheService + pluginsStore *plugins.FakePluginStore + } + testCases := []struct { name string - condition func(service *fakes.FakeCacheService) models.Condition + condition func(services services) models.Condition error bool }{ { name: "fail if no expressions", error: true, - condition: func(service *fakes.FakeCacheService) models.Condition { + condition: func(_ services) models.Condition { return models.Condition{ Condition: "A", Data: []models.AlertQuery{}, @@ -369,17 +376,25 @@ func TestValidate(t *testing.T) { { name: "fail if condition RefID does not exist", error: true, - condition: func(service *fakes.FakeCacheService) models.Condition { - ds := models.GenerateAlertQuery() - service.DataSources = append(service.DataSources, &datasources.DataSource{ - Uid: ds.DatasourceUID, + condition: func(services services) models.Condition { + dsQuery := models.GenerateAlertQuery() + ds := &datasources.DataSource{ + Uid: dsQuery.DatasourceUID, + Type: util.GenerateShortUID(), + } + services.cache.DataSources = append(services.cache.DataSources, ds) + services.pluginsStore.PluginList = append(services.pluginsStore.PluginList, plugins.PluginDTO{ + JSONData: plugins.JSONData{ + ID: ds.Type, + Backend: true, + }, }) return models.Condition{ Condition: "C", Data: []models.AlertQuery{ - ds, - models.CreateClassicConditionExpression("B", ds.RefID, "last", "gt", rand.Int()), + dsQuery, + models.CreateClassicConditionExpression("B", dsQuery.RefID, "last", "gt", rand.Int()), }, } }, @@ -387,17 +402,24 @@ func TestValidate(t *testing.T) { { name: "fail if condition RefID is empty", error: true, - condition: func(service *fakes.FakeCacheService) models.Condition { - ds := models.GenerateAlertQuery() - service.DataSources = append(service.DataSources, &datasources.DataSource{ - Uid: ds.DatasourceUID, + condition: func(services services) models.Condition { + dsQuery := models.GenerateAlertQuery() + ds := &datasources.DataSource{ + Uid: dsQuery.DatasourceUID, + Type: util.GenerateShortUID(), + } + services.cache.DataSources = append(services.cache.DataSources, ds) + services.pluginsStore.PluginList = append(services.pluginsStore.PluginList, plugins.PluginDTO{ + JSONData: plugins.JSONData{ + ID: ds.Type, + Backend: true, + }, }) - return models.Condition{ Condition: "", Data: []models.AlertQuery{ - ds, - models.CreateClassicConditionExpression("B", ds.RefID, "last", "gt", rand.Int()), + dsQuery, + models.CreateClassicConditionExpression("B", dsQuery.RefID, "last", "gt", rand.Int()), }, } }, @@ -405,13 +427,68 @@ func TestValidate(t *testing.T) { { name: "fail if datasource with UID does not exists", error: true, - condition: func(service *fakes.FakeCacheService) models.Condition { - ds := models.GenerateAlertQuery() + condition: func(services services) models.Condition { + dsQuery := models.GenerateAlertQuery() // do not update the cache service return models.Condition{ - Condition: ds.RefID, + Condition: dsQuery.RefID, Data: []models.AlertQuery{ - ds, + dsQuery, + }, + } + }, + }, + { + name: "fail if datasource cannot be found in plugin store", + error: true, + condition: func(services services) models.Condition { + dsQuery := models.GenerateAlertQuery() + ds := &datasources.DataSource{ + Uid: dsQuery.DatasourceUID, + Type: util.GenerateShortUID(), + } + services.cache.DataSources = append(services.cache.DataSources, ds) + // do not update the plugin store + return models.Condition{ + Condition: dsQuery.RefID, + Data: []models.AlertQuery{ + dsQuery, + }, + } + }, + }, + { + name: "fail if datasource is not backend one", + error: true, + condition: func(services services) models.Condition { + dsQuery1 := models.GenerateAlertQuery() + dsQuery2 := models.GenerateAlertQuery() + ds1 := &datasources.DataSource{ + Uid: dsQuery1.DatasourceUID, + Type: util.GenerateShortUID(), + } + ds2 := &datasources.DataSource{ + Uid: dsQuery2.DatasourceUID, + Type: util.GenerateShortUID(), + } + services.cache.DataSources = append(services.cache.DataSources, ds1, ds2) + services.pluginsStore.PluginList = append(services.pluginsStore.PluginList, plugins.PluginDTO{ + JSONData: plugins.JSONData{ + ID: ds1.Type, + Backend: false, + }, + }, plugins.PluginDTO{ + JSONData: plugins.JSONData{ + ID: ds2.Type, + Backend: true, + }, + }) + // do not update the plugin store + return models.Condition{ + Condition: dsQuery1.RefID, + Data: []models.AlertQuery{ + dsQuery1, + dsQuery2, }, } }, @@ -419,17 +496,25 @@ func TestValidate(t *testing.T) { { name: "pass if datasource exists and condition is correct", error: false, - condition: func(service *fakes.FakeCacheService) models.Condition { - ds := models.GenerateAlertQuery() - service.DataSources = append(service.DataSources, &datasources.DataSource{ - Uid: ds.DatasourceUID, + condition: func(services services) models.Condition { + dsQuery := models.GenerateAlertQuery() + ds := &datasources.DataSource{ + Uid: dsQuery.DatasourceUID, + Type: util.GenerateShortUID(), + } + services.cache.DataSources = append(services.cache.DataSources, ds) + services.pluginsStore.PluginList = append(services.pluginsStore.PluginList, plugins.PluginDTO{ + JSONData: plugins.JSONData{ + ID: ds.Type, + Backend: true, + }, }) return models.Condition{ Condition: "B", Data: []models.AlertQuery{ - ds, - models.CreateClassicConditionExpression("B", ds.RefID, "last", "gt", rand.Int()), + dsQuery, + models.CreateClassicConditionExpression("B", dsQuery.RefID, "last", "gt", rand.Int()), }, } }, @@ -441,12 +526,16 @@ func TestValidate(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { cacheService := &fakes.FakeCacheService{} - condition := testCase.condition(cacheService) + store := &plugins.FakePluginStore{} + condition := testCase.condition(services{ + cache: cacheService, + pluginsStore: store, + }) - evaluator := NewEvaluatorFactory(setting.UnifiedAlertingSettings{}, cacheService, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil)) + evaluator := NewEvaluatorFactory(setting.UnifiedAlertingSettings{}, cacheService, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil), store) evalCtx := Context(context.Background(), u) - _, err := evaluator.Create(evalCtx, condition) + err := evaluator.Validate(evalCtx, condition) if testCase.error { require.Error(t, err) } else { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index c1ca4c56ec0..12d4cc33f84 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -62,6 +63,7 @@ func ProvideService( bus bus.Bus, accesscontrolService accesscontrol.Service, annotationsRepo annotations.Repository, + pluginsStore plugins.Store, ) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, @@ -85,6 +87,7 @@ func ProvideService( bus: bus, accesscontrolService: accesscontrolService, annotationsRepo: annotationsRepo, + pluginsStore: pluginsStore, } if ng.IsDisabled() { @@ -129,7 +132,8 @@ type AlertNG struct { annotationsRepo annotations.Repository store *store.DBstore - bus bus.Bus + bus bus.Bus + pluginsStore plugins.Store } func (ng *AlertNG) init() error { @@ -182,7 +186,7 @@ func (ng *AlertNG) init() error { ng.AlertsRouter = alertsRouter - evalFactory := eval.NewEvaluatorFactory(ng.Cfg.UnifiedAlerting, ng.DataSourceCache, ng.ExpressionService) + evalFactory := eval.NewEvaluatorFactory(ng.Cfg.UnifiedAlerting, ng.DataSourceCache, ng.ExpressionService, ng.pluginsStore) schedCfg := schedule.SchedulerCfg{ MaxAttempts: ng.Cfg.UnifiedAlerting.MaxAttempts, C: clk, diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 00cd257ae0c..b595e831f3e 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -21,6 +21,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -651,7 +652,7 @@ func setupScheduler(t *testing.T, rs *fakeRulesStore, is *state.FakeInstanceStor var evaluator = evalMock if evalMock == nil { - evaluator = eval.NewEvaluatorFactory(setting.UnifiedAlertingSettings{}, nil, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil)) + evaluator = eval.NewEvaluatorFactory(setting.UnifiedAlertingSettings{}, nil, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil), &plugins.FakePluginStore{}) } if registry == nil { diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 0adc53b6e95..b03c54a751f 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" @@ -99,7 +100,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, ng, err := ngalert.ProvideService( cfg, &FakeFeatures{}, nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil), - secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, annotationstest.NewFakeAnnotationsRepo(), + secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, ) require.NoError(tb, err) return ng, &store.DBstore{ diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index d8ffa2432ae..f1943c51432 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -5,10 +5,15 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/apikey" @@ -38,9 +43,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/require" - "github.com/xorcare/pointer" ) func TestQuotaService(t *testing.T) { @@ -476,7 +478,7 @@ func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService m := metrics.NewNGAlert(prometheus.NewRegistry()) _, err = ngalert.ProvideService( sqlStore.Cfg, &ngalerttests.FakeFeatures{}, nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotaService, - secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(), + secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, ) require.NoError(t, err) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), sqlStore.Cfg, quotaService, storesrv.ProvideSystemUsersService()) From 4b56493789299c93748e1312e6d73caecb64f209 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:28:40 +0100 Subject: [PATCH 61/94] Elasticsearch: Add tracking for plugin adoption stats (#59954) * Elasticsearch: Add tracking * Update public/app/plugins/datasource/elasticsearch/tracking.ts Co-authored-by: Sven Grossmann * Update public/app/plugins/datasource/elasticsearch/tracking.ts Co-authored-by: Sven Grossmann * Update public/app/plugins/datasource/elasticsearch/tracking.ts Co-authored-by: Sven Grossmann * Refactor getLineLimit * Update public/app/plugins/datasource/elasticsearch/tracking.ts Co-authored-by: Sven Grossmann * Update public/app/plugins/datasource/elasticsearch/tracking.ts Co-authored-by: Sven Grossmann * Update not tracking for volume queries Co-authored-by: Sven Grossmann --- .../elasticsearch/datasource.test.ts | 53 +++++- .../datasource/elasticsearch/datasource.ts | 11 +- .../datasource/elasticsearch/module.test.ts | 152 ++++++++++++++++++ .../datasource/elasticsearch/module.ts | 8 +- .../datasource/elasticsearch/tracking.ts | 129 +++++++++++++++ 5 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/module.test.ts create mode 100644 public/app/plugins/datasource/elasticsearch/tracking.ts diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 2c582baca57..09b6dc8d122 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -18,7 +18,7 @@ import { TimeRange, toUtc, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse } from '@grafana/runtime'; +import { BackendSrvRequest, FetchResponse, reportInteraction } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -35,6 +35,7 @@ const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local'; jest.mock('@grafana/runtime', () => ({ ...(jest.requireActual('@grafana/runtime') as unknown as object), getBackendSrv: () => backendSrv, + reportInteraction: jest.fn(), getDataSourceSrv: () => { return { getInstanceSettings: () => { @@ -275,6 +276,23 @@ describe('ElasticDatasource', () => { const { body } = await runScenario(); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); }); + + it('should report query interaction', async () => { + await runScenario(); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_elasticsearch_query_executed', + expect.objectContaining({ + alias: '$varAlias', + app: 'test', + has_data: true, + has_error: false, + line_limit: undefined, + query_type: 'metric', + simultaneously_sent_query_count: 1, + with_lucene_query: true, + }) + ); + }); }); describe('When issuing logs query with interval pattern', () => { @@ -344,6 +362,23 @@ describe('ElasticDatasource', () => { expect(links[0].url).toBe('http://localhost:3000/${__value.raw}'); expect(links[0].title).toBe('Custom Label'); }); + + it('should report query interaction', async () => { + await setupDataSource(); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_elasticsearch_query_executed', + expect.objectContaining({ + alias: '$varAlias', + app: undefined, + has_data: true, + has_error: false, + line_limit: undefined, + query_type: 'logs', + simultaneously_sent_query_count: 1, + with_lucene_query: true, + }) + ); + }); }); describe('When issuing document query', () => { @@ -380,6 +415,22 @@ describe('ElasticDatasource', () => { const { body } = await runScenario(); expect(body.size).toBe(500); }); + it('should report query interaction', async () => { + await runScenario(); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_elasticsearch_query_executed', + expect.objectContaining({ + alias: undefined, + app: 'test', + has_data: false, + has_error: false, + line_limit: undefined, + query_type: 'raw_document', + simultaneously_sent_query_count: 1, + with_lucene_query: true, + }) + ); + }); }); describe('When getting an error on response', () => { diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 33e11d60eff..49e775194c9 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1,6 +1,6 @@ import { cloneDeep, find, first as _first, isNumber, isObject, isString, map as _map } from 'lodash'; import { generate, lastValueFrom, Observable, of, throwError } from 'rxjs'; -import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty } from 'rxjs/operators'; +import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators'; import { DataFrame, @@ -50,9 +50,11 @@ import { } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; import { defaultBucketAgg, hasMetricOfType } from './queryDef'; +import { trackQuery } from './tracking'; import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery, TermsQuery } from './types'; import { coerceESVersion, getScriptValue, isSupportedVersion } from './utils'; +export const REF_ID_STARTER_LOG_VOLUME = 'log-volume-'; // Those are metadata fields as defined in https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_identity_metadata_fields. // custom fields can start with underscores, therefore is not safe to exclude anything that starts with one. const ELASTIC_META_FIELDS = [ @@ -616,7 +618,7 @@ export class ElasticDatasource }); const logsVolumeQuery: ElasticsearchQuery = { - refId: target.refId, + refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`, query: target.query, metrics: [{ type: 'count', id: '1' }], timeField, @@ -636,7 +638,7 @@ export class ElasticDatasource const shouldRunTroughBackend = request.app === CoreApp.Explore && config.featureToggles.elasticsearchBackendMigration; if (shouldRunTroughBackend) { - return super.query(request); + return super.query(request).pipe(tap((response) => trackQuery(response, request.targets, request.app))); } let payload = ''; const targets = this.interpolateVariablesInQueries(cloneDeep(request.targets), request.scopedVars); @@ -718,7 +720,8 @@ export class ElasticDatasource } return er.getTimeSeries(); - }) + }), + tap((response) => trackQuery(response, request.targets, request.app)) ); } diff --git a/public/app/plugins/datasource/elasticsearch/module.test.ts b/public/app/plugins/datasource/elasticsearch/module.test.ts new file mode 100644 index 00000000000..d86a40ff06f --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/module.test.ts @@ -0,0 +1,152 @@ +import { DashboardLoadedEvent } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import './module'; + +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + getAppEvents: () => ({ + subscribe: jest.fn((_, handler) => { + // Trigger test event + handler( + new DashboardLoadedEvent({ + dashboardId: 'dashboard123', + orgId: 1, + userId: 2, + grafanaVersion: 'v9.0.0', + queries: { + elasticsearch: [ + { + alias: '', + bucketAggs: [], + datasource: { + type: 'elasticsearch', + uid: 'PE50363A9B6833EE7', + }, + metrics: [ + { + id: '1', + settings: { + limit: '501', + }, + type: 'logs', + }, + ], + query: 'abc:def', + refId: 'A', + timeField: '@timestamp', + }, + { + alias: '', + bucketAggs: [], + datasource: { + type: 'elasticsearch', + uid: 'es1', + }, + metrics: [ + { + id: '1', + settings: { + size: '600', + }, + type: 'raw_data', + }, + ], + query: '', + }, + { + alias: 'alias', + bucketAggs: [ + { + field: '@timestamp', + id: '2', + settings: { + interval: 'auto', + }, + type: 'date_histogram', + }, + ], + datasource: { + type: 'elasticsearch', + uid: 'es1', + }, + metrics: [ + { + id: '3', + type: 'count', + }, + ], + query: 'abc:def', + }, + { + alias: '', + bucketAggs: [], + datasource: { + type: 'elasticsearch', + uid: 'PE50363A9B6833EE7', + }, + metrics: [ + { + id: '1', + settings: { + size: '600', + }, + type: 'raw_document', + }, + ], + query: '', + refId: 'A', + timeField: '@timestamp', + }, + { + alias: '', + bucketAggs: [ + { + field: '@timestamp', + id: '2', + settings: { + interval: 'auto', + }, + type: 'date_histogram', + }, + ], + datasource: { + type: 'elasticsearch', + uid: 'es1', + }, + metrics: [ + { + field: 'counter', + id: '1', + type: 'avg', + }, + ], + query: '$test:abc', + }, + ], + }, + }) + ); + }), + }), + }; +}); + +describe('queriesOnInitDashboard', () => { + it('should report a grafana_elasticsearch_dashboard_loaded interaction ', () => { + expect(reportInteraction).toHaveBeenCalledWith('grafana_elasticsearch_dashboard_loaded', { + grafana_version: 'v9.0.0', + dashboard_id: 'dashboard123', + org_id: 1, + queries_count: 5, + queries_with_changed_line_limit_count: 1, + queries_with_lucene_query_count: 3, + queries_with_template_variables_count: 1, + raw_data_queries_count: 1, + raw_document_queries_count: 1, + logs_queries_count: 1, + metric_queries_count: 2, + }); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts index f1b5a850cb4..d6c0b285eb1 100644 --- a/public/app/plugins/datasource/elasticsearch/module.ts +++ b/public/app/plugins/datasource/elasticsearch/module.ts @@ -1,7 +1,13 @@ -import { DataSourcePlugin } from '@grafana/data'; +import { DashboardLoadedEvent, DataSourcePlugin } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; import { QueryEditor } from './components/QueryEditor'; import { ConfigEditor } from './configuration/ConfigEditor'; import { ElasticDatasource } from './datasource'; +import { onDashboardLoadedHandler } from './tracking'; +import { ElasticsearchQuery } from './types'; export const plugin = new DataSourcePlugin(ElasticDatasource).setQueryEditor(QueryEditor).setConfigEditor(ConfigEditor); + +// Subscribe to on dashboard loaded event so that we can track plugin adoption +getAppEvents().subscribe>(DashboardLoadedEvent, onDashboardLoadedHandler); diff --git a/public/app/plugins/datasource/elasticsearch/tracking.ts b/public/app/plugins/datasource/elasticsearch/tracking.ts new file mode 100644 index 00000000000..20da97e0bb8 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/tracking.ts @@ -0,0 +1,129 @@ +import { DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { variableRegex } from 'app/features/variables/utils'; + +import { REF_ID_STARTER_LOG_VOLUME } from './datasource'; +import pluginJson from './plugin.json'; +import { ElasticsearchQuery } from './types'; + +type ElasticSearchOnDashboardLoadedTrackingEvent = { + grafana_version?: string; + dashboard_id?: string; + org_id?: number; + + /* The number of Elasticsearch queries present in the dashboard*/ + queries_count: number; + + /* The number of Elasticsearch logs queries present in the dashboard*/ + logs_queries_count: number; + + /* The number of Elasticsearch metric queries present in the dashboard*/ + metric_queries_count: number; + + /* The number of Elasticsearch raw data queries present in the dashboard*/ + raw_data_queries_count: number; + + /* The number of Elasticsearch raw documents queries present in the dashboard*/ + raw_document_queries_count: number; + + /* The number of Elasticsearch queries with used template variables present in the dashboard*/ + queries_with_template_variables_count: number; + + /* The number of Elasticsearch queries with changed line limit present in the dashboard*/ + queries_with_changed_line_limit_count: number; + + /* The number of Elasticsearch queries with lucene query present in the dashboard*/ + queries_with_lucene_query_count: number; +}; + +export const onDashboardLoadedHandler = ({ + payload: { dashboardId, orgId, grafanaVersion, queries }, +}: DashboardLoadedEvent) => { + try { + // We only want to track visible ElasticSearch queries + const elasticsearchQueries = queries[pluginJson.id].filter((query) => !query.hide); + if (!elasticsearchQueries?.length) { + return; + } + + const queriesWithTemplateVariables = elasticsearchQueries.filter(isQueryWithTemplateVariables); + const queriesWithLuceneQuery = elasticsearchQueries.filter((query) => !!query.query); + const logsQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'logs'); + const metricQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'metric'); + const rawDataQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'raw_data'); + const rawDocumentQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'raw_document'); + const queriesWithChangedLineLimit = elasticsearchQueries.filter(isQueryWithChangedLineLimit); + + const event: ElasticSearchOnDashboardLoadedTrackingEvent = { + grafana_version: grafanaVersion, + dashboard_id: dashboardId, + org_id: orgId, + queries_count: elasticsearchQueries.length, + logs_queries_count: logsQueries.length, + metric_queries_count: metricQueries.length, + raw_data_queries_count: rawDataQueries.length, + raw_document_queries_count: rawDocumentQueries.length, + queries_with_template_variables_count: queriesWithTemplateVariables.length, + queries_with_changed_line_limit_count: queriesWithChangedLineLimit.length, + queries_with_lucene_query_count: queriesWithLuceneQuery.length, + }; + + reportInteraction('grafana_elasticsearch_dashboard_loaded', event); + } catch (error) { + console.error('error in elasticsearch tracking handler', error); + } +}; + +const getQueryType = (query: ElasticsearchQuery): string | undefined => { + if (!query.metrics || !query.metrics.length) { + return undefined; + } + const nonMetricQueryTypes = ['logs', 'raw_data', 'raw_document']; + if (nonMetricQueryTypes.includes(query.metrics[0].type)) { + return query.metrics[0].type; + } + return 'metric'; +}; + +const getLineLimit = (query: ElasticsearchQuery): number | undefined => { + if (query.metrics?.[0]?.type !== 'logs') { + return undefined; + } + + const lineLimit = query.metrics?.[0].settings?.limit; + return lineLimit ? parseInt(lineLimit, 10) : undefined; +}; + +const isQueryWithChangedLineLimit = (query: ElasticsearchQuery): boolean => { + const lineLimit = getLineLimit(query); + return lineLimit !== undefined && lineLimit !== 500; +}; + +const isQueryWithTemplateVariables = (query: ElasticsearchQuery): boolean => { + return variableRegex.test(query.query ?? ''); +}; + +const shouldNotReportBasedOnRefId = (refId: string): boolean => { + if (refId.startsWith(REF_ID_STARTER_LOG_VOLUME)) { + return true; + } + return false; +}; + +export function trackQuery(response: DataQueryResponse, queries: ElasticsearchQuery[], app: string): void { + for (const query of queries) { + if (shouldNotReportBasedOnRefId(query.refId)) { + return; + } + reportInteraction('grafana_elasticsearch_query_executed', { + app, + with_lucene_query: query.query ? true : false, + query_type: getQueryType(query), + line_limit: getLineLimit(query), + alias: query.alias, + has_error: response.error !== undefined, + has_data: response.data.some((frame) => frame.length > 0), + simultaneously_sent_query_count: queries.length, + }); + } +} From e2077e9659f5d186b740470a728766c34fd03a54 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:52:28 +0100 Subject: [PATCH 62/94] Dashboard: Run requests with correct app type (#60010) Dashboard: Run quries with correct core app --- public/app/features/dashboard/state/PanelModel.ts | 2 ++ public/app/features/query/state/PanelQueryRunner.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index da359a94c3f..f8b584b794f 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -16,6 +16,7 @@ import { urlUtil, PanelModel as IPanelModel, DataSourceRef, + CoreApp, } from '@grafana/data'; import { getTemplateSrv, RefreshEvent } from '@grafana/runtime'; import config from 'app/core/config'; @@ -366,6 +367,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { scopedVars: this.scopedVars, cacheTimeout: this.cacheTimeout, transformations: this.transformations, + app: this.isEditing ? CoreApp.PanelEditor : this.isViewing ? CoreApp.PanelViewer : CoreApp.Dashboard, }); } diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 3f3a2c480b8..9509d089e13 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -58,6 +58,7 @@ export interface QueryRunnerOptions< scopedVars?: ScopedVars; cacheTimeout?: string | null; transformations?: DataTransformerConfig[]; + app?: CoreApp; } let counter = 100; @@ -213,6 +214,7 @@ export class PanelQueryRunner { maxDataPoints, scopedVars, minInterval, + app, } = options; if (isSharedDashboardQuery(datasource)) { @@ -221,7 +223,7 @@ export class PanelQueryRunner { } const request: DataQueryRequest = { - app: CoreApp.Dashboard, + app: app ?? CoreApp.Dashboard, requestId: getNextRequestId(), timezone, panelId, From 3c9ccd45121aeef5cac4ed0e5f1718bb33c436f1 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 8 Dec 2022 13:04:05 +0100 Subject: [PATCH 63/94] Loki: Usage for queries not from dashboards (#60017) Loki: Usage for queries not from dashboars --- .../datasource/elasticsearch/tracking.ts | 6 +++- .../datasource/loki/datasource.test.ts | 30 +++++++++++++++---- .../app/plugins/datasource/loki/tracking.ts | 7 ++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/tracking.ts b/public/app/plugins/datasource/elasticsearch/tracking.ts index 20da97e0bb8..733d946eba7 100644 --- a/public/app/plugins/datasource/elasticsearch/tracking.ts +++ b/public/app/plugins/datasource/elasticsearch/tracking.ts @@ -1,4 +1,4 @@ -import { DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; +import { CoreApp, DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { variableRegex } from 'app/features/variables/utils'; @@ -111,6 +111,10 @@ const shouldNotReportBasedOnRefId = (refId: string): boolean => { }; export function trackQuery(response: DataQueryResponse, queries: ElasticsearchQuery[], app: string): void { + if (app === CoreApp.Dashboard || app === CoreApp.PanelViewer) { + return; + } + for (const query of queries) { if (shouldNotReportBasedOnRefId(query.refId)) { return; diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index c79f9327d75..eff45c8ea56 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -6,6 +6,7 @@ import { AbstractLabelOperator, AnnotationQueryRequest, ArrayVector, + CoreApp, DataFrame, dataFrameToJSON, DataQueryResponse, @@ -134,7 +135,8 @@ describe('LokiDatasource', () => { const runTest = async ( queryMaxLines: number | undefined, dsMaxLines: string | undefined, - expectedMaxLines: number + expectedMaxLines: number, + app: CoreApp | undefined ) => { const settings = { jsonData: { @@ -150,6 +152,7 @@ describe('LokiDatasource', () => { const options = getQueryOptions({ targets: [{ expr: '{a="b"}', refId: 'B', maxLines: queryMaxLines }], + app: app ?? CoreApp.Dashboard, }); const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); @@ -162,19 +165,36 @@ describe('LokiDatasource', () => { }; it('should use datasource max lines when no query max lines', async () => { - await runTest(undefined, '40', 40); + await runTest(undefined, '40', 40, undefined); }); it('should use query max lines, if exists', async () => { - await runTest(80, undefined, 80); + await runTest(80, undefined, 80, undefined); }); it('should use query max lines, if both exist, even if it is higher than ds max lines', async () => { - await runTest(80, '40', 80); + await runTest(80, '40', 80, undefined); }); it('should report query interaction', async () => { - await runTest(80, '40', 80); + await runTest(80, '40', 80, CoreApp.Explore); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_loki_query_executed', + expect.objectContaining({ + query_type: 'logs', + line_limit: 80, + parsed_query: parseToNodeNamesArray('{a="b"}').join(','), + }) + ); + }); + + it('should not report query interaction for dashboard query', async () => { + await runTest(80, '40', 80, CoreApp.Dashboard); + expect(reportInteraction).not.toBeCalled(); + }); + + it('should not report query interaction for panel edit query', async () => { + await runTest(80, '40', 80, CoreApp.PanelEditor); expect(reportInteraction).toHaveBeenCalledWith( 'grafana_loki_query_executed', expect.objectContaining({ diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index 82165597465..eb3eaf4c233 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -1,4 +1,4 @@ -import { DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; +import { CoreApp, DashboardLoadedEvent, DataQueryResponse } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { variableRegex } from 'app/features/variables/utils'; @@ -133,6 +133,11 @@ const shouldNotReportBasedOnRefId = (refId: string): boolean => { }; export function trackQuery(response: DataQueryResponse, queries: LokiQuery[], app: string): void { + // We only want to track usage for these specific apps + if (app === CoreApp.Dashboard || app === CoreApp.PanelViewer) { + return; + } + for (const query of queries) { if (shouldNotReportBasedOnRefId(query.refId)) { return; From 9094153b3027d9cb9eef9628c300caa473ba9ead Mon Sep 17 00:00:00 2001 From: Jo Date: Thu, 8 Dec 2022 13:26:18 +0000 Subject: [PATCH 64/94] Fix feature toggle test main (#60024) fix feature toggle test --- .../setup-grafana/configure-grafana/feature-toggles/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index d9531c63b80..e5ed210d965 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -90,6 +90,7 @@ Alpha features might be changed or removed without prior notice. | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | | `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | | `elasticsearchBackendMigration` | Use Elasticsearch as backend data source | +| `datasourceOnboarding` | Enable data source onboarding page | | `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources | | `authnService` | Use new auth service to perform authentication | | `sessionRemoteCache` | Enable using remote cache for user sessions | From 1131bac5dadf2643b9a71027dbe647d7f9fbb772 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Thu, 8 Dec 2022 21:49:17 +0800 Subject: [PATCH 65/94] =?UTF-8?q?[Nested=20Folder]=20Block=20move=20operat?= =?UTF-8?q?ion=20that=20could=20introduce=20more=20than=208=20level=20of?= =?UTF-8?q?=20depth,=E2=80=A6=20(#59832)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * block move operation that could introduce more than 8 level of depth, forbid circular reference * move getHeight to store, mock store in service * fix linter --- pkg/services/folder/folderimpl/folder.go | 29 ++++- pkg/services/folder/folderimpl/folder_test.go | 54 +++++++++- pkg/services/folder/folderimpl/sqlstore.go | 24 +++++ .../folder/folderimpl/sqlstore_test.go | 100 +++++++++++------- pkg/services/folder/folderimpl/store.go | 4 + pkg/services/folder/folderimpl/store_fake.go | 21 ++-- pkg/services/folder/model.go | 1 + 7 files changed, 185 insertions(+), 48 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 92a9d0c0501..9fefe7383e7 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -456,9 +456,36 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol return nil, err } + // if the new parent is the same as the current parent, we don't need to do anything + if foldr.ParentUID == cmd.NewParentUID { + return foldr, nil + } + + // here we get the folder, we need to get the height of current folder + // and the depth of the new parent folder, the sum can't bypass 8 + folderHeight, err := s.store.GetHeight(ctx, foldr.UID, cmd.OrgID, &cmd.NewParentUID) + if err != nil { + return nil, err + } + parents, err := s.GetParents(ctx, &folder.GetParentsQuery{UID: cmd.NewParentUID, OrgID: cmd.OrgID}) + if err != nil { + return nil, err + } + + // current folder height + current folder + parent folder + parent folder depth should be less than or equal 8 + if folderHeight+len(parents)+2 > folder.MaxNestedFolderDepth { + return nil, folder.ErrMaximumDepthReached + } + + // if the current folder is already a parent of newparent, we should return error + for _, parent := range parents { + if parent.UID == foldr.UID { + return nil, folder.ErrCircularReference + } + } + return s.store.Update(ctx, folder.UpdateFolderCommand{ Folder: foldr, - // NewParentUID: &cmd.NewParentUID, }) } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index eb3d3ab0d2f..06e23d061e8 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -347,7 +347,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { }) t.Run("get children folder", func(t *testing.T) { - folderStore.ExpectedFolders = []*folder.Folder{ + folderStore.ExpectedChildFolders = []*folder.Folder{ { UID: "test", }, @@ -517,6 +517,56 @@ func TestNestedFolderService(t *testing.T) { require.NotNil(t, f) }) + t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) { + g := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + t.Cleanup(func() { + guardian.New = g + }) + + store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} + store.ExpectedError = folder.ErrCircularReference + f, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) + require.Error(t, err, folder.ErrCircularReference) + require.Nil(t, f) + store.ExpectedChildFolders = []*folder.Folder{} + }) + + t.Run("move when new parentUID depth + subTree height bypassed maximum depth returns error", func(t *testing.T) { + g := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + t.Cleanup(func() { + guardian.New = g + }) + + store.ExpectedError = nil + store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} + store.ExpectedParentFolders = []*folder.Folder{ + {UID: "newFolder", ParentUID: "newFolder"}, + {UID: "newFolder2", ParentUID: "newFolder2"}, + } + store.ExpectedFolderHeight = 5 + f, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) + require.Error(t, err, folder.ErrMaximumDepthReached) + require.Nil(t, f) + }) + + t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) { + g := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + t.Cleanup(func() { + guardian.New = g + }) + + store.ExpectedError = nil + store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} + store.ExpectedParentFolders = []*folder.Folder{{UID: "myFolder", ParentUID: "12345"}, {UID: "12345", ParentUID: ""}} + f, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) + require.Error(t, err, folder.ErrCircularReference) + require.Nil(t, f) + store.ExpectedChildFolders = []*folder.Folder{} + }) + t.Run("delete with success", func(t *testing.T) { var actualCmd *models.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { @@ -561,7 +611,7 @@ func TestNestedFolderService(t *testing.T) { for i := 0; i < folder.MaxNestedFolderDepth; i++ { parents = append(parents, &folder.Folder{UID: fmt.Sprintf("folder%d", i)}) } - store.ExpectedFolders = parents + store.ExpectedParentFolders = parents store.ExpectedError = nil _, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 7b5ab9518a6..d66c35894a0 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -264,3 +264,27 @@ func (ss *sqlStore) getParentsMySQL(ctx context.Context, cmd folder.GetParentsQu }) return util.Reverse(folders), err } + +func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) { + height := -1 + queue := []string{foldrUID} + for len(queue) > 0 { + length := len(queue) + height++ + for i := 0; i < length; i++ { + ele := queue[0] + queue = queue[1:] + if parentUID != nil && *parentUID == ele { + return 0, folder.ErrCircularReference + } + folders, err := ss.GetChildren(ctx, folder.GetTreeQuery{UID: ele, OrgID: orgID}) + if err != nil { + return 0, err + } + for _, f := range folders { + queue = append(queue, f.UID) + } + } + } + return height, nil +} diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index ee1104b078d..f66c4636eb0 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -19,6 +19,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var folderTitle string = "folder1" +var folderDsc string = "folder desc" + func TestIntegrationCreate(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -32,8 +35,8 @@ func TestIntegrationCreate(t *testing.T) { t.Run("creating a folder without providing a UID should fail", func(t *testing.T) { _, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: "folder1", - Description: "folder desc", + Title: folderTitle, + Description: folderDsc, OrgID: orgID, }) require.Error(t, err) @@ -41,10 +44,10 @@ func TestIntegrationCreate(t *testing.T) { t.Run("creating a folder with unknown parent should fail", func(t *testing.T) { _, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: "folder1", + Title: folderTitle, OrgID: orgID, ParentUID: "unknown", - Description: "folder desc", + Description: folderDsc, UID: util.GenerateShortUID(), }) require.Error(t, err) @@ -53,8 +56,8 @@ func TestIntegrationCreate(t *testing.T) { t.Run("creating a folder without providing a parent should default to the empty parent folder", func(t *testing.T) { uid := util.GenerateShortUID() f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: "folder1", - Description: "folder desc", + Title: folderTitle, + Description: folderDsc, OrgID: orgID, UID: uid, }) @@ -65,8 +68,8 @@ func TestIntegrationCreate(t *testing.T) { require.NoError(t, err) }) - assert.Equal(t, "folder1", f.Title) - assert.Equal(t, "folder desc", f.Description) + assert.Equal(t, folderTitle, f.Title) + assert.Equal(t, folderDsc, f.Description) assert.NotEmpty(t, f.ID) assert.Equal(t, uid, f.UID) assert.Empty(t, f.ParentUID) @@ -76,8 +79,8 @@ func TestIntegrationCreate(t *testing.T) { OrgID: orgID, }) assert.NoError(t, err) - assert.Equal(t, "folder1", ff.Title) - assert.Equal(t, "folder desc", ff.Description) + assert.Equal(t, folderTitle, ff.Title) + assert.Equal(t, folderDsc, ff.Description) assert.Empty(t, ff.ParentUID) assertAncestorUIDs(t, folderStore, f, []string{folder.GeneralFolderUID}) @@ -103,10 +106,10 @@ func TestIntegrationCreate(t *testing.T) { uid := util.GenerateShortUID() f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: "folder1", + Title: folderTitle, OrgID: orgID, ParentUID: parent.UID, - Description: "folder desc", + Description: folderDsc, UID: uid, }) require.NoError(t, err) @@ -115,8 +118,8 @@ func TestIntegrationCreate(t *testing.T) { require.NoError(t, err) }) - assert.Equal(t, "folder1", f.Title) - assert.Equal(t, "folder desc", f.Description) + assert.Equal(t, folderTitle, f.Title) + assert.Equal(t, folderDsc, f.Description) assert.NotEmpty(t, f.ID) assert.Equal(t, uid, f.UID) assert.Equal(t, parentUID, f.ParentUID) @@ -129,8 +132,8 @@ func TestIntegrationCreate(t *testing.T) { OrgID: f.OrgID, }) assert.NoError(t, err) - assert.Equal(t, "folder1", ff.Title) - assert.Equal(t, "folder desc", ff.Description) + assert.Equal(t, folderTitle, ff.Title) + assert.Equal(t, folderDsc, ff.Description) assert.Equal(t, parentUID, ff.ParentUID) }) } @@ -195,11 +198,9 @@ func TestIntegrationUpdate(t *testing.T) { orgID := CreateOrg(t, db) // create folder - origTitle := "folder1" - origDesc := "folder desc" f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: origTitle, - Description: origDesc, + Title: folderTitle, + Description: folderDsc, OrgID: orgID, UID: util.GenerateShortUID(), }) @@ -300,12 +301,10 @@ func TestIntegrationGet(t *testing.T) { orgID := CreateOrg(t, db) // create folder - title1 := "folder1" - desc1 := "folder desc" uid1 := util.GenerateShortUID() f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: title1, - Description: desc1, + Title: folderTitle, + Description: folderDsc, OrgID: orgID, UID: uid1, }) @@ -381,12 +380,10 @@ func TestIntegrationGetParents(t *testing.T) { orgID := CreateOrg(t, db) // create folder - title1 := "folder1" - desc1 := "folder desc" uid1 := util.GenerateShortUID() f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: title1, - Description: desc1, + Title: folderTitle, + Description: folderDsc, OrgID: orgID, UID: uid1, }) @@ -450,12 +447,10 @@ func TestIntegrationGetChildren(t *testing.T) { orgID := CreateOrg(t, db) // create folder - title1 := "folder1" - desc1 := "folder desc" uid1 := util.GenerateShortUID() parent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: title1, - Description: desc1, + Title: folderTitle, + Description: folderDsc, OrgID: orgID, UID: uid1, }) @@ -565,6 +560,40 @@ func TestIntegrationGetChildren(t *testing.T) { }) } +func TestIntegrationGetHeight(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + t.Skip("skipping until folder migration is merged") + + db := sqlstore.InitTestDB(t) + folderStore := ProvideStore(db, db.Cfg, &featuremgmt.FeatureManager{}) + + orgID := CreateOrg(t, db) + + // create folder + uid1 := util.GenerateShortUID() + parent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ + Title: folderTitle, + Description: folderDsc, + OrgID: orgID, + UID: uid1, + }) + require.NoError(t, err) + subTree := CreateSubTree(t, folderStore, orgID, parent.UID, 4, "sub") + + t.Run("should successfully get height", func(t *testing.T) { + height, err := folderStore.GetHeight(context.Background(), parent.UID, orgID, nil) + require.NoError(t, err) + require.Equal(t, 4, height) + }) + + t.Run("should failed when the parent folder exist in the subtree", func(t *testing.T) { + _, err = folderStore.GetHeight(context.Background(), parent.UID, orgID, &subTree[0]) + require.Error(t, err, folder.ErrCircularReference) + }) +} + func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 { t.Helper() @@ -583,18 +612,15 @@ func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 { func CreateSubTree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string { t.Helper() - ancestorUIDs := []string{} + ancestorUIDs := []string{parentUID} for i := 0; i < depth; i++ { title := fmt.Sprintf("%sfolder-%d", prefix, i) cmd := folder.CreateFolderCommand{ Title: title, OrgID: orgID, - ParentUID: parentUID, + ParentUID: ancestorUIDs[len(ancestorUIDs)-1], UID: util.GenerateShortUID(), } - if len(ancestorUIDs) > 0 { - cmd.ParentUID = ancestorUIDs[len(ancestorUIDs)-1] - } f, err := store.Create(context.Background(), cmd) require.NoError(t, err) require.Equal(t, title, f.Title) diff --git a/pkg/services/folder/folderimpl/store.go b/pkg/services/folder/folderimpl/store.go index 9ba0933e4ab..902fddd7cfe 100644 --- a/pkg/services/folder/folderimpl/store.go +++ b/pkg/services/folder/folderimpl/store.go @@ -27,4 +27,8 @@ type store interface { // GetChildren returns the set of immediate children folders (depth=1) of the // given folder. GetChildren(ctx context.Context, cmd folder.GetTreeQuery) ([]*folder.Folder, error) + + // GetHeight returns the height of the folder tree. When parentUID is set, the function would + // verify in the meanwhile that parentUID is not present in the subtree of the folder with the given UID. + GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) } diff --git a/pkg/services/folder/folderimpl/store_fake.go b/pkg/services/folder/folderimpl/store_fake.go index 8fa2a9c1beb..25c39096e20 100644 --- a/pkg/services/folder/folderimpl/store_fake.go +++ b/pkg/services/folder/folderimpl/store_fake.go @@ -7,12 +7,13 @@ import ( ) type FakeStore struct { - ExpectedFolders []*folder.Folder - ExpectedFolder *folder.Folder - ExpectedError error - - CreateCalled bool - DeleteCalled bool + ExpectedChildFolders []*folder.Folder + ExpectedParentFolders []*folder.Folder + ExpectedFolder *folder.Folder + ExpectedError error + ExpectedFolderHeight int + CreateCalled bool + DeleteCalled bool } func NewFakeStore() *FakeStore { @@ -44,9 +45,13 @@ func (f *FakeStore) Get(ctx context.Context, cmd folder.GetFolderQuery) (*folder } func (f *FakeStore) GetParents(ctx context.Context, cmd folder.GetParentsQuery) ([]*folder.Folder, error) { - return f.ExpectedFolders, f.ExpectedError + return f.ExpectedParentFolders, f.ExpectedError } func (f *FakeStore) GetChildren(ctx context.Context, cmd folder.GetTreeQuery) ([]*folder.Folder, error) { - return f.ExpectedFolders, f.ExpectedError + return f.ExpectedChildFolders, f.ExpectedError +} + +func (f *FakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) { + return f.ExpectedFolderHeight, f.ExpectedError } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 12143abef99..8dfc80e3b53 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -14,6 +14,7 @@ var ErrBadRequest = errutil.NewBase(errutil.StatusBadRequest, "folder.bad-reques var ErrDatabaseError = errutil.NewBase(errutil.StatusInternal, "folder.database-error") var ErrInternal = errutil.NewBase(errutil.StatusInternal, "folder.internal") var ErrFolderTooDeep = errutil.NewBase(errutil.StatusInternal, "folder.too-deep") +var ErrCircularReference = errutil.NewBase(errutil.StatusBadRequest, "folder.circular-reference", errutil.WithPublicMessage("Circular reference detected")) const ( GeneralFolderUID = "general" From 694dc60724c44e469b3f0b0a98273eb3066fa29d Mon Sep 17 00:00:00 2001 From: Ludovic Viaud Date: Thu, 8 Dec 2022 14:53:07 +0100 Subject: [PATCH 66/94] Prometheus: Add traceID field on top of the exemplar popover (#59920) Prioritize trace link --- .../app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 6d2e18a582b..8cb06f34c3e 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -70,6 +70,10 @@ export const ExemplarMarker: React.FC = ({ }, [setIsOpen]); const renderMarker = useCallback(() => { + // Put the traceID field in front. + const traceIDField = dataFrame.fields.find((field) => field.name === 'traceID') || dataFrame.fields[0]; + const orderedDataFrameFields = [traceIDField, ...dataFrame.fields.filter((field) => traceIDField !== field)]; + const timeFormatter = (value: number) => { return dateTimeFormat(value, { format: systemDateFormats.fullDate, @@ -94,7 +98,7 @@ export const ExemplarMarker: React.FC = ({
- {dataFrame.fields.map((field, i) => { + {orderedDataFrameFields.map((field, i) => { const value = field.values.get(dataFrameFieldIndex.fieldIndex); const links = field.config.links?.length ? getFieldLinks(field, dataFrameFieldIndex.fieldIndex) From e7b25baf46e8c9aa114cf927bcd4d30427f1cd50 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Thu, 8 Dec 2022 16:24:01 +0200 Subject: [PATCH 67/94] Chore: Fix failing feature toggle test (#60030) From e6743a7e9a33ca5772656c8fa34ae6c1f02adebe Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Thu, 8 Dec 2022 22:34:46 +0800 Subject: [PATCH 68/94] Alerting: Use the QuotaTargetSrv instead of the QuotaTarget in quota check (#60026) Before this change, the alerting provisioning system incorrectly used the QuotaTarget to check if alerting's request quota had been reached. The quota service requires the QuotaTargetSrv, which is what's registered with the service at startup time. This is leading to errors in the provisioning system. --- pkg/services/ngalert/provisioning/alert_rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 429d76526c0..f360deed48f 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -318,7 +318,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int6 // checkLimitsTransactionCtx checks whether the current transaction (as identified by the ctx) breaches configured alert rule limits. func (service *AlertRuleService) checkLimitsTransactionCtx(ctx context.Context, orgID, userID int64) error { - limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", "a.ScopeParameters{ + limitReached, err := service.quotas.CheckQuotaReached(ctx, models.QuotaTargetSrv, "a.ScopeParameters{ OrgID: orgID, UserID: userID, }) From a3681a33535af08cdae082b079f85031c2df0223 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 8 Dec 2022 15:00:07 +0000 Subject: [PATCH 69/94] Command Palette: Links now work when grafana is served under a subpath (#60033) strip base when pushing urls to locationService --- .../features/commandPalette/actions/global.static.actions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/commandPalette/actions/global.static.actions.tsx b/public/app/features/commandPalette/actions/global.static.actions.tsx index d376a93b461..3ab6ca206fe 100644 --- a/public/app/features/commandPalette/actions/global.static.actions.tsx +++ b/public/app/features/commandPalette/actions/global.static.actions.tsx @@ -1,7 +1,7 @@ import { Action, Priority } from 'kbar'; import React from 'react'; -import { isIconName, NavModelItem } from '@grafana/data'; +import { isIconName, locationUtil, NavModelItem } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { Icon } from '@grafana/ui'; import { changeTheme } from 'app/core/services/theme'; @@ -35,7 +35,7 @@ function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): Actio id: idForNavItem(navItem), name: text, // TODO: translate section: isCreateAction ? SECTION_ACTIONS : SECTION_PAGES, - perform: url ? () => locationService.push(url) : undefined, + perform: url ? () => locationService.push(locationUtil.stripBaseFromUrl(url)) : undefined, parent: parent && idForNavItem(parent), // Only show icons for top level items From 64ccbf0a982dd62abf7b3377c9b83dd84f2af4b1 Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Thu, 8 Dec 2022 17:31:59 +0100 Subject: [PATCH 70/94] Update docs regarding Grafana as an alert receiver (#59938) * Update docs regarding Grafana as an alert receiver * Add link to AM docs --- docs/sources/alerting/performance-limitations/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/alerting/performance-limitations/index.md b/docs/sources/alerting/performance-limitations/index.md index adb926af43f..73b734f2a9c 100644 --- a/docs/sources/alerting/performance-limitations/index.md +++ b/docs/sources/alerting/performance-limitations/index.md @@ -42,10 +42,10 @@ We support the latest two minor versions of both Prometheus and Alertmanager. We As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`. -## Grafana is not an alert receiver +## The Grafana Alertmanager can only receive Grafana managed alerts -Grafana is not an alert receiver; it is an alert generator. This means that Grafana cannot receive alerts from anything other than its internal alert generator. +Grafana cannot be used to receive external alerts. You can only send alerts to the Grafana Alertmanager using Grafana managed alerts. -Receiving alerts from Prometheus (or anything else) is not supported at the time. +You have the option to send Grafana managed alerts to an external Alertmanager, you can find this option in the admin tab on the Alerting page. -For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773). +For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773). To learn more about the different Alertmanagers, read [this documentation]({{< relref "../alerting/manage-notifications/alertmanager/" >}}) From 316870c658d369f19e00d85059ef133cbb8dce0a Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 8 Dec 2022 11:49:27 -0500 Subject: [PATCH 71/94] Alerting: PagerDuty receiver to let user configure fields Source, Client and Client URL (#59895) * add support for source field * add client_url * use real host name for source placeholder --- .../ngalert/notifier/channels/pagerduty.go | 28 ++++++++--- .../notifier/channels/pagerduty_test.go | 50 +++++++++++++++++-- .../channels_config/available_channels.go | 32 +++++++++++- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index a84347bb1d1..7d69b35634e 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -26,6 +26,7 @@ const ( defaultSeverity = "critical" defaultClass = "default" defaultGroup = "default" + defaultClient = "Grafana" ) var ( @@ -52,6 +53,9 @@ type pagerdutySettings struct { Component string `json:"component,omitempty" yaml:"component,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Source string `json:"source,omitempty" yaml:"source,omitempty"` + Client string `json:"client,omitempty" yaml:"client,omitempty"` + ClientURL string `json:"client_url,omitempty" yaml:"client_url,omitempty"` } func buildPagerdutySettings(fc FactoryConfig) (*pagerdutySettings, error) { @@ -88,7 +92,19 @@ func buildPagerdutySettings(fc FactoryConfig) (*pagerdutySettings, error) { if settings.Summary == "" { settings.Summary = DefaultMessageTitleEmbed } - + if settings.Client == "" { + settings.Client = defaultClient + } + if settings.ClientURL == "" { + settings.ClientURL = "{{ .ExternalURL }}" + } + if settings.Source == "" { + source, err := os.Hostname() + if err != nil { + source = settings.Client + } + settings.Source = source + } return &settings, nil } @@ -190,8 +206,8 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m } msg := &pagerDutyMessage{ - Client: "Grafana", - ClientURL: pn.tmpl.ExternalURL.String(), + Client: tmpl(pn.settings.Client), + ClientURL: tmpl(pn.settings.ClientURL), RoutingKey: pn.settings.Key, EventAction: eventType, DedupKey: key.Hash(), @@ -200,6 +216,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m Text: "External URL", }}, Payload: pagerDutyPayload{ + Source: tmpl(pn.settings.Source), Component: tmpl(pn.settings.Component), Summary: tmpl(pn.settings.Summary), Severity: severity, @@ -224,11 +241,6 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m msg.Payload.Summary = summary } - if hostname, err := os.Hostname(); err == nil { - // TODO: should this be configured like in Prometheus AM? - msg.Payload.Source = hostname - } - if tmplErr != nil { pn.log.Warn("failed to template PagerDuty message", "error", tmplErr.Error()) } diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 21ee9c88acc..7325d81f96a 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -56,7 +56,7 @@ func TestPagerdutyNotifier(t *testing.T) { Payload: pagerDutyPayload{ Summary: "[FIRING:1] (val1)", Source: hostname, - Severity: "critical", + Severity: defaultSeverity, Class: "default", Component: "Grafana", Group: "default", @@ -108,6 +108,50 @@ func TestPagerdutyNotifier(t *testing.T) { }, expMsgError: nil, }, + { + name: "Should expand templates in fields", + settings: `{ + "integrationKey": "abcdefgh0123456789", + "severity" : "{{ .CommonLabels.severity }}", + "class": "{{ .CommonLabels.class }}", + "component": "{{ .CommonLabels.component }}", + "group" : "{{ .CommonLabels.group }}", + "source": "{{ .CommonLabels.source }}", + "client": "client-{{ .CommonLabels.source }}", + "client_url": "http://localhost:20200/{{ .CommonLabels.group }}" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1", "severity": "critical", "class": "test-class", "group": "test-group", "component": "test-component", "source": "test-source"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: &pagerDutyMessage{ + RoutingKey: "abcdefgh0123456789", + DedupKey: "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + EventAction: "trigger", + Payload: pagerDutyPayload{ + Summary: "[FIRING:1] (test-class test-component test-group val1 critical test-source)", + Source: "test-source", + Severity: "critical", + Class: "test-class", + Component: "test-component", + Group: "test-group", + CustomDetails: map[string]string{ + "firing": "\nValue: [no value]\nLabels:\n - alertname = alert1\n - class = test-class\n - component = test-component\n - group = test-group\n - lbl1 = val1\n - severity = critical\n - source = test-source\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=class%3Dtest-class&matcher=component%3Dtest-component&matcher=group%3Dtest-group&matcher=lbl1%3Dval1&matcher=severity%3Dcritical&matcher=source%3Dtest-source\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "num_firing": "1", + "num_resolved": "0", + "resolved": "", + }, + }, + Client: "client-test-source", + ClientURL: "http://localhost:20200/test-group", + Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}}, + }, + expMsgError: nil, + }, { name: "Default config with one alert and custom summary", settings: `{"integrationKey": "abcdefgh0123456789", "summary": "Alerts firing: {{ len .Alerts.Firing }}"}`, @@ -126,7 +170,7 @@ func TestPagerdutyNotifier(t *testing.T) { Payload: pagerDutyPayload{ Summary: "Alerts firing: 1", Source: hostname, - Severity: "critical", + Severity: defaultSeverity, Class: "default", Component: "Grafana", Group: "default", @@ -206,7 +250,7 @@ func TestPagerdutyNotifier(t *testing.T) { Payload: pagerDutyPayload{ Summary: fmt.Sprintf("%s…", strings.Repeat("1", 1023)), Source: hostname, - Severity: "critical", + Severity: defaultSeverity, Class: "default", Component: "Grafana", Group: "default", diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index f9b063bc022..400b8f202ff 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -1,11 +1,15 @@ package channels_config import ( + "os" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" ) // GetAvailableNotifiers returns the metadata of all the notification channels that can be configured. func GetAvailableNotifiers() []*NotifierPlugin { + hostname, _ := os.Hostname() + pushoverSoundOptions := []SelectOption{ { Value: "default", @@ -242,8 +246,8 @@ func GetAvailableNotifiers() []*NotifierPlugin { Label: "Severity", Element: ElementTypeInput, InputType: InputTypeText, - Placeholder: "critical", - Description: "Severity of the event. It must be critical, error, warning, info - otherwise, the default is set which is critical. You can use templates", + Placeholder: "error", + Description: "Severity of the event. It must be critical, error, warning, info - otherwise, the default is set which is error. You can use templates", PropertyName: "severity", }, { // New in 8.0. @@ -276,6 +280,30 @@ func GetAvailableNotifiers() []*NotifierPlugin { Placeholder: channels.DefaultMessageTitleEmbed, PropertyName: "summary", }, + { // New in 9.4. + Label: "Source", + Description: "The unique location of the affected system, preferably a hostname or FQDN. You can use templates", + Element: ElementTypeInput, + InputType: InputTypeText, + Placeholder: hostname, + PropertyName: "source", + }, + { // New in 9.4. + Label: "Client", + Description: "The name of the monitoring client that is triggering this event. You can use templates", + Element: ElementTypeInput, + InputType: InputTypeText, + Placeholder: "Grafana", + PropertyName: "client", + }, + { // New in 9.4. + Label: "Client URL", + Description: "The URL of the monitoring client that is triggering this event. You can use templates", + Element: ElementTypeInput, + InputType: InputTypeText, + Placeholder: "{{ .ExternalURL }}", + PropertyName: "client_url", + }, }, }, { From dedf2922284c92b3c00f797aaf2d7bd40d5f420a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 17:27:38 +0000 Subject: [PATCH 72/94] Update dependency @reduxjs/toolkit to v1.9.0 (#58247) * Update dependency @reduxjs/toolkit to v1.9.0 * fix types on mockToolkitActionCreator * Fix external AM selector tests * timeout added in shared public dashboard test * Revert "timeout added in shared public dashboard test" This reverts commit 205c29519201384ea36eb33b8c34f5114c1a78c6. * update to @reduxjs/toolkit@1.9.1 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison Co-authored-by: Konrad Lalik Co-authored-by: Juan Cabanas --- package.json | 2 +- .../hooks/useExternalAMSelector.test.tsx | 16 +++---- public/test/core/redux/mocks.ts | 2 +- yarn.lock | 48 +++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index f82549dbcb6..979ef5b3ae0 100644 --- a/package.json +++ b/package.json @@ -275,7 +275,7 @@ "@react-stately/collections": "3.4.1", "@react-stately/menu": "3.4.1", "@react-stately/tree": "3.3.1", - "@reduxjs/toolkit": "1.8.6", + "@reduxjs/toolkit": "1.9.1", "@sentry/browser": "6.19.7", "@sentry/types": "6.19.7", "@sentry/utils": "6.19.7", diff --git a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx index 0f2c8d3f112..b5dc9e6a228 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx +++ b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx @@ -81,8 +81,8 @@ describe('useExternalDataSourceAlertmanagers', () => { const wrapper: React.FC = ({ children }) => {children}; // Act - const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); - await waitForNextUpdate(); + const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); + await waitForValueToChange(() => result.current[0].status); // Assert const { current } = result; @@ -114,8 +114,8 @@ describe('useExternalDataSourceAlertmanagers', () => { const wrapper: React.FC = ({ children }) => {children}; // Act - const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); - await waitForNextUpdate(); + const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); + await waitForValueToChange(() => result.current[0].status); // Assert const { current } = result; @@ -180,8 +180,8 @@ describe('useExternalDataSourceAlertmanagers', () => { const wrapper: React.FC = ({ children }) => {children}; // Act - const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); - await waitForNextUpdate(); + const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); + await waitForValueToChange(() => result.current[0].status); // Assert const { current } = result; @@ -213,11 +213,11 @@ describe('useExternalDataSourceAlertmanagers', () => { const wrapper: React.FC = ({ children }) => {children}; // Act - const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { + const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper, }); - await waitForNextUpdate(); + await waitForValueToChange(() => result.current[0].status); // Assert expect(result.current).toHaveLength(1); diff --git a/public/test/core/redux/mocks.ts b/public/test/core/redux/mocks.ts index 38c3a75ce4a..c25f96728e1 100644 --- a/public/test/core/redux/mocks.ts +++ b/public/test/core/redux/mocks.ts @@ -1,6 +1,6 @@ import { ActionCreatorWithoutPayload, PayloadActionCreator } from '@reduxjs/toolkit'; -export const mockToolkitActionCreator = (creator: PayloadActionCreator) => { +export const mockToolkitActionCreator = (creator: PayloadActionCreator) => { return Object.assign(jest.fn(), creator); }; diff --git a/yarn.lock b/yarn.lock index 84ee48674ed..6f4ab0374b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7994,14 +7994,14 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:1.8.6": - version: 1.8.6 - resolution: "@reduxjs/toolkit@npm:1.8.6" +"@reduxjs/toolkit@npm:1.9.1": + version: 1.9.1 + resolution: "@reduxjs/toolkit@npm:1.9.1" dependencies: - immer: ^9.0.7 - redux: ^4.1.2 - redux-thunk: ^2.4.1 - reselect: ^4.1.5 + immer: ^9.0.16 + redux: ^4.2.0 + redux-thunk: ^2.4.2 + reselect: ^4.1.7 peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.0.2 @@ -8010,7 +8010,7 @@ __metadata: optional: true react-redux: optional: true - checksum: f2a5f20f638a981eacd6becb040b923a825013064ece9b1555a658cab1e9f6c46a14d8c50aa095cda49ff0b89518b855ae88c774d95bcc9692e9297d3bc3b2d6 + checksum: e6700a0d45198ab525c96ff0425fa0125fbdc37ce514f0c77c30225837113279ceec9190ac3da35cb20e77553e56342021788bbf17465819068c4db34cb3d87f languageName: node linkType: hard @@ -21495,7 +21495,7 @@ __metadata: "@react-types/menu": 3.7.2 "@react-types/overlays": 3.6.4 "@react-types/shared": 3.15.0 - "@reduxjs/toolkit": 1.8.6 + "@reduxjs/toolkit": 1.9.1 "@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1 "@sentry/browser": 6.19.7 "@sentry/types": 6.19.7 @@ -22743,7 +22743,7 @@ __metadata: languageName: node linkType: hard -"immer@npm:9.0.16": +"immer@npm:9.0.16, immer@npm:^9.0.16": version: 9.0.16 resolution: "immer@npm:9.0.16" checksum: e9a5ca65c929b329da7a3b7beccf7984271cda7bdd47b2cab619eac3277dcd56598c211b55cc340786b6eff0c06652ac018808d9fd744443f06882364dece6bc @@ -33114,7 +33114,7 @@ __metadata: languageName: node linkType: hard -"redux-thunk@npm:2.4.1, redux-thunk@npm:^2.4.1": +"redux-thunk@npm:2.4.1": version: 2.4.1 resolution: "redux-thunk@npm:2.4.1" peerDependencies: @@ -33123,6 +33123,15 @@ __metadata: languageName: node linkType: hard +"redux-thunk@npm:^2.4.2": + version: 2.4.2 + resolution: "redux-thunk@npm:2.4.2" + peerDependencies: + redux: ^4 + checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c + languageName: node + linkType: hard + "redux@npm:4.2.0, redux@npm:^4.2.0": version: 4.2.0 resolution: "redux@npm:4.2.0" @@ -33141,15 +33150,6 @@ __metadata: languageName: node linkType: hard -"redux@npm:^4.1.2": - version: 4.1.2 - resolution: "redux@npm:4.1.2" - dependencies: - "@babel/runtime": ^7.9.2 - checksum: 6a839cee5bd580c5298d968e9e2302150e961318253819bcd97f9d945a5a409559eacddf6026f4118bb68b681c593d90e8a2c5bbf278f014aff9bf0d2d8fa084 - languageName: node - linkType: hard - "refractor@npm:^3.6.0": version: 3.6.0 resolution: "refractor@npm:3.6.0" @@ -33595,10 +33595,10 @@ __metadata: languageName: node linkType: hard -"reselect@npm:^4.1.5": - version: 4.1.5 - resolution: "reselect@npm:4.1.5" - checksum: 54c13c1e795b2ea70cba8384138aebe78adda00cbea303cc94b64da0a70d74c896cc9a03115ae38b8bff990e7a60dcd6452ab68cbec01b0b38c1afda70714cf0 +"reselect@npm:^4.1.7": + version: 4.1.7 + resolution: "reselect@npm:4.1.7" + checksum: 738d8e2b8f0dca154ad29de6a209c9fbca2d70ae6788fd85df87f2c74b95a65bbf2d16d43a9e2faff39de34d17a29d706ba08a6b2ee5660c09589edbd19af7e1 languageName: node linkType: hard From 6f930f48365fa06baca6cb095f216ef4a08a0c06 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 8 Dec 2022 09:32:22 -0800 Subject: [PATCH 73/94] Panel Edit: Fix data links edit icons being off screen when provided title is too long (#59829) * Fix data links edit icons being off screen * Remove unused imports * Review --- .../DataLinksListItem.tsx | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 2c9e721d332..228da8ffe24 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -7,7 +7,6 @@ import { stylesFactory, useTheme2 } from '../../../themes'; import { isCompactUrl } from '../../../utils/dataLinks'; import { FieldValidationMessage } from '../../Forms/FieldValidationMessage'; import { IconButton } from '../../IconButton/IconButton'; -import { HorizontalGroup, VerticalGroup } from '../../Layout/Layout'; export interface DataLinksListItemProps { index: number; @@ -31,26 +30,24 @@ export const DataLinksListItem: FC = ({ link, onEdit, on return (
- - -
- {hasTitle ? title : 'Data link title not provided'} -
- - - - -
-
- {hasUrl ? url : 'Data link url not provided'} +
+
+ {hasTitle ? title : 'Data link title not provided'}
- {isCompactExploreUrl && ( - Explore data link may not work in the future. Please edit. - )} - +
+ + +
+
+
+ {hasUrl ? url : 'Data link url not provided'} +
+ {isCompactExploreUrl && ( + Explore data link may not work in the future. Please edit. + )}
); }; @@ -63,6 +60,19 @@ const getDataLinkListItemStyles = stylesFactory((theme: GrafanaTheme2) => { &:last-child { margin-bottom: 0; } + display: flex; + flex-direction: column; + `, + titleWrapper: css` + label: data-links-list-item-title; + justify-content: space-between; + display: flex; + width: 100%; + align-items: center; + `, + actionButtons: css` + margin-left: ${theme.spacing(1)}; + display: flex; `, errored: css` color: ${theme.colors.error.text}; From 22f828300dfbb226d44fc74e1c64ae62d5032cb5 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:46:00 -0600 Subject: [PATCH 74/94] Prometheus: Fix exemplars not respecting corresponding series display status. (#59743) * Exemplar filtering when series are toggled in legend UI --- .../uPlot/config/UPlotConfigBuilder.ts | 2 +- .../panel/candlestick/CandlestickPanel.tsx | 6 +- .../panel/timeseries/TimeSeriesPanel.tsx | 5 +- .../plugins/ExemplarsPlugin.test.tsx | 97 +++++++++++++++++++ .../timeseries/plugins/ExemplarsPlugin.tsx | 87 ++++++++++++++++- 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 839935cd1e5..f3c8a3824d4 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -39,7 +39,7 @@ type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData; type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData; export class UPlotConfigBuilder { - private series: UPlotSeriesBuilder[] = []; + series: UPlotSeriesBuilder[] = []; private axes: Record = {}; private scales: UPlotScaleBuilder[] = []; private bands: Band[] = []; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 67f5d264c89..5a9d216cb7d 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -4,10 +4,10 @@ import React, { useMemo } from 'react'; import uPlot from 'uplot'; -import { Field, getDisplayProcessor, PanelProps, getLinksSupplier } from '@grafana/data'; +import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder, useTheme2 } from '@grafana/ui'; +import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; import { config } from 'app/core/config'; @@ -21,7 +21,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin'; import { prepareCandlestickFields } from './fields'; -import { defaultColors, CandlestickOptions, VizDisplayMode } from './models.gen'; +import { CandlestickOptions, defaultColors, VizDisplayMode } from './models.gen'; import { drawMarkers, FieldIndices } from './utils'; interface CandlestickPanelProps extends PanelProps {} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 78203d51ba0..2a9367e96ca 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -3,14 +3,14 @@ import React, { useMemo } from 'react'; import { Field, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, KeyboardPlugin } from '@grafana/ui'; +import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; import { config } from 'app/core/config'; import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin'; import { AnnotationsPlugin } from './plugins/AnnotationsPlugin'; import { ContextMenuPlugin } from './plugins/ContextMenuPlugin'; -import { ExemplarsPlugin } from './plugins/ExemplarsPlugin'; +import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from './plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin'; import { TimeSeriesOptions } from './types'; @@ -133,6 +133,7 @@ export const TimeSeriesPanel: React.FC = ({ )} {data.annotations && ( { + const dataFrameSeries1 = new MutableDataFrame({ + name: 'tns/app', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/app', + }, + values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167], + }, + ] as unknown as Field[], + length: 4, + }); + const dataFrameSeries2 = new MutableDataFrame({ + name: 'tns/db', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/db', + }, + values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + }, + ] as unknown as Field[], + length: 4, + }); + const dataFrameSeries3 = new MutableDataFrame({ + name: 'tns/loadgen', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/loadgen', + }, + values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + }, + ] as unknown as Field[], + length: 4, + }); + const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3]; + const config: UPlotConfigBuilder = { + addHook: (type, hook) => {}, + series: [ + { + props: { + dataFrameFieldIndex: { frameIndex: 0, fieldIndex: 1 }, + show: true, + }, + }, + { + props: { + dataFrameFieldIndex: { frameIndex: 1, fieldIndex: 1 }, + show: true, + }, + }, + { + props: { + dataFrameFieldIndex: { frameIndex: 2, fieldIndex: 1 }, + show: false, + }, + }, + ], + } as UPlotConfigBuilder; + + it('function should only return labels associated with actively visible series', () => { + const expected: { labels: Labels[]; totalSeriesCount: number } = { + totalSeriesCount: 3, + labels: [{ job: 'tns/app' }, { job: 'tns/db' }], + }; + + // Base case + expect(getVisibleLabels(config, [])).toEqual({ totalSeriesCount: 3, labels: [] }); + + expect(getVisibleLabels(config, frames)).toEqual(expected); + }); +}); diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 6e8e663f940..1952863eb3c 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -5,10 +5,11 @@ import { DataFrame, DataFrameFieldIndex, Field, + Labels, LinkModel, - TimeZone, TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, + TimeZone, } from '@grafana/data'; import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui'; @@ -19,9 +20,16 @@ interface ExemplarsPluginProps { exemplars: DataFrame[]; timeZone: TimeZone; getFieldLinks: (field: Field, rowIndex: number) => Array>; + visibleLabels?: { labels: Labels[]; totalSeriesCount: number }; } -export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, getFieldLinks, config }) => { +export const ExemplarsPlugin: React.FC = ({ + exemplars, + timeZone, + getFieldLinks, + config, + visibleLabels, +}) => { const plotInstance = useRef(); useLayoutEffect(() => { @@ -63,6 +71,14 @@ export const ExemplarsPlugin: React.FC = ({ exemplars, tim const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + // If the parent provided series/labels: filter the exemplars, otherwise default to show all exemplars + let showMarker = + visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true; + + if (!showMarker) { + return <>; + } + return ( = ({ exemplars, tim /> ); }, - [config, timeZone, getFieldLinks] + [config, timeZone, getFieldLinks, visibleLabels] ); return ( @@ -86,3 +102,68 @@ export const ExemplarsPlugin: React.FC = ({ exemplars, tim /> ); }; + +/** + * Function to get labels that are currently displayed in the legend + */ +export const getVisibleLabels = ( + config: UPlotConfigBuilder, + frames: DataFrame[] | null +): { labels: Labels[]; totalSeriesCount: number } => { + const visibleSeries = config.series.filter((series) => series.props.show); + const visibleLabels: Labels[] = []; + if (frames?.length) { + visibleSeries.forEach((plotInstance) => { + const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex; + const fieldIndex = plotInstance.props?.dataFrameFieldIndex?.fieldIndex; + + if (frameIndex !== undefined && fieldIndex !== undefined) { + const field = frames[frameIndex].fields[fieldIndex]; + if (field.labels) { + // Note that this may be an empty object in the case of a metric being rendered with no labels + visibleLabels.push(field.labels); + } + } + }); + } + + return { labels: visibleLabels, totalSeriesCount: config.series.length }; +}; + +/** + * Determine if the current exemplar marker is filtered by what series are selected in the legend UI + */ +const showExemplarMarker = ( + visibleLabels: { labels: Labels[]; totalSeriesCount: number }, + dataFrame: DataFrame, + dataFrameFieldIndex: DataFrameFieldIndex +) => { + let showMarker = false; + if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { + showMarker = true; + } else { + visibleLabels.labels.forEach((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel); + // If there aren't any labels, the graph is only displaying a single series with exemplars, let's show all exemplars in this case as well + if (Object.keys(visibleLabel).length === 0) { + showMarker = true; + } else { + // If there are labels, lets only show the exemplars with labels associated with series that are currently visible + const fields = dataFrame.fields.filter((field) => { + return labelKeys.find((labelKey) => labelKey === field.name); + }); + + // Check to see if at least one value matches each field + if (fields.length) { + showMarker = visibleLabels.labels.some((series) => { + return Object.keys(series).every((label) => { + const value = series[label]; + return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value); + }); + }); + } + } + }); + } + return showMarker; +}; From 551a2ac9ec8f4dbe67a0a008736339d0eab5c816 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 8 Dec 2022 20:52:27 +0300 Subject: [PATCH 75/94] Prometheus: Tweak query builder select component (#59749) * Have consistent color for operators * Add more operators and reorder them to align with dashboard filters --- .../querybuilder/components/LabelFilterItem.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx index bb1ce8c8bfd..d5e31f675e3 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx @@ -6,9 +6,10 @@ import { selectors } from '@grafana/e2e-selectors'; import { AccessoryButton, InputGroup } from '@grafana/experimental'; import { AsyncSelect, Select } from '@grafana/ui'; -import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from '../components/MetricSelect'; import { QueryBuilderLabelFilter } from '../shared/types'; +import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect'; + export interface Props { defaultOp: string; item: Partial; @@ -89,6 +90,7 @@ export function LabelFilterItem({ {/* Operator select i.e. = =~ != !~ */}
- + ))} {mode === TooltipDisplayMode.Single && columnIndex && ( - - - )} - {links.length > 0 && ( - - + )} @@ -100,6 +78,30 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }: ); }; +const renderWithLinks = (key: string, val: string, links: Record>>) => + links[key] ? ( + + <> + {val} + {links[key].map((link, i) => ( + + {link.title} + + ))} + + + ) : ( + <>{val} + ); + const getStyles = (theme: GrafanaTheme2) => ({ infoWrap: css` padding: 8px; diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index a615bc6d8f4..79733b54a1f 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useRef, useState } from 'react'; -import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data'; +import { + DataFrameType, + Field, + FieldType, + formattedValueToString, + getFieldDisplayName, + LinkModel, + TimeRange, +} from '@grafana/data'; import { LinkButton, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; @@ -15,6 +23,7 @@ type Props = { data: HeatmapData; hover: HeatmapHoverEvent; showHistogram?: boolean; + timeRange: TimeRange; }; export const HeatmapHoverView = (props: Props) => { diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index f46a42591e8..73f3f9eb0b3 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { DataFrameType, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data'; +import { DataFrame, DataFrameType, Field, getLinksSupplier, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { ScaleDistributionConfig } from '@grafana/schema'; import { @@ -47,13 +47,20 @@ export const HeatmapPanel: React.FC = ({ let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; + const getFieldLinksSupplier = useCallback( + (exemplars: DataFrame, field: Field) => { + return getLinksSupplier(exemplars, field, field.state?.scopedVars ?? {}, replaceVariables); + }, + [replaceVariables] + ); + const info = useMemo(() => { try { - return prepareHeatmapData(data, options, theme); + return prepareHeatmapData(data, options, theme, getFieldLinksSupplier); } catch (ex) { return { warning: `${ex}` }; } - }, [data, options, theme]); + }, [data, options, theme, getFieldLinksSupplier]); const facets = useMemo(() => { let exemplarsXFacet: number[] = []; // "Time" field @@ -218,7 +225,12 @@ export const HeatmapPanel: React.FC = ({ /> )} - + )} diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index d6491a31161..74f0bf429c0 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -6,9 +6,11 @@ import { formattedValueToString, getDisplayProcessor, GrafanaTheme2, + LinkModel, outerJoinDataFrames, PanelData, ValueFormatter, + ValueLinkConfig, } from '@grafana/data'; import { calculateHeatmapFromData, @@ -52,7 +54,12 @@ export interface HeatmapData { warning?: string; } -export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme: GrafanaTheme2): HeatmapData { +export function prepareHeatmapData( + data: PanelData, + options: PanelOptions, + theme: GrafanaTheme2, + getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array> +): HeatmapData { let frames = data.series; if (!frames?.length) { return {}; @@ -60,6 +67,12 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme const exemplars = data.annotations?.find((f) => f.name === 'exemplar'); + if (getFieldLinks) { + exemplars?.fields.forEach((field, index) => { + exemplars.fields[index].getLinks = getFieldLinks(exemplars, field); + }); + } + if (options.calculate) { return getDenseHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, options, theme); } From a83268db7b162910d55026aabf6c6b3987aaaafe Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 8 Dec 2022 13:09:45 -0500 Subject: [PATCH 77/94] Alerting: skip flaky test `RuleEditor recording rules.can create a new cloud recording rule` (#60044) --- .../features/alerting/unified/RuleEditorRecordingRule.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx index 73aa134df89..bce401c0d92 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx @@ -61,7 +61,7 @@ describe('RuleEditor recording rules', () => { }); disableRBAC(); - it('can create a new cloud recording rule', async () => { + it.skip('can create a new cloud recording rule', async () => { const dataSources = { default: mockDataSource( { From 29d2f86955fc1ac366af5feeed17b97b45df58d4 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 8 Dec 2022 21:55:39 +0300 Subject: [PATCH 78/94] Update heatmap exemplar popover styles (#59988) * Put traceID on top * Update exemplar popover styles * Remove console.log * Make exemplar header opt-in via optional prop Co-authored-by: nmarrs --- .../panel/geomap/components/DataHoverRow.tsx | 2 +- .../panel/geomap/components/DataHoverView.tsx | 109 +++++++++++++----- .../plugins/panel/heatmap/HeatmapPanel.tsx | 1 + 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/panel/geomap/components/DataHoverRow.tsx b/public/app/plugins/panel/geomap/components/DataHoverRow.tsx index 5c8a38a6480..0aaf6a32f9a 100644 --- a/public/app/plugins/panel/geomap/components/DataHoverRow.tsx +++ b/public/app/plugins/panel/geomap/components/DataHoverRow.tsx @@ -24,5 +24,5 @@ export const DataHoverRow = ({ feature }: Props) => { data = new ArrayDataFrame([properties]); } - return ; + return ; }; diff --git a/public/app/plugins/panel/geomap/components/DataHoverView.tsx b/public/app/plugins/panel/geomap/components/DataHoverView.tsx index 23accd4617b..b0c50e6f442 100644 --- a/public/app/plugins/panel/geomap/components/DataHoverView.tsx +++ b/public/app/plugins/panel/geomap/components/DataHoverView.tsx @@ -19,25 +19,36 @@ export interface Props { columnIndex?: number | null; // the hover column sortOrder?: SortOrder; mode?: TooltipDisplayMode | null; + displayExemplarHeader?: boolean; } -export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }: Props) => { +export const DataHoverView = ({ + data, + rowIndex, + columnIndex, + sortOrder, + mode, + displayExemplarHeader = true, +}: Props) => { const styles = useStyles2(getStyles); if (!data || rowIndex == null) { return null; } + // Put the traceID field in front. const visibleFields = data.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); + const traceIDField = visibleFields.find((field) => field.name === 'traceID') || data.fields[0]; + const orderedVisibleFields = [traceIDField, ...visibleFields.filter((field) => traceIDField !== field)]; - if (visibleFields.length === 0) { + if (orderedVisibleFields.length === 0) { return null; } const displayValues: Array<[string, unknown, string]> = []; const links: Record>> = {}; - for (const f of visibleFields) { + for (const f of orderedVisibleFields) { const v = f.values.get(rowIndex); const disp = f.display ? f.display(v) : { text: `${v}`, numeric: +v }; if (f.getLinks) { @@ -58,23 +69,30 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }: } return ( -
{v[0]}:{v[2]}{renderWithLinks(v[0], v[2], links)}
{displayValues[columnIndex][0]}:{displayValues[columnIndex][2]}
- - {links.map((link, i) => ( - - {link.title} - - ))} - - {renderWithLinks(displayValues[columnIndex][0], displayValues[columnIndex][2], links)}
- - {(mode === TooltipDisplayMode.Multi || mode == null) && - displayValues.map((v, i) => ( - - - +
+ {displayExemplarHeader && ( +
+ Exemplar +
+ )} +
{v[0]}:{renderWithLinks(v[0], v[2], links)}
+ + {(mode === TooltipDisplayMode.Multi || mode == null) && + displayValues.map((v, i) => ( + + + + + ))} + {mode === TooltipDisplayMode.Single && columnIndex && ( + + + - ))} - {mode === TooltipDisplayMode.Single && columnIndex && ( - - - - - )} - -
{v[0]}:{renderWithLinks(v[0], v[2], links)}
{displayValues[columnIndex][0]}:{renderWithLinks(displayValues[columnIndex][0], displayValues[columnIndex][2], links)}
{displayValues[columnIndex][0]}:{renderWithLinks(displayValues[columnIndex][0], displayValues[columnIndex][2], links)}
+ )} + + +
); }; @@ -102,15 +120,46 @@ const renderWithLinks = (key: string, val: string, links: Record{val} ); -const getStyles = (theme: GrafanaTheme2) => ({ - infoWrap: css` - padding: 8px; - th { +const getStyles = (theme: GrafanaTheme2) => { + const bg = theme.isDark ? theme.v1.palette.dark2 : theme.v1.palette.white; + const headerBg = theme.isDark ? theme.v1.palette.dark9 : theme.v1.palette.gray5; + const tableBgOdd = theme.isDark ? theme.v1.palette.dark3 : theme.v1.palette.gray6; + + return { + wrapper: css` + background: ${bg}; + border: 1px solid ${headerBg}; + border-radius: ${theme.shape.borderRadius(2)}; + `, + header: css` + background: ${headerBg}; + padding: 6px 10px; + display: flex; + `, + title: css` font-weight: ${theme.typography.fontWeightMedium}; - padding: ${theme.spacing(0.25, 2)}; - } - `, - highlight: css` - background: ${theme.colors.action.hover}; - `, -}); + padding-right: ${theme.spacing(2)}; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; + `, + infoWrap: css` + padding: 8px; + th { + font-weight: ${theme.typography.fontWeightMedium}; + padding: ${theme.spacing(0.25, 2)}; + } + tr { + background-color: ${theme.colors.background.primary}; + &:nth-child(even) { + background-color: ${tableBgOdd}; + } + } + `, + highlight: css` + background: ${theme.colors.action.hover}; + `, + }; +}; diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 73f3f9eb0b3..5c80756f07d 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -212,6 +212,7 @@ export const HeatmapPanel: React.FC = ({ width: '100%', display: 'flex', justifyContent: 'flex-end', + paddingBottom: '6px', }} > Date: Thu, 8 Dec 2022 10:57:33 -0800 Subject: [PATCH 79/94] Git: Update experimental workflow POC (#60049) --- pkg/api/api.go | 5 ++++- pkg/services/navtree/models.go | 2 ++ pkg/services/store/service.go | 3 +-- .../components/SaveDashboard/forms/SaveToStorageForm.tsx | 5 +++-- public/app/features/storage/RootView.tsx | 3 --- public/app/features/storage/StoragePage.tsx | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 75d9cf3b763..4f57e1815e9 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -114,8 +114,11 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/admin/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index) r.Get("/admin/orgs/edit/:id", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index) r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index) - r.Get("/admin/storage/*", reqGrafanaAdmin, hs.Index) r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index) + if hs.Features.IsEnabled(featuremgmt.FlagStorage) { + r.Get("/admin/storage", reqSignedIn, hs.Index) + r.Get("/admin/storage/*", reqSignedIn, hs.Index) + } r.Get("/styleguide", reqSignedIn, hs.Index) r.Get("/live", reqGrafanaAdmin, hs.Index) diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index 276318892c2..d1de2fbc52c 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -245,6 +245,8 @@ func ApplyAdminIA(root *NavTreeRoot) { adminNodeLinks = append(adminNodeLinks, accessNode) } + adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("storage")) + if len(adminNodeLinks) > 0 { orgAdminNode.Children = adminNodeLinks } else { diff --git a/pkg/services/store/service.go b/pkg/services/store/service.go index fc17c6b85b1..bc77c693ea4 100644 --- a/pkg/services/store/service.go +++ b/pkg/services/store/service.go @@ -592,8 +592,7 @@ func (s *standardStorageService) getWorkflowOptions(ctx context.Context, user *u Workflows: make([]workflowInfo, 0), } - scope, _ := splitFirstSegment(path) - root, _ := s.tree.getRoot(user.OrgID, scope) + root, _ := s.tree.getRoot(user.OrgID, path) if root == nil { return options, fmt.Errorf("can not read") } diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveToStorageForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveToStorageForm.tsx index 052f293733c..5b1c71d2532 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveToStorageForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveToStorageForm.tsx @@ -96,7 +96,8 @@ export function SaveToStorageForm(props: Props) { } setSaving(true); - let uid = saveModel.clone.uid; + // Save dashboard without the UID + let { uid, ...body } = saveModel.clone; if (isNew || isCopy) { uid = path; if (!uid.endsWith('-dash.json')) { @@ -104,7 +105,7 @@ export function SaveToStorageForm(props: Props) { } } const rsp = await getGrafanaStorage().write(uid, { - body: saveModel.clone, + body, kind: 'dashboard', title: data.title, message: data.message, diff --git a/public/app/features/storage/RootView.tsx b/public/app/features/storage/RootView.tsx index d667758fdcc..e1c56fe1ee4 100644 --- a/public/app/features/storage/RootView.tsx +++ b/public/app/features/storage/RootView.tsx @@ -133,9 +133,6 @@ function getTags(v: StorageInfo) { if (v.builtin) { tags.push('Builtin'); } - if (!v.editable) { - tags.push('Read only'); - } // Error if (!v.ready) { diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index 5e94332cd56..dfd86c879cb 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -149,7 +149,7 @@ export default function StoragePage(props: Props) { // Lets only apply permissions to folders (for now) if (isFolder) { - opts.push({ what: StorageView.Perms, text: 'Permissions' }); + // opts.push({ what: StorageView.Perms, text: 'Permissions' }); } else { // TODO: only if the file exists in a storage engine with opts.push({ what: StorageView.History, text: 'History' }); @@ -181,7 +181,7 @@ export default function StoragePage(props: Props) { {canViewDashboard && ( - + Dashboard )} From 76601f3ae7f55f04498ce4814736883a88e89c26 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 8 Dec 2022 20:12:13 +0000 Subject: [PATCH 80/94] Alerting: Better define how we set states (#59977) This commit better defines how we set states in resultNormal, resultAlerting, resultError and resultNoData. It changes the existing code to call methods such as SetAlerting, SetPending, SetNormal, SetError and NoData instead of assigning values to each individual field whenever the state is changed. This should make it easier to understand what fields should be set for which states and avoid cases where states are missing, or have additional unexpected fields. --- pkg/services/ngalert/models/alert_rule.go | 3 +- pkg/services/ngalert/state/state.go | 149 +++++--- pkg/services/ngalert/state/state_test.go | 414 +++++++++++++++++----- 3 files changed, 426 insertions(+), 140 deletions(-) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 53b3f19a25e..2e759b818c7 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -116,8 +116,9 @@ const ( ValueStringAnnotation = "__value_string__" ) -var ( +const ( StateReasonMissingSeries = "MissingSeries" + StateReasonError = "Error" ) var ( diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 3bc95340e59..e804bb5aa61 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -82,11 +82,62 @@ func (a *State) GetAlertInstanceKey() (models.AlertInstanceKey, error) { return models.AlertInstanceKey{RuleOrgID: a.OrgID, RuleUID: a.AlertRuleUID, LabelsHash: labelsHash}, nil } +// SetAlerting sets the state to Alerting. It changes both the start and end time. +func (a *State) SetAlerting(reason string, startsAt, endsAt time.Time) { + a.State = eval.Alerting + a.StateReason = reason + a.StartsAt = startsAt + a.EndsAt = endsAt + a.Error = nil +} + +// SetPending the state to Pending. It changes both the start and end time. +func (a *State) SetPending(reason string, startsAt, endsAt time.Time) { + a.State = eval.Pending + a.StateReason = reason + a.StartsAt = startsAt + a.EndsAt = endsAt + a.Error = nil +} + +// SetNoData sets the state to NoData. It changes both the start and end time. +func (a *State) SetNoData(reason string, startsAt, endsAt time.Time) { + a.State = eval.NoData + a.StateReason = reason + a.StartsAt = startsAt + a.EndsAt = endsAt + a.Error = nil +} + +// SetError sets the state to Error. It changes both the start and end time. +func (a *State) SetError(err error, startsAt, endsAt time.Time) { + a.State = eval.Error + a.StateReason = models.StateReasonError + a.StartsAt = startsAt + a.EndsAt = endsAt + a.Error = err +} + +// SetNormal sets the state to Normal. It changes both the start and end time. +func (a *State) SetNormal(reason string, startsAt, endsAt time.Time) { + a.State = eval.Normal + a.StateReason = reason + a.StartsAt = startsAt + a.EndsAt = endsAt + a.Error = nil +} + +// Resolve sets the State to Normal. It updates the StateReason, the end time, and sets Resolved to true. func (a *State) Resolve(reason string, endsAt time.Time) { a.State = eval.Normal a.StateReason = reason - a.EndsAt = endsAt a.Resolved = true + a.EndsAt = endsAt +} + +// Maintain updates the end time using the most recent evaluation. +func (a *State) Maintain(interval int64, evaluatedAt time.Time) { + a.EndsAt = nextEndsTime(interval, evaluatedAt) } // StateTransition describes the transition from one state to another. @@ -129,83 +180,77 @@ func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float } func resultNormal(state *State, _ *models.AlertRule, result eval.Result, logger log.Logger) { - state.Error = nil // should be nil since state is not error - - if state.State != eval.Normal { + if state.State == eval.Normal { + logger.Debug("Keeping state", "state", state.State) + } else { logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Normal) - state.State = eval.Normal - state.StartsAt = result.EvaluatedAt - state.EndsAt = result.EvaluatedAt + // Normal states have the same start and end timestamps + state.SetNormal("", result.EvaluatedAt, result.EvaluatedAt) } } func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) { - state.Error = result.Error - switch state.State { case eval.Alerting: - // If the previous state is Alerting then update the expiration time - state.setEndsAt(rule, result) + logger.Debug("Keeping state", "state", state.State) + state.Maintain(rule.IntervalSeconds, result.EvaluatedAt) case eval.Pending: // If the previous state is Pending then check if the For duration has been observed if result.EvaluatedAt.Sub(state.StartsAt) >= rule.For { logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Alerting) - state.State = eval.Alerting - state.StartsAt = result.EvaluatedAt - state.setEndsAt(rule, result) + state.SetAlerting("", result.EvaluatedAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)) } default: if rule.For > 0 { // If the alert rule has a For duration that should be observed then the state should be set to Pending logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Pending) - state.State = eval.Pending + state.SetPending("", result.EvaluatedAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)) } else { logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Alerting) - state.State = eval.Alerting + state.SetAlerting("", result.EvaluatedAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)) } - state.StartsAt = result.EvaluatedAt - state.setEndsAt(rule, result) } } - func resultError(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) { switch rule.ExecErrState { case models.AlertingErrState: + logger.Debug("Execution error state is Alerting", "handler", "resultAlerting", "previous_handler", "resultError") resultAlerting(state, rule, result, logger) - case models.ErrorErrState: + // This is a special case where Alerting and Pending should also have an error and reason state.Error = result.Error - if result.Error != nil { - // If the evaluation failed because a query returned an error then add the Ref ID and - // Datasource UID as labels - var queryError expr.QueryError - if errors.As(state.Error, &queryError) { - for _, next := range rule.Data { - if next.RefID == queryError.RefID { - state.Labels["ref_id"] = next.RefID - state.Labels["datasource_uid"] = next.DatasourceUID - break - } - } - state.Annotations["Error"] = queryError.Error() - } - } - + state.StateReason = "error" + case models.ErrorErrState: if state.State == eval.Error { - // If the previous state is Error then update the expiration time - state.setEndsAt(rule, result) + logger.Debug("Keeping state", "state", state.State) + state.Maintain(rule.IntervalSeconds, result.EvaluatedAt) } else { // This is the first occurrence of an error logger.Debug("Changing state", "previous_state", state.State, "next_state", eval.Error) - state.State = eval.Error - state.StartsAt = result.EvaluatedAt - state.setEndsAt(rule, result) + state.SetError(result.Error, result.EvaluatedAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)) + + if result.Error != nil { + // If the evaluation failed because a query returned an error then add the Ref ID and + // Datasource UID as labels + var queryError expr.QueryError + if errors.As(state.Error, &queryError) { + for _, next := range rule.Data { + if next.RefID == queryError.RefID { + state.Labels["ref_id"] = next.RefID + state.Labels["datasource_uid"] = next.DatasourceUID + break + } + } + state.Annotations["Error"] = queryError.Error() + } + } } case models.OkErrState: + logger.Debug("Execution error state is Normal", "handler", "resultNormal", "previous_handler", "resultError") resultNormal(state, rule, result, logger) default: - state.State = eval.Error - state.Error = fmt.Errorf("unsupported execution error state: %s", rule.ExecErrState) - state.Annotations["Error"] = state.Error.Error() + err := fmt.Errorf("unsupported execution error state: %s", rule.ExecErrState) + state.SetError(err, state.StartsAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)) + state.Annotations["Error"] = err.Error() } } @@ -215,7 +260,7 @@ func resultNoData(state *State, rule *models.AlertRule, result eval.Result, _ lo if state.StartsAt.IsZero() { state.StartsAt = result.EvaluatedAt } - state.setEndsAt(rule, result) + state.EndsAt = nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt) switch rule.NoDataState { case models.Alerting: @@ -268,17 +313,13 @@ func (a *State) TrimResults(alertRule *models.AlertRule) { a.Results = newResults } -// setEndsAt sets the ending timestamp of the alert. -// The internal Alertmanager will use this time to know when it should automatically resolve the alert -// in case it hasn't received additional alerts. Under regular operations the scheduler will continue to send the -// alert with an updated EndsAt, if the alert is resolved then a last alert is sent with EndsAt = last evaluation time. -func (a *State) setEndsAt(alertRule *models.AlertRule, result eval.Result) { +func nextEndsTime(interval int64, evaluatedAt time.Time) time.Time { ends := ResendDelay - if alertRule.IntervalSeconds > int64(ResendDelay.Seconds()) { - ends = time.Second * time.Duration(alertRule.IntervalSeconds) + intv := time.Second * time.Duration(interval) + if intv > ResendDelay { + ends = intv } - - a.EndsAt = result.EvaluatedAt.Add(ends * 3) + return evaluatedAt.Add(3 * ends) } func (a *State) GetLabels(opts ...models.LabelOption) map[string]string { diff --git a/pkg/services/ngalert/state/state_test.go b/pkg/services/ngalert/state/state_test.go index b99fd3e32ab..45680d8c2a1 100644 --- a/pkg/services/ngalert/state/state_test.go +++ b/pkg/services/ngalert/state/state_test.go @@ -8,15 +8,341 @@ import ( "testing" "time" + "github.com/benbjohnson/clock" "github.com/golang/mock/gomock" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/screenshot" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/services/ngalert/eval" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/screenshot" ) +func TestSetAlerting(t *testing.T) { + mock := clock.NewMock() + tests := []struct { + name string + state State + reason string + startsAt time.Time + endsAt time.Time + expected State + }{{ + name: "state is set to Alerting", + reason: "this is a reason", + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Alerting, + StateReason: "this is a reason", + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }, { + name: "previous state is removed", + state: State{ + State: eval.Normal, + StateReason: "this is a reason", + Error: errors.New("this is an error"), + }, + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Alerting, + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.state + actual.SetAlerting(test.reason, test.startsAt, test.endsAt) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestSetPending(t *testing.T) { + mock := clock.NewMock() + tests := []struct { + name string + state State + reason string + startsAt time.Time + endsAt time.Time + expected State + }{{ + name: "state is set to Pending", + reason: "this is a reason", + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Pending, + StateReason: "this is a reason", + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }, { + name: "previous state is removed", + state: State{ + State: eval.Pending, + StateReason: "this is a reason", + Error: errors.New("this is an error"), + }, + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Pending, + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.state + actual.SetPending(test.reason, test.startsAt, test.endsAt) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestNormal(t *testing.T) { + mock := clock.NewMock() + tests := []struct { + name string + state State + reason string + startsAt time.Time + endsAt time.Time + expected State + }{{ + name: "state is set to Normal", + reason: "this is a reason", + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Normal, + StateReason: "this is a reason", + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }, { + name: "previous state is removed", + state: State{ + State: eval.Normal, + StateReason: "this is a reason", + Error: errors.New("this is an error"), + }, + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.Normal, + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.state + actual.SetNormal(test.reason, test.startsAt, test.endsAt) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestNoData(t *testing.T) { + mock := clock.NewMock() + tests := []struct { + name string + state State + reason string + startsAt time.Time + endsAt time.Time + expected State + }{{ + name: "state is set to No Data", + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.NoData, + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }, { + name: "previous state is removed", + state: State{ + State: eval.NoData, + StateReason: "this is a reason", + Error: errors.New("this is an error"), + }, + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + expected: State{ + State: eval.NoData, + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.state + actual.SetNoData(test.reason, test.startsAt, test.endsAt) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestSetError(t *testing.T) { + mock := clock.NewMock() + tests := []struct { + name string + state State + startsAt time.Time + endsAt time.Time + error error + expected State + }{{ + name: "state is set to Error", + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + error: errors.New("this is an error"), + expected: State{ + State: eval.Error, + StateReason: ngmodels.StateReasonError, + Error: errors.New("this is an error"), + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }, { + name: "previous state is removed", + state: State{ + State: eval.Error, + StateReason: "this is a reason", + Error: errors.New("this is an error"), + }, + startsAt: mock.Now(), + endsAt: mock.Now().Add(time.Minute), + error: errors.New("this is another error"), + expected: State{ + State: eval.Error, + StateReason: ngmodels.StateReasonError, + Error: errors.New("this is another error"), + StartsAt: mock.Now(), + EndsAt: mock.Now().Add(time.Minute), + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.state + actual.SetError(test.error, test.startsAt, test.endsAt) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestMaintain(t *testing.T) { + mock := clock.NewMock() + now := mock.Now() + + // the interval is less than the resend interval of 30 seconds + s := State{State: eval.Alerting, StartsAt: now, EndsAt: now.Add(time.Second)} + s.Maintain(10, now.Add(10*time.Second)) + // 10 seconds + 3 x 30 seconds is 100 seconds + assert.Equal(t, now.Add(100*time.Second), s.EndsAt) + + // the interval is above the resend interval of 30 seconds + s = State{State: eval.Alerting, StartsAt: now, EndsAt: now.Add(time.Second)} + s.Maintain(60, now.Add(10*time.Second)) + // 10 seconds + 3 x 60 seconds is 190 seconds + assert.Equal(t, now.Add(190*time.Second), s.EndsAt) +} + +func TestEnd(t *testing.T) { + evaluationTime, _ := time.Parse("2006-01-02", "2021-03-25") + testCases := []struct { + name string + expected time.Time + testRule *ngmodels.AlertRule + testResult eval.Result + }{ + { + name: "less than resend delay: for=unset,interval=10s - endsAt = resendDelay * 3", + expected: evaluationTime.Add(ResendDelay * 3), + testRule: &ngmodels.AlertRule{ + IntervalSeconds: 10, + }, + }, + { + name: "less than resend delay: for=0s,interval=10s - endsAt = resendDelay * 3", + expected: evaluationTime.Add(ResendDelay * 3), + testRule: &ngmodels.AlertRule{ + For: 0 * time.Second, + IntervalSeconds: 10, + }, + }, + { + name: "less than resend delay: for=10s,interval=10s - endsAt = resendDelay * 3", + expected: evaluationTime.Add(ResendDelay * 3), + testRule: &ngmodels.AlertRule{ + For: 10 * time.Second, + IntervalSeconds: 10, + }, + }, + { + name: "less than resend delay: for=10s,interval=20s - endsAt = resendDelay * 3", + expected: evaluationTime.Add(ResendDelay * 3), + testRule: &ngmodels.AlertRule{ + For: 10 * time.Second, + IntervalSeconds: 20, + }, + }, + { + name: "more than resend delay: for=unset,interval=1m - endsAt = interval * 3", + expected: evaluationTime.Add(time.Second * 60 * 3), + testRule: &ngmodels.AlertRule{ + IntervalSeconds: 60, + }, + }, + { + name: "more than resend delay: for=0s,interval=1m - endsAt = resendDelay * 3", + expected: evaluationTime.Add(time.Second * 60 * 3), + testRule: &ngmodels.AlertRule{ + For: 0 * time.Second, + IntervalSeconds: 60, + }, + }, + { + name: "more than resend delay: for=1m,interval=5m - endsAt = interval * 3", + expected: evaluationTime.Add(time.Second * 300 * 3), + testRule: &ngmodels.AlertRule{ + For: time.Minute, + IntervalSeconds: 300, + }, + }, + { + name: "more than resend delay: for=5m,interval=1m - endsAt = interval * 3", + expected: evaluationTime.Add(time.Second * 60 * 3), + testRule: &ngmodels.AlertRule{ + For: 300 * time.Second, + IntervalSeconds: 60, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := eval.Result{EvaluatedAt: evaluationTime} + assert.Equal(t, tc.expected, nextEndsTime(tc.testRule.IntervalSeconds, r.EvaluatedAt)) + }) + } +} + func TestNeedsSending(t *testing.T) { evaluationTime, _ := time.Parse("2006-01-02", "2021-03-25") testCases := []struct { @@ -144,88 +470,6 @@ func TestNeedsSending(t *testing.T) { } } -func TestSetEndsAt(t *testing.T) { - evaluationTime, _ := time.Parse("2006-01-02", "2021-03-25") - testCases := []struct { - name string - expected time.Time - testRule *ngmodels.AlertRule - testResult eval.Result - }{ - { - name: "less than resend delay: for=unset,interval=10s - endsAt = resendDelay * 3", - expected: evaluationTime.Add(ResendDelay * 3), - testRule: &ngmodels.AlertRule{ - IntervalSeconds: 10, - }, - }, - { - name: "less than resend delay: for=0s,interval=10s - endsAt = resendDelay * 3", - expected: evaluationTime.Add(ResendDelay * 3), - testRule: &ngmodels.AlertRule{ - For: 0 * time.Second, - IntervalSeconds: 10, - }, - }, - { - name: "less than resend delay: for=10s,interval=10s - endsAt = resendDelay * 3", - expected: evaluationTime.Add(ResendDelay * 3), - testRule: &ngmodels.AlertRule{ - For: 10 * time.Second, - IntervalSeconds: 10, - }, - }, - { - name: "less than resend delay: for=10s,interval=20s - endsAt = resendDelay * 3", - expected: evaluationTime.Add(ResendDelay * 3), - testRule: &ngmodels.AlertRule{ - For: 10 * time.Second, - IntervalSeconds: 20, - }, - }, - { - name: "more than resend delay: for=unset,interval=1m - endsAt = interval * 3", - expected: evaluationTime.Add(time.Second * 60 * 3), - testRule: &ngmodels.AlertRule{ - IntervalSeconds: 60, - }, - }, - { - name: "more than resend delay: for=0s,interval=1m - endsAt = resendDelay * 3", - expected: evaluationTime.Add(time.Second * 60 * 3), - testRule: &ngmodels.AlertRule{ - For: 0 * time.Second, - IntervalSeconds: 60, - }, - }, - { - name: "more than resend delay: for=1m,interval=5m - endsAt = interval * 3", - expected: evaluationTime.Add(time.Second * 300 * 3), - testRule: &ngmodels.AlertRule{ - For: time.Minute, - IntervalSeconds: 300, - }, - }, - { - name: "more than resend delay: for=5m,interval=1m - endsAt = interval * 3", - expected: evaluationTime.Add(time.Second * 60 * 3), - testRule: &ngmodels.AlertRule{ - For: 300 * time.Second, - IntervalSeconds: 60, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := &State{} - r := eval.Result{EvaluatedAt: evaluationTime} - s.setEndsAt(tc.testRule, r) - assert.Equal(t, tc.expected, s.EndsAt) - }) - } -} - func TestGetLastEvaluationValuesForCondition(t *testing.T) { genState := func(results []Evaluation) *State { return &State{ From dfc15163daae890ab915e9b5ef5b36c87a1190c8 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:15:05 -0600 Subject: [PATCH 81/94] Prometheus: Fix exemplar fill color to match series color in time series. (#59908) * Implementing new optional color property to ExemplarMarker component * Refactor some changes from #59743 --- .../panel/timeseries/TimeSeriesPanel.tsx | 2 +- .../timeseries/plugins/ExemplarMarker.tsx | 24 +++++- .../plugins/ExemplarsPlugin.test.tsx | 21 ++++- .../timeseries/plugins/ExemplarsPlugin.tsx | 85 ++++++++++++++----- 4 files changed, 101 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 2a9367e96ca..bf757319022 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -133,7 +133,7 @@ export const TimeSeriesPanel: React.FC = ({ )} {data.annotations && ( Array>; + exemplarColor?: string; } export const ExemplarMarker: React.FC = ({ @@ -30,6 +31,7 @@ export const ExemplarMarker: React.FC = ({ dataFrameFieldIndex, config, getFieldLinks, + exemplarColor, }) => { const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); @@ -40,19 +42,33 @@ export const ExemplarMarker: React.FC = ({ const getSymbol = () => { const symbols = [ - , + , , - , - , - , + , + , , + , ]; + return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; }; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx index 6e24c04d8fe..cd3436bddf2 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx @@ -1,7 +1,7 @@ -import { Field, Labels, MutableDataFrame } from '@grafana/data/src'; +import { MutableDataFrame, Field } from '@grafana/data/src'; import { UPlotConfigBuilder } from '@grafana/ui/src'; -import { getVisibleLabels } from './ExemplarsPlugin'; +import { getVisibleLabels, VisibleExemplarLabels } from './ExemplarsPlugin'; describe('getVisibleLabels()', () => { const dataFrameSeries1 = new MutableDataFrame({ @@ -84,9 +84,22 @@ describe('getVisibleLabels()', () => { } as UPlotConfigBuilder; it('function should only return labels associated with actively visible series', () => { - const expected: { labels: Labels[]; totalSeriesCount: number } = { + const expected: VisibleExemplarLabels = { totalSeriesCount: 3, - labels: [{ job: 'tns/app' }, { job: 'tns/db' }], + labels: [ + { + color: '', + labels: { + job: 'tns/app', + }, + }, + { + color: '', + labels: { + job: 'tns/db', + }, + }, + ], }; // Base case diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 1952863eb3c..91d3647cae8 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -20,7 +20,7 @@ interface ExemplarsPluginProps { exemplars: DataFrame[]; timeZone: TimeZone; getFieldLinks: (field: Field, rowIndex: number) => Array>; - visibleLabels?: { labels: Labels[]; totalSeriesCount: number }; + visibleSeries?: VisibleExemplarLabels; } export const ExemplarsPlugin: React.FC = ({ @@ -28,7 +28,7 @@ export const ExemplarsPlugin: React.FC = ({ timeZone, getFieldLinks, config, - visibleLabels, + visibleSeries, }) => { const plotInstance = useRef(); @@ -71,9 +71,11 @@ export const ExemplarsPlugin: React.FC = ({ const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - // If the parent provided series/labels: filter the exemplars, otherwise default to show all exemplars - let showMarker = - visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true; + const showMarker = + visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, dataFrameFieldIndex) : true; + + const markerColor = + visibleSeries !== undefined ? getExemplarColor(dataFrame, dataFrameFieldIndex, visibleSeries) : undefined; if (!showMarker) { return <>; @@ -86,10 +88,11 @@ export const ExemplarsPlugin: React.FC = ({ dataFrame={dataFrame} dataFrameFieldIndex={dataFrameFieldIndex} config={config} + exemplarColor={markerColor} /> ); }, - [config, timeZone, getFieldLinks, visibleLabels] + [config, timeZone, getFieldLinks, visibleSeries] ); return ( @@ -103,15 +106,13 @@ export const ExemplarsPlugin: React.FC = ({ ); }; +export type VisibleExemplarLabels = { labels: LabelWithExemplarUIData[]; totalSeriesCount: number }; /** - * Function to get labels that are currently displayed in the legend + * Get labels that are currently visible/active in the legend */ -export const getVisibleLabels = ( - config: UPlotConfigBuilder, - frames: DataFrame[] | null -): { labels: Labels[]; totalSeriesCount: number } => { +export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] | null): VisibleExemplarLabels => { const visibleSeries = config.series.filter((series) => series.props.show); - const visibleLabels: Labels[] = []; + const visibleLabels: LabelWithExemplarUIData[] = []; if (frames?.length) { visibleSeries.forEach((plotInstance) => { const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex; @@ -121,7 +122,10 @@ export const getVisibleLabels = ( const field = frames[frameIndex].fields[fieldIndex]; if (field.labels) { // Note that this may be an empty object in the case of a metric being rendered with no labels - visibleLabels.push(field.labels); + visibleLabels.push({ + labels: field.labels, + color: plotInstance.props?.lineColor ?? '', + }); } } }); @@ -130,22 +134,59 @@ export const getVisibleLabels = ( return { labels: visibleLabels, totalSeriesCount: config.series.length }; }; +interface LabelWithExemplarUIData { + labels: Labels; + color?: string; +} +/** + * Get color of active series in legend + */ +const getExemplarColor = ( + dataFrame: DataFrame, + dataFrameFieldIndex: DataFrameFieldIndex, + visibleLabels: VisibleExemplarLabels +) => { + let exemplarColor; + visibleLabels.labels.some((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel.labels); + const fields = dataFrame.fields.filter((field) => { + return labelKeys.find((labelKey) => labelKey === field.name); + }); + if (fields.length) { + const hasMatch = fields.every((field, index, fields) => { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + return visibleLabel.labels[field.name] === value; + }); + + if (hasMatch) { + exemplarColor = visibleLabel.color; + return true; + } + } + return false; + }); + return exemplarColor; +}; + /** * Determine if the current exemplar marker is filtered by what series are selected in the legend UI */ const showExemplarMarker = ( - visibleLabels: { labels: Labels[]; totalSeriesCount: number }, + visibleSeries: VisibleExemplarLabels, dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex ) => { let showMarker = false; - if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { + // If all series are visible, don't filter any exemplars + if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) { showMarker = true; } else { - visibleLabels.labels.forEach((visibleLabel) => { - const labelKeys = Object.keys(visibleLabel); + visibleSeries.labels.forEach((visibleLabel) => { + // Get the label names + const labelKeys = Object.keys(visibleLabel.labels); + // If there aren't any labels, the graph is only displaying a single series with exemplars, let's show all exemplars in this case as well - if (Object.keys(visibleLabel).length === 0) { + if (Object.keys(visibleLabel.labels).length === 0) { showMarker = true; } else { // If there are labels, lets only show the exemplars with labels associated with series that are currently visible @@ -153,11 +194,11 @@ const showExemplarMarker = ( return labelKeys.find((labelKey) => labelKey === field.name); }); - // Check to see if at least one value matches each field if (fields.length) { - showMarker = visibleLabels.labels.some((series) => { - return Object.keys(series).every((label) => { - const value = series[label]; + // Check to see if at least one value matches each field + showMarker = visibleSeries.labels.some((series) => { + return Object.keys(series.labels).every((label) => { + const value = series.labels[label]; return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value); }); }); From e85763065cac63ddb0bec4c96a7d2b802eab8e13 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:20:27 -0600 Subject: [PATCH 82/94] Prometheus: Exemplar optimization (#60055) replace inefficient forEach --- .../app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 91d3647cae8..8f15fae1e88 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -181,7 +181,7 @@ const showExemplarMarker = ( if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) { showMarker = true; } else { - visibleSeries.labels.forEach((visibleLabel) => { + visibleSeries.labels.some((visibleLabel) => { // Get the label names const labelKeys = Object.keys(visibleLabel.labels); @@ -204,6 +204,7 @@ const showExemplarMarker = ( }); } } + return showMarker; }); } return showMarker; From 896cb9ddceae6c02811a33e8a323b877cf100151 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:09:29 +0200 Subject: [PATCH 83/94] Fix index out of range in regenerateLinksSupplier (#60011) --- public/app/plugins/panel/timeseries/utils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index f8008bb0c27..316c765c633 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -142,20 +142,16 @@ export function regenerateLinksSupplier( timeZone: string ): DataFrame { alignedDataFrame.fields.forEach((field) => { - const frameIndex = field.state?.origin?.frameIndex; - - if (frameIndex === undefined) { + if (field.state?.origin?.frameIndex === undefined || frames[field.state?.origin?.frameIndex] === undefined) { return; } - const frame = frames[frameIndex]; - const tempFields: Field[] = []; - /* check if field has sortedVector values if it does, sort all string fields in the original frame by the order array already used for the field otherwise just attach the fields to the temporary frame used to get the links */ - for (const frameField of frame.fields) { + const tempFields: Field[] = []; + for (const frameField of frames[field.state?.origin?.frameIndex].fields) { if (frameField.type === FieldType.string) { if (field.values instanceof SortedVector) { const copiedField = { ...frameField }; From 2db8ed94410936c9856cee7626fd1747a9ea2ebe Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 9 Dec 2022 08:11:56 +0100 Subject: [PATCH 84/94] Chore: All tests under pkg/tests should be integration tests (#59521) * Chore: All tests under pkg/tests should be integrationtests * run alerting integration tests only for sqlite --- pkg/infra/db/db.go | 8 +++++ .../alerting/api_admin_configuration_test.go | 4 ++- .../api_alertmanager_configuration_test.go | 8 +++-- .../api/alerting/api_alertmanager_test.go | 32 ++++++++++++++----- .../alerting/api_available_channel_test.go | 4 ++- .../alerting/api_notification_channel_test.go | 12 +++++-- pkg/tests/api/alerting/api_prometheus_test.go | 12 +++++-- .../api/alerting/api_provisioning_test.go | 8 +++-- pkg/tests/api/alerting/api_ruler_test.go | 20 +++++++++--- .../api/dashboards/api_dashboards_test.go | 12 +++++-- pkg/tests/api/plugins/api_plugins_test.go | 6 +++- pkg/tests/testinfra/testinfra.go | 8 +++++ pkg/tests/web/index_view_test.go | 8 +++-- 13 files changed, 111 insertions(+), 31 deletions(-) diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index d85b7db05e9..14e3f654504 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -31,6 +31,14 @@ var InitTestDBwithCfg = sqlstore.InitTestDBWithCfg var ProvideService = sqlstore.ProvideService var NewSqlBuilder = sqlstore.NewSqlBuilder +func IsTestDbSQLite() bool { + if db, present := os.LookupEnv("GRAFANA_TEST_DB"); !present || db == "sqlite" { + return true + } + + return !IsTestDbMySQL() && !IsTestDbPostgres() +} + func IsTestDbMySQL() bool { if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { return db == migrator.MySQL diff --git a/pkg/tests/api/alerting/api_admin_configuration_test.go b/pkg/tests/api/alerting/api_admin_configuration_test.go index 1633da33995..aa26e9ef568 100644 --- a/pkg/tests/api/alerting/api_admin_configuration_test.go +++ b/pkg/tests/api/alerting/api_admin_configuration_test.go @@ -25,7 +25,9 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestAdminConfiguration_SendingToExternalAlertmanagers(t *testing.T) { +func TestIntegrationAdminConfiguration_SendingToExternalAlertmanagers(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + const disableOrgID int64 = 3 dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index e3daa09d5ea..59b06869c94 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -20,7 +20,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestAlertmanagerConfigurationIsTransactional(t *testing.T) { +func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -137,7 +139,9 @@ func TestAlertmanagerConfigurationIsTransactional(t *testing.T) { } } -func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) { +func TestIntegrationAlertmanagerConfigurationPersistSecrets(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index b6815a3278f..d7aff484a71 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -34,7 +34,9 @@ type Response struct { TraceID string `json:"traceID"` } -func TestAMConfigAccess(t *testing.T) { +func TestIntegrationAMConfigAccess(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -392,7 +394,9 @@ func TestAMConfigAccess(t *testing.T) { }) } -func TestAlertAndGroupsQuery(t *testing.T) { +func TestIntegrationAlertAndGroupsQuery(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -549,7 +553,9 @@ func TestAlertAndGroupsQuery(t *testing.T) { } } -func TestRulerAccess(t *testing.T) { +func TestIntegrationRulerAccess(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -664,7 +670,9 @@ func TestRulerAccess(t *testing.T) { } } -func TestDeleteFolderWithRules(t *testing.T) { +func TestIntegrationDeleteFolderWithRules(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -824,7 +832,9 @@ func TestDeleteFolderWithRules(t *testing.T) { } } -func TestAlertRuleCRUD(t *testing.T) { +func TestIntegrationAlertRuleCRUD(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -1802,7 +1812,9 @@ func TestAlertRuleCRUD(t *testing.T) { } } -func TestAlertmanagerStatus(t *testing.T) { +func TestIntegrationAlertmanagerStatus(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -1865,7 +1877,9 @@ func TestAlertmanagerStatus(t *testing.T) { } } -func TestQuota(t *testing.T) { +func TestIntegrationQuota(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -2064,7 +2078,9 @@ func TestQuota(t *testing.T) { }) } -func TestEval(t *testing.T) { +func TestIntegrationEval(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/alerting/api_available_channel_test.go b/pkg/tests/api/alerting/api_available_channel_test.go index bbf605de55c..bd1ba992612 100644 --- a/pkg/tests/api/alerting/api_available_channel_test.go +++ b/pkg/tests/api/alerting/api_available_channel_test.go @@ -15,7 +15,9 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestAvailableChannels(t *testing.T) { +func TestIntegrationAvailableChannels(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index d743bb4123d..3fc09ffba95 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -31,7 +31,9 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestTestReceivers(t *testing.T) { +func TestIntegrationTestReceivers(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + t.Run("assert no receivers returns 400 Bad Request", func(t *testing.T) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ @@ -423,7 +425,9 @@ func TestTestReceivers(t *testing.T) { }) } -func TestTestReceiversAlertCustomization(t *testing.T) { +func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + t.Run("assert custom annotations and labels are sent", func(t *testing.T) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ @@ -693,7 +697,9 @@ func TestTestReceiversAlertCustomization(t *testing.T) { }) } -func TestNotificationChannels(t *testing.T) { +func TestIntegrationNotificationChannels(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 2f5672756f3..64dbfe9387f 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -24,7 +24,9 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestPrometheusRules(t *testing.T) { +func TestIntegrationPrometheusRules(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -319,7 +321,9 @@ func TestPrometheusRules(t *testing.T) { } } -func TestPrometheusRulesFilterByDashboard(t *testing.T) { +func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ EnableFeatureToggles: []string{"ngalert"}, DisableAnonymous: true, @@ -612,7 +616,9 @@ func TestPrometheusRulesFilterByDashboard(t *testing.T) { } } -func TestPrometheusRulesPermissions(t *testing.T) { +func TestIntegrationPrometheusRulesPermissions(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 33c4495a608..ce115c647ee 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -13,7 +13,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestProvisioning(t *testing.T) { +func TestIntegrationProvisioning(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -59,7 +61,7 @@ func TestProvisioning(t *testing.T) { "name": "test-receiver", "type": "slack", "settings": { - "recipient": "value_recipient", + "recipient": "value_recipient", "token": "value_token" } }` @@ -157,7 +159,7 @@ func TestProvisioning(t *testing.T) { "name": "my-contact-point", "type": "slack", "settings": { - "recipient": "value_recipient", + "recipient": "value_recipient", "token": "value_token" } }` diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 4e3186c9f99..3aa8b53617b 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -22,7 +22,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func TestAlertRulePermissions(t *testing.T) { +func TestIntegrationAlertRulePermissions(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -311,7 +313,9 @@ func createRule(t *testing.T, client apiClient, folder string) { require.JSONEq(t, `{"message":"rule group updated successfully"}`, body) } -func TestAlertRuleConflictingTitle(t *testing.T) { +func TestIntegrationAlertRuleConflictingTitle(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -382,7 +386,9 @@ func TestAlertRuleConflictingTitle(t *testing.T) { }) } -func TestRulerRulesFilterByDashboard(t *testing.T) { +func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ EnableFeatureToggles: []string{"ngalert"}, DisableAnonymous: true, @@ -719,7 +725,9 @@ func TestRulerRulesFilterByDashboard(t *testing.T) { } } -func TestRuleGroupSequence(t *testing.T) { +func TestIntegrationRuleGroupSequence(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, @@ -815,7 +823,9 @@ func TestRuleGroupSequence(t *testing.T) { }) } -func TestRuleUpdate(t *testing.T) { +func TestIntegrationRuleUpdate(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 5eefc5c15f5..65bd8f4e4cc 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -29,7 +29,11 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestDashboardQuota(t *testing.T) { +func TestIntegrationDashboardQuota(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + // enable quota and set low dashboard quota // Setup Grafana and its Database dashboardQuota := int64(1) @@ -114,7 +118,11 @@ func createUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserComma return u.ID } -func TestUpdatingProvisionionedDashboards(t *testing.T) { +func TestIntegrationUpdatingProvisionionedDashboards(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 0ebffe901b3..251c9389310 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -30,7 +30,11 @@ const ( var updateSnapshotFlag = false -func TestPlugins(t *testing.T) { +func TestIntegrationPlugins(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ PluginAdminEnabled: true, }) diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index eb565baa74b..9d1550f56eb 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -345,6 +345,14 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { return tmpDir, cfgPath } +func SQLiteIntegrationTest(t *testing.T) { + t.Helper() + + if testing.Short() || !db.IsTestDbSQLite() { + t.Skip("skipping integration test") + } +} + type GrafanaOpts struct { EnableCSP bool EnableFeatureToggles []string diff --git a/pkg/tests/web/index_view_test.go b/pkg/tests/web/index_view_test.go index 72cf4376b30..e2ef840ede6 100644 --- a/pkg/tests/web/index_view_test.go +++ b/pkg/tests/web/index_view_test.go @@ -12,8 +12,12 @@ import ( "github.com/stretchr/testify/require" ) -// TestIndexView tests the Grafana index view. -func TestIndexView(t *testing.T) { +// TestIntegrationIndexView tests the Grafana index view. +func TestIntegrationIndexView(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + t.Run("CSP enabled", func(t *testing.T) { grafDir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ EnableCSP: true, From 4893acaeb4888f09dac82469e961497f0d8f32a6 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Fri, 9 Dec 2022 11:50:13 +0400 Subject: [PATCH 85/94] EntityAPI: disable integration tests (#60073) disable entity api int tests --- pkg/services/store/entity/tests/server_integration_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/store/entity/tests/server_integration_test.go b/pkg/services/store/entity/tests/server_integration_test.go index c83ff12f6ae..29d7762e1d3 100644 --- a/pkg/services/store/entity/tests/server_integration_test.go +++ b/pkg/services/store/entity/tests/server_integration_test.go @@ -119,6 +119,11 @@ func requireVersionMatch(t *testing.T, obj *entity.EntityVersionInfo, m objectVe } func TestIntegrationEntityServer(t *testing.T) { + if true { + // FIXME + t.Skip() + } + if testing.Short() { t.Skip("skipping integration test") } From a6e8f651acce4dfd6cd54206589a51f0a1fc7de6 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Fri, 9 Dec 2022 03:07:34 -0500 Subject: [PATCH 86/94] [DOC] Change AWS to Google Cloud Monitoring (#60065) Change AWS to GCP --- docs/sources/datasources/google-cloud-monitoring/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/google-cloud-monitoring/_index.md b/docs/sources/datasources/google-cloud-monitoring/_index.md index 49fe7d0ff44..26482b21147 100644 --- a/docs/sources/datasources/google-cloud-monitoring/_index.md +++ b/docs/sources/datasources/google-cloud-monitoring/_index.md @@ -38,7 +38,7 @@ Once you've added the Google Cloud Monitoring data source, you can [configure it 1. Hover the cursor over the **Configuration** (gear) icon. 1. Select **Data Sources**. -1. Select the AWS CloudWatch data source. +1. Select the **Google Cloud Monitoring** data source. Set the data source's basic configuration options carefully: From a7a2c0f373539aca4fc97ed2f86b9674b2068c27 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 9 Dec 2022 09:18:19 +0100 Subject: [PATCH 87/94] Linting: Speed up betterer by sharing eslint instances by groups (#60019) --- .betterer.ts | 104 ++++++++++++++++++++++++++++++++------------------- package.json | 1 + yarn.lock | 11 ++++++ 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/.betterer.ts b/.betterer.ts index 6d40d7a9179..bf1d956fe3b 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -2,6 +2,9 @@ import { regexp } from '@betterer/regexp'; import { BettererFileTest } from '@betterer/betterer'; import { ESLint, Linter } from 'eslint'; import { existsSync } from 'fs'; +import { exec } from 'child_process'; +import path from 'path'; +import glob from 'glob'; export default { 'no enzyme tests': () => regexp(/from 'enzyme'/g).include('**/*.test.*'), @@ -22,54 +25,79 @@ function countUndocumentedStories() { }); } +async function findEslintConfigFiles(): Promise { + return new Promise((resolve, reject) => { + glob('**/.eslintrc', (err, files) => { + if (err) { + reject(err); + } + resolve(files); + }); + }); +} + function countEslintErrors() { return new BettererFileTest(async (filePaths, fileTestResult, resolver) => { const { baseDirectory } = resolver; const cli = new ESLint({ cwd: baseDirectory }); - await Promise.all( - filePaths.map(async (filePath) => { - const linterOptions = (await cli.calculateConfigForFile(filePath)) as Linter.Config; + const eslintConfigFiles = await findEslintConfigFiles(); + const eslintConfigMainPaths = eslintConfigFiles.map((file) => path.resolve(path.dirname(file))); - const rules: Partial = { - '@typescript-eslint/no-explicit-any': 'error', - }; + const baseRules: Partial = { + '@typescript-eslint/no-explicit-any': 'error', + }; - const isTestFile = - filePath.endsWith('.test.tsx') || - filePath.endsWith('.test.ts') || - filePath.includes('__mocks__') || - filePath.includes('public/test/'); + const nonTestFilesRules: Partial = { + ...baseRules, + '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }], + }; - if (!isTestFile) { - rules['@typescript-eslint/consistent-type-assertions'] = [ - 'error', - { - assertionStyle: 'never', - }, - ]; - } + // group files by eslint config file + // this will create two file groups for each eslint config file + // one for test files and one for non-test files + const fileGroups: Record = {}; - const runner = new ESLint({ - baseConfig: { - ...linterOptions, - rules, - }, - useEslintrc: false, - cwd: baseDirectory, - }); + for (const filePath of filePaths) { + let configPath = eslintConfigMainPaths.find((configPath) => filePath.startsWith(configPath)) ?? ''; + const isTestFile = + filePath.endsWith('.test.tsx') || + filePath.endsWith('.test.ts') || + filePath.includes('__mocks__') || + filePath.includes('public/test/'); - const lintResults = await runner.lintFiles([filePath]); - lintResults - .filter((lintResult) => lintResult.source) - .forEach((lintResult) => { - const { messages } = lintResult; - const file = fileTestResult.addFile(filePath, ''); - messages.forEach((message, index) => { - file.addIssue(0, 0, message.message, `${index}`); - }); + if (isTestFile) { + configPath += '-test'; + } + if (!fileGroups[configPath]) { + fileGroups[configPath] = []; + } + fileGroups[configPath].push(filePath); + } + + for (const configPath of Object.keys(fileGroups)) { + const rules = configPath.endsWith('-test') ? baseRules : nonTestFilesRules; + // this is by far the slowest part of this code. It takes eslint about 2 seconds just to find the config + const linterOptions = (await cli.calculateConfigForFile(fileGroups[configPath][0])) as Linter.Config; + const runner = new ESLint({ + baseConfig: { + ...linterOptions, + rules: rules, + }, + useEslintrc: false, + cwd: baseDirectory, + }); + const lintResults = await runner.lintFiles(fileGroups[configPath]); + lintResults + .filter((lintResult) => lintResult.source) + .forEach((lintResult) => { + const { messages } = lintResult; + const filePath = lintResult.filePath; + const file = fileTestResult.addFile(filePath, ''); + messages.forEach((message, index) => { + file.addIssue(0, 0, message.message, `${index}`); }); - }) - ); + }); + } }); } diff --git a/package.json b/package.json index 979ef5b3ae0..044f5c0a9ae 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "@types/enzyme-adapter-react-16": "1.0.6", "@types/eslint": "8.4.9", "@types/file-saver": "2.0.5", + "@types/glob": "^8.0.0", "@types/google.analytics": "^0.0.42", "@types/gtag.js": "^0.0.12", "@types/history": "4.7.11", diff --git a/yarn.lock b/yarn.lock index 6f4ab0374b9..9e9cc78e692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10781,6 +10781,16 @@ __metadata: languageName: node linkType: hard +"@types/glob@npm:^8.0.0": + version: 8.0.0 + resolution: "@types/glob@npm:8.0.0" + dependencies: + "@types/minimatch": "*" + "@types/node": "*" + checksum: 1817b05f5a8aed851d102a65b5e926d5c777bef927ea62b36d635860eef5364f2046bb5a692d135b6f2b28f34e4a9d44ade9396122c0845bcc7636d35f624747 + languageName: node + linkType: hard + "@types/google.analytics@npm:^0.0.42": version: 0.0.42 resolution: "@types/google.analytics@npm:0.0.42" @@ -21518,6 +21528,7 @@ __metadata: "@types/enzyme-adapter-react-16": 1.0.6 "@types/eslint": 8.4.9 "@types/file-saver": 2.0.5 + "@types/glob": ^8.0.0 "@types/google.analytics": ^0.0.42 "@types/gtag.js": ^0.0.12 "@types/history": 4.7.11 From c91c828c462e52b35fe7d9b71ecb7f9730282f30 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 9 Dec 2022 09:45:38 +0000 Subject: [PATCH 88/94] Renovate: use rebaseWhen: conflicted to try to speed up renovate runs (#60028) --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index a5396c0faa9..8e2f1bd364e 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -80,6 +80,7 @@ "enabled": false }, "prConcurrentLimit": 10, + "rebaseWhen": "conflicted", "reviewers": ["team:grafana/frontend-ops"], "separateMajorMinor": false, "vulnerabilityAlerts": { From 10c2bdea3eec6c143bf59e326eacf000cdbb78d8 Mon Sep 17 00:00:00 2001 From: Manoj Vivek Date: Fri, 9 Dec 2022 10:01:39 +0000 Subject: [PATCH 89/94] Parca: Logo image fixed with theme agnostic colors (#59835) --- public/app/plugins/datasource/parca/img/logo-small.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/parca/img/logo-small.svg b/public/app/plugins/datasource/parca/img/logo-small.svg index b2f64efb565..6e31daa1c63 100644 --- a/public/app/plugins/datasource/parca/img/logo-small.svg +++ b/public/app/plugins/datasource/parca/img/logo-small.svg @@ -1 +1 @@ - + From 0c9aed905ab5a96fab398c00a46019fd49cc0d81 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 9 Dec 2022 10:07:32 +0000 Subject: [PATCH 90/94] Chore: normalize monaco colors to hex string (#59784) normalize monaco colors to hex string --- .../grafana-ui/src/components/Monaco/theme.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/components/Monaco/theme.ts b/packages/grafana-ui/src/components/Monaco/theme.ts index d073f40377e..d5d14dd3e30 100644 --- a/packages/grafana-ui/src/components/Monaco/theme.ts +++ b/packages/grafana-ui/src/components/Monaco/theme.ts @@ -1,3 +1,5 @@ +import tinycolor from 'tinycolor2'; + import { GrafanaTheme2 } from '@grafana/data'; import { Monaco, monacoTypes } from './types'; @@ -6,13 +8,24 @@ function getColors(theme?: GrafanaTheme2): monacoTypes.editor.IColors { if (theme === undefined) { return {}; } else { - return { + const colors: Record = { 'editor.background': theme.components.input.background, 'minimap.background': theme.colors.background.secondary, }; + + Object.keys(colors).forEach((resultKey) => { + colors[resultKey] = normalizeColorForMonaco(colors[resultKey]); + }); + return colors; } } +function normalizeColorForMonaco(color?: string): string { + // monaco needs 6char hex colors + // see https://github.com/grafana/grafana/issues/43158 + return tinycolor(color).toHexString(); +} + // we support calling this without a theme, it will make sure the themes // are registered in monaco, even if the colors are not perfect. export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) { @@ -24,9 +37,9 @@ export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) { colors: colors, // fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math) rules: [ - { token: 'predefined', foreground: theme?.visualization.getColorByName('purple') }, - { token: 'operator', foreground: theme?.visualization.getColorByName('orange') }, - { token: 'tag', foreground: theme?.visualization.getColorByName('green') }, + { token: 'predefined', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('purple')) }, + { token: 'operator', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('orange')) }, + { token: 'tag', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('green')) }, ], }); @@ -36,9 +49,9 @@ export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) { colors: colors, // fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math) rules: [ - { token: 'predefined', foreground: theme?.visualization.getColorByName('purple') }, - { token: 'operator', foreground: theme?.visualization.getColorByName('orange') }, - { token: 'tag', foreground: theme?.visualization.getColorByName('green') }, + { token: 'predefined', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('purple')) }, + { token: 'operator', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('orange')) }, + { token: 'tag', foreground: normalizeColorForMonaco(theme?.visualization.getColorByName('green')) }, ], }); } From 6280780e0c54656f64cb934f159ecfc91617e0ea Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 9 Dec 2022 11:28:57 +0000 Subject: [PATCH 91/94] TimeRangePicker: Fix recent ranges not showing all items (#59836) * Fix not all recently used time ranges showing * Refactor time picker history to store simpler json objects * Don't store duplicate items * update todo tests: * Add tests for TimePickerWithHistory * better fix for focus scope issues in test --- .../DateTimePickers/TimeRangePicker.tsx | 8 +- .../TimeRangePicker/TimePickerContent.tsx | 4 +- .../components/Dropdown/ButtonSelect.test.tsx | 56 ++++++++ .../TimePicker/TimePickerWithHistory.test.tsx | 132 ++++++++++++++++++ .../TimePicker/TimePickerWithHistory.tsx | 58 +++++--- 5 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/Dropdown/ButtonSelect.test.tsx create mode 100644 public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 131ac5db6a7..ba5968020a0 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -122,8 +122,8 @@ export function TimeRangePicker(props: TimeRangePickerProps) { {isOpen && ( - -
+
+ -
- + +
)} {timeSyncButton} diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx index 519af17d1d9..5de2391d182 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx @@ -160,7 +160,6 @@ const NarrowScreenForm = (props: FormProps) => {
-

{showHistory && ( mapRangeToTimeOption(range, timeZone)); + + return ranges.map((range) => mapRangeToTimeOption(range, timeZone)); } EmptyRecentList.displayName = 'EmptyRecentList'; diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.test.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.test.tsx new file mode 100644 index 00000000000..f1c5c63a055 --- /dev/null +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { SelectableValue } from '@grafana/data'; + +import { ButtonSelect } from './ButtonSelect'; + +const OPTIONS: SelectableValue[] = [ + { + label: 'Hello', + value: 'a', + }, + { + label: 'World', + value: 'b', + }, +]; + +describe('ButtonSelect', () => { + it('initially renders the selected value with the menu closed', () => { + const selected = OPTIONS[0]; + render( {}} />); + + expect(screen.getByText('Hello')).toBeInTheDocument(); + expect(screen.queryAllByRole('menuitemradio')).toHaveLength(0); + }); + + it('opens the menu when clicking the button', async () => { + const selected = OPTIONS[0]; + render( {}} />); + + const button = screen.getByText('Hello'); + await userEvent.click(button); + + expect(screen.queryAllByRole('menuitemradio')).toHaveLength(2); + }); + + it('closes the menu when clicking an option', async () => { + const selected = OPTIONS[0]; + const onChange = jest.fn(); + render(); + + const button = screen.getByText('Hello'); + await userEvent.click(button); + + const option = screen.getByText('World'); + await userEvent.click(option); + + expect(screen.queryAllByRole('menuitemradio')).toHaveLength(0); + expect(onChange).toHaveBeenCalledWith({ + label: 'World', + value: 'b', + }); + }); +}); diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx new file mode 100644 index 00000000000..c0f294619d5 --- /dev/null +++ b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx @@ -0,0 +1,132 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { getDefaultTimeRange } from '@grafana/data'; + +import { TimePickerWithHistory } from './TimePickerWithHistory'; + +describe('TimePickerWithHistory', () => { + // In some of the tests we close and re-open the picker. When we do that we must re-find these inputs + // as new elements will have been mounted + const getFromField = () => screen.getByLabelText('Time Range from field'); + const getToField = () => screen.getByLabelText('Time Range to field'); + const getApplyButton = () => screen.getByRole('button', { name: 'Apply time range' }); + + const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history'; + const OLD_LOCAL_STORAGE = [ + { + from: '2022-12-03T00:00:00.000Z', + to: '2022-12-03T23:59:59.000Z', + raw: { from: '2022-12-03T00:00:00.000Z', to: '2022-12-03T23:59:59.000Z' }, + }, + { + from: '2022-12-02T00:00:00.000Z', + to: '2022-12-02T23:59:59.000Z', + raw: { from: '2022-12-02T00:00:00.000Z', to: '2022-12-02T23:59:59.000Z' }, + }, + ]; + + const NEW_LOCAL_STORAGE = [ + { from: '2022-12-03T00:00:00.000Z', to: '2022-12-03T23:59:59.000Z' }, + { from: '2022-12-02T00:00:00.000Z', to: '2022-12-02T23:59:59.000Z' }, + ]; + + const props = { + timeZone: 'utc', + onChange: () => {}, + onChangeTimeZone: () => {}, + onMoveBackward: () => {}, + onMoveForward: () => {}, + onZoom: () => {}, + }; + + afterEach(() => window.localStorage.clear()); + + it('Should load with no history', async () => { + const timeRange = getDefaultTimeRange(); + render(); + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + expect(screen.getByText(/It looks like you haven't used this time picker before/i)).toBeInTheDocument(); + }); + + it('Should load with old TimeRange history', async () => { + window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(OLD_LOCAL_STORAGE)); + + const timeRange = getDefaultTimeRange(); + render(); + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + expect(screen.getByText(/2022-12-03 00:00:00 to 2022-12-03 23:59:59/i)).toBeInTheDocument(); + expect(screen.queryByText(/2022-12-02 00:00:00 to 2022-12-02 23:59:59/i)).toBeInTheDocument(); + }); + + it('Should load with new TimePickerHistoryItem history', async () => { + window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(NEW_LOCAL_STORAGE)); + + const timeRange = getDefaultTimeRange(); + render(); + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + expect(screen.queryByText(/2022-12-03 00:00:00 to 2022-12-03 23:59:59/i)).toBeInTheDocument(); + expect(screen.queryByText(/2022-12-02 00:00:00 to 2022-12-02 23:59:59/i)).toBeInTheDocument(); + }); + + it('Saves changes into local storage without duplicates', async () => { + const timeRange = getDefaultTimeRange(); + render(); + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + await clearAndType(getFromField(), '2022-12-03 00:00:00'); + await clearAndType(getToField(), '2022-12-03 23:59:59'); + await userEvent.click(getApplyButton()); + + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + // Same range again! + await clearAndType(getFromField(), '2022-12-03 00:00:00'); + await clearAndType(getToField(), '2022-12-03 23:59:59'); + await userEvent.click(getApplyButton()); + + const newLsValue = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY) ?? '[]'); + expect(newLsValue).toEqual([{ from: '2022-12-03T00:00:00.000Z', to: '2022-12-03T23:59:59.000Z' }]); + }); + + it('Should show 4 most recently used time ranges', async () => { + const inputRanges: Array<[string, string]> = [ + ['2022-12-10 00:00:00', '2022-12-10 23:59:59'], + ['2022-12-11 00:00:00', '2022-12-11 23:59:59'], + ['2022-12-12 00:00:00', '2022-12-12 23:59:59'], + ['2022-12-13 00:00:00', '2022-12-13 23:59:59'], + ['2022-12-14 00:00:00', '2022-12-14 23:59:59'], + ]; + + const expectedLocalStorage = [ + { from: '2022-12-14T00:00:00.000Z', to: '2022-12-14T23:59:59.000Z' }, + { from: '2022-12-13T00:00:00.000Z', to: '2022-12-13T23:59:59.000Z' }, + { from: '2022-12-12T00:00:00.000Z', to: '2022-12-12T23:59:59.000Z' }, + { from: '2022-12-11T00:00:00.000Z', to: '2022-12-11T23:59:59.000Z' }, + ]; + + const timeRange = getDefaultTimeRange(); + render(); + await userEvent.click(screen.getByLabelText(/Time range selected/)); + + for (const [inputFrom, inputTo] of inputRanges) { + await userEvent.click(screen.getByLabelText(/Time range selected/)); + await clearAndType(getFromField(), inputFrom); + await clearAndType(getToField(), inputTo); + + await userEvent.click(getApplyButton()); + } + + const newLsValue = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY) ?? '[]'); + expect(newLsValue).toEqual(expectedLocalStorage); + }); +}); + +async function clearAndType(field: HTMLElement, text: string) { + await userEvent.clear(field); + return await userEvent.type(field, text); +} diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx index 4cacb3dda58..4a18ada7875 100644 --- a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx +++ b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx @@ -1,6 +1,7 @@ +import { uniqBy } from 'lodash'; import React from 'react'; -import { TimeRange, isDateTime, toUtc } from '@grafana/data'; +import { TimeRange, isDateTime, rangeUtil, TimeZone } from '@grafana/data'; import { TimeRangePickerProps, TimeRangePicker } from '@grafana/ui'; import { LocalStorageValueProvider } from '../LocalStorageValueProvider'; @@ -9,14 +10,26 @@ const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history'; interface Props extends Omit {} +// Simplified object to store in local storage +interface TimePickerHistoryItem { + from: string; + to: string; +} + +// We should only be storing TimePickerHistoryItem, but in the past we also stored TimeRange +type LSTimePickerHistoryItem = TimePickerHistoryItem | TimeRange; + export const TimePickerWithHistory = (props: Props) => { return ( - storageKey={LOCAL_STORAGE_KEY} defaultValue={[]}> - {(values, onSaveToStore) => { + storageKey={LOCAL_STORAGE_KEY} defaultValue={[]}> + {(rawValues, onSaveToStore) => { + const values = migrateHistory(rawValues); + const history = deserializeHistory(values, props.timeZone); + return ( { onAppendToHistory(value, values, onSaveToStore); props.onChange(value); @@ -28,24 +41,37 @@ export const TimePickerWithHistory = (props: Props) => { ); }; -function convertIfJson(history: TimeRange[]): TimeRange[] { - return history.map((time) => { - if (isDateTime(time.from)) { - return time; - } +function deserializeHistory(values: TimePickerHistoryItem[], timeZone: TimeZone | undefined): TimeRange[] { + return values.map((item) => rangeUtil.convertRawToRange(item, timeZone)); +} + +function migrateHistory(values: LSTimePickerHistoryItem[]): TimePickerHistoryItem[] { + return values.map((item) => { + const fromValue = typeof item.from === 'string' ? item.from : item.from.toISOString(); + const toValue = typeof item.to === 'string' ? item.to : item.to.toISOString(); return { - from: toUtc(time.from), - to: toUtc(time.to), - raw: time.raw, + from: fromValue, + to: toValue, }; }); } -function onAppendToHistory(toAppend: TimeRange, values: TimeRange[], onSaveToStore: (values: TimeRange[]) => void) { - if (!isAbsolute(toAppend)) { +function onAppendToHistory( + newTimeRange: TimeRange, + values: TimePickerHistoryItem[], + onSaveToStore: (values: TimePickerHistoryItem[]) => void +) { + if (!isAbsolute(newTimeRange)) { return; } + + // Convert DateTime objects to strings + const toAppend = { + from: typeof newTimeRange.raw.from === 'string' ? newTimeRange.raw.from : newTimeRange.raw.from.toISOString(), + to: typeof newTimeRange.raw.to === 'string' ? newTimeRange.raw.to : newTimeRange.raw.to.toISOString(), + }; + const toStore = limit([toAppend, ...values]); onSaveToStore(toStore); } @@ -54,6 +80,6 @@ function isAbsolute(value: TimeRange): boolean { return isDateTime(value.raw.from) || isDateTime(value.raw.to); } -function limit(value: TimeRange[]): TimeRange[] { - return value.slice(0, 4); +function limit(value: TimePickerHistoryItem[]): TimePickerHistoryItem[] { + return uniqBy(value, (v) => v.from + v.to).slice(0, 4); } From 341d7b01ee6f8a4f73a8d7b46d12e0a4db476134 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Fri, 9 Dec 2022 13:30:27 +0200 Subject: [PATCH 92/94] Login: Fix failure to login a new user via an external provider if quota are enabled (#60015) * Login: Fix failure to login a new user via an external provider if quota are enabled --- pkg/services/quota/model.go | 2 ++ pkg/services/quota/quotaimpl/quota.go | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/services/quota/model.go b/pkg/services/quota/model.go index 091c60c4f36..c9915bebb0c 100644 --- a/pkg/services/quota/model.go +++ b/pkg/services/quota/model.go @@ -11,7 +11,9 @@ import ( var ErrBadRequest = errutil.NewBase(errutil.StatusBadRequest, "quota.bad-request") var ErrInvalidTargetSrv = errutil.NewBase(errutil.StatusBadRequest, "quota.invalid-target") var ErrInvalidScope = errutil.NewBase(errutil.StatusBadRequest, "quota.invalid-scope") +var ErrFailedToGetScope = errutil.NewBase(errutil.StatusInternal, "quota.failed-get-scope") var ErrInvalidTarget = errutil.NewBase(errutil.StatusInternal, "quota.invalid-target-table") +var ErrUsageFoundForTarget = errutil.NewBase(errutil.StatusNotFound, "quota.missing-target-usage") var ErrTargetSrvConflict = errutil.NewBase(errutil.StatusBadRequest, "quota.target-srv-conflict") var ErrDisabled = errutil.NewBase(errutil.StatusForbidden, "quota.disabled", errutil.WithPublicMessage("Quotas not enabled")) var ErrInvalidTagFormat = errutil.NewBase(errutil.StatusInternal, "quota.invalid-invalid-tag-format") diff --git a/pkg/services/quota/quotaimpl/quota.go b/pkg/services/quota/quotaimpl/quota.go index 9cc8be38af5..65763f20463 100644 --- a/pkg/services/quota/quotaimpl/quota.go +++ b/pkg/services/quota/quotaimpl/quota.go @@ -2,7 +2,6 @@ package quotaimpl import ( "context" - "fmt" "sync" "github.com/grafana/grafana/pkg/infra/db" @@ -205,9 +204,24 @@ func (s *service) CheckQuotaReached(ctx context.Context, targetSrv quota.TargetS case limit == 0: return true, nil default: + scope, err := t.GetScope() + if err != nil { + return false, quota.ErrFailedToGetScope.Errorf("failed to get the scope for target: %s", t) + } + + // do not check user quota if the user information is not available (eg no user is signed in) + if scope == quota.UserScope && (scopeParams == nil || scopeParams.UserID == 0) { + continue + } + + // do not check user quota if the org information is not available (eg no user is signed in) + if scope == quota.OrgScope && (scopeParams == nil || scopeParams.OrgID == 0) { + continue + } + u, ok := targetUsage.Get(t) if !ok { - return false, fmt.Errorf("no usage for target:%s", t) + return false, quota.ErrUsageFoundForTarget.Errorf("no usage for target:%s", t) } if u >= limit { return true, nil From cf342f1933e34f3b92d36fdca85520cb854a24e6 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 9 Dec 2022 12:31:42 +0100 Subject: [PATCH 93/94] Alerting: Reduce number of scroll bars in rule group modal (#59725) Reduce number of scroll bars in rule group modal --- .../alerting/unified/components/rules/EditRuleGroupModal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index b55647a0878..466ba2a0431 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -451,8 +451,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ tableWrapper: css` margin-top: ${theme.spacing(2)}; margin-bottom: ${theme.spacing(2)}; - height: 225px; - overflow: auto; + height: 100%; `, evalRequiredLabel: css` font-size: ${theme.typography.bodySmall.fontSize}; From 07d6b632b5374ee60ef39965777b3cf60e75cbfb Mon Sep 17 00:00:00 2001 From: Ludovic Viaud Date: Fri, 9 Dec 2022 12:56:47 +0100 Subject: [PATCH 94/94] Remove PromExploreQueryEditor and PromQueryEditor (#60020) Remove Prom editor --- .betterer.results | 10 - .../PromExploreQueryEditor.test.tsx | 144 ----------- .../components/PromExploreQueryEditor.tsx | 54 ----- .../components/PromQueryEditor.test.tsx | 75 ------ .../prometheus/components/PromQueryEditor.tsx | 223 ------------------ .../components/PromQueryEditorByApp.test.tsx | 4 +- .../components/PromQueryEditorByApp.tsx | 13 +- .../components/PromQueryBuilderOptions.tsx | 2 +- .../components/PromQueryCodeEditor.tsx | 2 - .../components/PromQueryEditorSelector.tsx | 14 +- 10 files changed, 16 insertions(+), 525 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx delete mode 100644 public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx delete mode 100644 public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx delete mode 100644 public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx diff --git a/.betterer.results b/.betterer.results index b3c1494e10e..32bd5512dd8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6306,22 +6306,12 @@ exports[`better eslint`] = { "public/app/plugins/datasource/prometheus/components/PromExploreExtraField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/plugins/datasource/prometheus/components/PromLink.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/prometheus/components/PromLink.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx deleted file mode 100644 index f8b9d9486ae..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { LoadingState, PanelData, toUtc, TimeRange } from '@grafana/data'; - -import { PrometheusDatasource } from '../datasource'; -import { PromQuery } from '../types'; - -import { testIds as extraFieldTestIds } from './PromExploreExtraField'; -import { PromExploreQueryEditor, testIds } from './PromExploreQueryEditor'; - -// the monaco-based editor uses lazy-loading and that does not work -// well with this test, and we do not need the monaco-related -// functionality in this test anyway, so we mock it out. -jest.mock('./monaco-query-field/MonacoQueryFieldWrapper', () => { - const fakeQueryField = () =>
prometheus query field
; - return { - MonacoQueryFieldWrapper: fakeQueryField, - }; -}); - -const setup = (propOverrides?: object) => { - const datasourceMock: unknown = { - languageProvider: { - syntax: () => {}, - getLabelKeys: () => [], - metrics: [], - start: () => Promise.resolve([]), - }, - getInitHints: () => [], - exemplarsAvailable: true, - }; - const datasource: PrometheusDatasource = datasourceMock as PrometheusDatasource; - const onRunQuery = jest.fn(); - const onChange = jest.fn(); - const query: PromQuery = { expr: '', refId: 'A', interval: '1s', exemplar: true }; - const range: TimeRange = { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - raw: { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - }, - }; - const data: PanelData = { - state: LoadingState.NotStarted, - series: [], - request: { - requestId: '1', - dashboardId: 1, - intervalMs: 1000, - interval: '1s', - panelId: 1, - range: { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - raw: { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - }, - }, - scopedVars: {}, - targets: [], - timezone: 'GMT', - app: 'Grafana', - startTime: 0, - }, - timeRange: { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - raw: { - from: toUtc('2020-01-01', 'YYYY-MM-DD'), - to: toUtc('2020-01-02', 'YYYY-MM-DD'), - }, - }, - }; - const history: any[] = []; - const exploreMode = 'Metrics'; - - const props: any = { - query, - data, - range, - datasource, - exploreMode, - history, - onChange, - onRunQuery, - }; - - Object.assign(props, propOverrides); - - return ; -}; - -describe('PromExploreQueryEditor', () => { - it('should render component', () => { - render(setup()); - expect(screen.getByTestId(testIds.editor)).toBeInTheDocument(); - }); - - it('should render PromQueryField with ExtraFieldElement', async () => { - render(setup()); - expect(screen.getByTestId(extraFieldTestIds.extraFieldEditor)).toBeInTheDocument(); - }); - - it('should set default value for expr if it is undefined', async () => { - const onChange = jest.fn(); - const query = { expr: undefined, exemplar: false, instant: false, range: true }; - render(setup({ onChange, query })); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ expr: '' })); - }); - - it('should set default value for exemplars if it is undefined', async () => { - const onChange = jest.fn(); - const query = { expr: '', instant: false, range: true }; - render(setup({ onChange, query })); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ exemplar: true })); - }); - - it('should set default value for instant and range if expr is falsy', async () => { - const onChange = jest.fn(); - let query = { expr: '', exemplar: true }; - render(setup({ onChange, query })); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ instant: true, range: true })); - }); - - it('should not set default value for instant and range with truthy expr', async () => { - const onChange = jest.fn(); - let query = { expr: 'foo', exemplar: true }; - render(setup({ onChange, query })); - expect(onChange).toHaveBeenCalledTimes(0); - }); - - it('should add default values for multiple missing values', async () => { - const onChange = jest.fn(); - let query = {}; - render(setup({ onChange, query })); - expect(onChange).toHaveBeenCalledTimes(3); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx deleted file mode 100644 index 5517ef1c8da..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { memo, useEffect } from 'react'; - -import { QueryEditorProps, CoreApp } from '@grafana/data'; - -import { PrometheusDatasource } from '../datasource'; -import { PromQuery, PromOptions } from '../types'; - -import { PromExploreExtraField } from './PromExploreExtraField'; -import PromQueryField from './PromQueryField'; - -export type Props = QueryEditorProps; - -export const PromExploreQueryEditor = memo((props: Props) => { - const { range, query, data, datasource, history, onChange, onRunQuery } = props; - - // Setting default values - useEffect(() => { - if (query.expr === undefined) { - onChange({ ...query, expr: '' }); - } - if (query.exemplar === undefined) { - onChange({ ...query, exemplar: true }); - } - - // Override query type to "Both" only for new queries (no query.expr). - if (!query.instant && !query.range && !query.expr) { - onChange({ ...query, instant: true, range: true }); - } - }, [onChange, query]); - - return ( - {}} - history={history} - data={data} - data-testid={testIds.editor} - ExtraFieldElement={ - - } - /> - ); -}); - -PromExploreQueryEditor.displayName = 'PromExploreQueryEditor'; - -export const testIds = { - editor: 'prom-editor-explore', -}; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx deleted file mode 100644 index b54fe94a74d..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { screen, render } from '@testing-library/react'; -import React from 'react'; - -import { dateTime, CoreApp } from '@grafana/data'; - -import { PrometheusDatasource } from '../datasource'; -import { PromQuery } from '../types'; - -import { PromQueryEditor, testIds } from './PromQueryEditor'; - -jest.mock('app/features/dashboard/services/TimeSrv', () => { - return { - getTimeSrv: () => ({ - timeRange: () => ({ - from: dateTime(), - to: dateTime(), - }), - }), - }; -}); - -jest.mock('./monaco-query-field/MonacoQueryFieldWrapper', () => { - const fakeQueryField = () =>
prometheus query field
; - return { - MonacoQueryFieldWrapper: fakeQueryField, - }; -}); - -const setup = (propOverrides?: object) => { - const datasourceMock: unknown = { - createQuery: jest.fn((q) => q), - getPrometheusTime: jest.fn((date, roundup) => 123), - languageProvider: { - start: () => Promise.resolve([]), - syntax: () => {}, - getLabelKeys: () => [], - metrics: [], - }, - getInitHints: () => [], - }; - const datasource: PrometheusDatasource = datasourceMock as PrometheusDatasource; - const onRunQuery = jest.fn(); - const onChange = jest.fn(); - const query: PromQuery = { expr: '', refId: 'A' }; - - const props: any = { - datasource, - onChange, - onRunQuery, - query, - }; - - Object.assign(props, propOverrides); - - return render(); -}; - -describe('Render PromQueryEditor with basic options', () => { - it('should render editor', () => { - setup(); - expect(screen.getByTestId(testIds.editor)).toBeInTheDocument(); - }); - - it('should render exemplar editor for dashboard', () => { - setup({ app: CoreApp.Dashboard }); - expect(screen.getByTestId(testIds.editor)).toBeInTheDocument(); - expect(screen.getByTestId(testIds.exemplar)).toBeInTheDocument(); - }); - - it('should not render exemplar editor for unified alerting', () => { - setup({ app: CoreApp.UnifiedAlerting }); - expect(screen.getByTestId(testIds.editor)).toBeInTheDocument(); - expect(screen.queryByTestId(testIds.exemplar)).not.toBeInTheDocument(); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx deleted file mode 100644 index 0ba79277e90..00000000000 --- a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { map } from 'lodash'; -import React, { PureComponent } from 'react'; - -// Types -import { CoreApp, SelectableValue } from '@grafana/data'; -import { InlineFormLabel, LegacyForms, Select } from '@grafana/ui'; - -import { PromQuery } from '../types'; - -import { PromExemplarField } from './PromExemplarField'; -import PromLink from './PromLink'; -import PromQueryField from './PromQueryField'; -import { PromQueryEditorProps } from './types'; - -const { Switch } = LegacyForms; - -export const FORMAT_OPTIONS: Array> = [ - { label: 'Time series', value: 'time_series' }, - { label: 'Table', value: 'table' }, - { label: 'Heatmap', value: 'heatmap' }, -]; - -export const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, 5, 10], (value: number) => ({ - value, - label: '1/' + value, -})); - -interface State { - legendFormat?: string; - formatOption: SelectableValue; - interval?: string; - intervalFactorOption: SelectableValue; - instant: boolean; - exemplar: boolean; -} - -export class PromQueryEditor extends PureComponent { - // Query target to be modified and used for queries - query: PromQuery; - - constructor(props: PromQueryEditorProps) { - super(props); - // Use default query to prevent undefined input values - const defaultQuery: Partial = { - expr: '', - legendFormat: '', - interval: '', - // Set exemplar to false for alerting queries - exemplar: props.app === CoreApp.UnifiedAlerting ? false : true, - }; - const query = Object.assign({}, defaultQuery, props.query); - this.query = query; - // Query target properties that are fully controlled inputs - this.state = { - // Fully controlled text inputs - interval: query.interval, - legendFormat: query.legendFormat, - // Select options - formatOption: FORMAT_OPTIONS.find((option) => option.value === query.format) || FORMAT_OPTIONS[0], - intervalFactorOption: - INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0], - // Switch options - instant: Boolean(query.instant), - exemplar: Boolean(query.exemplar), - }; - } - - onFieldChange = (query: PromQuery, override?: any) => { - this.query.expr = query.expr; - }; - - onFormatChange = (option: SelectableValue) => { - this.query.format = option.value; - this.setState({ formatOption: option }, this.onRunQuery); - }; - - onInstantChange = (e: React.SyntheticEvent) => { - const instant = e.currentTarget.checked; - this.query.instant = instant; - this.setState({ instant }, this.onRunQuery); - }; - - onIntervalChange = (e: React.SyntheticEvent) => { - const interval = e.currentTarget.value; - this.query.interval = interval; - this.setState({ interval }); - }; - - onIntervalFactorChange = (option: SelectableValue) => { - this.query.intervalFactor = option.value; - this.setState({ intervalFactorOption: option }, this.onRunQuery); - }; - - onLegendChange = (e: React.SyntheticEvent) => { - const legendFormat = e.currentTarget.value; - this.query.legendFormat = legendFormat; - this.setState({ legendFormat }); - }; - - onExemplarChange = (isEnabled: boolean) => { - this.query.exemplar = isEnabled; - this.setState({ exemplar: isEnabled }, this.onRunQuery); - }; - - onRunQuery = () => { - const { query } = this; - // Change of query.hide happens outside of this component and is just passed as prop. We have to update it when running queries. - const { hide } = this.props.query; - this.props.onChange({ ...query, hide }); - this.props.onRunQuery(); - }; - - render() { - const { datasource, query, range, data } = this.props; - const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state; - //We want to hide exemplar field for unified alerting as exemplars in alerting don't make sense and are source of confusion - const showExemplarField = this.props.app !== CoreApp.UnifiedAlerting; - - return ( - -
- - Legend - - -
- -
- - An additional lower limit for the step parameter of the Prometheus query and for the{' '} - $__interval and $__rate_interval variables. The limit is absolute and not - modified by the "Resolution" setting. - - } - > - Min step - - -
- -
-
Resolution
- - - - - - -
- {showExemplarField && ( - - )} -
- } - /> - ); - } -} - -export const testIds = { - editor: 'prom-editor', - exemplar: 'exemplar-editor', -}; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx index 9d80cddb841..fdc69769f89 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx @@ -6,7 +6,6 @@ import { CoreApp } from '@grafana/data'; import { PrometheusDatasource } from '../datasource'; -import { testIds as regularTestIds } from './PromQueryEditor'; import { PromQueryEditorByApp } from './PromQueryEditorByApp'; import { testIds as alertingTestIds } from './PromQueryEditorForAlerting'; @@ -70,10 +69,9 @@ function setup(app: CoreApp): RenderResult & { onRunQuery: jest.Mock } { describe('PromQueryEditorByApp', () => { it('should render simplified query editor for cloud alerting', () => { - const { getByTestId, queryByTestId } = setup(CoreApp.CloudAlerting); + const { getByTestId } = setup(CoreApp.CloudAlerting); expect(getByTestId(alertingTestIds.editor)).toBeInTheDocument(); - expect(queryByTestId(regularTestIds.editor)).toBeNull(); }); it('should render editor selector for unkown apps', () => { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.tsx index 3e15b9a297d..f10a6e3279a 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.tsx @@ -1,12 +1,9 @@ import React, { memo } from 'react'; import { CoreApp } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { PromQueryEditorSelector } from '../querybuilder/components/PromQueryEditorSelector'; -import { PromExploreQueryEditor } from './PromExploreQueryEditor'; -import { PromQueryEditor } from './PromQueryEditor'; import { PromQueryEditorForAlerting } from './PromQueryEditorForAlerting'; import { PromQueryEditorProps } from './types'; @@ -16,16 +13,8 @@ export function PromQueryEditorByApp(props: PromQueryEditorProps) { switch (app) { case CoreApp.CloudAlerting: return ; - case CoreApp.Explore: - if (config.featureToggles.promQueryBuilder) { - return ; - } - return ; default: - if (config.featureToggles.promQueryBuilder) { - return ; - } - return ; + return ; } } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index 4a846cff03c..a258a3b65d6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -5,10 +5,10 @@ import { EditorField, EditorRow, EditorSwitch } from '@grafana/experimental'; import { AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui'; import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField'; -import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from '../../components/PromQueryEditor'; import { PromQuery } from '../../types'; import { QueryOptionGroup } from '../shared/QueryOptionGroup'; +import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from './PromQueryEditorSelector'; import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor'; export interface UIOptions { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx index 949d8e3a9a9..f9bfe24bf7d 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -import { testIds } from '../../components/PromQueryEditor'; import PromQueryField from '../../components/PromQueryField'; import { PromQueryEditorProps } from '../../components/types'; @@ -28,7 +27,6 @@ export function PromQueryCodeEditor(props: Props) { onChange={onChange} history={[]} data={data} - data-testid={testIds.editor} app={app} /> diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index ff7f9a88e32..85ad07a6d1b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -1,6 +1,7 @@ +import { map } from 'lodash'; import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react'; -import { CoreApp, LoadingState } from '@grafana/data'; +import { CoreApp, LoadingState, SelectableValue } from '@grafana/data'; import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental'; import { reportInteraction } from '@grafana/runtime'; import { ConfirmModal, Button } from '@grafana/ui'; @@ -19,6 +20,17 @@ import { PromQueryBuilderContainer } from './PromQueryBuilderContainer'; import { PromQueryBuilderOptions } from './PromQueryBuilderOptions'; import { PromQueryCodeEditor } from './PromQueryCodeEditor'; +export const FORMAT_OPTIONS: Array> = [ + { label: 'Time series', value: 'time_series' }, + { label: 'Table', value: 'table' }, + { label: 'Heatmap', value: 'heatmap' }, +]; + +export const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, 5, 10], (value: number) => ({ + value, + label: '1/' + value, +})); + type Props = PromQueryEditorProps; export const PromQueryEditorSelector = React.memo((props) => {