mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Alerting] Preconfigured alert history index connector (#94909)
* Adding preconfigured alert history index * Adding functions to build alert history document * Adding functions to build alert history document * Moving index template creation to plugin start * Adding unit tests * Adding unit tests * Adding unit tests * Simplifying * Revert "Merge branch 'master' of https://github.com/elastic/kibana into alerting/default-es-index-schema" This reverts commit957c333aa4
, reversing changes made to4b1b78761e
. * Reverting some changes * Reverting some changes * Adding index override * Updating UI with index override * Only allow indexOverride for preconfigured alert history connector * Handling preconfigured connector id clashes * Cleanup * UI unit tests * Fixing default schema shown in UI * Fixing functional tests * Adding functional test * Fixing functional tests * Adding docs and link to docs * Adding config to docker allowlist * Fixing wrong typescript operator * Changing default for config to false * Cleanup * Adding note about index privileges to docs * Fixing i18n * PR fixes * PR fixes * PR fixes * PR fixes - wording * PR fixes * Fixing unit and functional tests * Fixing types check * ES -> Elasticsearch * Moving files * Adding kibana- to beginning of prefix * Namespacing alert data within schema with kibana * Fix i18n * Updating docs * Fixing unit tests * Fixing doc links * Fixing types check * PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3336db0baa
commit
71ed148cfe
42 changed files with 1579 additions and 61 deletions
|
@ -53,8 +53,12 @@ You can configure the following settings in the `kibana.yml` file.
|
|||
+
|
||||
Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function.
|
||||
|
||||
| `xpack.actions`
|
||||
`.preconfiguredAlertHistoryEsIndex` {ess-icon}
|
||||
| Enables a preconfigured alert history {es} <<index-action-type, Index>> connector. Defaults to `false`.
|
||||
|
||||
| `xpack.actions.preconfigured`
|
||||
| Specifies preconfigured action IDs and configs. Defaults to {}.
|
||||
| Specifies preconfigured connector IDs and configs. Defaults to {}.
|
||||
|
||||
| `xpack.actions.proxyUrl` {ess-icon}
|
||||
| Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used.
|
||||
|
|
|
@ -82,3 +82,38 @@ PUT test
|
|||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
[float]
|
||||
[[preconfigured-connector-alert-history]]
|
||||
=== Alert history {es} index connector
|
||||
|
||||
experimental[] {kib} offers a preconfigured index connector to facilitate indexing active alert data into {es}.
|
||||
|
||||
[WARNING]
|
||||
==================================================
|
||||
This functionality is experimental and may be changed or removed completely in a future release.
|
||||
==================================================
|
||||
|
||||
To use this connector, set the <<action-settings, `xpack.actions.preconfiguredAlertHistoryEsIndex`>> configuration to `true`.
|
||||
|
||||
```js
|
||||
xpack.actions.preconfiguredAlertHistoryEsIndex: true
|
||||
```
|
||||
|
||||
When creating a new rule, add an <<index-action-type, Index action>> and select the `Alert history Elasticsearch index (preconfigured)` connector.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/pre-configured-alert-history-connector.png[Select pre-configured alert history connectors]
|
||||
|
||||
Documents are indexed using a preconfigured schema that captures the <<defining-alerts-actions-variables, action variables>> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template.
|
||||
|
||||
[IMPORTANT]
|
||||
==============================================
|
||||
To write documents to the preconfigured index, you must have `all` or `write` privileges to the `kibana-alert-history-*` indices. Refer to <<xpack-kibana-role-management>> for more information.
|
||||
==============================================
|
||||
|
||||
[NOTE]
|
||||
==================================================
|
||||
The `kibana-alert-history-*` indices are not configured to use ILM so they must be maintained manually. If the index size grows large,
|
||||
consider using the {ref}/docs-delete-by-query.html[delete by query] API to clean up older documents in the index.
|
||||
==================================================
|
|
@ -51,6 +51,14 @@ two out-of-the box connectors: <<slack-action-type, Slack>> and <<webhook-action
|
|||
Sensitive properties, such as passwords, can also be stored in the <<creating-keystore, {kib} keystore>>.
|
||||
==============================================
|
||||
|
||||
[float]
|
||||
[[build-in-preconfigured-connectors]]
|
||||
==== Built-in preconfigured connectors
|
||||
|
||||
{kib} provides one built-in preconfigured connector:
|
||||
|
||||
* <<preconfigured-connector-alert-history, Alert history preconfigured {es} index connector>>
|
||||
|
||||
[float]
|
||||
[[managing-pre-configured-connectors]]
|
||||
==== View preconfigured connectors
|
||||
|
@ -63,4 +71,4 @@ image::images/pre-configured-connectors-managing.png[Connectors managing tab wit
|
|||
Clicking a preconfigured connector shows the description, but not the configuration. A message indicates that this is a preconfigured connector.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details]
|
||||
image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details]
|
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
|
@ -209,6 +209,7 @@ export class DocLinksService {
|
|||
indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`,
|
||||
pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`,
|
||||
preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`,
|
||||
preconfiguredAlertHistoryConnector: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html#preconfigured-connector-alert-history`,
|
||||
serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`,
|
||||
setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`,
|
||||
slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`,
|
||||
|
|
|
@ -159,6 +159,7 @@ kibana_vars=(
|
|||
xpack.actions.allowedHosts
|
||||
xpack.actions.enabled
|
||||
xpack.actions.enabledActionTypes
|
||||
xpack.actions.preconfiguredAlertHistoryEsIndex
|
||||
xpack.actions.preconfigured
|
||||
xpack.actions.proxyHeaders
|
||||
xpack.actions.proxyRejectUnauthorizedCertificates
|
||||
|
|
122
x-pack/plugins/actions/common/alert_history_schema.test.ts
Normal file
122
x-pack/plugins/actions/common/alert_history_schema.test.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildAlertHistoryDocument } from './alert_history_schema';
|
||||
|
||||
function getVariables(overrides = {}) {
|
||||
return {
|
||||
date: '2021-01-01T00:00:00.000Z',
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: 'rule-name',
|
||||
type: 'rule-type',
|
||||
spaceId: 'space-id',
|
||||
},
|
||||
context: {
|
||||
contextVar1: 'contextValue1',
|
||||
contextVar2: 'contextValue2',
|
||||
},
|
||||
params: {
|
||||
ruleParam: 1,
|
||||
ruleParamString: 'another param',
|
||||
},
|
||||
tags: ['abc', 'def'],
|
||||
alert: {
|
||||
id: 'alert-id',
|
||||
actionGroup: 'action-group-id',
|
||||
actionGroupName: 'Action Group',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildAlertHistoryDocument', () => {
|
||||
it('handles empty variables', () => {
|
||||
expect(buildAlertHistoryDocument({})).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if rule type is not defined', () => {
|
||||
expect(buildAlertHistoryDocument(getVariables({ rule: { type: undefined } }))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if alert variables are not defined', () => {
|
||||
expect(buildAlertHistoryDocument(getVariables({ alert: undefined }))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if rule variables are not defined', () => {
|
||||
expect(buildAlertHistoryDocument(getVariables({ rule: undefined }))).toBeNull();
|
||||
});
|
||||
|
||||
it('includes @timestamp field if date is null', () => {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ date: undefined }));
|
||||
expect(alertHistoryDoc).not.toBeNull();
|
||||
expect(alertHistoryDoc!['@timestamp']).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`doesn't include context if context is empty`, () => {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ context: {} }));
|
||||
expect(alertHistoryDoc).not.toBeNull();
|
||||
expect(alertHistoryDoc!.kibana?.alert?.context).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`doesn't include params if params is empty`, () => {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ params: {} }));
|
||||
expect(alertHistoryDoc).not.toBeNull();
|
||||
expect(alertHistoryDoc!.rule?.params).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`doesn't include tags if tags is empty array`, () => {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ tags: [] }));
|
||||
expect(alertHistoryDoc).not.toBeNull();
|
||||
expect(alertHistoryDoc!.tags).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`included message if context contains message`, () => {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(
|
||||
getVariables({
|
||||
context: { contextVar1: 'contextValue1', contextVar2: 'contextValue2', message: 'hello!' },
|
||||
})
|
||||
);
|
||||
expect(alertHistoryDoc).not.toBeNull();
|
||||
expect(alertHistoryDoc!.message).toEqual('hello!');
|
||||
});
|
||||
|
||||
it('builds alert history document from variables', () => {
|
||||
expect(buildAlertHistoryDocument(getVariables())).toEqual({
|
||||
'@timestamp': '2021-01-01T00:00:00.000Z',
|
||||
kibana: {
|
||||
alert: {
|
||||
actionGroup: 'action-group-id',
|
||||
actionGroupName: 'Action Group',
|
||||
context: {
|
||||
'rule-type': {
|
||||
contextVar1: 'contextValue1',
|
||||
contextVar2: 'contextValue2',
|
||||
},
|
||||
},
|
||||
id: 'alert-id',
|
||||
},
|
||||
},
|
||||
event: {
|
||||
kind: 'alert',
|
||||
},
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: 'rule-name',
|
||||
params: {
|
||||
'rule-type': {
|
||||
ruleParam: 1,
|
||||
ruleParamString: 'another param',
|
||||
},
|
||||
},
|
||||
space: 'space-id',
|
||||
type: 'rule-type',
|
||||
},
|
||||
tags: ['abc', 'def'],
|
||||
});
|
||||
});
|
||||
});
|
90
x-pack/plugins/actions/common/alert_history_schema.ts
Normal file
90
x-pack/plugins/actions/common/alert_history_schema.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export const ALERT_HISTORY_PREFIX = 'kibana-alert-history-';
|
||||
export const AlertHistoryDefaultIndexName = `${ALERT_HISTORY_PREFIX}default`;
|
||||
export const AlertHistoryEsIndexConnectorId = 'preconfigured-alert-history-es-index';
|
||||
|
||||
export const buildAlertHistoryDocument = (variables: Record<string, unknown>) => {
|
||||
const { date, alert: alertVariables, context, params, tags, rule: ruleVariables } = variables as {
|
||||
date: string;
|
||||
alert: Record<string, unknown>;
|
||||
context: Record<string, unknown>;
|
||||
params: Record<string, unknown>;
|
||||
rule: Record<string, unknown>;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
if (!alertVariables || !ruleVariables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { actionGroup, actionGroupName, id: alertId } = alertVariables as {
|
||||
actionGroup: string;
|
||||
actionGroupName: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const { id: ruleId, name, spaceId, type } = ruleVariables as {
|
||||
id: string;
|
||||
name: string;
|
||||
spaceId: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
if (!type) {
|
||||
// can't build the document without a type
|
||||
return null;
|
||||
}
|
||||
|
||||
const ruleType = type.replace(/\./g, '__');
|
||||
|
||||
const rule = {
|
||||
...(ruleId ? { id: ruleId } : {}),
|
||||
...(name ? { name } : {}),
|
||||
...(!isEmpty(params) ? { params: { [ruleType]: params } } : {}),
|
||||
...(spaceId ? { space: spaceId } : {}),
|
||||
...(type ? { type } : {}),
|
||||
};
|
||||
const alert = {
|
||||
...(alertId ? { id: alertId } : {}),
|
||||
...(!isEmpty(context) ? { context: { [ruleType]: context } } : {}),
|
||||
...(actionGroup ? { actionGroup } : {}),
|
||||
...(actionGroupName ? { actionGroupName } : {}),
|
||||
};
|
||||
|
||||
const alertHistoryDoc = {
|
||||
'@timestamp': date ? date : new Date().toISOString(),
|
||||
...(tags && tags.length > 0 ? { tags } : {}),
|
||||
...(context?.message ? { message: context.message } : {}),
|
||||
...(!isEmpty(rule) ? { rule } : {}),
|
||||
...(!isEmpty(alert) ? { kibana: { alert } } : {}),
|
||||
};
|
||||
|
||||
return !isEmpty(alertHistoryDoc) ? { ...alertHistoryDoc, event: { kind: 'alert' } } : null;
|
||||
};
|
||||
|
||||
export const AlertHistoryDocumentTemplate = Object.freeze(
|
||||
buildAlertHistoryDocument({
|
||||
rule: {
|
||||
id: '{{rule.id}}',
|
||||
name: '{{rule.name}}',
|
||||
type: '{{rule.type}}',
|
||||
spaceId: '{{rule.spaceId}}',
|
||||
},
|
||||
context: '{{context}}',
|
||||
params: '{{params}}',
|
||||
tags: '{{rule.tags}}',
|
||||
alert: {
|
||||
id: '{{alert.id}}',
|
||||
actionGroup: '{{alert.actionGroup}}',
|
||||
actionGroupName: '{{alert.actionGroupName}}',
|
||||
},
|
||||
})
|
||||
);
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './alert_history_schema';
|
||||
export * from './rewrite_request_case';
|
||||
|
||||
export const BASE_ACTION_API_PATH = '/api/actions';
|
||||
|
||||
export * from './rewrite_request_case';
|
||||
|
|
|
@ -405,6 +405,7 @@ describe('create()', () => {
|
|||
enabled: true,
|
||||
enabledActionTypes: ['some-not-ignored-action-type'],
|
||||
allowedHosts: ['*'],
|
||||
preconfiguredAlertHistoryEsIndex: false,
|
||||
preconfigured: {},
|
||||
proxyRejectUnauthorizedCertificates: true,
|
||||
rejectUnauthorized: true,
|
||||
|
|
|
@ -18,6 +18,7 @@ const defaultActionsConfig: ActionsConfig = {
|
|||
enabled: false,
|
||||
allowedHosts: [],
|
||||
enabledActionTypes: [],
|
||||
preconfiguredAlertHistoryEsIndex: false,
|
||||
preconfigured: {},
|
||||
proxyRejectUnauthorizedCertificates: true,
|
||||
rejectUnauthorized: true,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
ESIndexActionType,
|
||||
ESIndexActionTypeExecutorOptions,
|
||||
} from './es_index';
|
||||
import { AlertHistoryEsIndexConnectorId } from '../../common';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks';
|
||||
|
||||
|
@ -115,6 +116,7 @@ describe('params validation', () => {
|
|||
test('params validation succeeds when params is valid', () => {
|
||||
const params: Record<string, unknown> = {
|
||||
documents: [{ rando: 'thing' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
expect(validateParams(actionType, params)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -123,6 +125,7 @@ describe('params validation', () => {
|
|||
"rando": "thing",
|
||||
},
|
||||
],
|
||||
"indexOverride": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -159,6 +162,7 @@ describe('execute()', () => {
|
|||
config = { index: 'index-value', refresh: false, executionTimeField: null };
|
||||
params = {
|
||||
documents: [{ jim: 'bob' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
|
||||
const actionId = 'some-id';
|
||||
|
@ -200,6 +204,7 @@ describe('execute()', () => {
|
|||
config = { index: 'index-value', executionTimeField: 'field_to_use_for_time', refresh: true };
|
||||
params = {
|
||||
documents: [{ jimbob: 'jr' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
|
||||
executorOptions = { actionId, config, secrets, params, services };
|
||||
|
@ -237,6 +242,7 @@ describe('execute()', () => {
|
|||
config = { index: 'index-value', executionTimeField: null, refresh: false };
|
||||
params = {
|
||||
documents: [{ jim: 'bob' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
|
||||
executorOptions = { actionId, config, secrets, params, services };
|
||||
|
@ -270,6 +276,7 @@ describe('execute()', () => {
|
|||
config = { index: 'index-value', executionTimeField: null, refresh: false };
|
||||
params = {
|
||||
documents: [{ a: 1 }, { b: 2 }],
|
||||
indexOverride: null,
|
||||
};
|
||||
|
||||
executorOptions = { actionId, config, secrets, params, services };
|
||||
|
@ -305,12 +312,244 @@ describe('execute()', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
documents: [{ hello: '{{who}}' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
const variables = {
|
||||
who: 'world',
|
||||
};
|
||||
const renderedParams = actionType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables,
|
||||
'action-type-id'
|
||||
);
|
||||
expect(renderedParams).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"documents": Array [
|
||||
Object {
|
||||
"hello": "world",
|
||||
},
|
||||
],
|
||||
"indexOverride": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('ignores indexOverride for generic es index connector', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
documents: [{ hello: '{{who}}' }],
|
||||
indexOverride: 'hello-world',
|
||||
};
|
||||
const variables = {
|
||||
who: 'world',
|
||||
};
|
||||
const renderedParams = actionType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables,
|
||||
'action-type-id'
|
||||
);
|
||||
expect(renderedParams).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"documents": Array [
|
||||
Object {
|
||||
"hello": "world",
|
||||
},
|
||||
],
|
||||
"indexOverride": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected for preconfigured alert history connector', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
documents: [{ hello: '{{who}}' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
const variables = {
|
||||
date: '2021-01-01T00:00:00.000Z',
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: 'rule-name',
|
||||
type: 'rule-type',
|
||||
},
|
||||
context: {
|
||||
contextVar1: 'contextValue1',
|
||||
contextVar2: 'contextValue2',
|
||||
},
|
||||
params: {
|
||||
ruleParam: 1,
|
||||
ruleParamString: 'another param',
|
||||
},
|
||||
tags: ['abc', 'xyz'],
|
||||
alert: {
|
||||
id: 'alert-id',
|
||||
actionGroup: 'action-group-id',
|
||||
actionGroupName: 'Action Group',
|
||||
},
|
||||
state: {
|
||||
alertStateValue: true,
|
||||
alertStateAnotherValue: 'yes',
|
||||
},
|
||||
};
|
||||
const renderedParams = actionType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables,
|
||||
AlertHistoryEsIndexConnectorId
|
||||
);
|
||||
expect(renderedParams).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"documents": Array [
|
||||
Object {
|
||||
"@timestamp": "2021-01-01T00:00:00.000Z",
|
||||
"event": Object {
|
||||
"kind": "alert",
|
||||
},
|
||||
"kibana": Object {
|
||||
"alert": Object {
|
||||
"actionGroup": "action-group-id",
|
||||
"actionGroupName": "Action Group",
|
||||
"context": Object {
|
||||
"rule-type": Object {
|
||||
"contextVar1": "contextValue1",
|
||||
"contextVar2": "contextValue2",
|
||||
},
|
||||
},
|
||||
"id": "alert-id",
|
||||
},
|
||||
},
|
||||
"rule": Object {
|
||||
"id": "rule-id",
|
||||
"name": "rule-name",
|
||||
"params": Object {
|
||||
"rule-type": Object {
|
||||
"ruleParam": 1,
|
||||
"ruleParamString": "another param",
|
||||
},
|
||||
},
|
||||
"type": "rule-type",
|
||||
},
|
||||
"tags": Array [
|
||||
"abc",
|
||||
"xyz",
|
||||
],
|
||||
},
|
||||
],
|
||||
"indexOverride": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('passes through indexOverride for preconfigured alert history connector', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
documents: [{ hello: '{{who}}' }],
|
||||
indexOverride: 'hello-world',
|
||||
};
|
||||
const variables = {
|
||||
date: '2021-01-01T00:00:00.000Z',
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: 'rule-name',
|
||||
type: 'rule-type',
|
||||
},
|
||||
context: {
|
||||
contextVar1: 'contextValue1',
|
||||
contextVar2: 'contextValue2',
|
||||
},
|
||||
params: {
|
||||
ruleParam: 1,
|
||||
ruleParamString: 'another param',
|
||||
},
|
||||
tags: ['abc', 'xyz'],
|
||||
alert: {
|
||||
id: 'alert-id',
|
||||
actionGroup: 'action-group-id',
|
||||
actionGroupName: 'Action Group',
|
||||
},
|
||||
state: {
|
||||
alertStateValue: true,
|
||||
alertStateAnotherValue: 'yes',
|
||||
},
|
||||
};
|
||||
const renderedParams = actionType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables,
|
||||
AlertHistoryEsIndexConnectorId
|
||||
);
|
||||
expect(renderedParams).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"documents": Array [
|
||||
Object {
|
||||
"@timestamp": "2021-01-01T00:00:00.000Z",
|
||||
"event": Object {
|
||||
"kind": "alert",
|
||||
},
|
||||
"kibana": Object {
|
||||
"alert": Object {
|
||||
"actionGroup": "action-group-id",
|
||||
"actionGroupName": "Action Group",
|
||||
"context": Object {
|
||||
"rule-type": Object {
|
||||
"contextVar1": "contextValue1",
|
||||
"contextVar2": "contextValue2",
|
||||
},
|
||||
},
|
||||
"id": "alert-id",
|
||||
},
|
||||
},
|
||||
"rule": Object {
|
||||
"id": "rule-id",
|
||||
"name": "rule-name",
|
||||
"params": Object {
|
||||
"rule-type": Object {
|
||||
"ruleParam": 1,
|
||||
"ruleParamString": "another param",
|
||||
},
|
||||
},
|
||||
"type": "rule-type",
|
||||
},
|
||||
"tags": Array [
|
||||
"abc",
|
||||
"xyz",
|
||||
],
|
||||
},
|
||||
],
|
||||
"indexOverride": "hello-world",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws error for preconfigured alert history index when no variables are available', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
documents: [{ hello: '{{who}}' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
const variables = {};
|
||||
|
||||
expect(() =>
|
||||
actionType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables,
|
||||
AlertHistoryEsIndexConnectorId
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error creating alert history document for ${AlertHistoryEsIndexConnectorId} connector"`
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves with an error when an error occurs in the indexing operation', async () => {
|
||||
const secrets = {};
|
||||
// minimal params
|
||||
const config = { index: 'index-value', refresh: false, executionTimeField: null };
|
||||
const params = {
|
||||
documents: [{ '': 'bob' }],
|
||||
indexOverride: null,
|
||||
};
|
||||
|
||||
const actionId = 'some-id';
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
import { curry, find } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
|
||||
import { renderMustacheObject } from '../lib/mustache_renderer';
|
||||
import { buildAlertHistoryDocument, AlertHistoryEsIndexConnectorId } from '../../common';
|
||||
import { ALERT_HISTORY_PREFIX } from '../../common/alert_history_schema';
|
||||
|
||||
export type ESIndexActionType = ActionType<ActionTypeConfigType, {}, ActionParamsType, unknown>;
|
||||
export type ESIndexActionTypeExecutorOptions = ActionTypeExecutorOptions<
|
||||
|
@ -38,6 +40,15 @@ export type ActionParamsType = TypeOf<typeof ParamsSchema>;
|
|||
// eventually: https://github.com/elastic/kibana/projects/26#card-24087404
|
||||
const ParamsSchema = schema.object({
|
||||
documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())),
|
||||
indexOverride: schema.nullable(
|
||||
schema.string({
|
||||
validate: (pattern) => {
|
||||
if (!pattern.startsWith(ALERT_HISTORY_PREFIX)) {
|
||||
return `index must start with "${ALERT_HISTORY_PREFIX}"`;
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const ActionTypeId = '.index';
|
||||
|
@ -54,6 +65,7 @@ export function getActionType({ logger }: { logger: Logger }): ESIndexActionType
|
|||
params: ParamsSchema,
|
||||
},
|
||||
executor: curry(executor)({ logger }),
|
||||
renderParameterTemplates,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -68,7 +80,7 @@ async function executor(
|
|||
const params = execOptions.params;
|
||||
const services = execOptions.services;
|
||||
|
||||
const index = config.index;
|
||||
const index = params.indexOverride || config.index;
|
||||
|
||||
const bulkBody = [];
|
||||
for (const document of params.documents) {
|
||||
|
@ -107,6 +119,24 @@ async function executor(
|
|||
}
|
||||
}
|
||||
|
||||
function renderParameterTemplates(
|
||||
params: ActionParamsType,
|
||||
variables: Record<string, unknown>,
|
||||
actionId: string
|
||||
): ActionParamsType {
|
||||
const { documents, indexOverride } = renderMustacheObject<ActionParamsType>(params, variables);
|
||||
|
||||
if (actionId === AlertHistoryEsIndexConnectorId) {
|
||||
const alertHistoryDoc = buildAlertHistoryDocument(variables);
|
||||
if (!alertHistoryDoc) {
|
||||
throw new Error(`error creating alert history document for ${actionId} connector`);
|
||||
}
|
||||
return { documents: [alertHistoryDoc], indexOverride };
|
||||
}
|
||||
|
||||
return { documents, indexOverride: null };
|
||||
}
|
||||
|
||||
function wrapErr(
|
||||
errMessage: string,
|
||||
actionId: string,
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('config validation', () => {
|
|||
"valueInBytes": 1048576,
|
||||
},
|
||||
"preconfigured": Object {},
|
||||
"preconfiguredAlertHistoryEsIndex": false,
|
||||
"proxyRejectUnauthorizedCertificates": true,
|
||||
"rejectUnauthorized": true,
|
||||
"responseTimeout": "PT1M",
|
||||
|
@ -74,6 +75,7 @@ describe('config validation', () => {
|
|||
"secrets": Object {},
|
||||
},
|
||||
},
|
||||
"preconfiguredAlertHistoryEsIndex": false,
|
||||
"proxyRejectUnauthorizedCertificates": false,
|
||||
"rejectUnauthorized": false,
|
||||
"responseTimeout": "PT1M",
|
||||
|
|
|
@ -37,6 +37,7 @@ export const configSchema = schema.object({
|
|||
defaultValue: [AllowedHosts.Any],
|
||||
}
|
||||
),
|
||||
preconfiguredAlertHistoryEsIndex: schema.boolean({ defaultValue: false }),
|
||||
preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, {
|
||||
defaultValue: {},
|
||||
validate: validatePreconfigured,
|
||||
|
|
|
@ -40,10 +40,11 @@ const createStartMock = () => {
|
|||
// this is a default renderer that escapes nothing
|
||||
export function renderActionParameterTemplatesDefault<RecordType>(
|
||||
actionTypeId: string,
|
||||
actionId: string,
|
||||
params: Record<string, unknown>,
|
||||
variables: Record<string, unknown>
|
||||
) {
|
||||
return renderActionParameterTemplates(undefined, actionTypeId, params, variables);
|
||||
return renderActionParameterTemplates(undefined, actionTypeId, actionId, params, variables);
|
||||
}
|
||||
|
||||
const createServicesMock = () => {
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
ActionsPluginsStart,
|
||||
PluginSetupContract,
|
||||
} from './plugin';
|
||||
import { AlertHistoryEsIndexConnectorId } from '../common';
|
||||
|
||||
describe('Actions Plugin', () => {
|
||||
describe('setup()', () => {
|
||||
|
@ -36,6 +37,7 @@ describe('Actions Plugin', () => {
|
|||
enabled: true,
|
||||
enabledActionTypes: ['*'],
|
||||
allowedHosts: ['*'],
|
||||
preconfiguredAlertHistoryEsIndex: false,
|
||||
preconfigured: {},
|
||||
proxyRejectUnauthorizedCertificates: true,
|
||||
rejectUnauthorized: true,
|
||||
|
@ -180,6 +182,7 @@ describe('Actions Plugin', () => {
|
|||
});
|
||||
|
||||
describe('start()', () => {
|
||||
let context: PluginInitializerContext;
|
||||
let plugin: ActionsPlugin;
|
||||
let coreSetup: ReturnType<typeof coreMock.createSetup>;
|
||||
let coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
|
@ -187,10 +190,11 @@ describe('Actions Plugin', () => {
|
|||
let pluginsStart: jest.Mocked<ActionsPluginsStart>;
|
||||
|
||||
beforeEach(() => {
|
||||
const context = coreMock.createPluginInitializerContext<ActionsConfig>({
|
||||
context = coreMock.createPluginInitializerContext<ActionsConfig>({
|
||||
enabled: true,
|
||||
enabledActionTypes: ['*'],
|
||||
allowedHosts: ['*'],
|
||||
preconfiguredAlertHistoryEsIndex: false,
|
||||
preconfigured: {
|
||||
preconfiguredServerLog: {
|
||||
actionTypeId: '.server-log',
|
||||
|
@ -223,15 +227,6 @@ describe('Actions Plugin', () => {
|
|||
});
|
||||
|
||||
describe('getActionsClientWithRequest()', () => {
|
||||
it('should handle preconfigured actions', async () => {
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not throw error when ESO plugin has encryption key', async () => {
|
||||
await plugin.setup(coreSetup, {
|
||||
...pluginsSetup,
|
||||
|
@ -258,6 +253,99 @@ describe('Actions Plugin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Preconfigured connectors', () => {
|
||||
function getConfig(overrides = {}) {
|
||||
return {
|
||||
enabled: true,
|
||||
enabledActionTypes: ['*'],
|
||||
allowedHosts: ['*'],
|
||||
preconfiguredAlertHistoryEsIndex: false,
|
||||
preconfigured: {
|
||||
preconfiguredServerLog: {
|
||||
actionTypeId: '.server-log',
|
||||
name: 'preconfigured-server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
},
|
||||
proxyRejectUnauthorizedCertificates: true,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
rejectUnauthorized: true,
|
||||
maxResponseContentLength: new ByteSizeValue(1000000),
|
||||
responseTimeout: moment.duration('60s'),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function setup(config: ActionsConfig) {
|
||||
context = coreMock.createPluginInitializerContext<ActionsConfig>(config);
|
||||
plugin = new ActionsPlugin(context);
|
||||
coreSetup = coreMock.createSetup();
|
||||
coreStart = coreMock.createStart();
|
||||
pluginsSetup = {
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
|
||||
licensing: licensingMock.createSetup(),
|
||||
eventLog: eventLogMock.createSetup(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
features: featuresPluginMock.createSetup(),
|
||||
};
|
||||
pluginsStart = {
|
||||
licensing: licensingMock.createStart(),
|
||||
taskManager: taskManagerMock.createStart(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
};
|
||||
}
|
||||
|
||||
it('should handle preconfigured actions', async () => {
|
||||
setup(getConfig());
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart.preconfiguredActions.length).toEqual(1);
|
||||
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle preconfiguredAlertHistoryEsIndex = true', async () => {
|
||||
setup(getConfig({ preconfiguredAlertHistoryEsIndex: true }));
|
||||
|
||||
await plugin.setup(coreSetup, pluginsSetup);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart.preconfiguredActions.length).toEqual(2);
|
||||
expect(
|
||||
pluginStart.isActionExecutable('preconfigured-alert-history-es-index', '.index')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow preconfigured connector with same ID as AlertHistoryEsIndexConnectorId', async () => {
|
||||
setup(
|
||||
getConfig({
|
||||
preconfigured: {
|
||||
[AlertHistoryEsIndexConnectorId]: {
|
||||
actionTypeId: '.index',
|
||||
name: 'clashing preconfigured index connector',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart.preconfiguredActions.length).toEqual(0);
|
||||
expect(context.logger.get().warn).toHaveBeenCalledWith(
|
||||
`Preconfigured connectors cannot have the id "${AlertHistoryEsIndexConnectorId}" because this is a reserved id.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActionTypeEnabled()', () => {
|
||||
const actionType: ActionType = {
|
||||
id: 'my-action-type',
|
||||
|
|
|
@ -68,6 +68,9 @@ import {
|
|||
} from './authorization/get_authorization_mode_by_source';
|
||||
import { ensureSufficientLicense } from './lib/ensure_sufficient_license';
|
||||
import { renderMustacheObject } from './lib/mustache_renderer';
|
||||
import { getAlertHistoryEsIndex } from './preconfigured_connectors/alert_history_es_index/alert_history_es_index';
|
||||
import { createAlertHistoryIndexTemplate } from './preconfigured_connectors/alert_history_es_index/create_alert_history_index_template';
|
||||
import { AlertHistoryEsIndexConnectorId } from '../common';
|
||||
|
||||
const EVENT_LOG_PROVIDER = 'actions';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -98,6 +101,7 @@ export interface PluginStartContract {
|
|||
preconfiguredActions: PreConfiguredAction[];
|
||||
renderActionParameterTemplates<Params extends ActionTypeParams = ActionTypeParams>(
|
||||
actionTypeId: string,
|
||||
actionId: string,
|
||||
params: Params,
|
||||
variables: Record<string, unknown>
|
||||
): Params;
|
||||
|
@ -178,12 +182,22 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
const taskRunnerFactory = new TaskRunnerFactory(actionExecutor);
|
||||
const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig);
|
||||
|
||||
if (this.actionsConfig.preconfiguredAlertHistoryEsIndex) {
|
||||
this.preconfiguredActions.push(getAlertHistoryEsIndex());
|
||||
}
|
||||
|
||||
for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) {
|
||||
this.preconfiguredActions.push({
|
||||
...this.actionsConfig.preconfigured[preconfiguredId],
|
||||
id: preconfiguredId,
|
||||
isPreconfigured: true,
|
||||
});
|
||||
if (preconfiguredId !== AlertHistoryEsIndexConnectorId) {
|
||||
this.preconfiguredActions.push({
|
||||
...this.actionsConfig.preconfigured[preconfiguredId],
|
||||
id: preconfiguredId,
|
||||
isPreconfigured: true,
|
||||
});
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Preconfigured connectors cannot have the id "${AlertHistoryEsIndexConnectorId}" because this is a reserved id.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const actionTypeRegistry = new ActionTypeRegistry({
|
||||
|
@ -355,6 +369,13 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
|
||||
scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager);
|
||||
|
||||
if (this.actionsConfig.preconfiguredAlertHistoryEsIndex) {
|
||||
createAlertHistoryIndexTemplate({
|
||||
client: core.elasticsearch.client.asInternalUser,
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isActionTypeEnabled: (id, options = { notifyUsage: false }) => {
|
||||
return this.actionTypeRegistry!.isActionTypeEnabled(id, options);
|
||||
|
@ -468,12 +489,13 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
export function renderActionParameterTemplates<Params extends ActionTypeParams = ActionTypeParams>(
|
||||
actionTypeRegistry: ActionTypeRegistry | undefined,
|
||||
actionTypeId: string,
|
||||
actionId: string,
|
||||
params: Params,
|
||||
variables: Record<string, unknown>
|
||||
): Params {
|
||||
const actionType = actionTypeRegistry?.get(actionTypeId);
|
||||
if (actionType?.renderParameterTemplates) {
|
||||
return actionType.renderParameterTemplates(params, variables) as Params;
|
||||
return actionType.renderParameterTemplates(params, variables, actionId) as Params;
|
||||
} else {
|
||||
return renderMustacheObject(params, variables);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PreConfiguredAction } from '../../types';
|
||||
import { ActionTypeId as EsIndexActionTypeId } from '../../builtin_action_types/es_index';
|
||||
import { AlertHistoryEsIndexConnectorId, AlertHistoryDefaultIndexName } from '../../../common';
|
||||
|
||||
export function getAlertHistoryEsIndex(): Readonly<PreConfiguredAction> {
|
||||
return Object.freeze({
|
||||
name: i18n.translate('xpack.actions.alertHistoryEsIndexConnector.name', {
|
||||
defaultMessage: 'Alert history Elasticsearch index',
|
||||
}),
|
||||
actionTypeId: EsIndexActionTypeId,
|
||||
id: AlertHistoryEsIndexConnectorId,
|
||||
isPreconfigured: true,
|
||||
config: {
|
||||
index: AlertHistoryDefaultIndexName,
|
||||
},
|
||||
secrets: {},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient } from 'src/core/server';
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
import {
|
||||
createAlertHistoryIndexTemplate,
|
||||
getAlertHistoryIndexTemplate,
|
||||
} from './create_alert_history_index_template';
|
||||
|
||||
type MockedLogger = ReturnType<typeof loggingSystemMock['createLogger']>;
|
||||
|
||||
describe('createAlertHistoryIndexTemplate', () => {
|
||||
let logger: MockedLogger;
|
||||
let clusterClient: DeeplyMockedKeys<ElasticsearchClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
});
|
||||
|
||||
test(`should create index template if it doesn't exist`, async () => {
|
||||
// Response type for existsIndexTemplate is still TODO
|
||||
clusterClient.indices.existsIndexTemplate.mockResolvedValue({
|
||||
body: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
await createAlertHistoryIndexTemplate({ client: clusterClient, logger });
|
||||
expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({
|
||||
name: `kibana-alert-history-template`,
|
||||
body: getAlertHistoryIndexTemplate(),
|
||||
create: true,
|
||||
});
|
||||
});
|
||||
|
||||
test(`shouldn't create index template if it already exists`, async () => {
|
||||
// Response type for existsIndexTemplate is still TODO
|
||||
clusterClient.indices.existsIndexTemplate.mockResolvedValue({
|
||||
body: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
await createAlertHistoryIndexTemplate({ client: clusterClient, logger });
|
||||
expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient, Logger } from 'src/core/server';
|
||||
import { ALERT_HISTORY_PREFIX } from '../../../common';
|
||||
import mappings from './mappings.json';
|
||||
|
||||
export function getAlertHistoryIndexTemplate() {
|
||||
return {
|
||||
index_patterns: [`${ALERT_HISTORY_PREFIX}*`],
|
||||
_meta: {
|
||||
description:
|
||||
'System generated mapping for preconfigured alert history Elasticsearch index connector.',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
auto_expand_replicas: '0-1',
|
||||
},
|
||||
mappings,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function doesIndexTemplateExist({
|
||||
client,
|
||||
templateName,
|
||||
}: {
|
||||
client: ElasticsearchClient;
|
||||
templateName: string;
|
||||
}) {
|
||||
let result;
|
||||
try {
|
||||
result = (await client.indices.existsIndexTemplate({ name: templateName })).body;
|
||||
} catch (err) {
|
||||
throw new Error(`error checking existence of index template: ${err.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function createIndexTemplate({
|
||||
client,
|
||||
template,
|
||||
templateName,
|
||||
}: {
|
||||
client: ElasticsearchClient;
|
||||
template: Record<string, unknown>;
|
||||
templateName: string;
|
||||
}) {
|
||||
try {
|
||||
await client.indices.putIndexTemplate({
|
||||
name: templateName,
|
||||
body: template,
|
||||
create: true,
|
||||
});
|
||||
} catch (err) {
|
||||
// The error message doesn't have a type attribute we can look to guarantee it's due
|
||||
// to the template already existing (only long message) so we'll check ourselves to see
|
||||
// if the template now exists. This scenario would happen if you startup multiple Kibana
|
||||
// instances at the same time.
|
||||
const existsNow = await doesIndexTemplateExist({ client, templateName });
|
||||
if (!existsNow) {
|
||||
throw new Error(`error creating index template: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createIndexTemplateIfNotExists({
|
||||
client,
|
||||
template,
|
||||
templateName,
|
||||
}: {
|
||||
client: ElasticsearchClient;
|
||||
template: Record<string, unknown>;
|
||||
templateName: string;
|
||||
}) {
|
||||
const indexTemplateExists = await doesIndexTemplateExist({ client, templateName });
|
||||
|
||||
if (!indexTemplateExists) {
|
||||
await createIndexTemplate({ client, template, templateName });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAlertHistoryIndexTemplate({
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
client: ElasticsearchClient;
|
||||
logger: Logger;
|
||||
}) {
|
||||
try {
|
||||
const indexTemplate = getAlertHistoryIndexTemplate();
|
||||
await createIndexTemplateIfNotExists({
|
||||
client,
|
||||
templateName: `${ALERT_HISTORY_PREFIX}template`,
|
||||
template: indexTemplate,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`Could not initialize alert history index with mappings: ${err.message}.`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"dynamic": "false",
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"kibana": {
|
||||
"properties": {
|
||||
"alert": {
|
||||
"properties": {
|
||||
"actionGroup": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"actionGroupName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"actionSubgroup": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"context": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword",
|
||||
"meta": {
|
||||
"isArray": "true"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"norms": false,
|
||||
"type": "text"
|
||||
},
|
||||
"event": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rule": {
|
||||
"properties": {
|
||||
"author": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"category": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"license": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"space": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,7 +107,11 @@ export interface ActionType<
|
|||
config?: ValidatorType<Config>;
|
||||
secrets?: ValidatorType<Secrets>;
|
||||
};
|
||||
renderParameterTemplates?(params: Params, variables: Record<string, unknown>): Params;
|
||||
renderParameterTemplates?(
|
||||
params: Params,
|
||||
variables: Record<string, unknown>,
|
||||
actionId?: string
|
||||
): Params;
|
||||
executor: ExecutorType<Config, Secrets, Params, ExecutorResultData>;
|
||||
}
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ export function createExecutionHandler<
|
|||
params: transformActionParams({
|
||||
actionsPlugin,
|
||||
alertId,
|
||||
alertType: alertType.id,
|
||||
actionTypeId: action.actionTypeId,
|
||||
alertName,
|
||||
spaceId,
|
||||
|
@ -127,6 +128,7 @@ export function createExecutionHandler<
|
|||
alertActionSubgroup: actionSubgroup,
|
||||
context,
|
||||
actionParams: action.params,
|
||||
actionId: action.id,
|
||||
state,
|
||||
kibanaBaseUrl,
|
||||
alertParams,
|
||||
|
|
|
@ -153,7 +153,7 @@ describe('Task Runner', () => {
|
|||
actionsClient
|
||||
);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation(
|
||||
(actionTypeId, params) => params
|
||||
(actionTypeId, actionId, params) => params
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ test('skips non string parameters', () => {
|
|||
context: {},
|
||||
state: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -68,6 +70,8 @@ test('missing parameters get emptied out', () => {
|
|||
context: {},
|
||||
state: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -95,6 +99,8 @@ test('context parameters are passed to templates', () => {
|
|||
state: {},
|
||||
context: { foo: 'fooVal' },
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -121,6 +127,8 @@ test('state parameters are passed to templates', () => {
|
|||
state: { bar: 'barVal' },
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -147,6 +155,8 @@ test('alertId is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -173,6 +183,8 @@ test('alertName is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -199,6 +211,8 @@ test('tags is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -225,6 +239,8 @@ test('undefined tags is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
|
@ -250,6 +266,8 @@ test('empty tags is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: [],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -276,6 +294,8 @@ test('spaceId is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -302,6 +322,8 @@ test('alertInstanceId is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -328,6 +350,8 @@ test('alertActionGroup is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -354,6 +378,8 @@ test('alertActionGroupName is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -380,6 +406,8 @@ test('rule variables are passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -408,6 +436,8 @@ test('rule alert variables are passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -436,6 +466,8 @@ test('date is passed to templates', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -464,6 +496,8 @@ test('works recursively', () => {
|
|||
state: { value: 'state' },
|
||||
context: { value: 'context' },
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
@ -494,6 +528,8 @@ test('works recursively with arrays', () => {
|
|||
state: { value: 'state' },
|
||||
context: { value: 'context' },
|
||||
alertId: '1',
|
||||
alertType: 'rule-type-id',
|
||||
actionId: 'action-id',
|
||||
alertName: 'alert-name',
|
||||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
|
|
|
@ -16,6 +16,8 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../../acti
|
|||
interface TransformActionParamsOptions {
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
alertId: string;
|
||||
alertType: string;
|
||||
actionId: string;
|
||||
actionTypeId: string;
|
||||
alertName: string;
|
||||
spaceId: string;
|
||||
|
@ -34,6 +36,8 @@ interface TransformActionParamsOptions {
|
|||
export function transformActionParams({
|
||||
actionsPlugin,
|
||||
alertId,
|
||||
alertType,
|
||||
actionId,
|
||||
actionTypeId,
|
||||
alertName,
|
||||
spaceId,
|
||||
|
@ -68,6 +72,7 @@ export function transformActionParams({
|
|||
rule: {
|
||||
id: alertId,
|
||||
name: alertName,
|
||||
type: alertType,
|
||||
spaceId,
|
||||
tags,
|
||||
},
|
||||
|
@ -78,5 +83,10 @@ export function transformActionParams({
|
|||
actionSubgroup: alertActionSubgroup,
|
||||
},
|
||||
};
|
||||
return actionsPlugin.renderActionParameterTemplates(actionTypeId, actionParams, variables);
|
||||
return actionsPlugin.renderActionParameterTemplates(
|
||||
actionTypeId,
|
||||
actionId,
|
||||
actionParams,
|
||||
variables
|
||||
);
|
||||
}
|
||||
|
|
|
@ -82,32 +82,71 @@ describe('index connector validation with minimal config', () => {
|
|||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
const actionParams = {
|
||||
documents: [{ test: 1234 }],
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
test('action params validation succeeds when action params are valid', () => {
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
documents: [{ test: 1234 }],
|
||||
})
|
||||
).toEqual({
|
||||
errors: {
|
||||
documents: [],
|
||||
indexOverride: [],
|
||||
},
|
||||
});
|
||||
|
||||
const emptyActionParams = {};
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
documents: [{ test: 1234 }],
|
||||
indexOverride: 'kibana-alert-history-anything',
|
||||
})
|
||||
).toEqual({
|
||||
errors: {
|
||||
documents: [],
|
||||
indexOverride: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({
|
||||
test('action params validation fails when action params are invalid', () => {
|
||||
expect(actionTypeModel.validateParams({})).toEqual({
|
||||
errors: {
|
||||
documents: ['Document is required and should be a valid JSON object.'],
|
||||
indexOverride: [],
|
||||
},
|
||||
});
|
||||
|
||||
const invalidDocumentActionParams = {
|
||||
documents: [{}],
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(invalidDocumentActionParams)).toEqual({
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
})
|
||||
).toEqual({
|
||||
errors: {
|
||||
documents: ['Document is required and should be a valid JSON object.'],
|
||||
indexOverride: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
indexOverride: 'kibana-alert-history-',
|
||||
})
|
||||
).toEqual({
|
||||
errors: {
|
||||
documents: ['Document is required and should be a valid JSON object.'],
|
||||
indexOverride: ['Alert history index must contain valid suffix.'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
indexOverride: 'this.is-a_string',
|
||||
})
|
||||
).toEqual({
|
||||
errors: {
|
||||
documents: ['Document is required and should be a valid JSON object.'],
|
||||
indexOverride: ['Alert history index must begin with "kibana-alert-history-".'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
ALERT_HISTORY_PREFIX,
|
||||
} from '../../../../types';
|
||||
import { EsIndexActionConnector, EsIndexConfig, IndexActionParams } from '../types';
|
||||
|
||||
|
@ -56,6 +57,7 @@ export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexAc
|
|||
): GenericValidationResult<IndexActionParams> => {
|
||||
const errors = {
|
||||
documents: new Array<string>(),
|
||||
indexOverride: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) {
|
||||
|
@ -68,6 +70,32 @@ export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexAc
|
|||
)
|
||||
);
|
||||
}
|
||||
if (actionParams.indexOverride) {
|
||||
if (!actionParams.indexOverride.startsWith(ALERT_HISTORY_PREFIX)) {
|
||||
errors.indexOverride.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue',
|
||||
{
|
||||
defaultMessage: 'Alert history index must begin with "{alertHistoryPrefix}".',
|
||||
values: { alertHistoryPrefix: ALERT_HISTORY_PREFIX },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const indexSuffix = actionParams.indexOverride.replace(ALERT_HISTORY_PREFIX, '');
|
||||
if (indexSuffix.length === 0) {
|
||||
errors.indexOverride.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix',
|
||||
{
|
||||
defaultMessage: 'Alert history index must contain valid suffix.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,12 +6,63 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
||||
import { act } from '@testing-library/react';
|
||||
import ParamsFields from './es_index_params';
|
||||
import { AlertHistoryEsIndexConnectorId } from '../../../../types';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const actionConnector = {
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: 'test-index',
|
||||
},
|
||||
id: 'es index connector',
|
||||
isPreconfigured: false,
|
||||
name: 'test name',
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
const preconfiguredActionConnector = {
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: 'kibana-alert-history-default',
|
||||
},
|
||||
id: AlertHistoryEsIndexConnectorId,
|
||||
isPreconfigured: true,
|
||||
name: 'Alert history Elasticsearch index',
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
describe('IndexParamsFields renders', () => {
|
||||
test('all params fields is rendered', () => {
|
||||
test('all params fields are rendered correctly when params are undefined', () => {
|
||||
const actionParams = {
|
||||
documents: undefined,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ index: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
actionConnector={actionConnector}
|
||||
messageVariables={[
|
||||
{
|
||||
name: 'myVar',
|
||||
description: 'My variable description',
|
||||
useWithTripleBracesInTemplates: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(``);
|
||||
expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeFalsy();
|
||||
});
|
||||
|
||||
test('all params fields are rendered when document params are defined', () => {
|
||||
const actionParams = {
|
||||
documents: [{ test: 123 }],
|
||||
};
|
||||
|
@ -22,6 +73,7 @@ describe('IndexParamsFields renders', () => {
|
|||
errors={{ index: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
actionConnector={actionConnector}
|
||||
messageVariables={[
|
||||
{
|
||||
name: 'myVar',
|
||||
|
@ -35,5 +87,76 @@ describe('IndexParamsFields renders', () => {
|
|||
"test": 123
|
||||
}`);
|
||||
expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeFalsy();
|
||||
});
|
||||
|
||||
test('all params fields are rendered correctly for preconfigured alert history connector when params are undefined', () => {
|
||||
const actionParams = {
|
||||
documents: undefined,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ index: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
actionConnector={preconfiguredActionConnector}
|
||||
messageVariables={[
|
||||
{
|
||||
name: 'myVar',
|
||||
description: 'My variable description',
|
||||
useWithTripleBracesInTemplates: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe(
|
||||
'default'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('all params fields are rendered correctly for preconfigured alert history connector when params are defined', async () => {
|
||||
const actionParams = {
|
||||
documents: undefined,
|
||||
indexOverride: 'kibana-alert-history-not-the-default',
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ index: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
actionConnector={preconfiguredActionConnector}
|
||||
messageVariables={[
|
||||
{
|
||||
name: 'myVar',
|
||||
description: 'My variable description',
|
||||
useWithTripleBracesInTemplates: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe(
|
||||
'not-the-default'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeTruthy();
|
||||
|
||||
wrapper.find('EuiLink[data-test-subj="resetDefaultIndex"]').simulate('click');
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe(
|
||||
'default'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiCodeBlock,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import {
|
||||
ActionParamsProps,
|
||||
AlertHistoryEsIndexConnectorId,
|
||||
AlertHistoryDocumentTemplate,
|
||||
AlertHistoryDefaultIndexName,
|
||||
ALERT_HISTORY_PREFIX,
|
||||
} from '../../../../types';
|
||||
import { IndexActionParams } from '.././types';
|
||||
import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -20,38 +34,152 @@ export const IndexParamsFields = ({
|
|||
editAction,
|
||||
messageVariables,
|
||||
errors,
|
||||
actionConnector,
|
||||
}: ActionParamsProps<IndexActionParams>) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const { documents } = actionParams;
|
||||
const { documents, indexOverride } = actionParams;
|
||||
|
||||
const defaultAlertHistoryIndexSuffix = AlertHistoryDefaultIndexName.replace(
|
||||
ALERT_HISTORY_PREFIX,
|
||||
''
|
||||
);
|
||||
|
||||
const getDocumentToIndex = (doc: Array<Record<string, any>> | undefined) =>
|
||||
doc && doc.length > 0 ? ((doc[0] as unknown) as string) : undefined;
|
||||
|
||||
const [documentToIndex, setDocumentToIndex] = useState<string | undefined>(
|
||||
getDocumentToIndex(documents)
|
||||
);
|
||||
const [alertHistoryIndexSuffix, setAlertHistoryIndexSuffix] = useState<string>(
|
||||
indexOverride ? indexOverride.replace(ALERT_HISTORY_PREFIX, '') : defaultAlertHistoryIndexSuffix
|
||||
);
|
||||
const [usePreconfiguredSchema, setUsePreconfiguredSchema] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentToIndex(getDocumentToIndex(documents));
|
||||
}, [documents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionConnector?.id === AlertHistoryEsIndexConnectorId) {
|
||||
setUsePreconfiguredSchema(true);
|
||||
editAction('documents', [JSON.stringify(AlertHistoryDocumentTemplate)], index);
|
||||
setDocumentToIndex(JSON.stringify(AlertHistoryDocumentTemplate));
|
||||
} else {
|
||||
setUsePreconfiguredSchema(false);
|
||||
editAction('documents', undefined, index);
|
||||
setDocumentToIndex(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector?.id]);
|
||||
|
||||
const onDocumentsChange = (updatedDocuments: string) => {
|
||||
try {
|
||||
const documentsJSON = JSON.parse(updatedDocuments);
|
||||
editAction('documents', [documentsJSON], index);
|
||||
setDocumentToIndex(updatedDocuments);
|
||||
} catch (e) {
|
||||
// set document as empty to turn on the validation for non empty valid JSON object
|
||||
editAction('documents', [{}], index);
|
||||
setDocumentToIndex(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
const documentsFieldLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Document to index',
|
||||
}
|
||||
);
|
||||
|
||||
const resetDefaultIndex =
|
||||
indexOverride && indexOverride !== AlertHistoryDefaultIndexName ? (
|
||||
<EuiText size="xs">
|
||||
<EuiLink
|
||||
data-test-subj="resetDefaultIndex"
|
||||
onClick={() => {
|
||||
editAction('indexOverride', AlertHistoryDefaultIndexName, index);
|
||||
setAlertHistoryIndexSuffix(defaultAlertHistoryIndexSuffix);
|
||||
}}
|
||||
>
|
||||
<EuiIcon type="refresh" />
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.resetDefaultIndexLabel"
|
||||
defaultMessage="Reset default index"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
const preconfiguredDocumentSchema = (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.indexOverride as string[]}
|
||||
isInvalid={(errors.indexOverride as string[]) && errors.indexOverride.length > 0}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndex',
|
||||
{
|
||||
defaultMessage: 'Elasticsearch index',
|
||||
}
|
||||
)}
|
||||
labelAppend={resetDefaultIndex}
|
||||
helpText={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndexHelpText"
|
||||
defaultMessage="Documents are indexed into the {alertHistoryIndex} index. "
|
||||
values={{ alertHistoryIndex: `${ALERT_HISTORY_PREFIX}${alertHistoryIndexSuffix}` }}
|
||||
/>
|
||||
<EuiLink
|
||||
href={docLinks.links.alerting.preconfiguredAlertHistoryConnector}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndexDocLink"
|
||||
defaultMessage="View docs."
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
data-test-subj="preconfiguredIndexToUse"
|
||||
prepend={ALERT_HISTORY_PREFIX}
|
||||
value={alertHistoryIndexSuffix}
|
||||
onChange={(e) => {
|
||||
editAction('indexOverride', `${ALERT_HISTORY_PREFIX}${e.target.value}`, index);
|
||||
setAlertHistoryIndexSuffix(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={documentsFieldLabel}>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="s"
|
||||
data-test-subj="preconfiguredDocumentToIndex"
|
||||
>
|
||||
{JSON.stringify(AlertHistoryDocumentTemplate, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const jsonDocumentEditor = (
|
||||
<JsonEditorWithMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'documents'}
|
||||
data-test-subj="documentToIndex"
|
||||
inputTargetValue={
|
||||
documents === null
|
||||
documentToIndex === null
|
||||
? '{}' // need this to trigger validation
|
||||
: documents && documents.length > 0
|
||||
? ((documents[0] as unknown) as string)
|
||||
: undefined
|
||||
: documentToIndex
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Document to index',
|
||||
}
|
||||
)}
|
||||
label={documentsFieldLabel}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel',
|
||||
{
|
||||
|
@ -69,15 +197,15 @@ export const IndexParamsFields = ({
|
|||
</EuiLink>
|
||||
}
|
||||
onBlur={() => {
|
||||
if (
|
||||
!(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined)
|
||||
) {
|
||||
if (!documentToIndex) {
|
||||
// set document as empty to turn on the validation for non empty valid JSON object
|
||||
onDocumentsChange('{}');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return usePreconfiguredSchema ? preconfiguredDocumentSchema : jsonDocumentEditor;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -42,6 +42,7 @@ export interface PagerDutyActionParams {
|
|||
|
||||
export interface IndexActionParams {
|
||||
documents: Array<Record<string, any>>;
|
||||
indexOverride?: string;
|
||||
}
|
||||
|
||||
export enum ServerLogLevelOptions {
|
||||
|
|
|
@ -32,6 +32,10 @@ describe('transformActionVariables', () => {
|
|||
"description": "The tags of the rule.",
|
||||
"name": "rule.tags",
|
||||
},
|
||||
Object {
|
||||
"description": "The type of rule.",
|
||||
"name": "rule.type",
|
||||
},
|
||||
Object {
|
||||
"description": "The date the rule scheduled the action.",
|
||||
"name": "date",
|
||||
|
@ -127,6 +131,10 @@ describe('transformActionVariables', () => {
|
|||
"description": "The tags of the rule.",
|
||||
"name": "rule.tags",
|
||||
},
|
||||
Object {
|
||||
"description": "The type of rule.",
|
||||
"name": "rule.type",
|
||||
},
|
||||
Object {
|
||||
"description": "The date the rule scheduled the action.",
|
||||
"name": "date",
|
||||
|
@ -230,6 +238,10 @@ describe('transformActionVariables', () => {
|
|||
"description": "The tags of the rule.",
|
||||
"name": "rule.tags",
|
||||
},
|
||||
Object {
|
||||
"description": "The type of rule.",
|
||||
"name": "rule.type",
|
||||
},
|
||||
Object {
|
||||
"description": "The date the rule scheduled the action.",
|
||||
"name": "date",
|
||||
|
@ -336,6 +348,10 @@ describe('transformActionVariables', () => {
|
|||
"description": "The tags of the rule.",
|
||||
"name": "rule.tags",
|
||||
},
|
||||
Object {
|
||||
"description": "The type of rule.",
|
||||
"name": "rule.type",
|
||||
},
|
||||
Object {
|
||||
"description": "The date the rule scheduled the action.",
|
||||
"name": "date",
|
||||
|
@ -460,6 +476,10 @@ describe('transformActionVariables', () => {
|
|||
"description": "The tags of the rule.",
|
||||
"name": "rule.tags",
|
||||
},
|
||||
Object {
|
||||
"description": "The type of rule.",
|
||||
"name": "rule.type",
|
||||
},
|
||||
Object {
|
||||
"description": "The date the rule scheduled the action.",
|
||||
"name": "date",
|
||||
|
|
|
@ -26,6 +26,7 @@ export enum AlertProvidedActionVariables {
|
|||
ruleName = 'rule.name',
|
||||
ruleSpaceId = 'rule.spaceId',
|
||||
ruleTags = 'rule.tags',
|
||||
ruleType = 'rule.type',
|
||||
date = 'date',
|
||||
alertId = 'alert.id',
|
||||
alertActionGroup = 'alert.actionGroup',
|
||||
|
@ -83,6 +84,13 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
|
|||
}),
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: AlertProvidedActionVariables.ruleType,
|
||||
description: i18n.translate('xpack.triggersActionsUI.actionVariables.ruleTypeLabel', {
|
||||
defaultMessage: 'The type of rule.',
|
||||
}),
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: AlertProvidedActionVariables.date,
|
||||
description: i18n.translate('xpack.triggersActionsUI.actionVariables.dateLabel', {
|
||||
|
|
|
@ -10,7 +10,13 @@ import type { DocLinksStart } from 'kibana/public';
|
|||
import { ComponentType } from 'react';
|
||||
import { ChartsPluginSetup } from 'src/plugins/charts/public';
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { ActionType } from '../../actions/common';
|
||||
import {
|
||||
ActionType,
|
||||
AlertHistoryEsIndexConnectorId,
|
||||
AlertHistoryDocumentTemplate,
|
||||
ALERT_HISTORY_PREFIX,
|
||||
AlertHistoryDefaultIndexName,
|
||||
} from '../../actions/common';
|
||||
import { TypeRegistry } from './application/type_registry';
|
||||
import {
|
||||
ActionGroup,
|
||||
|
@ -45,7 +51,13 @@ export {
|
|||
AlertNotifyWhenType,
|
||||
AlertTypeParams,
|
||||
};
|
||||
export { ActionType };
|
||||
export {
|
||||
ActionType,
|
||||
AlertHistoryEsIndexConnectorId,
|
||||
AlertHistoryDocumentTemplate,
|
||||
AlertHistoryDefaultIndexName,
|
||||
ALERT_HISTORY_PREFIX,
|
||||
};
|
||||
|
||||
export type ActionTypeIndex = Record<string, ActionType>;
|
||||
export type AlertTypeIndex = Map<string, AlertType>;
|
||||
|
|
|
@ -84,6 +84,7 @@ function getIndexActionParams(): IndexActionParams {
|
|||
observerLocation: '{{state.observerLocation}}',
|
||||
},
|
||||
],
|
||||
indexOverride: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ interface CreateTestConfigOptions {
|
|||
enableActionsProxy: boolean;
|
||||
rejectUnauthorized?: boolean;
|
||||
publicBaseUrl?: boolean;
|
||||
preconfiguredAlertHistoryEsIndex?: boolean;
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
|
@ -47,6 +48,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
disabledPlugins = [],
|
||||
ssl = false,
|
||||
rejectUnauthorized = true,
|
||||
preconfiguredAlertHistoryEsIndex = false,
|
||||
} = options;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
|
@ -119,6 +121,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
...actionsProxyUrl,
|
||||
|
||||
'--xpack.eventLog.logEntries=true',
|
||||
`--xpack.actions.preconfiguredAlertHistoryEsIndex=${preconfiguredAlertHistoryEsIndex}`,
|
||||
`--xpack.actions.preconfigured=${JSON.stringify({
|
||||
'my-slack1': {
|
||||
actionTypeId: '.slack',
|
||||
|
|
|
@ -13,4 +13,5 @@ export default createTestConfig('spaces_only', {
|
|||
license: 'trial',
|
||||
enableActionsProxy: false,
|
||||
rejectUnauthorized: false,
|
||||
preconfiguredAlertHistoryEsIndex: true,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { getTestAlertData, ObjectRemover } from '../../../../common/lib';
|
||||
import { AlertHistoryDefaultIndexName } from '../../../../../../plugins/actions/common';
|
||||
|
||||
const ALERT_HISTORY_OVERRIDE_INDEX = 'kibana-alert-history-not-the-default';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function preconfiguredAlertHistoryConnectorTests({
|
||||
getService,
|
||||
}: FtrProviderContext) {
|
||||
const es = getService('legacyEs');
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
|
||||
describe('preconfigured alert history connector', () => {
|
||||
const spaceId = 'default';
|
||||
const ruleTypeId = 'test.patternFiring';
|
||||
const alertId = 'instance';
|
||||
|
||||
function getTestData(params = {}) {
|
||||
return getTestAlertData({
|
||||
rule_type_id: ruleTypeId,
|
||||
schedule: { interval: '1s' },
|
||||
params: {
|
||||
pattern: { [alertId]: new Array(100).fill(true) },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: 'preconfigured-alert-history-es-index',
|
||||
params,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
beforeEach(() => {
|
||||
esDeleteAllIndices(AlertHistoryDefaultIndexName);
|
||||
esDeleteAllIndices(ALERT_HISTORY_OVERRIDE_INDEX);
|
||||
});
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
it('should index document with preconfigured schema', async () => {
|
||||
const testRuleData = getTestData({
|
||||
documents: [{}],
|
||||
});
|
||||
const response = await supertest
|
||||
.post(`/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(testRuleData);
|
||||
expect(response.status).to.eql(200);
|
||||
objectRemover.add(spaceId, response.body.id, 'rule', 'alerting');
|
||||
|
||||
// Wait for alert to be active
|
||||
await waitForStatus(response.body.id, new Set(['active']));
|
||||
|
||||
await retry.try(async () => {
|
||||
const result = await es.search({
|
||||
index: AlertHistoryDefaultIndexName,
|
||||
});
|
||||
const indexedItems = result.hits.hits;
|
||||
expect(indexedItems.length).to.eql(1);
|
||||
|
||||
const indexedDoc = indexedItems[0]._source;
|
||||
|
||||
const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(indexedDoc['@timestamp']).to.match(timestampPattern);
|
||||
expect(indexedDoc.tags).to.eql(testRuleData.tags);
|
||||
expect(indexedDoc.rule.name).to.eql(testRuleData.name);
|
||||
expect(indexedDoc.rule.params[ruleTypeId.replace('.', '__')]).to.eql(testRuleData.params);
|
||||
expect(indexedDoc.rule.space).to.eql(spaceId);
|
||||
expect(indexedDoc.rule.type).to.eql(ruleTypeId);
|
||||
expect(indexedDoc.kibana.alert.id).to.eql(alertId);
|
||||
expect(indexedDoc.kibana.alert.context[ruleTypeId.replace('.', '__')] != null).to.eql(true);
|
||||
expect(indexedDoc.kibana.alert.actionGroup).to.eql('default');
|
||||
expect(indexedDoc.kibana.alert.actionGroupName).to.eql('Default');
|
||||
});
|
||||
});
|
||||
|
||||
it('should index document with preconfigured schema when indexOverride is defined', async () => {
|
||||
const testRuleData = getTestData({
|
||||
documents: [{}],
|
||||
indexOverride: ALERT_HISTORY_OVERRIDE_INDEX,
|
||||
});
|
||||
const response = await supertest
|
||||
.post(`/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(testRuleData);
|
||||
expect(response.status).to.eql(200);
|
||||
objectRemover.add(spaceId, response.body.id, 'rule', 'alerting');
|
||||
|
||||
// Wait for alert to be active
|
||||
await waitForStatus(response.body.id, new Set(['active']));
|
||||
|
||||
await retry.try(async () => {
|
||||
const result = await es.search({
|
||||
index: ALERT_HISTORY_OVERRIDE_INDEX,
|
||||
});
|
||||
const indexedItems = result.hits.hits;
|
||||
expect(indexedItems.length).to.eql(1);
|
||||
|
||||
const indexedDoc = indexedItems[0]._source;
|
||||
|
||||
const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(indexedDoc['@timestamp']).to.match(timestampPattern);
|
||||
expect(indexedDoc.tags).to.eql(testRuleData.tags);
|
||||
expect(indexedDoc.rule.name).to.eql(testRuleData.name);
|
||||
expect(indexedDoc.rule.params[ruleTypeId.replace('.', '__')]).to.eql(testRuleData.params);
|
||||
expect(indexedDoc.rule.space).to.eql(spaceId);
|
||||
expect(indexedDoc.rule.type).to.eql(ruleTypeId);
|
||||
expect(indexedDoc.kibana.alert.id).to.eql(alertId);
|
||||
expect(indexedDoc.kibana.alert.context[ruleTypeId.replace('.', '__')] != null).to.eql(true);
|
||||
expect(indexedDoc.kibana.alert.actionGroup).to.eql('default');
|
||||
expect(indexedDoc.kibana.alert.actionGroupName).to.eql('Default');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const WaitForStatusIncrement = 500;
|
||||
|
||||
async function waitForStatus(
|
||||
id: string,
|
||||
statuses: Set<string>,
|
||||
waitMillis: number = 10000
|
||||
): Promise<Record<string, any>> {
|
||||
if (waitMillis < 0) {
|
||||
expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`);
|
||||
}
|
||||
|
||||
const response = await supertest.get(`/api/alerts/alert/${id}`);
|
||||
expect(response.status).to.eql(200);
|
||||
|
||||
const { executionStatus } = response.body || {};
|
||||
const { status } = executionStatus || {};
|
||||
|
||||
const message = `waitForStatus(${Array.from(statuses)}): got ${JSON.stringify(
|
||||
executionStatus
|
||||
)}`;
|
||||
|
||||
if (statuses.has(status)) {
|
||||
return executionStatus;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${message}, retrying`);
|
||||
|
||||
await delay(WaitForStatusIncrement);
|
||||
return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement);
|
||||
}
|
||||
|
||||
async function delay(millis: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
}
|
|
@ -36,6 +36,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connectors`).expect(200, [
|
||||
{
|
||||
id: 'preconfigured-alert-history-es-index',
|
||||
name: 'Alert history Elasticsearch index',
|
||||
connector_type_id: '.index',
|
||||
is_preconfigured: true,
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: createdAction.id,
|
||||
is_preconfigured: false,
|
||||
|
@ -95,6 +102,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/actions/connectors`).expect(200, [
|
||||
{
|
||||
id: 'preconfigured-alert-history-es-index',
|
||||
name: 'Alert history Elasticsearch index',
|
||||
connector_type_id: '.index',
|
||||
is_preconfigured: true,
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'preconfigured-es-index-action',
|
||||
is_preconfigured: true,
|
||||
|
@ -145,6 +159,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions`).expect(200, [
|
||||
{
|
||||
id: 'preconfigured-alert-history-es-index',
|
||||
name: 'Alert history Elasticsearch index',
|
||||
actionTypeId: '.index',
|
||||
isPreconfigured: true,
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
id: createdAction.id,
|
||||
isPreconfigured: false,
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo
|
|||
loadTestFile(require.resolve('./execute'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/es_index'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/webhook'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/preconfigured_alert_history_connector'));
|
||||
loadTestFile(require.resolve('./type_not_enabled'));
|
||||
|
||||
// note that this test will destroy existing spaces
|
||||
|
|
|
@ -66,6 +66,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
`--xpack.actions.preconfiguredAlertHistoryEsIndex=false`,
|
||||
`--xpack.actions.preconfigured=${JSON.stringify({
|
||||
'my-slack1': {
|
||||
actionTypeId: '.slack',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue