Skip to content

Commit

Permalink
Extended existing alerting functionality to support preconfigured onl…
Browse files Browse the repository at this point in the history
…y action types (#64030)

* Extended existing alerting functionality to support preconfigured only action types

* fixed functional test

* Adding documentation

* Fixed UI part due to comments

* added missing tests

* fixed action type execution

* Fixed documentation

* Fixed due to comments

* fixed type checks

* extended isActionExecutable to check exact action id if it is in the preconfigured list
  • Loading branch information
YulNaumenko authored Apr 24, 2020
1 parent 74bf877 commit 6bf0e73
Show file tree
Hide file tree
Showing 28 changed files with 675 additions and 177 deletions.
5 changes: 4 additions & 1 deletion docs/user/alerting/action-types.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,19 @@ see https://www.elastic.co/subscriptions[the subscription page].

[float]
[[create-connectors]]
=== Connectors
=== Preconfigured connectors and action types

You can create connectors for actions in <<managing-alerts-and-actions, Alerts and Actions>> or via the action API.
For out-of-the-box and standardized connectors, you can <<pre-configured-connectors, preconfigure connectors>>
before {kib} starts.

Action type with only preconfigured connectors could be specified as a <<pre-configured-action-types, preconfigured action type>>.

include::action-types/email.asciidoc[]
include::action-types/index.asciidoc[]
include::action-types/pagerduty.asciidoc[]
include::action-types/server-log.asciidoc[]
include::action-types/slack.asciidoc[]
include::action-types/webhook.asciidoc[]
include::pre-configured-connectors.asciidoc[]
include::pre-configured-action-types.asciidoc[]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions docs/user/alerting/pre-configured-action-types.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[role="xpack"]
[[pre-configured-action-types]]

== Preconfigured action types

A preconfigure an action type has all the information it needs prior to startup.
A preconfigured action type offers the following capabilities:

- Requires no setup. Configuration and credentials needed to execute an
action are predefined.
- Has only <<pre-configured-connectors, preconfigured connectors>>.
- Connectors of the preconfigured action type cannot be edited or deleted.

[float]
[[preconfigured-action-type-example]]
=== Creating a preconfigured action

In the `kibana.yml` file:

. Exclude the action type from `xpack.actions.enabledActionTypes`.
. Add all its connectors.

The following example shows a valid configuration of preconfigured action type with one out-of-the box connector.

```js
xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1>
xpack.actions.preconfigured: <2>
- id: 'my-server-log'
actionTypeId: .server-log
name: 'Server log #xyz'
```

<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors.
<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type.

[float]
[[pre-configured-action-type-alert-form]]
=== Attaching a preconfigured action to an alert

To attach an action to an alert,
select from a list of available action types, and
then select the *Server log* type. This action type was configured previously.

[role="screenshot"]
image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type]

[float]
[[managing-pre-configured-action-types]]
=== Managing preconfigured actions

Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in.
They are tagged as “preconfigured” and cannot be deleted.

[role="screenshot"]
image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured]

Clicking *Create connector* shows the list of available action types.
Preconfigured action types are not included because you can't create a connector with a preconfigured action type.

[role="screenshot"]
image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu]
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/action_type_registry.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const createActionTypeRegistryMock = () => {
list: jest.fn(),
ensureActionTypeEnabled: jest.fn(),
isActionTypeEnabled: jest.fn(),
isActionExecutable: jest.fn(),
};
return mocked;
};
Expand Down
23 changes: 23 additions & 0 deletions x-pack/plugins/actions/server/action_type_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ beforeEach(() => {
),
actionsConfigUtils: mockedActionsConfig,
licenseState: mockedLicenseState,
preconfiguredActions: [
{
actionTypeId: 'foo',
config: {},
id: 'my-slack1',
name: 'Slack #xyz',
secrets: {},
isPreconfigured: true,
},
],
};
});

Expand Down Expand Up @@ -194,6 +204,19 @@ describe('isActionTypeEnabled', () => {
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo');
});

test('should call isActionExecutable of the actions config', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionExecutable('my-slack1', 'foo');
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo');
});

test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has preconfigured connectors', async () => {
mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false);
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });

expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true);
});

