diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a78f..92a624649d3c503 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -71,22 +71,20 @@ export function plugin(initializerContext: PluginInitializerContext) { *plugins/my_plugin/(public|server)/plugin.ts* [source,typescript] ---- -import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; import type { MyPluginConfig } from './config'; export class MyPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: MyPluginConfig; private readonly log: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: Record) { - const isEnabled = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup, deps: Record) { + const { someConfigValue } = this.config; } } ---- @@ -96,7 +94,7 @@ Additionally, some plugins need to access the runtime env configuration. [source,typescript] ---- export class MyPlugin implements Plugin { - public async setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup, deps: Record) { const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md new file mode 100644 index 000000000000000..cf315e1fd337e33 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-public.asyncplugin.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md new file mode 100644 index 000000000000000..54507b44cdd72a4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [setup](./kibana-plugin-core-public.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md new file mode 100644 index 000000000000000..f16d3c46bf84999 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [start](./kibana-plugin-core-public.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md new file mode 100644 index 000000000000000..f809f75783c26cb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [stop](./kibana-plugin-core-public.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index efd499823ffadcd..e307b5c9971b0b4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -39,6 +39,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md index 7fa05588a33012e..232851cd342cee1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md index 0d3c19a8217a67a..ec5ed211a9d2ba4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md index 1fcc2999dfd2edf..b7c3e11e492bd7b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `public` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md new file mode 100644 index 000000000000000..1ad1d87220b7485 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-server.asyncplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md new file mode 100644 index 000000000000000..1d033b7b88b0510 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [setup](./kibana-plugin-core-server.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md new file mode 100644 index 000000000000000..3cce90f01603bb3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [start](./kibana-plugin-core-server.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md new file mode 100644 index 000000000000000..9272fc2c4eba061 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [stop](./kibana-plugin-core-server.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 82f4a285409c951..5fe5eda7a817294 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -49,6 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md index b4e6623098736d5..a8b0aae28d251b9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md index 03e889a018b6f0e..851f84474fe11fe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index 839eabff29a1893..fe55e131065ddda 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 83e7edc5a016a6f..d7a9373a6e2a994 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -14,7 +14,7 @@ Name:: The name of the connector. The name is used to identify a connector Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -92,6 +92,8 @@ systems, refer to: * <> * <> +For other email servers, you can check the list of well-known services that Nodemailer supports in the JSON file https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. The properties of the objects in those files — `host`, `port`, and `secure` — correspond to the same email action configuration properties. A missing `secure` property in the "well-known/services.json" file is considered `false`. Typically, `port: 465` uses `secure: true`, and `port: 25` and `port: 587` use `secure: false`. + [float] [[gmail]] ===== Sending email from Gmail @@ -109,7 +111,6 @@ https://mail.google.com[Gmail] SMTP service: user: password: -------------------------------------------------- -// CONSOLE If you get an authentication error that indicates that you need to continue the sign-in process from a web browser when the action attempts to send email, you need @@ -131,9 +132,9 @@ https://www.outlook.com/[Outlook.com] SMTP service: [source,text] -------------------------------------------------- config: - host: smtp-mail.outlook.com - port: 465 - secure: true + host: smtp.office365.com + port: 587 + secure: false secrets: user: password: @@ -163,7 +164,7 @@ secrets: user: password: -------------------------------------------------- -<1> `smtp.host` varies depending on the region +<1> `config.host` varies depending on the region NOTE: You must use your Amazon SES SMTP credentials to send email through Amazon SES. For more information, see diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19a7..d79594c97cec782 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -12,7 +12,7 @@ export { get } from './get'; export { mapToObject } from './map_to_object'; export { merge } from './merge'; export { pick } from './pick'; -export { withTimeout } from './promise'; +export { withTimeout, isPromise } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index 61197a2a8bf70ab..f7c119acd0c7a49 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from './promise'; +import { withTimeout, isPromise } from './promise'; const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); @@ -50,3 +50,30 @@ describe('withTimeout', () => { ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); }); + +describe('isPromise', () => { + it('returns true when arg is a Promise', () => { + expect(isPromise(Promise.resolve('foo'))).toEqual(true); + expect(isPromise(Promise.reject('foo').catch(() => undefined))).toEqual(true); + }); + + it('returns false when arg is not a Promise', () => { + expect(isPromise(12)).toEqual(false); + expect(isPromise('foo')).toEqual(false); + expect(isPromise({ hello: 'dolly' })).toEqual(false); + expect(isPromise([1, 2, 3])).toEqual(false); + }); + + it('returns false for objects with a non-function `then` property', () => { + expect(isPromise({ then: 'bar' })).toEqual(false); + }); + + it('returns false for null and undefined', () => { + expect(isPromise(null)).toEqual(false); + expect(isPromise(undefined)).toEqual(false); + }); + + it('returns true for Promise-Like objects', () => { + expect(isPromise({ then: () => 12 })).toEqual(true); + }); +}); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index ce4e50bf9b2ac77..9d8f7703c026dc6 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -20,3 +20,7 @@ export function withTimeout({ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), ]) as Promise; } + +export function isPromise(maybePromise: T | Promise): maybePromise is Promise { + return maybePromise ? typeof (maybePromise as Promise).then === 'function' : false; +} diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc10..02c55b6af91dcc7 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,6 +95,8 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); + // workaround until https://github.com/elastic/kibana/issues/89828 is addressed + await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -160,3 +162,7 @@ async function silence(log, milliseconds) { ) .toPromise(); } + +async function delay(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f2c..a1cb036ce38f8f9 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -53,7 +53,13 @@ import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; +import { + Plugin, + AsyncPlugin, + PluginInitializer, + PluginInitializerContext, + PluginOpaqueId, +} from './plugins'; import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -304,6 +310,7 @@ export { NotificationsSetup, NotificationsStart, Plugin, + AsyncPlugin, PluginInitializer, PluginInitializerContext, SavedObjectsStart, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index d208ea76c48fe3f..e47de84ea12b2f9 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -110,14 +110,14 @@ function pluginInitializerContextMock(config: any = {}) { return mock; } -function createCoreContext(): CoreContext { +function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), env: { mode: { - dev: true, - name: 'development', - prod: false, + dev: !production, + name: production ? 'production' : 'development', + prod: production, }, packageInfo: { version: 'version', diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 76811d4908d22a2..be805c6a521ce85 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -7,6 +7,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, AsyncPlugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index e8e930a5befca67..ef919018f120b3c 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -39,16 +39,16 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if plugin.setup is not a function', async () => { + test('`setup` fails if plugin.setup is not a function', () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); - test('`setup` fails if plugin.start is not a function', async () => { + test('`setup` fails if plugin.start is not a function', () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); @@ -65,8 +65,8 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); - test('`start` fails if setup is not called first', async () => { - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + test('`start` fails if setup is not called first', () => { + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index af95e831a647219..a08a6cf0b431a0a 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { read } from './plugin_reader'; @@ -23,6 +24,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -40,7 +58,11 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -58,7 +80,9 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -81,10 +105,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { - this.instance = await this.createPluginInstance(); - - return await this.instance.setup(setupContext, plugins); + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { + this.instance = this.createPluginInstance(); + return this.instance.setup(setupContext, plugins); } /** @@ -94,16 +120,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - - this.startDependencies$.next([startContext, plugins, startContract]); - - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** @@ -121,7 +152,7 @@ export class PluginWrapper< this.instance = undefined; } - private async createPluginInstance() { + private createPluginInstance() { const initializer = read(this.name) as PluginInitializer< TSetup, TStart, diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index d44657f9039a358..1f85482569dbc23 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -7,9 +7,12 @@ */ import { PluginName } from 'kibana/server'; -import { Plugin } from './plugin'; +import { Plugin, AsyncPlugin } from './plugin'; -export type MockedPluginInitializer = jest.Mock>, any>; +export type MockedPluginInitializer = jest.Mock< + Plugin | AsyncPlugin, + any +>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index a22d48c50247a47..e70b78f237d7572 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -146,16 +146,16 @@ describe('PluginsService', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` - Map { - Symbol(pluginA) => Array [], - Symbol(pluginB) => Array [ - Symbol(pluginA), - ], - Symbol(pluginC) => Array [ - Symbol(pluginA), - ], - } - `); + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); }); }); @@ -264,7 +264,7 @@ describe('PluginsService', () => { jest.runAllTimers(); // setup plugins await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -344,7 +344,7 @@ describe('PluginsService', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -366,4 +366,124 @@ describe('PluginsService', () => { expect(pluginCInstance.stop).toHaveBeenCalled(); }); }); + + describe('asynchronous plugins', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + const coreContext = coreMock.createCoreContext({ production }); + + const syncPlugin = { id: 'sync-plugin', plugin: createManifest('sync-plugin') }; + mockPluginInitializers.set( + 'sync-plugin', + jest.fn(() => ({ + setup: jest.fn(() => 'setup-sync'), + start: jest.fn(() => 'start-sync'), + stop: jest.fn(), + })) + ); + + const asyncPlugin = { id: 'async-plugin', plugin: createManifest('async-plugin') }; + mockPluginInitializers.set( + 'async-plugin', + jest.fn(() => ({ + setup: jest.fn(() => (asyncSetup ? Promise.resolve('setup-async') : 'setup-sync')), + start: jest.fn(() => (asyncStart ? Promise.resolve('start-async') : 'start-sync')), + stop: jest.fn(), + })) + ); + + const pluginsService = new PluginsService(coreContext, [syncPlugin, asyncPlugin]); + + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 7a10ce1cdfc7726..57fbe4cbecd12f7 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -98,16 +98,29 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); - contracts.set(pluginName, contract); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -132,14 +145,28 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 75ed9aa5f150f6f..99579ada8ec588a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -194,6 +194,16 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public export interface Capabilities { [key: string]: Record>; @@ -990,15 +1000,15 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382a694bd2e418a..6f478004c204ef2 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -235,6 +235,7 @@ export { export { DiscoveredPlugin, Plugin, + AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index dda947972737a6d..a29fb01fbc00923 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,7 +20,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { AsyncPlugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -138,7 +138,7 @@ describe('PluginsService', () => { expect(startDependenciesResolved).toBe(false); return pluginStartContract; }, - } as Plugin); + } as AsyncPlugin); jest.doMock( join(pluginPath, 'server'), diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 68fdfdf62c30b00..c90d2e804225c71 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -100,7 +100,7 @@ test('`constructor` correctly initializes plugin instance', () => { expect(plugin.optionalPlugins).toEqual(['some-optional-dep']); }); -test('`setup` fails if `plugin` initializer is not exported', async () => { +test('`setup` fails if `plugin` initializer is not exported', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -115,14 +115,14 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Plugin "some-plugin-id" does not export "plugin" definition (plugin-without-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."` ); }); -test('`setup` fails if plugin initializer is not a function', async () => { +test('`setup` fails if plugin initializer is not a function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -137,14 +137,14 @@ test('`setup` fails if plugin initializer is not a function', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Definition of plugin "some-plugin-id" should be a function (plugin-with-wrong-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."` ); }); -test('`setup` fails if initializer does not return object', async () => { +test('`setup` fails if initializer does not return object', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -161,14 +161,14 @@ test('`setup` fails if initializer does not return object', async () => { mockPluginInitializer.mockReturnValue(null); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Initializer for plugin "some-plugin-id" is expected to return plugin instance, but returned "null".]` + ).toThrowErrorMatchingInlineSnapshot( + `"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."` ); }); -test('`setup` fails if object returned from initializer does not define `setup` function', async () => { +test('`setup` fails if object returned from initializer does not define `setup` function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -186,10 +186,10 @@ test('`setup` fails if object returned from initializer does not define `setup` const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Instance of plugin "some-plugin-id" does not define "setup" function.]` + ).toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."` ); }); @@ -223,7 +223,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () expect(mockPluginInstance.setup).toHaveBeenCalledWith(setupContext, setupDependencies); }); -test('`start` fails if setup is not called first', async () => { +test('`start` fails if setup is not called first', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -238,7 +238,7 @@ test('`start` fails if setup is not called first', async () => { ), }); - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 83b3fb53689a75c..ca7f11e28de75fb 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -10,11 +10,13 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { Plugin, + AsyncPlugin, PluginInitializerContext, PluginManifest, PluginInitializer, @@ -49,7 +51,9 @@ export class PluginWrapper< private readonly log: Logger; private readonly initializerContext: PluginInitializerContext; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -83,9 +87,11 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); } @@ -96,14 +102,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart): TStart | Promise { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - this.startDependencies$.next([startContext, plugins, startContract]); - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 1b5994c40c04107..5c38deeb5cf6ecd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -25,7 +25,6 @@ import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; -const logger = loggingSystemMock.create(); function createPlugin( id: string, { @@ -34,8 +33,8 @@ function createPlugin( server = true, ui = true, }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} -) { - return new PluginWrapper({ +): PluginWrapper { + return new PluginWrapper({ path: 'some-path', manifest: { id, @@ -53,27 +52,27 @@ function createPlugin( }); } +const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); + let pluginsSystem: PluginsSystem; -const configService = configServiceMock.create(); -configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let configService: ReturnType; +let logger: ReturnType; let env: Env; let coreContext: CoreContext; -const setupDeps = coreMock.createInternalSetup(); -const startDeps = coreMock.createInternalStart(); - beforeEach(() => { + logger = loggingSystemMock.create(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); + configService = configServiceMock.create(); + configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); -afterEach(() => { - jest.clearAllMocks(); -}); - test('can be setup even without plugins', async () => { const pluginsSetup = await pluginsSystem.setupPlugins(setupDeps); @@ -208,7 +207,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start start: { 'order-2': 'started-as-2' }, }, ], - ] as Array<[PluginWrapper, Contracts]>); + ] as Array<[PluginWrapper, Contracts]>); const setupContextMap = new Map(); const startContextMap = new Map(); @@ -434,7 +433,7 @@ describe('setup', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + it('throws timeout error if "setup" was not completed in 10 sec.', async () => { const plugin: PluginWrapper = createPlugin('timeout-setup'); jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise((i) => i)); pluginsSystem.addPlugin(plugin); @@ -444,7 +443,7 @@ describe('setup', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -471,8 +470,8 @@ describe('start', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "start" was not completed in 30 sec.', async () => { - const plugin: PluginWrapper = createPlugin('timeout-start'); + it('throws timeout error if "start" was not completed in 10 sec.', async () => { + const plugin = createPlugin('timeout-start'); jest.spyOn(plugin, 'setup').mockResolvedValue({}); jest.spyOn(plugin, 'start').mockImplementation(() => new Promise((i) => i)); @@ -485,7 +484,7 @@ describe('start', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -505,3 +504,120 @@ describe('start', () => { expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`); }); }); + +describe('asynchronous plugins', () => { + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + env = Env.createDefault( + REPO_ROOT, + getEnvOptions({ + cliArgs: { + dev: !production, + envName: production ? 'production' : 'development', + }, + }) + ); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + pluginsSystem = new PluginsSystem(coreContext); + + const syncPlugin = createPlugin('sync-plugin'); + jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); + jest.spyOn(syncPlugin, 'start').mockReturnValue('start-sync'); + pluginsSystem.addPlugin(syncPlugin); + + const asyncPlugin = createPlugin('async-plugin'); + jest + .spyOn(asyncPlugin, 'setup') + .mockReturnValue(asyncSetup ? Promise.resolve('setup-async') : 'setup-sync'); + jest + .spyOn(asyncPlugin, 'start') + .mockReturnValue(asyncStart ? Promise.resolve('start-async') : 'start-sync'); + pluginsSystem.addPlugin(asyncPlugin); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins(startDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1b5e3bbb06e71d2..b7b8c297ea57170 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -94,14 +94,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); @@ -132,14 +143,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 91ccc2dedf272c4..45db98201b75874 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -242,6 +242,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -383,4 +400,8 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3191c5625f8d9b..09207608908a45a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -203,6 +203,16 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -1815,9 +1825,9 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } @@ -1836,7 +1846,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -3141,9 +3151,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da502474..e504d5f0b9a9fa0 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -8,7 +8,6 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { APMOSSConfig } from './'; import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; import { tutorialProvider } from './tutorial'; @@ -17,10 +16,10 @@ export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { const config$ = this.initContext.config.create(); - const config = await config$.pipe(take(1)).toPromise(); + const config = this.initContext.config.get(); const apmTutorialProvider = tutorialProvider({ indexPatternTitle: config.indexPattern, @@ -35,6 +34,7 @@ export class APMOSSPlugin implements Plugin { plugins.home.tutorials.registerTutorial(apmTutorialProvider); return { + config, config$, getRegisteredTutorialProvider: () => apmTutorialProvider, }; @@ -45,6 +45,7 @@ export class APMOSSPlugin implements Plugin { } export interface APMOSSPluginSetup { + config: APMOSSConfig; config$: Observable; getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index b2f43b315aa9b6c..a5f1ca6107600ef 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { ProxyConfigCollection } from './lib'; @@ -28,7 +27,7 @@ export class ConsoleServerPlugin implements Plugin { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -36,8 +35,8 @@ export class ConsoleServerPlugin implements Plugin { }, })); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.ctx.config.get(); + const globalConfig = this.ctx.config.legacy.get(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2e1..93ffaa93cd80e9f 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -56,7 +56,7 @@ export class InspectorPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); this.views.register(getRequestsViewDescription()); diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 3433d076ee800a9..ac38f300bd02b51 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -7,16 +7,13 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + public setup({ http }: CoreSetup) { + const globalConfig = this.initContext.config.legacy.get(); const router = http.createRouter(); registerRoutes( diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 00d51da501834cb..4f35c1c1e5fc1bc 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -8,7 +8,6 @@ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; import { configSchema, MapsLegacyConfig } from '../config'; import { getUiSettings } from './ui_settings'; @@ -30,7 +29,7 @@ export const config: PluginConfigDescriptor = { }; export interface MapsLegacyPluginSetup { - config$: Observable; + config: MapsLegacyConfig; } export class MapsLegacyPlugin implements Plugin { @@ -43,10 +42,9 @@ export class MapsLegacyPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); - // @ts-ignore - const config$ = this._initializerContext.config.create(); + const pluginConfig = this._initializerContext.config.get(); return { - config$, + config: pluginConfig, }; } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 15efbf38e7b93af..6f74198bb56ab73 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -31,10 +31,10 @@ export class PresentationUtilPlugin return {}; } - public async start( + public start( coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps - ): Promise { + ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); return { diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index d5d57da400a519a..a3a2331cf8f76f0 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -79,7 +79,7 @@ export class RegionMapPlugin implements Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup) { - const config = await this.initializerContext.config - .create() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup) { + const config = this.initializerContext.config.get(); const collectorSet = new CollectorSet({ logger: this.logger.get('collector-set'), maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); setupRoutes({ diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 4792ceefde536e7..0a9d477c2669141 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; @@ -34,8 +34,7 @@ export interface TablePluginStartDependencies { /** @internal */ export class TableVisPlugin - implements - Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { + implements AsyncPlugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index e31aae7fcdda720..1dcb7263c481825 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -8,7 +8,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { Plugin } from './plugin'; +import { TimelionPlugin } from './plugin'; export { PluginSetupContract } from './plugin'; @@ -25,4 +25,4 @@ export const config: PluginConfigDescriptor = { ], }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new TimelionPlugin(initializerContext); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 2bb8f7214f9045a..c1800a09ba35c94 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -7,13 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { deepFreeze } from '@kbn/std'; import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; @@ -39,16 +38,12 @@ export interface TimelionPluginStartDeps { /** * Represents Timelion Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class TimelionPlugin + implements Plugin, void, TimelionPluginStartDeps> { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup( - core: CoreSetup - ): Promise> { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup): RecursiveReadonly { + const config = this.initializerContext.config.get>(); const configManager = new ConfigManager(this.initializerContext.config); diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 59ae89300705ec8..6900630ffa9710e 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -43,14 +43,14 @@ export interface MetricsPluginStartDependencies { } /** @internal */ -export class MetricsPlugin implements Plugin, void> { +export class MetricsPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea9911..7cc70f31589c7cb 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -54,14 +54,14 @@ export interface VegaPluginStartDependencies { } /** @internal */ -export class VegaPlugin implements Plugin, void> { +export class VegaPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index b266a681f803155..9d329c92bede0ca 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -46,7 +46,7 @@ export class VisTypeVislibPlugin Plugin { constructor(public initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 5be971a085d3ced..75a2f4fb6895c15 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -59,7 +59,7 @@ export class VisTypeXyPlugin VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies > { - public async setup( + public setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca39681..3d82e6c60a1b6ea 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -84,7 +84,7 @@ export class VisualizePlugin constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, { home, urlForwarding, data }: VisualizePluginSetupDependencies ) { diff --git a/test/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243d2..8d75cb09469bc8f 100644 --- a/test/plugin_functional/plugins/app_link_test/public/plugin.ts +++ b/test/plugin_functional/plugins/app_link_test/public/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { renderApp } from './app'; export class CoreAppLinkPlugin implements Plugin { - public async setup(core: CoreSetup, deps: {}) { + public setup(core: CoreSetup, deps: {}) { core.application.register({ id: 'applink_start', title: 'AppLink Start', diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 6a167b17befd136..48c8d85b21dac9d 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -42,7 +42,7 @@ export class CorePluginBPlugin }; } - public async start(core: CoreStart, deps: {}) { + public start(core: CoreStart, deps: {}) { return { sendSystemRequest: async (asSystemRequest: boolean) => { const response = await core.http.post('/core_plugin_b/system_request', { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d0d..8fbacc71d30cb38 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,9 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -136,11 +134,9 @@ const includedHiddenTypes = [ ALERT_SAVED_OBJECT_TYPE, ]; -export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly config: Promise; - +export class ActionsPlugin implements Plugin { private readonly logger: Logger; - private actionsConfig?: ActionsConfig; + private readonly actionsConfig: ActionsConfig; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -151,20 +147,20 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; - private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private readonly kibanaIndexConfig: { kibana: { index: string } }; constructor(initContext: PluginInitializerContext) { - this.config = initContext.config.create().pipe(first()).toPromise(); + this.actionsConfig = initContext.config.get(); this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; - this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; + this.kibanaIndexConfig = initContext.config.legacy.get(); } - public async setup( + public setup( core: CoreSetup, plugins: ActionsPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -190,7 +186,6 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - this.actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { @@ -229,20 +224,18 @@ export class ActionsPlugin implements Plugin, Plugi ); } - this.kibanaIndexConfig.subscribe((config) => { - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, config.kibana.index) + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, this.kibanaIndexConfig.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + this.kibanaIndexConfig.kibana.index ); - if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - config.kibana.index - ); - } - }); + } // Routes const router = core.http.createRouter(); @@ -304,7 +297,7 @@ export class ActionsPlugin implements Plugin, Plugi request ); - const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + const kibanaIndex = kibanaIndexConfig.kibana.index; return new ActionsClient({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e2840dbdf5ef7af..49fded8649c4695 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -61,7 +61,7 @@ export class APMPlugin implements Plugin { this.initContext = initContext; } - public async setup( + public setup( core: CoreSetup, plugins: { apmOss: APMOSSPluginSetup; @@ -98,7 +98,10 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = mergeConfigs( + plugins.apmOss.config, + this.initContext.config.get() + ); if ( plugins.taskManager && diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 6a814f68a67f493..3093d5d9b8d2999 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { take } from 'rxjs/operators'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; @@ -27,14 +26,17 @@ interface StartDeps { } export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private readonly logger: Logger; private securitySetup?: SecurityPluginSetup; private beatsLibs?: CMServerLibs; constructor( private readonly initializerContext: PluginInitializerContext - ) {} + ) { + this.logger = initializerContext.logger.get(); + } - public async setup(core: CoreSetup, { features, security }: SetupDeps) { + public setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -64,8 +66,8 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return {}; } - public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = this.initializerContext.config.get(); const logger = this.initializerContext.logger.get(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -78,7 +80,9 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep kibanaVersion, }); - await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { + this.logger.error(`Error create beats template: ${e.message}`); + }); return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 7387ae1a203c205..345f6099009fc3c 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; @@ -34,7 +33,7 @@ export class CanvasPlugin implements Plugin { this.logger = initializerContext.logger.get(); } - public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); @@ -84,9 +83,7 @@ export class CanvasPlugin implements Plugin { ); // we need the kibana index provided by global config for the Canvas usage collector - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); setupInterpreter(plugins.expressions); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0d0..8b4fdc73dab44ed 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -38,8 +37,8 @@ import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; -function createConfig$(context: PluginInitializerContext) { - return context.config.create().pipe(map((config) => config)); +function createConfig(context: PluginInitializerContext) { + return context.config.get(); } export interface PluginsSetup { @@ -60,7 +59,7 @@ export class CasePlugin { } public async setup(core: CoreSetup, plugins: PluginsSetup) { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return; @@ -118,7 +117,7 @@ export class CasePlugin { }); } - public async start(core: CoreStart) { + public start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); this.alertsService!.initialize(core.elasticsearch.client); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index eeb295b264f6085..4c12aa3d92b47bc 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -45,7 +45,7 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 55ed72ca01957e0..6abfb864d1cd08a 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { CloudConfigType } from './config'; @@ -28,25 +26,24 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: CloudConfigType; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup, { usageCollection }: PluginsSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup) { this.logger.debug('Setting up Cloud plugin'); - const config = await this.config$.pipe(first()).toPromise(); - const isCloudEnabled = getIsCloudEnabled(config.id); + const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); return { - cloudId: config.id, + cloudId: this.config.id, isCloudEnabled, apm: { - url: config.apm?.url, - secretToken: config.apm?.secret_token, + url: this.config.apm?.url, + secretToken: this.config.apm?.secret_token, }, }; } diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index c9197a30b5214eb..eb7481d12387d0b 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, Plugin } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** * Represents Code Plugin instance that will be managed by the Kibana plugin system. */ -export class CodePlugin { +export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup() { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + const config = this.initializerContext.config.get>(); if (config && Object.keys(config).length > 0) { this.initializerContext.logger diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 8097c22cfbabc65..53b020e5b824113 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; @@ -15,4 +15,4 @@ export { EncryptedSavedObjectsClient } from './saved_objects'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 2324c31b13d004c..823a6b0afa9dc8b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; import { ConfigSchema } from './config'; import { coreMock } from 'src/core/server/mocks'; @@ -13,12 +13,12 @@ import { securityMock } from '../../security/server/mocks'; describe('EncryptedSavedObjects Plugin', () => { describe('setup()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) - .resolves.toMatchInlineSnapshot(` + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` Object { "createMigration": [Function], "registerType": [Function], @@ -29,14 +29,14 @@ describe('EncryptedSavedObjects Plugin', () => { }); describe('start()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); + plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); const startContract = plugin.start(); - await expect(startContract).toMatchInlineSnapshot(` + expect(startContract).toMatchInlineSnapshot(` Object { "getClient": [Function], "isEncryptionError": [Function], diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index bfc757accaa82f7..e846b133c26e0bf 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../security/server'; import { createConfig, ConfigSchema } from './config'; @@ -40,7 +39,9 @@ export interface EncryptedSavedObjectsPluginStart { /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class EncryptedSavedObjectsPlugin + implements + Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; @@ -48,17 +49,11 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( - core: CoreSetup, - deps: PluginsSetup - ): Promise { - const config = await this.initializerContext.config - .create>() - .pipe( - map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'))) - ) - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { + const config = createConfig( + this.initializerContext.config.get>(), + this.initializerContext.logger.get('config') + ); const auditLogger = new EncryptedSavedObjectsAuditLogger( deps.security?.audit.getLogger('encryptedSavedObjects') ); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4ea8ef2c089e4ac..569479f921cddc9 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Plugin, PluginInitializerContext, @@ -66,19 +64,19 @@ export interface RouteDependencies { } export class EnterpriseSearchPlugin implements Plugin { - private config: Observable; - private logger: Logger; + private readonly config: ConfigType; + private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { - this.config = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.logger = initializerContext.logger.get(); } - public async setup( + public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { - const config = await this.config.pipe(first()).toPromise(); + const config = this.config; const log = this.logger; /** diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 04be4ce67c12d71..9cc874735cc0e4c 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -24,7 +22,6 @@ import type { IEventLogConfig, IEventLogService, IEventLogger, - IEventLogConfig$, IEventLogClientService, } from './types'; import { findRoute } from './routes'; @@ -48,32 +45,29 @@ interface PluginStartDeps { } export class Plugin implements CorePlugin { - private readonly config$: IEventLogConfig$; + private readonly config: IEventLogConfig; private systemLogger: Logger; private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; - private globalConfig$: Observable; + private globalConfig: SharedGlobalConfig; private eventLogClientService?: EventLogClientService; private savedObjectProviderRegistry: SavedObjectProviderRegistry; private kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); - this.config$ = this.context.config.create(); - this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.config = this.context.config.get(); + this.globalConfig = this.context.config.legacy.get(); this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); this.kibanaVersion = this.context.env.packageInfo.version; } - async setup(core: CoreSetup): Promise { - const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); - const kibanaIndex = globalConfig.kibana.index; + setup(core: CoreSetup): IEventLogService { + const kibanaIndex = this.globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); - const config = await this.config$.pipe(first()).toPromise(); - this.esContext = createEsContext({ logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) @@ -85,7 +79,7 @@ export class Plugin implements CorePlugin { + start(core: CoreStart, { spaces }: PluginStartDeps): IEventLogClientService { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 786f5ba587d267b..0e5e62b591290c9 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; @@ -25,7 +24,6 @@ export const ConfigSchema = schema.object({ }); export type IEventLogConfig = TypeOf; -export type IEventLogConfig$ = Observable>; // the object exposed by plugin.setup() export interface IEventLogService { diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 111f294b6ad55e1..0890274fed950cf 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -6,7 +6,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; // These exports are part of public Features plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -25,4 +25,4 @@ export { export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new FeaturesPlugin(initializerContext); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 4462edeed9510c3..0de03e54e1f7905 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; describe('Features Plugin', () => { let initContext: ReturnType; @@ -31,7 +31,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); registerKibanaFeature({ id: 'baz', @@ -58,7 +58,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features with timelion when available', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); @@ -88,7 +88,7 @@ describe('Features Plugin', () => { }); it('registers kibana features with not hidden saved objects types', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -101,7 +101,7 @@ describe('Features Plugin', () => { }); it('returns registered elasticsearch features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); registerElasticsearchFeature({ id: 'baz', @@ -123,7 +123,7 @@ describe('Features Plugin', () => { }); it('registers a capabilities provider', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index e96c257516b98d0..6a9fd1da826a6a9 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreStart, SavedObjectsServiceStart, Logger, + Plugin, PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -59,7 +60,9 @@ interface TimelionSetupContract { /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class FeaturesPlugin + implements + Plugin, RecursiveReadonly> { private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; @@ -68,10 +71,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } - ): Promise> { + ): RecursiveReadonly { this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled; defineRoutes({ diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 57c42d887bc83c3..2e18d427272ce57 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -130,8 +130,8 @@ interface AgentBase { enrolled_at: string; unenrolled_at?: string; unenrollment_started_at?: string; - upgraded_at?: string; - upgrade_started_at?: string; + upgraded_at?: string | null; + upgrade_started_at?: string | null; access_api_key_id?: string; default_api_key?: string; default_api_key_id?: string; @@ -155,3 +155,163 @@ export interface AgentSOAttributes extends AgentBase { current_error_events?: string; packages?: string[]; } + +// Generated from FleetServer schema.json + +/** + * An Elastic Agent that has enrolled into Fleet + */ +export interface FleetServerAgent { + /** + * The version of the document in the index + */ + _version?: number; + /** + * Shared ID + */ + shared_id?: string; + /** + * Type + */ + type: AgentType; + /** + * Active flag + */ + active: boolean; + /** + * Date/time the Elastic Agent enrolled + */ + enrolled_at: string; + /** + * Date/time the Elastic Agent unenrolled + */ + unenrolled_at?: string; + /** + * Date/time the Elastic Agent unenrolled started + */ + unenrollment_started_at?: string; + /** + * Date/time the Elastic Agent was last upgraded + */ + upgraded_at?: string | null; + /** + * Date/time the Elastic Agent started the current upgrade + */ + upgrade_started_at?: string | null; + /** + * ID of the API key the Elastic Agent must used to contact Fleet Server + */ + access_api_key_id?: string; + agent?: FleetServerAgentMetadata; + /** + * User provided metadata information for the Elastic Agent + */ + user_provided_metadata: AgentMetadata; + /** + * Local metadata information for the Elastic Agent + */ + local_metadata: AgentMetadata; + /** + * The policy ID for the Elastic Agent + */ + policy_id?: string; + /** + * The current policy revision_idx for the Elastic Agent + */ + policy_revision_idx?: number | null; + /** + * The current policy coordinator for the Elastic Agent + */ + policy_coordinator_idx?: number; + /** + * Date/time the Elastic Agent was last updated + */ + last_updated?: string; + /** + * Date/time the Elastic Agent checked in last time + */ + last_checkin?: string; + /** + * Lst checkin status + */ + last_checkin_status?: 'error' | 'online' | 'degraded' | 'updating'; + /** + * ID of the API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key_id?: string; + /** + * API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key?: string; + /** + * Date/time the Elastic Agent was last updated + */ + updated_at?: string; + /** + * Packages array + */ + packages?: string[]; + /** + * The last acknowledged action sequence number for the Elastic Agent + */ + action_seq_no?: number; +} +/** + * An Elastic Agent metadata + */ +export interface FleetServerAgentMetadata { + /** + * The unique identifier for the Elastic Agent + */ + id: string; + /** + * The version of the Elastic Agent + */ + version: string; + [k: string]: any; +} + +/** + * An Elastic Agent action + */ +export interface FleetServerAgentAction { + /** + * The unique identifier for action document + */ + _id?: string; + /** + * The action sequence number + */ + _seq_no?: number; + /** + * The unique identifier for the Elastic Agent action. There could be multiple documents with the same action_id if the action is split into two separate documents. + */ + action_id?: string; + /** + * Date/time the action was created + */ + '@timestamp'?: string; + /** + * The action expiration date/time + */ + expiration?: string; + /** + * The action type. APP_ACTION is the value for the actions that suppose to be routed to the endpoints/beats. + */ + type?: string; + /** + * The input identifier the actions should be routed to. + */ + input_id?: string; + /** + * The Agent IDs the action is intended for. No support for json.RawMessage with the current generator. Could be useful to lazy parse the agent ids + */ + agents?: string[]; + /** + * The opaque payload. + */ + data?: { + [k: string]: unknown; + }; + [k: string]: unknown; +} diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573dfc..50e647e271ecc70 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -155,7 +155,7 @@ export class FleetPlugin implements Plugin { + public start(core: CoreStart): FleetStart { let successPromise: ReturnType; return { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a3425..7378d45e1bb3aa0 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -12,7 +12,7 @@ import { CoreStart, ElasticsearchServiceStart, Logger, - Plugin, + AsyncPlugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -169,7 +169,7 @@ export interface FleetStartContract { } export class FleetPlugin - implements Plugin { + implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; private cloud: CloudSetup | undefined; diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index 2d7c884edad83cf..22b5035378a20d9 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -20,7 +20,7 @@ export const postAgentAcksHandlerBuilder = function ( try { const soClient = ackService.getSavedObjectsClientContract(request); const esClient = ackService.getElasticsearchClientContract(); - const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, esClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index bf0cfd2d476dd8d..d032945245faf0b 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -30,7 +30,7 @@ export const postNewAgentActionHandlerBuilder = function ( const newAgentAction = request.body.action; - const savedAgentAction = await actionsService.createAgentAction(soClient, { + const savedAgentAction = await actionsService.createAgentAction(soClient, esClient, { created_at: new Date().toISOString(), ...newAgentAction, agent_id: agent.id, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 411da6da0223c6d..cd91e8c325c066e 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -132,8 +132,8 @@ export const updateAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await AgentService.updateAgent(soClient, request.params.agentId, { - userProvidedMetatada: request.body.user_provided_metadata, + await AgentService.updateAgent(soClient, esClient, request.params.agentId, { + user_provided_metadata: request.body.user_provided_metadata, }); const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); @@ -164,12 +164,13 @@ export const postAgentCheckinHandler: RequestHandler< try { const soClient = appContextService.getInternalUserSOClient(request); const esClient = appContextService.getInternalUserESClient(); - const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, esClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); }); const signal = abortController.signal; + const { actions } = await AgentService.agentCheckin( soClient, esClient, @@ -205,8 +206,13 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = context.core.elasticsearch.client.asInternalUser; const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); - const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); + const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById( + soClient, + esClient, + apiKeyId + ); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { return response.unauthorized({ @@ -311,21 +317,16 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; try { - // Reassign by array of IDs - const result = Array.isArray(request.body.agents) - ? await AgentService.reassignAgents( - soClient, - esClient, - { agentIds: request.body.agents }, - request.body.policy_id - ) - : await AgentService.reassignAgents( - soClient, - esClient, - { kuery: request.body.agents }, - request.body.policy_id - ); - const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + const results = await AgentService.reassignAgents( + soClient, + esClient, + Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }, + request.body.policy_id + ); + + const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { return { ...acc, [so.id]: { diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 0215b8f27b39322..086a9411f20b8d9 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -8,18 +8,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import semverCoerce from 'semver/functions/coerce'; -import { - AgentSOAttributes, - PostAgentUpgradeResponse, - PostBulkAgentUpgradeResponse, -} from '../../../common/types'; +import { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types'; import { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types'; import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { savedObjectToAgent } from '../../services/agents/saved_objects'; import { isAgentUpgradeable } from '../../../common/services'; +import { getAgent } from '../../services/agents'; export const postAgentUpgradeHandler: RequestHandler< TypeOf, @@ -27,6 +22,7 @@ export const postAgentUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -39,12 +35,8 @@ export const postAgentUpgradeHandler: RequestHandler< }, }); } - - const agentSO = await soClient.get( - AGENT_SAVED_OBJECT_TYPE, - request.params.agentId - ); - if (agentSO.attributes.unenrollment_started_at || agentSO.attributes.unenrolled_at) { + const agent = await getAgent(soClient, esClient, request.params.agentId); + if (agent.unenrollment_started_at || agent.unenrolled_at) { return response.customError({ statusCode: 400, body: { @@ -53,7 +45,6 @@ export const postAgentUpgradeHandler: RequestHandler< }); } - const agent = savedObjectToAgent(agentSO); if (!force && !isAgentUpgradeable(agent, kibanaVersion)) { return response.customError({ statusCode: 400, @@ -66,6 +57,7 @@ export const postAgentUpgradeHandler: RequestHandler< try { await AgentService.sendUpgradeAgentAction({ soClient, + esClient, agentId: request.params.agentId, version, sourceUri, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index dfe5c19bc417b55..ca131efeff68ccb 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -488,17 +488,17 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, agentPolicyId: string ) { - return appContextService.getConfig()?.agents.fleetServerEnabled - ? this.createFleetPolicyChangeFleetServer( - soClient, - appContextService.getInternalUserESClient(), - agentPolicyId - ) - : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + const esClient = appContextService.getInternalUserESClient(); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); + } + + return this.createFleetPolicyChangeActionSO(soClient, esClient, agentPolicyId); } public async createFleetPolicyChangeActionSO( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions @@ -518,7 +518,7 @@ class AgentPolicyService { return acc; }, []); - await createAgentPolicyAction(soClient, { + await createAgentPolicyAction(soClient, esClient, { type: 'POLICY_CHANGE', data: { policy }, ack_data: { packages }, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index c1a6067195c979d..5aec696f5e14407 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -106,19 +106,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -168,19 +168,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -230,8 +230,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { @@ -277,8 +277,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should fail for actions that cannot be found on agent actions list', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index a09107b90a01563..c639a9b0332ac62 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -24,14 +24,11 @@ import { AgentSOAttributes, AgentActionSOAttributes, } from '../../types'; -import { - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, -} from '../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionByIds } from './actions'; import { forceUnenrollAgent } from './unenroll'; import { ackAgentUpgraded } from './upgrade'; +import { updateAgent } from './crud'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -87,26 +84,23 @@ export async function acknowledgeAgentActions( const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); if (upgradeAction) { - await ackAgentUpgraded(soClient, upgradeAction); + await ackAgentUpgraded(soClient, esClient, upgradeAction); } const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); - await soClient.bulkUpdate([ - ...(configChangeAction - ? [ - { - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - policy_revision: configChangeAction.policy_revision, - packages: configChangeAction?.ack_data?.packages, - }, - }, - ] - : []), - ...buildUpdateAgentActionSentAt(agentActionsIds), - ]); + if (configChangeAction) { + await updateAgent(soClient, esClient, agent.id, { + policy_revision: configChangeAction.policy_revision, + packages: configChangeAction?.ack_data?.packages, + }); + } + + if (agentActionsIds.length > 0) { + await soClient.bulkUpdate([ + ...buildUpdateAgentActionSentAt(agentActionsIds), + ]); + } return actions; } @@ -206,6 +200,7 @@ export interface AcksService { authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ) => Promise; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 5b3c2ea5ce70882..3d391cc89a7e316 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -8,12 +8,12 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; import { AgentAction } from '../../../common/types/models'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - + const mockedEsClient = elasticsearchServiceMock.createInternalClient(); const newAgentAction: Omit = { agent_id: 'agentid', type: 'POLICY_CHANGE', @@ -32,7 +32,7 @@ describe('test agent actions services', () => { }, } as SavedObject) ); - await createAgentAction(mockSavedObjectsClient, newAgentAction); + await createAgentAction(mockSavedObjectsClient, mockedEsClient, newAgentAction); const createdAction = (mockSavedObjectsClient.create.mock .calls[0][1] as unknown) as AgentAction; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index b45b7836eb46f96..8dfeac11dacf3b5 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -13,8 +13,9 @@ import { BaseAgentActionSOAttributes, AgentActionSOAttributes, AgentPolicyActionSOAttributes, + FleetServerAgentAction, } from '../../../common/types/models'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_ACTIONS_INDEX } from '../../../common/constants'; import { isAgentActionSavedObject, isPolicyActionSavedObject, @@ -23,37 +24,45 @@ import { import { appContextService } from '../app_context'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; +const ONE_MONTH_IN_MS = 2592000000; + export async function createAgentAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } export async function bulkCreateAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise { - return bulkCreateActions(soClient, newAgentActions); + return bulkCreateActions(soClient, esClient, newAgentActions); } export function createAgentPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit | Omit ): Promise { const actionSO = await soClient.create( @@ -65,6 +74,27 @@ async function createAction( } ); + if ( + appContextService.getConfig()?.agents?.fleetServerEnabled && + isAgentActionSavedObject(actionSO) + ) { + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: newAgentAction.data, + type: newAgentAction.type, + }; + + await esClient.create({ + index: AGENT_ACTIONS_INDEX, + id: actionSO.id, + body, + refresh: 'wait_for', + }); + } + if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); // Action `data` is encrypted, so is not returned from the saved object @@ -84,14 +114,17 @@ async function createAction( async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array | Omit> ): Promise> { const { saved_objects: actionSOs } = await soClient.bulkCreate( @@ -105,6 +138,34 @@ async function bulkCreateActions( })) ); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await esClient.bulk({ + index: AGENT_ACTIONS_INDEX, + body: actionSOs.flatMap((actionSO) => { + if (!isAgentActionSavedObject(actionSO)) { + return []; + } + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: actionSO.attributes.data ? JSON.parse(actionSO.attributes.data) : undefined, + type: actionSO.type, + }; + + return [ + { + create: { + _id: actionSO.id, + }, + }, + body, + ]; + }), + }); + } + return actionSOs.map((actionSO) => { if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); @@ -316,6 +377,7 @@ export interface ActionsService { createAgentAction: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ) => Promise; } diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts index c59e2decebd9928..5a1e86c15c00242 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts @@ -6,10 +6,12 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { authenticateAgentWithAccessToken } from './authenticate'; +const mockEsClient = elasticsearchServiceMock.createInternalClient(); + describe('test agent autenticate services', () => { it('should succeed with a valid API key and an active agent', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); @@ -32,7 +34,7 @@ describe('test agent autenticate services', () => { ], }) ); - await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + await authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -62,7 +64,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: false }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -93,7 +95,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'aaaa', @@ -124,7 +126,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -144,7 +146,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.ts index a773173b1ddc102..a03c35bdc6e7377 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.ts @@ -6,13 +6,14 @@ */ import Boom from '@hapi/boom'; -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { KibanaRequest, SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { Agent } from '../../types'; import * as APIKeyService from '../api_keys'; import { getAgentByAccessAPIKeyId } from './crud'; export async function authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise { if (!request.auth.isAuthenticated) { @@ -25,7 +26,7 @@ export async function authenticateAgentWithAccessToken( throw Boom.unauthorized(err.message); } - const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await getAgentByAccessAPIKeyId(soClient, esClient, res.apiKeyId); return agent; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 9a60abdc69423d2..bcebedae2e07a1e 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -19,9 +19,10 @@ import { AgentEventSOAttributes, } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { agentCheckinState } from './state'; import { getAgentActionsForCheckin } from '../actions'; +import { updateAgent } from '../crud'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -35,13 +36,7 @@ export async function agentCheckin( options?: { signal: AbortSignal } ) { const updateData: Partial = {}; - const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); - if ( - updatedErrorEvents && - !(updatedErrorEvents.length === 0 && agent.current_error_events.length === 0) - ) { - updateData.current_error_events = JSON.stringify(updatedErrorEvents); - } + await processEventsForCheckin(soClient, agent, data.events); if (data.localMetadata && !deepEqual(data.localMetadata, agent.local_metadata)) { updateData.local_metadata = data.localMetadata; } @@ -50,9 +45,8 @@ export async function agentCheckin( } // Update agent only if something changed if (Object.keys(updateData).length > 0) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); + await updateAgent(soClient, esClient, agent.id, updateData); } - // Check if some actions are not acknowledged let actions = await getAgentActionsForCheckin(soClient, agent.id); if (actions.length > 0) { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts index 83fd139a1e8e828..6156212a6320328 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsBulkUpdateObject } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { appContextService } from '../../app_context'; -import { AgentSOAttributes } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { bulkUpdateAgents } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -57,20 +56,17 @@ export function agentCheckinStateConnectedAgentsFactory() { if (agentToUpdate.size === 0) { return; } + const esClient = appContextService.getInternalUserESClient(); const internalSOClient = getInternalUserSOClient(); const now = new Date().toISOString(); - const updates: Array> = [ - ...agentToUpdate.values(), - ].map((agentId) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: { + const updates = [...agentToUpdate.values()].map((agentId) => ({ + agentId, + data: { last_checkin: now, }, })); - agentToUpdate = new Set([...connectedAgentsIds.values()]); - await internalSOClient.bulkUpdate(updates, { refresh: false }); + await bulkUpdateAgents(internalSOClient, esClient, updates); } return { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts index ca8378c117b7dfa..cd6e0ef61e3f081 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { take } from 'rxjs/operators'; import { @@ -17,6 +18,7 @@ import { outputType } from '../../../../common/constants'; jest.mock('../../app_context', () => ({ appContextService: { + getConfig: () => ({}), getInternalUserSOClient: () => { return {}; }, @@ -42,6 +44,8 @@ function getMockedNewActionSince() { return getNewActionsSince as jest.MockedFunction; } +const mockedEsClient = {} as ElasticsearchClient; + describe('test agent checkin new action services', () => { describe('newAgetActionObservable', () => { beforeEach(() => { @@ -161,12 +165,18 @@ describe('test agent checkin new action services', () => { ]; expect( - await createAgentActionFromPolicyAction(mockSavedObjectsClient, mockAgent, mockPolicyAction) + await createAgentActionFromPolicyAction( + mockSavedObjectsClient, + mockedEsClient, + mockAgent, + mockPolicyAction + ) ).toEqual(expectedResult); expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -175,6 +185,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.2' } } } }, mockPolicyAction ) @@ -183,6 +194,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0' } } } }, mockPolicyAction ) @@ -191,6 +203,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -218,6 +231,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.0' } } } }, mockPolicyAction ) @@ -226,6 +240,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.3' } } } }, mockPolicyAction ) @@ -234,6 +249,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.1-SNAPSHOT' } } } }, mockPolicyAction ) @@ -242,6 +258,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.8.2' } } } }, mockPolicyAction ) diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 624b7bbcae5721e..01759c2015cdf6c 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -45,7 +45,7 @@ import { } from '../actions'; import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; -import { getAgent } from '../crud'; +import { getAgent, updateAgent } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -106,31 +106,45 @@ function createAgentPolicyActionSharedObservable(agentPolicyId: string) { ); } -async function getOrCreateAgentDefaultOutputAPIKey( +async function getAgentDefaultOutputAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent -): Promise { - const { - attributes: { default_api_key: defaultApiKey }, - } = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + return agent.default_api_key; + } else { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); - if (defaultApiKey) { return defaultApiKey; } +} + +async function getOrCreateAgentDefaultOutputAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agent: Agent +): Promise { + const defaultAPIKey = await getAgentDefaultOutputAPIKey(soClient, esClient, agent); + if (defaultAPIKey) { + return defaultAPIKey; + } const outputAPIKey = await APIKeysService.generateOutputApiKey(soClient, 'default', agent.id); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + await updateAgent(soClient, esClient, agent.id, { default_api_key: outputAPIKey.key, default_api_key_id: outputAPIKey.id, }); - return outputAPIKey.key; } export async function createAgentActionFromPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, policyAction: AgentPolicyAction ) { @@ -168,7 +182,7 @@ export async function createAgentActionFromPolicyAction( ); // Mutate the policy to set the api token for this agent - const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, agent); + const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, esClient, agent); if (newAgentAction.data.policy) { newAgentAction.data.policy.outputs.default.api_key = apiKey; } @@ -249,7 +263,9 @@ export function agentCheckinStateNewActionsFactory() { (!agent.policy_revision || action.policy_revision > agent.policy_revision) ), rateLimiter(), - concatMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), + concatMap((policyAction) => + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) + ), merge(newActions$), concatMap((data: AgentAction[] | undefined) => { if (data === undefined) { @@ -274,7 +290,7 @@ export function agentCheckinStateNewActionsFactory() { }), rateLimiter(), concatMap((policyAction) => - createAgentActionFromPolicyAction(soClient, agent, policyAction) + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) ) ); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 36506d05905958d..c80fd77fc11ecc3 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -5,13 +5,8 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; - -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; import { appContextService, agentPolicyService } from '../../services'; import * as crudServiceSO from './crud_so'; import * as crudServiceFleetServer from './crud_fleet_server'; @@ -75,15 +70,15 @@ export async function getAgent( : crudServiceSO.getAgent(soClient, agentId); } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); - return agents; +export async function getAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentIds: string[] +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgents(esClient, agentIds) + : crudServiceSO.getAgents(soClient, agentIds); } export async function getAgentPolicyForAgent( @@ -104,38 +99,39 @@ export async function getAgentPolicyForAgent( export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), - }); - const [agent] = response.saved_objects.map(savedObjectToAgent); - - if (!agent) { - throw Boom.notFound('Agent not found'); - } - if (agent.access_api_key_id !== accessAPIKeyId) { - throw new Error('Agent api key id is not matching'); - } - if (!agent.active) { - throw Boom.forbidden('Agent inactive'); - } - - return agent; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgentByAccessAPIKeyId(esClient, accessAPIKeyId) + : crudServiceSO.getAgentByAccessAPIKeyId(soClient, accessAPIKeyId); } export async function updateAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.updateAgent(esClient, agentId, data) + : crudServiceSO.updateAgent(soClient, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: Array<{ + agentId: string; + data: Partial; + }> ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.bulkUpdateAgents(esClient, data) + : crudServiceSO.bulkUpdateAgents(soClient, data); } export async function deleteAgent( diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts index c9aa221edf4d29a..caff15efff68c27 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -6,31 +6,46 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'src/core/server'; -import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { FleetServerAgent, isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; -import { searchHitToAgent } from './helpers'; +import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { appContextService } from '../../services'; +import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; const ACTIVE_AGENT_CONDITION = 'active:true'; const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } +function _joinFilters(filters: Array): KueryNode | undefined { + return filters + .filter((filter) => filter !== undefined) + .reduce((acc: KueryNode | undefined, kuery: string | KueryNode | undefined): + | KueryNode + | undefined => { + if (kuery === undefined) { + return acc; + } + const kueryNode: KueryNode = + typeof kuery === 'string' ? esKuery.fromKueryExpression(removeSOAttributes(kuery)) : kuery; - return `(${filter})`; - }, undefined); + if (!acc) { + return kueryNode; + } + + return { + type: 'function', + function: 'and', + arguments: [acc, kueryNode], + }; + }, undefined as KueryNode | undefined); } -function removeSOAttributes(kuery: string) { +export function removeSOAttributes(kuery: string) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } @@ -57,20 +72,23 @@ export async function listAgents( const filters = []; if (kuery && kuery !== '') { - filters.push(removeSOAttributes(kuery)); + filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, - q: _joinFilters(filters), + body, }); let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); @@ -121,18 +139,20 @@ export async function countInactiveAgents( filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, size: 0, track_total_hits: true, - q: _joinFilters(filters), + body, }); - return res.body.hits.total.value; } export async function getAgent(esClient: ElasticsearchClient, agentId: string) { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get>({ index: AGENTS_INDEX, id: agentId, }); @@ -141,27 +161,31 @@ export async function getAgent(esClient: ElasticsearchClient, agentId: string) { return agent; } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); +export async function getAgents( + esClient: ElasticsearchClient, + agentIds: string[] +): Promise { + const body = { docs: agentIds.map((_id) => ({ _id })) }; + + const res = await esClient.mget({ + body, + index: AGENTS_INDEX, + }); + + const agents = res.body.docs.map(searchHitToAgent); return agents; } export async function getAgentByAccessAPIKeyId( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), + const res = await esClient.search>({ + index: AGENTS_INDEX, + q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); - const [agent] = response.saved_objects.map(savedObjectToAgent); + + const [agent] = res.body.hits.hits.map(searchHitToAgent); if (!agent) { throw Boom.notFound('Agent not found'); @@ -177,15 +201,49 @@ export async function getAgentByAccessAPIKeyId( } export async function updateAgent( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, + await esClient.update({ + id: agentId, + index: AGENTS_INDEX, + body: { doc: agentSOAttributesToFleetServerAgentDoc(data) }, + refresh: 'wait_for', + }); +} + +export async function bulkUpdateAgents( + esClient: ElasticsearchClient, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const body = updateData.flatMap(({ agentId, data }) => [ + { + update: { + _id: agentId, + }, + }, + { + doc: { ...agentSOAttributesToFleetServerAgentDoc(data) }, + }, + ]); + + const res = await esClient.bulk({ + body, + index: AGENTS_INDEX, + refresh: 'wait_for', }); + + return { + items: res.body.items.map((item: { update: { _id: string; error?: Error } }) => ({ + id: item.update._id, + success: !item.update.error, + error: item.update.error, + })), + }; } export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { @@ -193,7 +251,7 @@ export async function deleteAgent(esClient: ElasticsearchClient, agentId: string id: agentId, index: AGENT_SAVED_OBJECT_TYPE, body: { - active: false, + doc: { active: false }, }, }); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts index 11991a971829a06..c3ceb4b7502e268 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsBulkUpdateObject, SavedObjectsClientContract } from 'src/core/server'; import { isAgentUpgradeable } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -197,13 +197,35 @@ export async function getAgentByAccessAPIKeyId( export async function updateAgent( soClient: SavedObjectsClientContract, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const updates: Array> = updateData.map( + ({ agentId, data }) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agentId, + attributes: data, + }) + ); + + const res = await soClient.bulkUpdate(updates); + + return { + items: res.saved_objects.map((so) => ({ + id: so.id, + success: !so.error, + error: so.error, + })), + }; } export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index b8be02af101b42b..c984a84ceea014b 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -6,14 +6,15 @@ */ import Boom from '@hapi/boom'; +import uuid from 'uuid/v4'; import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes } from '../../types'; +import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import * as APIKeyService from '../api_keys'; import { appContextService } from '../app_context'; @@ -26,6 +27,36 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + const esClient = appContextService.getInternalUserESClient(); + + const agentId = uuid(); + const accessAPIKey = await APIKeyService.generateAccessApiKey(soClient, agentId); + const fleetServerAgent: FleetServerAgent = { + active: true, + policy_id: agentPolicyId, + type, + enrolled_at: new Date().toISOString(), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, + access_api_key_id: accessAPIKey.id, + }; + await esClient.create({ + index: AGENTS_INDEX, + body: fleetServerAgent, + id: agentId, + refresh: 'wait_for', + }); + + return { + id: agentId, + current_error_events: [], + packages: [], + ...fleetServerAgent, + access_api_key: accessAPIKey.key, + } as Agent; + } + const agentData: AgentSOAttributes = { active: true, policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 1000a1b14593288..90d85e98ecd679d 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -6,17 +6,30 @@ */ import { ESSearchHit } from '../../../../../typings/elasticsearch'; -import { Agent, AgentSOAttributes } from '../../types'; +import { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -export function searchHitToAgent(hit: ESSearchHit): Agent { +export function searchHitToAgent(hit: ESSearchHit): Agent { return { id: hit._id, ...hit._source, - current_error_events: hit._source.current_error_events - ? JSON.parse(hit._source.current_error_events) - : [], + policy_revision: hit._source.policy_revision_idx, + current_error_events: [], access_api_key: undefined, status: undefined, packages: hit._source.packages ?? [], }; } + +export function agentSOAttributesToFleetServerAgentDoc( + data: Partial +): Partial> { + const { policy_revision: policyRevison, ...rest } = data; + + const doc: Partial> = { ...rest }; + + if (policyRevison !== undefined) { + doc.policy_revision_idx = policyRevison; + } + + return doc; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 7338c440483ea6a..466870bead71ce1 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -107,6 +107,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 9f4373ab553ecfb..62d59aada3b7ba9 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -7,11 +7,16 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import type { AgentSOAttributes } from '../../types'; -import { AgentReassignmentError } from '../../errors'; import { agentPolicyService } from '../agent_policy'; -import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgents, + getAgentPolicyForAgent, + listAllAgents, + updateAgent, + bulkUpdateAgents, +} from './crud'; +import { AgentReassignmentError } from '../../errors'; + import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -27,12 +32,12 @@ export async function reassignAgent( await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { policy_id: newAgentPolicyId, policy_revision: null, }); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: new Date().toISOString(), type: 'INTERNAL_POLICY_REASSIGN', @@ -73,7 +78,7 @@ export async function reassignAgents( kuery: string; }, newAgentPolicyId: string -) { +): Promise<{ items: Array<{ id: string; sucess: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); @@ -82,7 +87,7 @@ export async function reassignAgents( // Filter to agents that do not already use the new agent policy ID const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -99,20 +104,22 @@ export async function reassignAgents( (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId ); - // Update the necessary agents - const res = await soClient.bulkUpdate( + const res = await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { policy_id: newAgentPolicyId, policy_revision: null, }, })) ); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c75b91b3fbd11c5..42d3aff2b0d7022 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -14,6 +14,8 @@ import { AgentStatus } from '../../types'; import { AgentStatusKueryHelper } from '../../../common/services'; import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; import { normalizeKuery } from '../saved_object'; +import { appContextService } from '../app_context'; +import { removeSOAttributes } from './crud_fleet_server'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, @@ -27,6 +29,8 @@ export async function getAgentStatusById( export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; function joinKuerys(...kuerys: Array) { + const isFleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return kuerys .filter((kuery) => kuery !== undefined) .reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => { @@ -34,7 +38,9 @@ function joinKuerys(...kuerys: Array) { return acc; } const normalizedKuery: KueryNode = esKuery.fromKueryExpression( - normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') + isFleetServerEnabled + ? removeSOAttributes(kuery || '') + : normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') ); if (!acc) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index b8c1b7befb443c8..cd46cff0f8a174e 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -73,6 +73,7 @@ describe('unenrollAgents (plural)', () => { }); it('cannot unenroll from a managed policy', async () => { const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id]; await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); @@ -98,6 +99,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index e2fa83cf32b6371..72d551a12298010 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { AgentSOAttributes } from '../../types'; -import { AgentUnenrollmentError } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import * as APIKeyService from '../api_keys'; import { createAgentAction, bulkCreateAgentActions } from './actions'; -import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgent, + updateAgent, + getAgentPolicyForAgent, + getAgents, + listAllAgents, + bulkUpdateAgents, +} from './crud'; +import { AgentUnenrollmentError } from '../../errors'; async function unenrollAgentIsAllowed( soClient: SavedObjectsClientContract, @@ -35,12 +41,12 @@ export async function unenrollAgent( await unenrollAgentIsAllowed(soClient, esClient, agentId); const now = new Date().toISOString(); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, type: 'UNENROLL', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { unenrollment_started_at: now, }); } @@ -58,7 +64,7 @@ export async function unenrollAgents( ) { const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -83,6 +89,7 @@ export async function unenrollAgents( // Create unenroll action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -91,11 +98,12 @@ export async function unenrollAgents( ); // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { unenrollment_started_at: now, }, })) @@ -118,7 +126,7 @@ export async function forceUnenrollAgent( : undefined, ]); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { active: false, unenrolled_at: new Date().toISOString(), }); @@ -138,7 +146,7 @@ export async function forceUnenrollAgents( // Filter to agents that are not already unenrolled const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -163,13 +171,13 @@ export async function forceUnenrollAgents( if (apiKeys.length) { APIKeyService.invalidateAPIKeys(soClient, apiKeys); } - // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { active: false, unenrolled_at: now, }, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 7475ad496814277..5105e1453098279 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,20 +6,22 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentAction, AgentActionSOAttributes } from '../../types'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; -import { getAgents, listAllAgents } from './crud'; +import { getAgents, listAllAgents, updateAgent, bulkUpdateAgents } from './crud'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, + esClient, agentId, version, sourceUri, }: { soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; agentId: string; version: string; sourceUri: string | undefined; @@ -29,21 +31,22 @@ export async function sendUpgradeAgentAction({ version, source_uri: sourceUri, }; - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, data, ack_data: data, type: 'UPGRADE', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - upgraded_at: undefined, + await updateAgent(soClient, esClient, agentId, { + upgraded_at: null, upgrade_started_at: now, }); } export async function ackAgentUpgraded( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentAction: AgentAction ) { const { @@ -52,9 +55,9 @@ export async function ackAgentUpgraded( if (!ackData) throw new Error('data missing from UPGRADE action'); const { version } = JSON.parse(ackData); if (!version) throw new Error('version missing from UPGRADE action'); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, { + await updateAgent(soClient, esClient, agentAction.agent_id, { upgraded_at: new Date().toISOString(), - upgrade_started_at: undefined, + upgrade_started_at: null, }); } @@ -79,7 +82,7 @@ export async function sendUpgradeAgentsActions( // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -97,6 +100,7 @@ export async function sendUpgradeAgentsActions( // Create upgrade action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -106,12 +110,13 @@ export async function sendUpgradeAgentsActions( })) ); - return await soClient.bulkUpdate( + return await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - upgraded_at: undefined, + agentId: agent.id, + data: { + upgraded_at: null, upgrade_started_at: now, }, })) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 97d48702cf4c6ef..85812fee3885c85 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -79,3 +79,15 @@ export async function generateEnrollmentAPIKey( return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } } + +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + apiKeyId: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKeyById(esClient, apiKeyId); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKeyById(soClient, apiKeyId); + } +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts index d42cb19a340bdd4..f5d0015297daa0c 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -7,41 +7,15 @@ import uuid from 'uuid'; import Boom from '@hapi/boom'; +import { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { ESSearchResponse as SearchResponse } from '../../../../../typings/elasticsearch'; import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; - -// TODO Move these types to another file -interface SearchResponse { - took: number; - timed_out: boolean; - _scroll_id?: string; - hits: { - total: { - value: number; - relation: string; - }; - max_score: number; - hits: Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; - }>; - }; -} - -type SearchHit = SearchResponse['hits']['hits'][0]; +import { escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( esClient: ElasticsearchClient, @@ -54,7 +28,7 @@ export async function listEnrollmentApiKeys( ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; - const res = await esClient.search>({ + const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -78,7 +52,7 @@ export async function getEnrollmentAPIKey( id: string ): Promise { try { - const res = await esClient.get>({ + const res = await esClient.get>({ index: ENROLLMENT_API_KEYS_INDEX, id, }); @@ -185,6 +159,21 @@ export async function generateEnrollmentAPIKey( }; } +export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, + }); + + const [enrollmentAPIKey] = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { try { await agentPolicyService.get(soClient, agentPolicyId); @@ -196,7 +185,10 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } -function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { +function esDocToEnrollmentApiKey(doc: { + _id: string; + _source: FleetServerEnrollmentAPIKey; +}): EnrollmentAPIKey { return { id: doc._id, ...doc._source, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts index b3beab546c811b1..014bc58e747ea4d 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -13,7 +13,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import { normalizeKuery, escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -159,6 +159,25 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + apiKeyId: string +) { + const [enrollmentAPIKey] = ( + await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + searchFields: ['api_key_id'], + search: escapeSearchQueryPhrase(apiKeyId), + }) + ).saved_objects.map(savedObjectToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + function savedObjectToEnrollmentApiKey({ error, attributes, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index 5cdadeb0c82d84a..65051163c78c3a3 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core/server'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; +import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { createAPIKey } from './security'; -import { escapeSearchQueryPhrase } from '../saved_object'; export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; @@ -70,25 +67,6 @@ export async function generateAccessApiKey(soClient: SavedObjectsClientContract, return { id: key.id, key: Buffer.from(`${key.id}:${key.api_key}`).toString('base64') }; } -export async function getEnrollmentAPIKeyById( - soClient: SavedObjectsClientContract, - apiKeyId: string -) { - const [enrollmentAPIKey] = ( - await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - searchFields: ['api_key_id'], - search: escapeSearchQueryPhrase(apiKeyId), - }) - ).saved_objects.map(_savedObjectToEnrollmentApiKey); - - if (enrollmentAPIKey?.api_key_id !== apiKeyId) { - throw new Error('find enrollmentKeyById returned an incorrect key'); - } - - return enrollmentAPIKey; -} - export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; @@ -117,18 +95,3 @@ export function parseApiKey(apiKey: string) { apiKeyId, }; } - -function _savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index f982332886e943a..170bec54983c0e4 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -5,11 +5,18 @@ * 2.0. */ +import { isBoom } from '@hapi/boom'; import { KibanaRequest } from 'src/core/server'; import { ENROLLMENT_API_KEYS_INDEX, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + AGENT_POLICY_INDEX, + AGENTS_INDEX, FleetServerEnrollmentAPIKey, + AGENT_SAVED_OBJECT_TYPE, + AgentSOAttributes, + FleetServerAgent, + SO_SEARCH_LIMIT, FLEET_SERVER_PACKAGE, FLEET_SERVER_INDICES, } from '../../common'; @@ -17,6 +24,9 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollmen import { appContextService } from './app_context'; import { getInstallation } from './epm/packages'; +import { isAgentsSetup } from './agents'; +import { agentPolicyService } from './agent_policy'; + export async function isFleetServerSetup() { const pkgInstall = await getInstallation({ savedObjectsClient: getInternalUserSOClient(), @@ -28,7 +38,6 @@ export async function isFleetServerSetup() { } const esClient = appContextService.getInternalUserESClient(); - const exists = await Promise.all( FLEET_SERVER_INDICES.map(async (index) => { const res = await esClient.indices.exists({ @@ -42,7 +51,11 @@ export async function isFleetServerSetup() { } export async function runFleetServerMigration() { - await migrateEnrollmentApiKeys(); + // If Agents are not setup skip as there is nothing to migrate + if (!(await isAgentsSetup(getInternalUserSOClient()))) { + return; + } + await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); } function getInternalUserSOClient() { @@ -64,6 +77,65 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } +async function migrateAgents() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + page: 1, + perPage: 100, + }); + + if (res.total === 0) { + hasMore = false; + } + for (const so of res.saved_objects) { + try { + const { + attributes, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + + const body: FleetServerAgent = { + type: attributes.type, + active: attributes.active, + enrolled_at: attributes.enrolled_at, + unenrolled_at: attributes.unenrolled_at, + unenrollment_started_at: attributes.unenrollment_started_at, + upgraded_at: attributes.upgraded_at, + upgrade_started_at: attributes.upgrade_started_at, + access_api_key_id: attributes.access_api_key_id, + user_provided_metadata: attributes.user_provided_metadata, + local_metadata: attributes.local_metadata, + policy_id: attributes.policy_id, + policy_revision_idx: attributes.policy_revision || undefined, + last_checkin: attributes.last_checkin, + last_checkin_status: attributes.last_checkin_status, + default_api_key_id: attributes.default_api_key_id, + default_api_key: attributes.default_api_key, + packages: attributes.packages, + }; + await esClient.create({ + index: AGENTS_INDEX, + body, + id: so.id, + refresh: 'wait_for', + }); + + await soClient.delete(AGENT_SAVED_OBJECT_TYPE, so.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } + } + } +} + async function migrateEnrollmentApiKeys() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); @@ -77,24 +149,61 @@ async function migrateEnrollmentApiKeys() { hasMore = false; } for (const item of res.items) { - const key = await getEnrollmentAPIKey(soClient, item.id); - - const body: FleetServerEnrollmentAPIKey = { - api_key: key.api_key, - api_key_id: key.api_key_id, - active: key.active, - created_at: key.created_at, - name: key.name, - policy_id: key.policy_id, - }; - await esClient.create({ - index: ENROLLMENT_API_KEYS_INDEX, - body, - id: key.id, - refresh: 'wait_for', - }); + try { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); - await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } } } } + +async function migrateAgentPolicies() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + const { items: agentPolicies } = await agentPolicyService.list(soClient, { + perPage: SO_SEARCH_LIMIT, + }); + + await Promise.all( + agentPolicies.map(async (agentPolicy) => { + const res = await esClient.search({ + index: AGENT_POLICY_INDEX, + q: `policy_id:${agentPolicy.id}`, + track_total_hits: true, + }); + + if (res.body.hits.total.value === 0) { + return agentPolicyService.createFleetPolicyChangeFleetServer( + soClient, + esClient, + agentPolicy.id + ); + } + }) + ); +} + +function is404Error(error: any) { + return isBoom(error) && error.output.statusCode === 404; +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 9999ab91e31b228..77ce882275b6bff 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -49,6 +49,7 @@ export interface AgentService { */ authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise; /** diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index ab24c26e0cdfae3..f19ad4e7fe417de 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -93,6 +93,16 @@ async function createSetupSideEffects( await runFleetServerMigration(); } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: FLEET_SERVER_PACKAGE, + callCluster, + }); + await ensureFleetServerIndicesCreated(esClient); + await runFleetServerMigration(); + } + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 0c35fc29e01cd7b..fda1568c56e0e65 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -81,6 +81,9 @@ export { dataTypes, // Fleet Server types FleetServerEnrollmentAPIKey, + FleetServerAgent, + FleetServerAgentAction, + FleetServerPolicy, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 8d560d9a0f55341..d7c06a92f70e07d 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { LicensingPluginStart } from '../../licensing/server'; import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; @@ -33,20 +31,19 @@ export class GlobalSearchPlugin GlobalSearchPluginSetupDeps, GlobalSearchPluginStartDeps > { - private readonly config$: Observable; + private readonly config: GlobalSearchConfigType; private readonly searchService = new SearchService(); private searchServiceStart?: SearchServiceStart; private licenseChecker?: ILicenseChecker; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { - const config = await this.config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { const { registerResultProvider } = this.searchService.setup({ basePath: core.http.basePath, - config, + config: this.config, }); registerRoutes(core.http.createRouter()); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 5c13756842039aa..32dac5fba86f9ab 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -20,7 +20,7 @@ import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; - public async setup( + public setup( core: CoreSetup, { licensing, diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 532cf253c7f89b2..95793c0cad46555 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -52,22 +50,19 @@ const indexLifecycleDataEnricher = async ( }; export class IndexLifecycleManagementServerPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: IndexLifecycleManagementConfig; private readonly license: License; private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.license = new License(); } - async setup( - { http }: CoreSetup, - { licensing, indexManagement, features }: Dependencies - ): Promise { + setup({ http }: CoreSetup, { licensing, indexManagement, features }: Dependencies): void { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); + const config = this.config; this.license.setup( { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e91e085207cb756..99555fa56acd591 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,8 +8,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -79,22 +78,15 @@ export interface InfraPluginSetup { ) => void; } -export class InfraServerPlugin { - private config$: Observable; - public config = {} as InfraConfig; +export class InfraServerPlugin implements Plugin { + public config: InfraConfig; public libs: InfraBackendLibs | undefined; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { - await new Promise((resolve) => { - this.config$.subscribe((configValue) => { - this.config = configValue; - resolve(); - }); - }); + setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f7931027382..1db463a47dbf0e7 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -123,7 +123,7 @@ export class LicensingPlugin implements Plugin { private stop$ = new Subject(); private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; private featureUsage = new FeatureUsageService(); @@ -92,13 +91,12 @@ export class LicensingPlugin implements Plugin(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup<{}, LicensingPluginStart>) { + public setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); - const config = await this.config$.pipe(take(1)).toPromise(); - const pollingFrequency = config.api_polling_frequency; + const pollingFrequency = this.config.api_polling_frequency; async function callAsInternalUser( ...args: Parameters @@ -225,7 +223,7 @@ export class LicensingPlugin implements Plugin> => { - return context.config.create().pipe(map((config) => config)); -}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index bc064e236b658ec..b79d6a0b89a5758 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import type { CoreSetup, CoreStart } from 'src/core/server'; @@ -23,7 +22,6 @@ import type { ListsRequestHandlerContext, PluginsStart, } from './types'; -import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; @@ -32,17 +30,17 @@ import { ExceptionListClient } from './services/exception_lists/exception_list_c export class ListPlugin implements Plugin, ListsPluginStart, {}, PluginsStart> { private readonly logger: Logger; + private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; - private config: ConfigType | undefined | null; private security: SecurityPluginStart | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); + this.config = this.initializerContext.config.get(); } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); - this.config = config; + const { config } = this; initSavedObjects(core.savedObjects); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b04..7440b6ee1e1dfa8 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; // @ts-ignore @@ -134,12 +133,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - async setup(core: CoreSetup, plugins: SetupDeps) { + setup(core: CoreSetup, plugins: SetupDeps) { const { usageCollection, home, licensing, features, mapsLegacy } = plugins; - // @ts-ignore + const mapsLegacyConfig = mapsLegacy.config; const config$ = this._initializerContext.config.create(); - const mapsLegacyConfig = await mapsLegacy.config$.pipe(take(1)).toPromise(); - const currentConfig = await config$.pipe(take(1)).toPromise(); + const currentConfig = this._initializerContext.config.get(); // @ts-ignore const mapsEnabled = currentConfig.enabled; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a445..b950b064774b194 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -49,7 +49,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 012c050cd3fa8a8..97e572d15327c48 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -7,13 +7,15 @@ import { TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { MonitoringPlugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; export { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export { MonitoringConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); +export { MonitoringPluginSetup, IBulkUploader } from './types'; + +export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 2a5138d0d888017..08224980a558fa3 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -6,16 +6,9 @@ */ import { coreMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; -import { combineLatest } from 'rxjs'; +import { MonitoringPlugin } from './plugin'; import { AlertsFactory } from './alerts'; -jest.mock('rxjs', () => ({ - // @ts-ignore - ...jest.requireActual('rxjs'), - combineLatest: jest.fn(), -})); - jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, @@ -32,30 +25,11 @@ jest.mock('./kibana_monitoring/collectors', () => ({ registerCollectors: jest.fn(), })); -describe('Monitoring plugin', () => { - const initializerContext = { - logger: { - get: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - })), - }, - config: { - create: jest.fn().mockImplementation(() => ({ - pipe: jest.fn().mockImplementation(() => ({ - toPromise: jest.fn(), - })), - })), - legacy: { - globalConfig$: {}, - }, - }, - env: { - packageInfo: { - version: '1.0.0', - }, - }, - }; +jest.mock('./config', () => ({ + createConfig: (config: any) => config, +})); +describe('Monitoring plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); coreSetup.status.overall$.subscribe = jest.fn(); @@ -71,7 +45,6 @@ describe('Monitoring plugin', () => { }, }; - let config = {}; const defaultConfig = { ui: { elasticsearch: {}, @@ -83,20 +56,7 @@ describe('Monitoring plugin', () => { }, }; - beforeEach(() => { - config = defaultConfig; - (combineLatest as jest.Mock).mockImplementation(() => { - return { - pipe: jest.fn().mockImplementation(() => { - return { - toPromise: jest.fn().mockImplementation(() => { - return [config, 2]; - }), - }; - }), - }; - }); - }); + const initializerContext = coreMock.createPluginInitializerContext(defaultConfig); afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); @@ -104,14 +64,14 @@ describe('Monitoring plugin', () => { }); it('always create the bulk uploader', async () => { - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { const alerts = AlertsFactory.getAll(); - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); expect(setupPlugins.alerts.registerType).toHaveBeenCalledTimes(alerts.length); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6fd9e7534ac650d..654c3de7d81a96e 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,8 +6,6 @@ */ import Boom from '@hapi/boom'; -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; @@ -21,6 +19,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + Plugin, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -43,6 +42,7 @@ import { AlertsFactory } from './alerts'; import { MonitoringCore, MonitoringLicenseService, + MonitoringPluginSetup, LegacyShimDependencies, IBulkUploader, PluginsSetup, @@ -66,7 +66,8 @@ const wrapError = (error: any): CustomHttpResponseOptions => { }; }; -export class Plugin { +export class MonitoringPlugin + implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; @@ -82,15 +83,9 @@ export class Plugin { this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); } - async setup(core: CoreSetup, plugins: PluginsSetup) { - const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config - .create>() - .pipe(map((rawConfig) => createConfig(rawConfig))), - this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + setup(core: CoreSetup, plugins: PluginsSetup) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); this.legacyShimDependencies = { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0fd30189c541590..bb0b616d37eac36 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -94,6 +94,10 @@ export interface IBulkUploader { stop: () => void; } +export interface MonitoringPluginSetup { + getKibanaStats: IBulkUploader['getKibanaStats']; +} + export interface LegacyRequest { logger: Logger; getLogger: (...scopes: string[]) => Logger; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29c3..a5843d1c4ade14f 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,7 +6,6 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -28,10 +27,8 @@ export class ObservabilityPlugin implements Plugin { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: {}): Promise { - const config$ = this.initContext.config.create(); - - const config = await config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup, plugins: {}): ObservabilityPluginSetup { + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts index 19859ab05e6a978..d52f299a692cf5d 100644 --- a/x-pack/plugins/osquery/server/create_config.ts +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { map } from 'rxjs/operators'; import { PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { ConfigType } from './config'; -export const createConfig$ = ( - context: PluginInitializerContext -): Observable> => { - return context.config.create().pipe(map((config) => config)); +export const createConfig = (context: PluginInitializerContext): Readonly => { + return context.config.get(); }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77509275431e9ba..c30f4ac057ec0ce 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -14,7 +13,7 @@ import { Logger, } from '../../../../src/core/server'; -import { createConfig$ } from './create_config'; +import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; @@ -26,9 +25,9 @@ export class OsqueryPlugin implements Plugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('osquery: Setup'); - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return {}; diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts index aefb5429a1b131a..996adfdc13f6479 100644 --- a/x-pack/plugins/painless_lab/server/plugin.ts +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -22,7 +22,7 @@ export class PainlessLabServerPlugin implements Plugin { this.license = new License(); } - async setup({ http }: CoreSetup, { licensing }: Dependencies) { + setup({ http }: CoreSetup, { licensing }: Dependencies) { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index ba78cf411e36911..2b8d9afe979e896 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -29,17 +27,16 @@ export class RemoteClustersServerPlugin implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config$: Observable; + config: ConfigType; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config$ = config.create(); + this.config = config.get(); this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { + setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); const routeDependencies: RouteDependencies = { router, @@ -89,7 +86,7 @@ export class RemoteClustersServerPlugin }); return { - isUiEnabled: config.ui.enabled, + isUiEnabled: this.config.ui.enabled, }; } diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index cebcbb1a0dc92c9..ed85febac9a45bf 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -21,7 +21,7 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { + setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ router, diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 6026e42676c57ec..66b916ac7f70fec 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -15,7 +15,7 @@ import type { import { ConfigSchema } from './config'; import { securityConfigDeprecationProvider } from './config_deprecations'; import { - Plugin, + SecurityPlugin, SecurityPluginSetup, SecurityPluginStart, PluginSetupDependencies, @@ -51,4 +51,4 @@ export const plugin: PluginInitializer< RecursiveReadonly, RecursiveReadonly, PluginSetupDependencies -> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); +> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 84fc410c72cd013..d57951ecb5b1d4b 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; +import { SecurityPlugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; @@ -16,13 +16,13 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; describe('Security Plugin', () => { - let plugin: Plugin; + let plugin: SecurityPlugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { - plugin = new Plugin( + plugin = new SecurityPlugin( coreMock.createPluginInitializerContext( ConfigSchema.validate({ session: { idleTimeout: 1500 }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 3156af7e930bdae..cccfa7de6d177d6 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,6 +8,7 @@ import { combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { @@ -16,6 +17,7 @@ import { KibanaRequest, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; @@ -101,7 +103,13 @@ export interface PluginStartDependencies { /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class SecurityPlugin + implements + Plugin< + RecursiveReadonly, + RecursiveReadonly, + PluginSetupDependencies + > { private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetup; private auditSetup?: AuditServiceSetup; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 3791c63f662ae73..4658e6774b7269e 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; @@ -38,9 +37,7 @@ export const configSchema = schema.object({ validateArtifactDownloads: schema.boolean({ defaultValue: true }), }); -export const createConfig$ = (context: PluginInitializerContext) => - context.config.create>(); +export const createConfig = (context: PluginInitializerContext) => + context.config.get>(); -export type ConfigType = ReturnType extends Observable - ? T - : ReturnType; +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 6b58ca71f7f4e79..a2aff41b68df70f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -174,6 +174,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -218,6 +221,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -252,6 +258,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -280,6 +289,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -314,6 +326,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 95563c7c48ef58e..3dbaa137bb92817 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -54,7 +54,11 @@ export function registerDownloadExceptionListRoute( // The ApiKey must be associated with an enrolled Fleet agent try { scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); - await authenticateAgentWithAccessToken(scopedSOClient, req); + await authenticateAgentWithAccessToken( + scopedSOClient, + context.core.elasticsearch.client.asInternalUser, + req + ); } catch (err) { if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) { return res.unauthorized(); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0d5d83582b42ba0..8c35fd2ce8f8baa 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -6,7 +6,6 @@ */ import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -47,7 +46,7 @@ import { isNotificationAlertExecutor } from './lib/detection_engine/notification import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; -import { createConfig$, ConfigType } from './config'; +import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, @@ -119,8 +118,7 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; - private readonly config$: Observable; - private config?: ConfigType; + private readonly config: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; private setupPlugins?: SetupPlugins; @@ -137,7 +135,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); @@ -146,13 +144,12 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); this.setupPlugins = plugins; - const config = await this.config$.pipe(first()).toPromise(); - this.config = config; - const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.config; + const globalConfig = this.context.config.legacy.get(); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings); diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf602944e..c93b5dbc4c36de2 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -43,14 +42,11 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public async setup( + public setup( { http, getStartServices }: CoreSetup, { licensing, features, security, cloud }: Dependencies - ): Promise { - const pluginConfig = await this.context.config - .create() - .pipe(first()) - .toPromise(); + ): void { + const pluginConfig = this.context.config.get(); if (!pluginConfig.enabled) { return; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 9d2a075dc35f9e9..fb6c00c2f6f4809 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; -import { Plugin } from './plugin'; +import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -32,4 +32,4 @@ export const config: PluginConfigDescriptor = { deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index d576858c98e36b9..d1bf4d51700ba89 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsStart } from './plugin'; +import { SpacesPlugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { @@ -20,7 +20,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { @@ -43,7 +43,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -59,7 +59,7 @@ describe('Spaces Plugin', () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing, usageCollection }); @@ -72,7 +72,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -99,7 +99,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(coreSetup, { features, licensing }); const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d9d32dd68c95d7d..4b26b1016d5301c 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,6 +13,7 @@ import { CoreStart, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -62,7 +63,8 @@ export interface SpacesPluginStart { spacesService: SpacesServiceStart; } -export class Plugin { +export class SpacesPlugin + implements Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb8029..1343c46ecdd7297 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -19,10 +19,7 @@ export class AlertingBuiltinsPlugin this.logger = ctx.logger.get(); } - public async setup( - core: CoreSetup, - { alerts, features }: StackAlertsDeps - ): Promise { + public setup(core: CoreSetup, { alerts, features }: StackAlertsDeps) { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ @@ -34,6 +31,6 @@ export class AlertingBuiltinsPlugin }); } - public async start(): Promise {} - public async stop(): Promise {} + public start() {} + public stop() {} } diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 77031d476496884..0a879ce92cba6e1 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -39,7 +39,7 @@ describe('TaskManagerPlugin', () => { pluginInitializerContext.env.instanceUuid = ''; const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); - expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual( + expect(() => taskManagerPlugin.setup(coreMock.createSetup())).toThrow( new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`) ); }); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 62b8b75a38d7373..149d111b08f02a6 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -6,7 +6,7 @@ */ import { combineLatest, Observable, Subject } from 'rxjs'; -import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged } from 'rxjs/operators'; import { PluginInitializerContext, Plugin, @@ -46,7 +46,7 @@ export class TaskManagerPlugin implements Plugin { private taskPollingLifecycle?: TaskPollingLifecycle; private taskManagerId?: string; - private config?: TaskManagerConfig; + private config: TaskManagerConfig; private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); @@ -56,15 +56,11 @@ export class TaskManagerPlugin constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.logger = initContext.logger.get(); + this.config = initContext.config.get(); this.definitions = new TaskTypeDictionary(this.logger); } - public async setup(core: CoreSetup): Promise { - this.config = await this.initContext.config - .create() - .pipe(first()) - .toPromise(); - + public setup(core: CoreSetup): TaskManagerSetupContract { this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); setupSavedObjects(core.savedObjects, this.config); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08f8..3933751105cb441 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -30,7 +30,7 @@ export class TriggersActionsPlugin implements Plugin this.data = getService(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public setup(core: CoreSetup, plugins: PluginsSetup): void { const router = core.http.createRouter(); registerDataService({ logger: this.logger, @@ -42,7 +42,7 @@ export class TriggersActionsPlugin implements Plugin createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } - public async start(): Promise { + public start(): PluginStartContract { return { data: this.data, }; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index d7c0a465dd3e0e3..8bbbecf8108febb 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -50,10 +50,7 @@ export class UptimePlugin implements Plugin { constructor(_context: PluginInitializerContext) {} - public async setup( - core: CoreSetup, - plugins: ClientPluginsSetup - ): Promise { + public setup(core: CoreSetup, plugins: ClientPluginsSetup): void { if (plugins.home) { plugins.home.featureCatalogue.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e759a..ceade131fc5afc8 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -47,7 +47,7 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, diff --git a/x-pack/plugins/xpack_legacy/server/plugin.ts b/x-pack/plugins/xpack_legacy/server/plugin.ts index 9bd42171c75d5d0..ffef7117bbbd8a6 100644 --- a/x-pack/plugins/xpack_legacy/server/plugin.ts +++ b/x-pack/plugins/xpack_legacy/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; - import { CoreStart, CoreSetup, @@ -23,11 +21,9 @@ interface SetupPluginDeps { export class XpackLegacyPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { + public setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { const router = core.http.createRouter(); - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initContext.config.legacy.get(); const serverInfo = core.http.getServerInfo(); registerSettingsRoute({ diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ec1f37ca02be2f9..d7a9cfc0d08b40f 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('uptime', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90555 + describe.skip('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index cef1bdbba754b8a..3653d9916466d04 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -34,7 +34,8 @@ export default function ({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // Failing: See https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => {