[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 commit 957c333aa4, reversing
changes made to 4b1b78761e.

* 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:
ymao1 2021-04-08 18:18:44 -04:00 committed by GitHub
parent 3336db0baa
commit 71ed148cfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1579 additions and 61 deletions

View file

@ -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.

View file

@ -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.
==================================================

View file

@ -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

View file

@ -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`,

View file

@ -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

View 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'],
});
});
});

View 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}}',
},
})
);

View file

@ -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';

View file

@ -405,6 +405,7 @@ describe('create()', () => {
enabled: true,
enabledActionTypes: ['some-not-ignored-action-type'],
allowedHosts: ['*'],
preconfiguredAlertHistoryEsIndex: false,
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,

View file

@ -18,6 +18,7 @@ const defaultActionsConfig: ActionsConfig = {
enabled: false,
allowedHosts: [],
enabledActionTypes: [],
preconfiguredAlertHistoryEsIndex: false,
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,

View file

@ -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';

View file

@ -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,

View file

@ -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",

View file

@ -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,

View file

@ -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 = () => {

View file

@ -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',

View file

@ -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);
}

View file

@ -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: {},
});
}

View file

@ -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();
});
});

View file

@ -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}.`);
}
}

View file

@ -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"
}
}
}
}
}

View file

@ -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>;
}

View file

@ -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,

View file

@ -153,7 +153,7 @@ describe('Task Runner', () => {
actionsClient
);
taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation(
(actionTypeId, params) => params
(actionTypeId, actionId, params) => params
);
});

View file

@ -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',

View file

@ -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
);
}

View file

@ -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-".'],
},
});
});

View file

@ -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;
},
};

View file

@ -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'
);
});
});

View file

@ -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

View file

@ -42,6 +42,7 @@ export interface PagerDutyActionParams {
export interface IndexActionParams {
documents: Array<Record<string, any>>;
indexOverride?: string;
}
export enum ServerLogLevelOptions {

View file

@ -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",

View file

@ -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', {

View file

@ -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>;

View file

@ -84,6 +84,7 @@ function getIndexActionParams(): IndexActionParams {
observerLocation: '{{state.observerLocation}}',
},
],
indexOverride: null,
};
}

View file

@ -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',

View file

@ -13,4 +13,5 @@ export default createTestConfig('spaces_only', {
license: 'trial',
enableActionsProxy: false,
rejectUnauthorized: false,
preconfiguredAlertHistoryEsIndex: true,
});

View file

@ -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));
}
}

View file

@ -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,

View file

@ -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

View file

@ -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',