import { produce } from 'immer'; import { isEmpty, pick } from 'lodash'; import { DataSourceInstanceSettings, DataSourceJsonData, DataSourcePluginMeta, PluginExtensionLink, PluginExtensionTypes, ReducerID, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { defaultDashboard } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/grafanaRuler'; import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types'; import { AlertManagerCortexConfig, AlertState, AlertmanagerAlert, AlertmanagerGroup, AlertmanagerStatus, GrafanaManagedReceiverConfig, MatcherOperator, Silence, SilenceState, } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction, DashboardDTO, FolderDTO, NotifiersState, ReceiversState, StoreState } from 'app/types'; import { Alert, AlertingRule, CombinedRule, CombinedRuleGroup, CombinedRuleNamespace, RecordingRule, RuleGroup, RuleNamespace, RuleWithLocation, } from 'app/types/unified-alerting'; import { AlertDataQuery, AlertQuery, GrafanaAlertState, GrafanaAlertStateDecision, GrafanaRuleDefinition, PromAlertingRuleState, PromRuleType, RulerAlertingRuleDTO, RulerGrafanaRuleDTO, RulerRecordingRuleDTO, RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO, } from 'app/types/unified-alerting-dto'; import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types'; import { SimpleConditionIdentifier } from './components/rule-editor/query-and-alert-condition/SimpleCondition'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers'; let nextDataSourceId = 1; export function mockDataSource( partial: Partial> = {}, meta: Partial = {} ): DataSourceInstanceSettings { const id = partial.id ?? nextDataSourceId++; const uid = partial.uid ?? `mock-ds-${nextDataSourceId}`; return { id, uid, type: 'prometheus', name: `Prometheus-${id}`, access: 'proxy', url: `/api/datasources/proxy/uid/${uid}`, jsonData: {} as T, meta: { info: { logos: { small: 'https://prometheus.io/assets/prometheus_logo_grey.svg', large: 'https://prometheus.io/assets/prometheus_logo_grey.svg', }, }, ...meta, } as unknown as DataSourcePluginMeta, readOnly: false, ...partial, }; } export const mockPromAlert = (partial: Partial = {}): Alert => ({ activeAt: '2021-03-18T13:47:05.04938691Z', annotations: { message: 'alert with severity "warning"', }, labels: { alertname: 'myalert', severity: 'warning', }, state: PromAlertingRuleState.Firing, value: '1e+00', ...partial, }); export const mockRulerGrafanaRule = ( partial: Partial = {}, partialDef: Partial = {} ): RulerGrafanaRuleDTO => { return { for: '1m', grafana_alert: { uid: '123', title: 'myalert', namespace_uid: '123', rule_group: 'my-group', condition: 'A', no_data_state: GrafanaAlertStateDecision.Alerting, exec_err_state: GrafanaAlertStateDecision.Alerting, data: [ { datasourceUid: '123', refId: 'A', queryType: 'huh', model: {} as any, }, ], ...partialDef, }, annotations: { message: 'alert with severity "{{.warning}}}"', }, labels: { severity: 'warning', }, ...partial, }; }; export const mockRulerGrafanaRecordingRule = ( partial: Partial = {}, partialDef: Partial = {} ): RulerGrafanaRuleDTO => { return { grafana_alert: { uid: '123', title: 'myalert', namespace_uid: '123', rule_group: 'my-group', condition: 'A', record: { metric: 'myalert', from: 'A', }, data: [ { datasourceUid: '123', refId: 'A', queryType: 'huh', model: { refId: '', }, }, ], ...partialDef, }, annotations: { message: 'alert with severity "{{.warning}}}"', }, labels: { severity: 'warning', }, ...partial, }; }; export const mockRulerAlertingRule = (partial: Partial = {}): RulerAlertingRuleDTO => ({ alert: 'alert1', expr: 'up = 1', labels: { severity: 'warning', }, annotations: { summary: 'test alert', }, ...partial, }); export const mockRulerRecordingRule = (partial: Partial = {}): RulerRecordingRuleDTO => ({ record: 'alert1', expr: 'up = 1', labels: { severity: 'warning', }, ...partial, }); export const mockRulerRuleGroup = (partial: Partial = {}): RulerRuleGroupDTO => ({ name: 'group1', rules: [mockRulerAlertingRule()], ...partial, }); export const mockPromAlertingRule = (partial: Partial = {}): AlertingRule => { return { type: PromRuleType.Alerting, alerts: [mockPromAlert()], name: 'myalert', query: 'foo > 1', lastEvaluation: '2021-03-23T08:19:05.049595312Z', evaluationTime: 0.000395601, annotations: { message: 'alert with severity "{{.warning}}}"', }, labels: { severity: 'warning', }, state: PromAlertingRuleState.Firing, health: 'OK', totalsFiltered: { alerting: 1 }, ...partial, }; }; export const mockGrafanaRulerRule = (partial: Partial = {}): RulerGrafanaRuleDTO => { return { for: '', annotations: {}, labels: {}, grafana_alert: { uid: 'mock-rule-uid-123', title: 'my rule', namespace_uid: 'NAMESPACE_UID', rule_group: 'my-group', condition: '', no_data_state: GrafanaAlertStateDecision.NoData, exec_err_state: GrafanaAlertStateDecision.Error, data: [], ...partial, }, }; }; export const mockPromRecordingRule = (partial: Partial = {}): RecordingRule => { return { type: PromRuleType.Recording, query: 'bar < 3', labels: { cluster: 'eu-central', }, health: 'OK', name: 'myrecordingrule', lastEvaluation: '2021-03-23T08:19:05.049595312Z', evaluationTime: 0.000395601, ...partial, }; }; export const mockPromRuleGroup = (partial: Partial = {}): RuleGroup => { return { name: 'mygroup', interval: 60, rules: [mockPromAlertingRule()], ...partial, }; }; export const mockPromRuleNamespace = (partial: Partial = {}): RuleNamespace => { return { dataSourceName: 'Prometheus-1', name: 'default', groups: [mockPromRuleGroup()], ...partial, }; }; export const mockAlertmanagerAlert = (partial: Partial = {}): AlertmanagerAlert => { return { annotations: { summary: 'US-Central region is on fire', }, endsAt: '2021-06-22T21:49:28.562Z', fingerprint: '88e013643c3df34ac3', receivers: [{ name: 'pagerduty' }], startsAt: '2021-06-21T17:25:28.562Z', status: { inhibitedBy: [], silencedBy: [], state: AlertState.Active }, updatedAt: '2021-06-22T21:45:28.564Z', generatorURL: 'https://play.grafana.com/explore', labels: { severity: 'warning', region: 'US-Central' }, ...partial, }; }; export const mockAlertGroup = (partial: Partial = {}): AlertmanagerGroup => { return { labels: { severity: 'warning', region: 'US-Central', }, receiver: { name: 'pagerduty', }, alerts: [ mockAlertmanagerAlert(), mockAlertmanagerAlert({ status: { state: AlertState.Suppressed, silencedBy: ['123456abcdef'], inhibitedBy: [] }, labels: { severity: 'warning', region: 'US-Central', foo: 'bar', ...partial.labels }, }), ], ...partial, }; }; export const mockSilence = (partial: Partial = {}): Silence => { return { id: '1a2b3c4d5e6f', matchers: [{ name: 'foo', value: 'bar', isEqual: true, isRegex: false }], startsAt: new Date().toISOString(), endsAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), updatedAt: new Date().toISOString(), createdBy: config.bootData.user.name || 'admin', comment: 'Silence noisy alerts', status: { state: SilenceState.Active, }, accessControl: { create: true, read: true, write: true, }, ...partial, }; }; export const MOCK_SILENCE_ID_EXISTING = 'f209e273-0e4e-434f-9f66-e72f092025a2'; export const MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID = '5f7d08cd-ac62-432e-8449-8c20c95c19b6'; export const MOCK_SILENCE_ID_EXPIRED = '145884a8-ee20-4864-9f84-661305fb7d82'; export const MOCK_SILENCE_ID_LACKING_PERMISSIONS = '31063317-f0d2-4d98-baf3-ec9febc1fa83'; export const mockSilences = [ mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }), mockSilence({ id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7', matchers: parsePromQLStyleMatcherLooseSafe('foo!=bar'), comment: 'Silence with negated matcher', }), mockSilence({ id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, matchers: parsePromQLStyleMatcherLooseSafe(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`), comment: 'Silence with alert rule UID matcher', metadata: { rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE, }, }), mockSilence({ id: MOCK_SILENCE_ID_LACKING_PERMISSIONS, matchers: parsePromQLStyleMatcherLooseSafe('something=else'), comment: 'Silence without permissions to edit', accessControl: {}, }), mockSilence({ id: MOCK_SILENCE_ID_EXPIRED, status: { state: SilenceState.Expired }, comment: 'Silence which is expired', }), ]; export const mockNotifiersState = (partial: Partial = {}): NotifiersState => { return { email: [ { name: 'email', lastNotifyAttempt: new Date().toISOString(), lastNotifyAttemptError: 'this is the error message', lastNotifyAttemptDuration: '10s', }, ], ...partial, }; }; export const mockReceiversState = (partial: Partial = {}): ReceiversState => { return { 'broken-receiver': { active: false, errorCount: 1, notifiers: mockNotifiersState(), }, ...partial, }; }; export const mockGrafanaReceiver = ( type: string, overrides: Partial = {} ): GrafanaManagedReceiverConfig => ({ type: type, name: type, disableResolveMessage: false, settings: {}, ...overrides, }); export const someGrafanaAlertManagerConfig: AlertManagerCortexConfig = { template_files: { 'first template': 'first template content', 'second template': 'second template content', 'third template': 'third template', }, alertmanager_config: { route: { receiver: 'default', routes: [ { receiver: 'critical', object_matchers: [['severity', MatcherOperator.equal, 'critical']], }, ], }, receivers: [ { name: 'default', grafana_managed_receiver_configs: [mockGrafanaReceiver('email')], }, { name: 'critical', grafana_managed_receiver_configs: [mockGrafanaReceiver('slack'), mockGrafanaReceiver('pagerduty')], }, ], }, }; /** @deprecated Move into alertmanager status entities */ export const someCloudAlertManagerStatus: AlertmanagerStatus = { cluster: { peers: [], status: 'ok', }, uptime: '10 hours', versionInfo: { branch: '', version: '', goVersion: '', buildDate: '', buildUser: '', revision: '', }, config: { route: { receiver: 'default-email', }, receivers: [ { name: 'default-email', email_configs: [ { to: 'example@example.com', }, ], }, ], }, }; /** @deprecated Move into alertmanager config entities */ export const someCloudAlertManagerConfig: AlertManagerCortexConfig = { template_files: { 'foo template': 'foo content', }, alertmanager_config: { route: { receiver: 'cloud-receiver', routes: [ { receiver: 'foo-receiver', }, { receiver: 'bar-receiver', }, ], }, receivers: [ { name: 'cloud-receiver', email_configs: [ { to: 'domas.lapinskas@grafana.com', }, ], slack_configs: [ { api_url: 'http://slack1', channel: '#mychannel', actions: [ { text: 'action1text', type: 'action1type', url: 'http://action1', }, ], fields: [ { title: 'field1', value: 'text1', }, { title: 'field2', value: 'text2', }, ], }, ], }, ], }, }; export const somePromRules = (dataSourceName = 'Prometheus'): RuleNamespace[] => [ { dataSourceName, name: 'namespace1', groups: [ mockPromRuleGroup({ name: 'group1', rules: [mockPromAlertingRule({ name: 'alert1' })] }), mockPromRuleGroup({ name: 'group2', rules: [mockPromAlertingRule({ name: 'alert2' })] }), ], }, { dataSourceName, name: 'namespace2', groups: [mockPromRuleGroup({ name: 'group3', rules: [mockPromAlertingRule({ name: 'alert3' })] })], }, ]; export const someRulerRules: RulerRulesConfigDTO = { namespace1: [ mockRulerRuleGroup({ name: 'group1', rules: [mockRulerAlertingRule({ alert: 'alert1' }), mockRulerAlertingRule({ alert: 'alert1a' })], }), mockRulerRuleGroup({ name: 'group2', rules: [mockRulerAlertingRule({ alert: 'alert2' })] }), ], namespace2: [mockRulerRuleGroup({ name: 'group3', rules: [mockRulerAlertingRule({ alert: 'alert3' })] })], }; export const getPotentiallyPausedRulerRules: (isPaused: boolean) => RulerRulesConfigDTO = (isPaused) => ({ namespacePaused: [ mockRulerRuleGroup({ name: 'groupPaused', rules: [mockGrafanaRulerRule({ title: 'paused alert', is_paused: isPaused })], }), ], }); export const pausedPromRules = (dataSourceName = 'Prometheus'): RuleNamespace[] => [ { dataSourceName, name: 'namespacePaused', groups: [mockPromRuleGroup({ name: 'groupPaused', rules: [mockPromAlertingRule({ name: 'paused alert' })] })], }, ]; export const mockCombinedRule = (partial?: Partial): CombinedRule => ({ name: 'mockRule', query: 'expr', group: { name: 'mockCombinedRuleGroup', rules: [], totals: {}, }, namespace: { name: 'mockCombinedNamespace', groups: [{ name: 'mockCombinedRuleGroup', rules: [], totals: {} }], rulesSource: 'grafana', }, labels: {}, annotations: {}, promRule: mockPromAlertingRule(), rulerRule: mockRulerAlertingRule(), instanceTotals: {}, filteredInstanceTotals: {}, ...partial, }); export const mockRuleWithLocation = (rule: RulerRuleDTO, partial?: Partial): RuleWithLocation => { const ruleWithLocation: RuleWithLocation = { rule, ...{ ruleSourceName: 'grafana', namespace: 'namespace-1', group: mockRulerRuleGroup({ name: 'group-1', rules: [rule], }), }, ...partial, }; return ruleWithLocation; }; export const mockFolder = (partial?: Partial): FolderDTO => { return { id: 1, uid: 'gdev-1', title: 'Gdev', version: 1, url: '', canAdmin: true, canDelete: true, canEdit: true, canSave: true, created: '', createdBy: '', hasAcl: false, updated: '', updatedBy: '', ...partial, }; }; export const grantUserPermissions = (permissions: AccessControlAction[]) => { jest .spyOn(contextSrv, 'hasPermission') .mockImplementation((action) => permissions.includes(action as AccessControlAction)); }; export const grantUserRole = (role: string) => { jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true); }; export function mockUnifiedAlertingStore(unifiedAlerting?: Partial) { const defaultState = configureStore().getState(); return configureStore({ ...defaultState, unifiedAlerting: { ...defaultState.unifiedAlerting, ...unifiedAlerting, }, }); } export function mockStore(recipe: (state: StoreState) => void) { const defaultState = configureStore().getState(); return configureStore(produce(defaultState, recipe)); } export function mockAlertQuery(query: Partial = {}): AlertQuery { return { datasourceUid: '--uid--', refId: 'A', queryType: '', model: { refId: 'A' }, ...query, }; } export function mockCombinedRuleGroup(name: string, rules: CombinedRule[]): CombinedRuleGroup { return { name, rules, totals: {} }; } export function mockCombinedRuleNamespace(namespace: Partial): CombinedRuleNamespace { return { name: 'Grafana', groups: [], rulesSource: 'grafana', ...namespace, }; } export function mockCombinedCloudRuleNamespace( namespace: Partial, dataSourceName: string ): CombinedRuleNamespace { return { name: 'Grafana', groups: [], rulesSource: mockDataSource({ name: dataSourceName, uid: 'Prometheus-1' }), ...namespace, }; } export function getGrafanaRule(override?: Partial, rulerOverride?: Partial) { return mockCombinedRule({ namespace: { groups: [], name: 'Grafana', rulesSource: GRAFANA_RULES_SOURCE_NAME, }, rulerRule: mockGrafanaRulerRule(rulerOverride), ...override, }); } export function getCloudRule(override?: Partial, nsOverride?: Partial) { const promOverride = pick(override, ['name', 'labels', 'annotations']); const rulerOverride = pick(override, ['name', 'labels', 'annotations']); return mockCombinedRule({ namespace: { groups: [], name: 'Cortex', rulesSource: mockDataSource(), ...nsOverride, }, promRule: mockPromAlertingRule(isEmpty(promOverride) ? undefined : promOverride), rulerRule: mockRulerAlertingRule( isEmpty(rulerOverride) ? undefined : { ...rulerOverride, alert: rulerOverride.name } ), ...override, }); } export function getVanillaPromRule(override?: Partial>) { return mockCombinedRule({ namespace: { groups: [], name: 'Prometheus', rulesSource: mockDataSource(), }, promRule: mockPromAlertingRule(), rulerRule: undefined, ...override, }); } export function mockPluginLinkExtension(extension: Partial): PluginExtensionLink { return { type: PluginExtensionTypes.link, id: 'plugin-id', pluginId: 'grafana-test-app', title: 'Test plugin link', description: 'Test plugin link', path: '/test', ...extension, }; } export function mockAlertWithState(state: GrafanaAlertState, labels?: {}): Alert { return { activeAt: '', annotations: {}, labels: labels || {}, state: state, value: '' }; } export function mockDashboardSearchItem(searchItem: Partial) { return { title: '', uid: '', type: DashboardSearchItemType.DashDB, url: '', uri: '', items: [], tags: [], slug: '', isStarred: false, ...searchItem, }; } export function mockDashboardDto( dashboard: Partial, meta?: Partial ): DashboardDTO { return { dashboard: { uid: 'dashboard-test', title: 'Dashboard test', schemaVersion: defaultDashboard.schemaVersion, ...dashboard, }, meta: { ...meta }, }; } export const dataQuery: AlertQuery = { refId: SimpleConditionIdentifier.queryId, datasourceUid: 'abc123', queryType: '', model: { refId: SimpleConditionIdentifier.queryId }, }; export const reduceExpression: AlertQuery = { refId: SimpleConditionIdentifier.reducerId, queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.reduce, refId: SimpleConditionIdentifier.reducerId, settings: { mode: ReducerMode.Strict }, reducer: ReducerID.last, }, }; export const thresholdExpression: AlertQuery = { refId: SimpleConditionIdentifier.thresholdId, queryType: 'expression', datasourceUid: '__expr__', model: { type: ExpressionQueryType.threshold, refId: SimpleConditionIdentifier.thresholdId, }, };