Skip to content

Commit

Permalink
Add notifications plugin, offering basic email service (elastic#143303)
Browse files Browse the repository at this point in the history
* Misc enhancements following PR comments

* Adding functional tests

* Fixing types

* Fixing tests

* Removing unnecessary Promise.all

* Cleanup

* Misc fixes and simplifications

* Add missing tsconfig.json

* [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs'

* Add dependency to Actions plugin in tsconfig.json

* Separate setup logic from start logic

* Fix bulkEnqueueExecution params structure

* Update README

* Add UTs

* Check license type >platinum for email notifications

* Fix incorrect UTs

* Import types when possible

* Misc enhancements and code cleanup

* Transform factory => provider, update start contract

* Code cleanup, update README

* Fix TS error

* Fix CI types error

* Address PR remarks

* Address PR remarks #2

Co-authored-by: Ying Mao <ying.mao@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 3, 2022
1 parent e5271bd commit 8881539
Show file tree
Hide file tree
Showing 27 changed files with 1,108 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services
/test/plugin_functional/test_suites/panel_actions @elastic/kibana-app-services
/test/plugin_functional/test_suites/data_plugin @elastic/kibana-app-services
/x-pack/plugins/notifications/ @elastic/kibana-app-services

### Observability Plugins

Expand Down
4 changes: 4 additions & 0 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,10 @@ Elastic.
|This plugin allows for other plugins to add data to Kibana stack monitoring documents.
|{kib-repo}blob/{branch}/x-pack/plugins/notifications/README.md[notifications]
|The Notifications plugin provides a set of services to help Solutions and plugins send notifications to users.
|{kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability]
|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI.
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/telemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ To use the exposed plugin start and setup contracts:

import { TelemetryPluginsStart } from '../telemetry/server`;

interface MyPlyginStartDeps {
interface MyPluginStartDeps {
telemetry?: TelemetryPluginsStart;
}

class MyPlugin {
public async start(
core: CoreStart,
{ telemetry }: MyPlyginStartDeps
{ telemetry }: MyPluginStartDeps
) {
const isOptedIn = await telemetry?.getIsOptedIn();
...
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@
"@kbn/monitoring-collection-plugin/*": ["x-pack/plugins/monitoring_collection/*"],
"@kbn/monitoring-plugin": ["x-pack/plugins/monitoring"],
"@kbn/monitoring-plugin/*": ["x-pack/plugins/monitoring/*"],
"@kbn/notifications-plugin": ["x-pack/plugins/notifications"],
"@kbn/notifications-plugin/*": ["x-pack/plugins/notifications/*"],
"@kbn/observability-plugin": ["x-pack/plugins/observability"],
"@kbn/observability-plugin/*": ["x-pack/plugins/observability/*"],
"@kbn/osquery-plugin": ["x-pack/plugins/osquery"],
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const createStartMock = () => {
isActionTypeEnabled: jest.fn(),
isActionExecutable: jest.fn(),
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
getUnsecuredActionsClient: jest.fn().mockResolvedValue(unsecuredActionsClientMock.create()),
getUnsecuredActionsClient: jest.fn().mockReturnValue(unsecuredActionsClientMock.create()),
getActionsAuthorizationWithRequest: jest
.fn()
.mockReturnValue(actionsAuthorizationMock.create()),
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework
import { SubActionConnector } from './sub_action_framework/sub_action_connector';
import { CaseConnector } from './sub_action_framework/case';
import {
IUnsecuredActionsClient,
type IUnsecuredActionsClient,
UnsecuredActionsClient,
} from './unsecured_actions_client/unsecured_actions_client';
import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { IUnsecuredActionsClient } from './unsecured_actions_client';
import type { IUnsecuredActionsClient } from './unsecured_actions_client';

export type UnsecuredActionsClientMock = jest.Mocked<IUnsecuredActionsClient>;

Expand Down
70 changes: 70 additions & 0 deletions x-pack/plugins/notifications/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Kibana Notifications Plugin

The Notifications plugin provides a set of services to help Solutions and plugins send notifications to users.

## Notifications Plugin public API

### Start

The `start` function exposes the following interface:

- `isEmailServiceAvailable(): boolean`:
A function to check whether the deployment is properly configured and the EmailService can be correctly retrieved.
- `getEmailService(): EmailService`:
- A function to get the basic EmailService, which can be used to send plain text emails. If the EmailService is not available, trying to retrieve it will result in an Exception.


### Usage

To use the exposed plugin start contract:

1. Make sure `notifications` is in your `optionalPlugins` in the `kibana.json` file:

```json5
// <plugin>/kibana.json
{
"id": "...",
"requiredPlugins": ["notifications"]
}
```

2. Use the exposed contract:

```ts
// <plugin>/server/plugin.ts
import { NotificationsPluginStart } from '../notifications/server`;

interface MyPluginStartDeps {
notifications?: NotificationsPluginStart;
}

class MyPlugin {
public start(
core: CoreStart,
{ notifications }: MyPluginStartDeps
) {
if (notifications.isEmailServiceAvailable()) {
const emailService = notifications.getEmailService();
emailService.sendPlainTextEmail({
to: 'foo@bar.com',
subject: 'Some subject',
message: 'Hello world!',
});
}
...
}
}
```

### Requirements

- This plugin currently depends on the `'actions'` plugin, as it uses `Connectors` under the hood.
- Note also that for each notification channel the corresponding connector must be preconfigured. E.g. to enable email notifications, an `Email` connector must exist in the system.
- Once the appropriate connectors are preconfigured in `kibana.yaml`, you can configure the `'notifications'` plugin by adding:

```yaml
notifications:
connectors:
default:
email: elastic-cloud-email # The identifier of the configured connector
```
8 changes: 8 additions & 0 deletions x-pack/plugins/notifications/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export const PLUGIN_ID = 'notifications';
15 changes: 15 additions & 0 deletions x-pack/plugins/notifications/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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.
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/notifications'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/notifications',
coverageReporters: ['text', 'html'],
collectCoverageFrom: ['<rootDir>/x-pack/plugins/notifications/{common,server}/**/*.{js,ts,tsx}'],
};
12 changes: 12 additions & 0 deletions x-pack/plugins/notifications/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "notifications",
"owner": {
"name": "App Services",
"githubTeam": "kibana-app-services"
},
"version": "kibana",
"server": true,
"ui": false,
"requiredPlugins": ["actions", "licensing"],
"optionalPlugins": []
}
29 changes: 29 additions & 0 deletions x-pack/plugins/notifications/server/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { schema, type TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor } from '@kbn/core/server';

export const configSchema = schema.object(
{
connectors: schema.maybe(
schema.object({
default: schema.maybe(
schema.object({
email: schema.maybe(schema.string()),
})
),
})
),
},
{ defaultValue: {} }
);
export type NotificationsConfigType = TypeOf<typeof configSchema>;

export const config: PluginConfigDescriptor<NotificationsConfigType> = {
schema: configSchema,
};
8 changes: 8 additions & 0 deletions x-pack/plugins/notifications/server/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export { type NotificationsConfigType, config } from './config';
18 changes: 18 additions & 0 deletions x-pack/plugins/notifications/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 type { PluginInitializerContext } from '@kbn/core/server';
import { NotificationsPlugin } from './plugin';
export { config } from './config';

// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export type { NotificationsPluginStart } from './types';

export function plugin(initializerContext: PluginInitializerContext) {
return new NotificationsPlugin(initializerContext);
}
52 changes: 52 additions & 0 deletions x-pack/plugins/notifications/server/mocks.ts
Original file line number Diff line number Diff line change
@@ -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 type { PublicMethodsOf } from '@kbn/utility-types';
import type { EmailService } from './services';
import type { NotificationsPluginStart } from './types';
import type { NotificationsPlugin } from './plugin';

const emailServiceMock: jest.Mocked<EmailService> = {
sendPlainTextEmail: jest.fn(),
};

const createEmailServiceMock = () => {
return emailServiceMock;
};

const startMock: jest.Mocked<NotificationsPluginStart> = {
isEmailServiceAvailable: jest.fn(),
getEmailService: jest.fn(createEmailServiceMock),
};

const createStartMock = () => {
return startMock;
};

const notificationsPluginMock: jest.Mocked<PublicMethodsOf<NotificationsPlugin>> = {
setup: jest.fn(),
start: jest.fn(createStartMock) as jest.Mock<NotificationsPluginStart>,
stop: jest.fn(),
};

const createNotificationsPluginMock = () => {
return notificationsPluginMock;
};

export const notificationsMock = {
createNotificationsPlugin: createNotificationsPluginMock,
createEmailService: createEmailServiceMock,
createStart: createStartMock,
clear: () => {
emailServiceMock.sendPlainTextEmail.mockClear();
startMock.getEmailService.mockClear();
startMock.isEmailServiceAvailable.mockClear();
notificationsPluginMock.setup.mockClear();
notificationsPluginMock.start.mockClear();
notificationsPluginMock.stop.mockClear();
},
};
107 changes: 107 additions & 0 deletions x-pack/plugins/notifications/server/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 { coreMock } from '@kbn/core/server/mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import type { NotificationsConfigType } from './config';
import { NotificationsPlugin } from './plugin';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { EmailServiceProvider } from './services/connectors_email_service_provider';
import { EmailServiceStart } from './services';

jest.mock('./services/connectors_email_service_provider');

const emailServiceProviderMock = EmailServiceProvider as jest.MockedClass<
typeof EmailServiceProvider
>;

const validConnectorConfig = {
connectors: {
default: {
email: 'validConnectorId',
},
},
};

const createNotificationsPlugin = (config: NotificationsConfigType) => {
const context = coreMock.createPluginInitializerContext<NotificationsConfigType>(config);
const plugin = new NotificationsPlugin(context);
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();

const actionsSetup = actionsMock.createSetup();
actionsSetup.isPreconfiguredConnector.mockImplementationOnce(
(connectorId) => connectorId === 'validConnectorId'
);
const pluginSetup = {
actions: actionsSetup,
licensing: licensingMock.createSetup(),
};

const actionsStart = actionsMock.createStart();
const pluginStart = {
actions: actionsStart,
licensing: licensingMock.createStart(),
};

return {
context,
logger: context.logger.get(),
plugin,
coreSetup,
coreStart,
actionsSetup,
pluginSetup,
actionsStart,
pluginStart,
};
};

describe('Notifications Plugin', () => {
beforeEach(() => emailServiceProviderMock.mockClear());

it('should create an EmailServiceProvider passing in the configuration and logger from the initializer context', () => {
const { logger } = createNotificationsPlugin(validConnectorConfig);
expect(emailServiceProviderMock).toHaveBeenCalledTimes(1);
expect(emailServiceProviderMock).toHaveBeenCalledWith(validConnectorConfig, logger);
});

describe('setup()', () => {
it('should call setup() on the created EmailServiceProvider, passing in the setup plugin dependencies', () => {
const { plugin, coreSetup, pluginSetup } = createNotificationsPlugin(validConnectorConfig);
plugin.setup(coreSetup, pluginSetup);
expect(emailServiceProviderMock.mock.instances[0].setup).toHaveBeenCalledTimes(1);
expect(emailServiceProviderMock.mock.instances[0].setup).toBeCalledWith(pluginSetup);
});
});

describe('start()', () => {
it('should call start() on the created EmailServiceProvider, passing in the setup plugin dependencies', () => {
const { plugin, coreStart, pluginStart } = createNotificationsPlugin(validConnectorConfig);
plugin.start(coreStart, pluginStart);
expect(emailServiceProviderMock.mock.instances[0].start).toHaveBeenCalledTimes(1);
expect(emailServiceProviderMock.mock.instances[0].start).toBeCalledWith(pluginStart);
});

it('should return EmailServiceProvider.start() contract as part of its contract', () => {
const { plugin, coreStart, pluginStart } = createNotificationsPlugin(validConnectorConfig);

const emailStart: EmailServiceStart = {
getEmailService: jest.fn(),
isEmailServiceAvailable: jest.fn(),
};

const providerMock = emailServiceProviderMock.mock
.instances[0] as jest.Mocked<EmailServiceProvider>;
providerMock.start.mockReturnValue(emailStart);
const start = plugin.start(coreStart, pluginStart);
expect(emailServiceProviderMock.mock.instances[0].start).toHaveBeenCalledTimes(1);
expect(emailServiceProviderMock.mock.instances[0].start).toBeCalledWith(pluginStart);
expect(start).toEqual(expect.objectContaining(emailStart));
});
});
});
Loading

0 comments on commit 8881539

Please sign in to comment.