test('should call isLicenseValidForActionType of the license state', async () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
actionTypeRegistry.isActionTypeEnabled('foo');
Expand Down
18 changes: 17 additions & 1 deletion x-pack/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib';
import { ActionType } from './types';
import { ActionType, PreConfiguredAction } from './types';
import { ActionType as CommonActionType } from '../common';
import { ActionsConfigurationUtilities } from './actions_config';

Expand All @@ -17,6 +17,7 @@ export interface ActionTypeRegistryOpts {
taskRunnerFactory: TaskRunnerFactory;
actionsConfigUtils: ActionsConfigurationUtilities;
licenseState: ILicenseState;
preconfiguredActions: PreConfiguredAction[];
}

export class ActionTypeRegistry {
Expand All @@ -25,12 +26,14 @@ export class ActionTypeRegistry {
private readonly taskRunnerFactory: TaskRunnerFactory;
private readonly actionsConfigUtils: ActionsConfigurationUtilities;
private readonly licenseState: ILicenseState;
private readonly preconfiguredActions: PreConfiguredAction[];

constructor(constructorParams: ActionTypeRegistryOpts) {
this.taskManager = constructorParams.taskManager;
this.taskRunnerFactory = constructorParams.taskRunnerFactory;
this.actionsConfigUtils = constructorParams.actionsConfigUtils;
this.licenseState = constructorParams.licenseState;
this.preconfiguredActions = constructorParams.preconfiguredActions;
}

/**
Expand Down Expand Up @@ -58,6 +61,19 @@ export class ActionTypeRegistry {
);
}

/**
* Returns true if action type is enabled or it is a preconfigured action type.
*/
public isActionExecutable(actionId: string, actionTypeId: string) {
return (
this.isActionTypeEnabled(actionTypeId) ||
(!this.isActionTypeEnabled(actionTypeId) &&
this.preconfiguredActions.find(
preconfiguredAction => preconfiguredAction.id === actionId
) !== undefined)
);
}

/**
* Registers an action type to the action type registry
*/
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ beforeEach(() => {
),
actionsConfigUtils: actionsConfigMock.create(),
licenseState: mockedLicenseState,
preconfiguredActions: [],
};
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionsClient = new ActionsClient({
Expand Down Expand Up @@ -221,6 +222,7 @@ describe('create()', () => {
),
actionsConfigUtils: localConfigUtils,
licenseState: licenseStateMock.create(),
preconfiguredActions: [],
};

actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createActionTypeRegistry(): {
),
actionsConfigUtils: actionsConfigMock.create(),
licenseState: licenseStateMock.create(),
preconfiguredActions: [],
});
registerBuiltInActionTypes({
logger,
Expand Down
61 changes: 61 additions & 0 deletions x-pack/plugins/actions/server/create_execute_function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,65 @@ describe('execute()', () => {
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
});

test('should skip ensure action type if action type is preconfigured and license is valid', async () => {
const mockedActionTypeRegistry = actionTypeRegistryMock.create();
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
const executeFn = createExecuteFunction({
getBasePath,
taskManager: mockTaskManager,
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey: false,
actionTypeRegistry: mockedActionTypeRegistry,
preconfiguredActions: [
{
actionTypeId: 'mock-action',
config: {},
id: 'my-slack1',
name: 'Slack #xyz',
secrets: {},
isPreconfigured: true,
},
],
});
mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => {
throw new Error('Fail');
});
mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true);
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: 'action',
attributes: {
actionTypeId: 'mock-action',
},
references: [],
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
attributes: {},
references: [],
});

await executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: null,
});
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
getBasePath: expect.anything(),
headers: {},
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
});
});
});
4 changes: 3 additions & 1 deletion x-pack/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export function createExecuteFunction({
const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest);
const actionTypeId = await getActionTypeId(id);

actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) {
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}

const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
actionId: id,
Expand Down
44 changes: 44 additions & 0 deletions x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,50 @@ test('throws an error if actionType is not enabled', async () => {
expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test');
});

test('should not throws an error if actionType is preconfigured', async () => {
const actionType: jest.Mocked<ActionType> = {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
config: {
bar: true,
},
secrets: {
baz: true,
},
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
throw new Error('not enabled for test');
});
actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true);
await actionExecutor.execute(executeParams);

expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0);
expect(actionType.executor).toHaveBeenCalledWith({
actionId: '1',
services: expect.anything(),
config: {
bar: true,
},
secrets: {
baz: true,
},
params: { foo: true },
});
});

test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => {
const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true });
customActionExecutor.initialize({
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ export class ActionExecutor {
namespace.namespace
);

actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) {
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}
const actionType = actionTypeRegistry.get(actionTypeId);

let validatedParams: Record<string, unknown>;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const createStartMock = () => {
const mock: jest.Mocked<PluginStartContract> = {
execute: jest.fn(),
isActionTypeEnabled: jest.fn(),
isActionExecutable: jest.fn(),
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
preconfiguredActions: [],
};
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface PluginSetupContract {

export interface PluginStartContract {
isActionTypeEnabled(id: string): boolean;
isActionExecutable(actionId: string, actionTypeId: string): boolean;
execute(options: ExecuteOptions): Promise<void>;
getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>;
preconfiguredActions: PreConfiguredAction[];
Expand Down Expand Up @@ -170,6 +171,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
taskManager: plugins.taskManager,
actionsConfigUtils,
licenseState: this.licenseState,
preconfiguredActions: this.preconfiguredActions,
});
this.taskRunnerFactory = taskRunnerFactory;
this.actionTypeRegistry = actionTypeRegistry;
Expand Down Expand Up @@ -271,6 +273,9 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
isActionTypeEnabled: id => {
return this.actionTypeRegistry!.isActionTypeEnabled(id);
},
isActionExecutable: (actionId: string, actionTypeId: string) => {
return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId);
},
// Ability to get an actions client from legacy code
async getActionsClientWithRequest(request: KibanaRequest) {
if (isESOUsingEphemeralEncryptionKey === true) {
Expand Down
Loading

0 comments on commit 6bf0e73

Please sign in to comment.