(
route: RouteConfig,
@@ -71,7 +84,7 @@ export class HttpResourcesService implements CoreService {
},
plugins: { initialize: false },
});
+ await root.preboot();
}, 30000);
afterEach(async () => {
diff --git a/src/core/server/http_resources/types.ts b/src/core/server/http_resources/types.ts
index 3333574038ec7..1ec02272d151f 100644
--- a/src/core/server/http_resources/types.ts
+++ b/src/core/server/http_resources/types.ts
@@ -84,10 +84,16 @@ export type HttpResourcesRequestHandler<
* Allows to configure HTTP response parameters
* @internal
*/
-export interface InternalHttpResourcesSetup {
+export interface InternalHttpResourcesPreboot {
createRegistrar(router: IRouter): HttpResources;
}
+/**
+ * Allows to configure HTTP response parameters
+ * @internal
+ */
+export type InternalHttpResourcesSetup = InternalHttpResourcesPreboot;
+
/**
* HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP.
* Provides API allowing plug-ins to respond with:
diff --git a/src/core/server/i18n/i18n_service.mock.ts b/src/core/server/i18n/i18n_service.mock.ts
index 29859e95b63b2..a199acd00eff5 100644
--- a/src/core/server/i18n/i18n_service.mock.ts
+++ b/src/core/server/i18n/i18n_service.mock.ts
@@ -25,6 +25,7 @@ type I18nServiceContract = PublicMethodsOf;
const createMock = () => {
const mock: jest.Mocked = {
+ preboot: jest.fn(),
setup: jest.fn(),
};
diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts
index 913d5ee4aaa99..ad87b371aca33 100644
--- a/src/core/server/i18n/i18n_service.test.ts
+++ b/src/core/server/i18n/i18n_service.test.ts
@@ -17,7 +17,7 @@ import { I18nService } from './i18n_service';
import { configServiceMock } from '../config/mocks';
import { mockCoreContext } from '../core_context.mock';
-import { httpServiceMock } from '../http/http_service.mock';
+import { httpServiceMock } from '../mocks';
const getConfigService = (locale = 'en') => {
const configService = configServiceMock.create();
@@ -35,7 +35,8 @@ const getConfigService = (locale = 'en') => {
describe('I18nService', () => {
let service: I18nService;
let configService: ReturnType;
- let http: ReturnType;
+ let httpPreboot: ReturnType;
+ let httpSetup: ReturnType;
beforeEach(() => {
jest.clearAllMocks();
@@ -44,15 +45,60 @@ describe('I18nService', () => {
const coreContext = mockCoreContext.create({ configService });
service = new I18nService(coreContext);
- http = httpServiceMock.createInternalSetupContract();
+ httpPreboot = httpServiceMock.createInternalPrebootContract();
+ httpPreboot.registerRoutes.mockImplementation((type, callback) =>
+ callback(httpServiceMock.createRouter())
+ );
+ httpSetup = httpServiceMock.createInternalSetupContract();
+ });
+
+ describe('#preboot', () => {
+ it('calls `getKibanaTranslationFiles` with the correct parameters', async () => {
+ getKibanaTranslationFilesMock.mockResolvedValue([]);
+
+ const pluginPaths = ['/pathA', '/pathB'];
+ await service.preboot({ pluginPaths, http: httpPreboot });
+
+ expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1);
+ expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths);
+ });
+
+ it('calls `initTranslations` with the correct parameters', async () => {
+ const translationFiles = ['/path/to/file', 'path/to/another/file'];
+ getKibanaTranslationFilesMock.mockResolvedValue(translationFiles);
+
+ await service.preboot({ pluginPaths: [], http: httpPreboot });
+
+ expect(initTranslationsMock).toHaveBeenCalledTimes(1);
+ expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles);
+ });
+
+ it('calls `registerRoutesMock` with the correct parameters', async () => {
+ await service.preboot({ pluginPaths: [], http: httpPreboot });
+
+ expect(registerRoutesMock).toHaveBeenCalledTimes(1);
+ expect(registerRoutesMock).toHaveBeenCalledWith({
+ locale: 'en',
+ router: expect.any(Object),
+ });
+ });
});
describe('#setup', () => {
+ beforeEach(async () => {
+ await service.preboot({ pluginPaths: ['/pathPrebootA'], http: httpPreboot });
+
+ // Reset mocks that were used in the `preboot`.
+ getKibanaTranslationFilesMock.mockClear();
+ initTranslationsMock.mockClear();
+ registerRoutesMock.mockClear();
+ });
+
it('calls `getKibanaTranslationFiles` with the correct parameters', async () => {
getKibanaTranslationFilesMock.mockResolvedValue([]);
const pluginPaths = ['/pathA', '/pathB'];
- await service.setup({ pluginPaths, http });
+ await service.setup({ pluginPaths, http: httpSetup });
expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1);
expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths);
@@ -62,14 +108,14 @@ describe('I18nService', () => {
const translationFiles = ['/path/to/file', 'path/to/another/file'];
getKibanaTranslationFilesMock.mockResolvedValue(translationFiles);
- await service.setup({ pluginPaths: [], http });
+ await service.setup({ pluginPaths: [], http: httpSetup });
expect(initTranslationsMock).toHaveBeenCalledTimes(1);
expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles);
});
it('calls `registerRoutesMock` with the correct parameters', async () => {
- await service.setup({ pluginPaths: [], http });
+ await service.setup({ pluginPaths: [], http: httpSetup });
expect(registerRoutesMock).toHaveBeenCalledTimes(1);
expect(registerRoutesMock).toHaveBeenCalledWith({
@@ -82,7 +128,10 @@ describe('I18nService', () => {
const translationFiles = ['/path/to/file', 'path/to/another/file'];
getKibanaTranslationFilesMock.mockResolvedValue(translationFiles);
- const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [], http });
+ const { getLocale, getTranslationFiles } = await service.setup({
+ pluginPaths: [],
+ http: httpSetup,
+ });
expect(getLocale()).toEqual('en');
expect(getTranslationFiles()).toEqual(translationFiles);
diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts
index 0dffd8934a8ca..02a809b3c2c36 100644
--- a/src/core/server/i18n/i18n_service.ts
+++ b/src/core/server/i18n/i18n_service.ts
@@ -10,12 +10,17 @@ import { take } from 'rxjs/operators';
import { Logger } from '../logging';
import { IConfigService } from '../config';
import { CoreContext } from '../core_context';
-import { InternalHttpServiceSetup } from '../http';
+import { InternalHttpServicePreboot, InternalHttpServiceSetup } from '../http';
import { config as i18nConfigDef, I18nConfigType } from './i18n_config';
import { getKibanaTranslationFiles } from './get_kibana_translation_files';
import { initTranslations } from './init_translations';
import { registerRoutes } from './routes';
+interface PrebootDeps {
+ http: InternalHttpServicePreboot;
+ pluginPaths: string[];
+}
+
interface SetupDeps {
http: InternalHttpServiceSetup;
pluginPaths: string[];
@@ -45,7 +50,24 @@ export class I18nService {
this.configService = coreContext.configService;
}
+ public async preboot({ pluginPaths, http }: PrebootDeps) {
+ const { locale } = await this.initTranslations(pluginPaths);
+ http.registerRoutes('', (router) => registerRoutes({ router, locale }));
+ }
+
public async setup({ pluginPaths, http }: SetupDeps): Promise {
+ const { locale, translationFiles } = await this.initTranslations(pluginPaths);
+
+ const router = http.createRouter('');
+ registerRoutes({ router, locale });
+
+ return {
+ getLocale: () => locale,
+ getTranslationFiles: () => translationFiles,
+ };
+ }
+
+ private async initTranslations(pluginPaths: string[]) {
const i18nConfig = await this.configService
.atPath(i18nConfigDef.path)
.pipe(take(1))
@@ -59,12 +81,6 @@ export class I18nService {
this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`);
await initTranslations(locale, translationFiles);
- const router = http.createRouter('');
- registerRoutes({ router, locale });
-
- return {
- getLocale: () => locale,
- getTranslationFiles: () => translationFiles,
- };
+ return { locale, translationFiles };
}
}
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index e502fff6c69bf..c77a3a967364c 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -35,8 +35,9 @@ import {
configSchema as elasticsearchConfigSchema,
ElasticsearchServiceStart,
IScopedClusterClient,
+ ElasticsearchServicePreboot,
} from './elasticsearch';
-import { HttpServiceSetup, HttpServiceStart } from './http';
+import { HttpServicePreboot, HttpServiceSetup, HttpServiceStart } from './http';
import { HttpResources } from './http_resources';
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
@@ -58,7 +59,7 @@ import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logg
import { CoreUsageDataStart } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
import { DeprecationsServiceSetup } from './deprecations';
-// Because of #79265 we need to explicity import, then export these types for
+// Because of #79265 we need to explicitly import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
CoreUsageStats,
@@ -68,6 +69,9 @@ import {
CoreEnvironmentUsageData,
CoreServicesUsageData,
} from './core_usage_data';
+import { PrebootServicePreboot } from './preboot';
+
+export type { PrebootServicePreboot } from './preboot';
export type {
CoreUsageStats,
@@ -125,6 +129,7 @@ export type {
LegacyElasticsearchClientConfig,
LegacyElasticsearchError,
LegacyElasticsearchErrorHelpers,
+ ElasticsearchServicePreboot,
ElasticsearchServiceSetup,
ElasticsearchServiceStart,
ElasticsearchStatusMeta,
@@ -143,6 +148,7 @@ export type {
ShardsResponse,
GetResponse,
DeleteDocumentResponse,
+ ElasticsearchConfigPreboot,
} from './elasticsearch';
export type {
@@ -179,6 +185,7 @@ export type {
HttpResponseOptions,
HttpResponsePayload,
HttpServerInfo,
+ HttpServicePreboot,
HttpServiceSetup,
HttpServiceStart,
ErrorHttpResponseOptions,
@@ -260,8 +267,11 @@ export type {
AppenderConfigType,
} from './logging';
+export { PluginType } from './plugins';
+
export type {
DiscoveredPlugin,
+ PrebootPlugin,
Plugin,
AsyncPlugin,
PluginConfigDescriptor,
@@ -468,7 +478,20 @@ export interface RequestHandlerContext {
}
/**
- * Context passed to the plugins `setup` method.
+ * Context passed to the `setup` method of `preboot` plugins.
+ * @public
+ */
+export interface CorePreboot {
+ /** {@link ElasticsearchServicePreboot} */
+ elasticsearch: ElasticsearchServicePreboot;
+ /** {@link HttpServicePreboot} */
+ http: HttpServicePreboot;
+ /** {@link PrebootServicePreboot} */
+ preboot: PrebootServicePreboot;
+}
+
+/**
+ * Context passed to the `setup` method of `standard` plugins.
*
* @typeParam TPluginsStart - the type of the consuming plugin's start dependencies. Should be the same
* as the consuming {@link Plugin}'s `TPluginsStart` type. Used by `getStartServices`.
diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts
index f3253e32aa08e..540670274f416 100644
--- a/src/core/server/internal_types.ts
+++ b/src/core/server/internal_types.ts
@@ -10,23 +10,32 @@ import { Type } from '@kbn/config-schema';
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { ConfigDeprecationProvider } from './config';
-import { ContextSetup } from './context';
+import { InternalContextPreboot, ContextSetup } from './context';
import {
+ InternalElasticsearchServicePreboot,
InternalElasticsearchServiceSetup,
InternalElasticsearchServiceStart,
} from './elasticsearch';
-import { InternalHttpServiceSetup, InternalHttpServiceStart } from './http';
+import {
+ InternalHttpServicePreboot,
+ InternalHttpServiceSetup,
+ InternalHttpServiceStart,
+} from './http';
import {
InternalSavedObjectsServiceSetup,
InternalSavedObjectsServiceStart,
} from './saved_objects';
-import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings';
+import {
+ InternalUiSettingsServicePreboot,
+ InternalUiSettingsServiceSetup,
+ InternalUiSettingsServiceStart,
+} from './ui_settings';
import { InternalEnvironmentServiceSetup } from './environment';
import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metrics';
import { InternalRenderingServiceSetup } from './rendering';
-import { InternalHttpResourcesSetup } from './http_resources';
+import { InternalHttpResourcesPreboot, InternalHttpResourcesSetup } from './http_resources';
import { InternalStatusServiceSetup } from './status';
-import { InternalLoggingServiceSetup } from './logging';
+import { InternalLoggingServicePreboot, InternalLoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
import { InternalDeprecationsServiceSetup } from './deprecations';
@@ -34,6 +43,18 @@ import type {
InternalExecutionContextSetup,
InternalExecutionContextStart,
} from './execution_context';
+import { InternalPrebootServicePreboot } from './preboot';
+
+/** @internal */
+export interface InternalCorePreboot {
+ context: InternalContextPreboot;
+ http: InternalHttpServicePreboot;
+ elasticsearch: InternalElasticsearchServicePreboot;
+ uiSettings: InternalUiSettingsServicePreboot;
+ httpResources: InternalHttpResourcesPreboot;
+ logging: InternalLoggingServicePreboot;
+ preboot: InternalPrebootServicePreboot;
+}
/** @internal */
export interface InternalCoreSetup {
diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts
index 88c45962ce4a6..d8f9f035f44be 100644
--- a/src/core/server/legacy/integration_tests/logging.test.ts
+++ b/src/core/server/legacy/integration_tests/logging.test.ts
@@ -67,6 +67,7 @@ describe('logging service', () => {
beforeAll(async () => {
root = createRoot();
+ await root.preboot();
await root.setup();
await root.start();
}, 30000);
@@ -119,6 +120,7 @@ describe('logging service', () => {
it('"silent": true', async () => {
root = createRoot({ silent: true });
+ await root.preboot();
await root.setup();
await root.start();
@@ -150,6 +152,7 @@ describe('logging service', () => {
it('"quiet": true', async () => {
root = createRoot({ quiet: true });
+ await root.preboot();
await root.setup();
await root.start();
@@ -187,6 +190,7 @@ describe('logging service', () => {
it('"verbose": true', async () => {
root = createRoot({ verbose: true });
+ await root.preboot();
await root.setup();
await root.start();
diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts
index 9d17b289bfa4c..ba6aec5f128c1 100644
--- a/src/core/server/logging/index.ts
+++ b/src/core/server/logging/index.ts
@@ -32,6 +32,10 @@ export type {
export { LoggingSystem } from './logging_system';
export type { ILoggingSystem } from './logging_system';
export { LoggingService } from './logging_service';
-export type { InternalLoggingServiceSetup, LoggingServiceSetup } from './logging_service';
+export type {
+ InternalLoggingServicePreboot,
+ InternalLoggingServiceSetup,
+ LoggingServiceSetup,
+} from './logging_service';
export { appendersSchema } from './appenders/appenders';
export type { AppenderConfigType } from './appenders/appenders';
diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts
index b4eb98546de21..ade10fc1c0257 100644
--- a/src/core/server/logging/integration_tests/logging.test.ts
+++ b/src/core/server/logging/integration_tests/logging.test.ts
@@ -49,6 +49,7 @@ describe('logging service', () => {
mockConsoleLog = jest.spyOn(global.console, 'log');
root = createRoot();
+ await root.preboot();
await root.setup();
}, 30000);
@@ -151,6 +152,7 @@ describe('logging service', () => {
mockConsoleLog = jest.spyOn(global.console, 'log');
root = kbnTestServer.createRoot();
+ await root.preboot();
setup = await root.setup();
setup.logging.configure(['plugins', 'myplugin'], loggingConfig$);
}, 30000);
diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
index b40ce7a4e7b0e..b560748026ace 100644
--- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
+++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
@@ -79,6 +79,7 @@ describe('RollingFileAppender', () => {
pattern: '.%i',
},
});
+ await root.preboot();
await root.setup();
const logger = root.logger.get('test.rolling.file');
@@ -124,6 +125,7 @@ describe('RollingFileAppender', () => {
pattern: '-%i',
},
});
+ await root.preboot();
await root.setup();
const logger = root.logger.get('test.rolling.file');
@@ -174,6 +176,7 @@ describe('RollingFileAppender', () => {
pattern: '-%i',
},
});
+ await root.preboot();
await root.setup();
const logger = root.logger.get('test.rolling.file');
diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts
index 5f91c7b8734b8..75d358c8cbe6e 100644
--- a/src/core/server/logging/logging_service.mock.ts
+++ b/src/core/server/logging/logging_service.mock.ts
@@ -12,8 +12,13 @@ import {
LoggingService,
LoggingServiceSetup,
InternalLoggingServiceSetup,
+ InternalLoggingServicePreboot,
} from './logging_service';
+const createInternalPrebootMock = (): jest.Mocked => ({
+ configure: jest.fn(),
+});
+
const createInternalSetupMock = (): jest.Mocked => ({
configure: jest.fn(),
});
@@ -25,11 +30,13 @@ const createSetupMock = (): jest.Mocked => ({
type LoggingServiceContract = PublicMethodsOf;
const createMock = (): jest.Mocked => {
const service: jest.Mocked = {
+ preboot: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
+ service.preboot.mockReturnValue(createInternalPrebootMock());
service.setup.mockReturnValue(createInternalSetupMock());
return service;
@@ -38,5 +45,6 @@ const createMock = (): jest.Mocked => {
export const loggingServiceMock = {
create: createMock,
createSetupContract: createSetupMock,
+ createInternalPrebootContract: createInternalPrebootMock,
createInternalSetupContract: createInternalSetupMock,
};
diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts
index 341a04736b87a..9817f08c59bf8 100644
--- a/src/core/server/logging/logging_service.test.ts
+++ b/src/core/server/logging/logging_service.test.ts
@@ -8,83 +8,102 @@
import { of, Subject } from 'rxjs';
-import { LoggingService, InternalLoggingServiceSetup } from './logging_service';
+import {
+ LoggingService,
+ InternalLoggingServiceSetup,
+ InternalLoggingServicePreboot,
+} from './logging_service';
import { loggingSystemMock } from './logging_system.mock';
import { LoggerContextConfigType } from './logging_config';
describe('LoggingService', () => {
let loggingSystem: ReturnType;
let service: LoggingService;
- let setup: InternalLoggingServiceSetup;
+ let preboot: InternalLoggingServicePreboot;
beforeEach(() => {
loggingSystem = loggingSystemMock.create();
service = new LoggingService({ logger: loggingSystem.asLoggerFactory() } as any);
- setup = service.setup({ loggingSystem });
+ preboot = service.preboot({ loggingSystem });
});
afterEach(() => {
service.stop();
});
- describe('setup', () => {
- it('forwards configuration changes to logging system', () => {
- const config1: LoggerContextConfigType = {
- appenders: new Map(),
- loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
- };
- const config2: LoggerContextConfigType = {
- appenders: new Map(),
- loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }],
- };
+ function runTestSuite(
+ testSuiteName: string,
+ getContract: () => InternalLoggingServicePreboot | InternalLoggingServiceSetup
+ ) {
+ describe(testSuiteName, () => {
+ it('forwards configuration changes to logging system', async () => {
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
- setup.configure(['test', 'context'], of(config1, config2));
- expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
- 1,
- ['test', 'context'],
- config1
- );
- expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
- 2,
- ['test', 'context'],
- config2
- );
- });
+ getContract().configure(['test', 'context'], of(config1, config2));
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 2,
+ ['test', 'context'],
+ config2
+ );
+ });
- it('stops forwarding first observable when called a second time', () => {
- const updates$ = new Subject();
- const config1: LoggerContextConfigType = {
- appenders: new Map(),
- loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
- };
- const config2: LoggerContextConfigType = {
- appenders: new Map(),
- loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }],
- };
+ it('stops forwarding first observable when called a second time', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
- setup.configure(['test', 'context'], updates$);
- setup.configure(['test', 'context'], of(config1));
- updates$.next(config2);
- expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
- 1,
- ['test', 'context'],
- config1
- );
- expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config2);
+ const contract = getContract();
+ contract.configure(['test', 'context'], updates$);
+ contract.configure(['test', 'context'], of(config1));
+ updates$.next(config2);
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(
+ ['test', 'context'],
+ config2
+ );
+ });
});
- });
- describe('stop', () => {
- it('stops forwarding updates to logging system', () => {
- const updates$ = new Subject();
- const config1: LoggerContextConfigType = {
- appenders: new Map(),
- loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
- };
+ describe(`stop after ${testSuiteName}`, () => {
+ it('stops forwarding updates to logging system', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
- setup.configure(['test', 'context'], updates$);
- service.stop();
- updates$.next(config1);
- expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config1);
+ getContract().configure(['test', 'context'], updates$);
+ service.stop();
+ updates$.next(config1);
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(
+ ['test', 'context'],
+ config1
+ );
+ });
});
- });
+ }
+
+ runTestSuite('preboot', () => preboot);
+ runTestSuite('setup', () => service.setup());
});
diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts
index f5a4717fdbfaf..6c3eee4981725 100644
--- a/src/core/server/logging/logging_service.ts
+++ b/src/core/server/logging/logging_service.ts
@@ -42,11 +42,14 @@ export interface LoggingServiceSetup {
}
/** @internal */
-export interface InternalLoggingServiceSetup {
+export interface InternalLoggingServicePreboot {
configure(contextParts: string[], config$: Observable): void;
}
-interface SetupDeps {
+/** @internal */
+export type InternalLoggingServiceSetup = InternalLoggingServicePreboot;
+
+interface PrebootDeps {
loggingSystem: ILoggingSystem;
}
@@ -54,13 +57,14 @@ interface SetupDeps {
export class LoggingService implements CoreService {
private readonly subscriptions = new Map();
private readonly log: Logger;
+ private internalPreboot?: InternalLoggingServicePreboot;
constructor(coreContext: CoreContext) {
this.log = coreContext.logger.get('logging');
}
- public setup({ loggingSystem }: SetupDeps) {
- return {
+ public preboot({ loggingSystem }: PrebootDeps) {
+ this.internalPreboot = {
configure: (contextParts: string[], config$: Observable) => {
const contextName = LoggingConfig.getLoggerContext(contextParts);
this.log.debug(`Setting custom config for context [${contextName}]`);
@@ -80,6 +84,14 @@ export class LoggingService implements CoreService
);
},
};
+
+ return this.internalPreboot;
+ }
+
+ public setup() {
+ return {
+ configure: this.internalPreboot!.configure,
+ };
}
public start() {}
diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts
index 19e3e6a6c68f9..93589648ca0ae 100644
--- a/src/core/server/metrics/integration_tests/server_collector.test.ts
+++ b/src/core/server/metrics/integration_tests/server_collector.test.ts
@@ -29,6 +29,7 @@ describe('ServerMetricsCollector', () => {
beforeEach(async () => {
server = createHttpServer();
+ await server.preboot({ context: contextServiceMock.createPrebootContract() });
const contextSetup = contextServiceMock.createSetupContract();
const httpSetup = await server.setup({
context: contextSetup,
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index ff844f44aede0..f423e40fbcaf9 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -10,7 +10,13 @@ import { of } from 'rxjs';
import { duration } from 'moment';
import { ByteSizeValue } from '@kbn/config-schema';
import type { MockedKeys } from '@kbn/utility-types/jest';
-import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.';
+import {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ StartServicesAccessor,
+ CorePreboot,
+} from '.';
import { loggingSystemMock } from './logging/logging_system.mock';
import { loggingServiceMock } from './logging/logging_service.mock';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
@@ -31,6 +37,7 @@ import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_serv
import { i18nServiceMock } from './i18n/i18n_service.mock';
import { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
import { executionContextServiceMock } from './execution_context/execution_context_service.mock';
+import { prebootServiceMock } from './preboot/preboot_service.mock';
export { configServiceMock } from './config/mocks';
export { httpServerMock } from './http/http_server.mocks';
@@ -106,6 +113,7 @@ function pluginInitializerContextMock(config: T = {} as T) {
dist: false,
},
instanceUuid: 'instance-uuid',
+ configs: ['/some/path/to/config/kibana.yml'],
},
config: pluginInitializerContextConfigMock(config),
};
@@ -113,6 +121,20 @@ function pluginInitializerContextMock(config: T = {} as T) {
return mock;
}
+type CorePrebootMockType = MockedKeys & {
+ elasticsearch: ReturnType;
+};
+
+function createCorePrebootMock() {
+ const mock: CorePrebootMockType = {
+ elasticsearch: elasticsearchServiceMock.createPreboot(),
+ http: httpServiceMock.createPrebootContract(),
+ preboot: prebootServiceMock.createPrebootContract(),
+ };
+
+ return mock;
+}
+
type CoreSetupMockType = MockedKeys & {
elasticsearch: ReturnType;
getStartServices: jest.MockedFunction>;
@@ -170,6 +192,19 @@ function createCoreStartMock() {
return mock;
}
+function createInternalCorePrebootMock() {
+ const prebootDeps = {
+ context: contextServiceMock.createPrebootContract(),
+ elasticsearch: elasticsearchServiceMock.createInternalPreboot(),
+ http: httpServiceMock.createInternalPrebootContract(),
+ httpResources: httpResourcesMock.createPrebootContract(),
+ uiSettings: uiSettingsServiceMock.createPrebootContract(),
+ logging: loggingServiceMock.createInternalPrebootContract(),
+ preboot: prebootServiceMock.createInternalPrebootContract(),
+ };
+ return prebootDeps;
+}
+
function createInternalCoreSetupMock() {
const setupDeps = {
capabilities: capabilitiesServiceMock.createSetupContract(),
@@ -227,8 +262,10 @@ function createCoreRequestHandlerContextMock() {
}
export const coreMock = {
+ createPreboot: createCorePrebootMock,
createSetup: createCoreSetupMock,
createStart: createCoreStartMock,
+ createInternalPreboot: createInternalCorePrebootMock,
createInternalSetup: createInternalCoreSetupMock,
createInternalStart: createInternalCoreStartMock,
createPluginInitializerContext: pluginInitializerContextMock,
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
index f3a92c896b014..3e410e4b19c0e 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
@@ -226,6 +226,29 @@ test('return error when manifest contains unrecognized properties', async () =>
});
});
+test('returns error when manifest contains unrecognized `type`', async () => {
+ mockReadFile.mockImplementation((path, cb) => {
+ cb(
+ null,
+ Buffer.from(
+ JSON.stringify({
+ id: 'someId',
+ version: '7.0.0',
+ kibanaVersion: '7.0.0',
+ type: 'unknown',
+ server: true,
+ })
+ )
+ );
+ });
+
+ await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({
+ message: `The "type" in manifest for plugin "someId" is set to "unknown", but it should either be "standard" or "preboot". (invalid-manifest, ${pluginManifestPath})`,
+ type: PluginDiscoveryErrorType.InvalidManifest,
+ path: pluginManifestPath,
+ });
+});
+
describe('configPath', () => {
test('falls back to plugin id if not specified', async () => {
mockReadFile.mockImplementation((path, cb) => {
@@ -284,6 +307,7 @@ test('set defaults for all missing optional fields', async () => {
configPath: 'some_id',
version: '7.0.0',
kibanaVersion: '7.0.0',
+ type: 'standard',
optionalPlugins: [],
requiredPlugins: [],
requiredBundles: [],
@@ -302,6 +326,7 @@ test('return all set optional fields as they are in manifest', async () => {
configPath: ['some', 'path'],
version: 'some-version',
kibanaVersion: '7.0.0',
+ type: 'preboot',
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
optionalPlugins: ['some-optional-plugin'],
ui: true,
@@ -315,6 +340,7 @@ test('return all set optional fields as they are in manifest', async () => {
configPath: ['some', 'path'],
version: 'some-version',
kibanaVersion: '7.0.0',
+ type: 'preboot',
optionalPlugins: ['some-optional-plugin'],
requiredBundles: [],
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
@@ -345,6 +371,7 @@ test('return manifest when plugin expected Kibana version matches actual version
configPath: 'some-path',
version: 'some-version',
kibanaVersion: '7.0.0-alpha2',
+ type: 'standard',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
requiredBundles: [],
@@ -375,6 +402,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async ()
configPath: 'some_id',
version: 'some-version',
kibanaVersion: 'kibana',
+ type: 'standard',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
requiredBundles: [],
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
index b59418a67219e..57640ec6acc0a 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
@@ -12,7 +12,7 @@ import { coerce } from 'semver';
import { promisify } from 'util';
import { snakeCase } from 'lodash';
import { isConfigPath, PackageInfo } from '../../config';
-import { PluginManifest } from '../types';
+import { PluginManifest, PluginType } from '../types';
import { PluginDiscoveryError } from './plugin_discovery_error';
import { isCamelCase } from './is_camel_case';
@@ -39,6 +39,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
const manifestFields: { [P in keyof PluginManifest]: boolean } = {
id: true,
kibanaVersion: true,
+ type: true,
version: true,
configPath: true,
requiredPlugins: true,
@@ -178,10 +179,21 @@ export async function parseManifest(
);
}
+ const type = manifest.type ?? PluginType.standard;
+ if (type !== PluginType.preboot && type !== PluginType.standard) {
+ throw PluginDiscoveryError.invalidManifest(
+ manifestPath,
+ new Error(
+ `The "type" in manifest for plugin "${manifest.id}" is set to "${type}", but it should either be "standard" or "preboot".`
+ )
+ );
+ }
+
return {
id: manifest.id,
version: manifest.version,
kibanaVersion: expectedKibanaVersion,
+ type,
configPath: manifest.configPath || snakeCase(manifest.id),
requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [],
optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [],
diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts
index f6028fa8b099d..28f2ab799e092 100644
--- a/src/core/server/plugins/discovery/plugins_discovery.test.ts
+++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts
@@ -21,6 +21,7 @@ import { PluginsConfig, PluginsConfigType, config } from '../plugins_config';
import type { InstanceInfo } from '../plugin_context';
import { discover } from './plugins_discovery';
import { CoreContext } from '../../core_context';
+import { PluginType } from '../types';
const KIBANA_ROOT = process.cwd();
@@ -34,6 +35,15 @@ const Plugins = {
incompatible: () => ({
'kibana.json': JSON.stringify({ id: 'plugin', version: '1' }),
}),
+ incompatibleType: (id: string) => ({
+ 'kibana.json': JSON.stringify({
+ id,
+ version: '1',
+ kibanaVersion: '1.2.3',
+ type: 'evenEarlierThanPreboot',
+ server: true,
+ }),
+ }),
missingManifest: () => ({}),
inaccessibleManifest: () => ({
'kibana.json': mockFs.file({
@@ -52,6 +62,18 @@ const Plugins = {
server: true,
}),
}),
+ validPreboot: (id: string) => ({
+ 'kibana.json': JSON.stringify({
+ id,
+ configPath: ['plugins', id],
+ version: '1',
+ kibanaVersion: '1.2.3',
+ type: PluginType.preboot,
+ requiredPlugins: [],
+ optionalPlugins: [],
+ server: true,
+ }),
+ }),
};
const packageMock = {
@@ -132,6 +154,7 @@ describe('plugins discovery system', () => {
[`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'),
[`${KIBANA_ROOT}/plugins/plugin_b`]: Plugins.valid('pluginB'),
[`${KIBANA_ROOT}/x-pack/plugins/plugin_c`]: Plugins.valid('pluginC'),
+ [`${KIBANA_ROOT}/src/plugins/plugin_d`]: Plugins.validPreboot('pluginD'),
},
{ createCwd: false }
);
@@ -139,8 +162,10 @@ describe('plugins discovery system', () => {
const plugins = await plugin$.pipe(toArray()).toPromise();
const pluginNames = plugins.map((plugin) => plugin.name);
- expect(pluginNames).toHaveLength(3);
- expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC']));
+ expect(pluginNames).toHaveLength(4);
+ expect(pluginNames).toEqual(
+ expect.arrayContaining(['pluginA', 'pluginB', 'pluginC', 'pluginD'])
+ );
});
it('return errors when the manifest is invalid or incompatible', async () => {
@@ -155,6 +180,7 @@ describe('plugins discovery system', () => {
[`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.invalid(),
[`${KIBANA_ROOT}/src/plugins/plugin_b`]: Plugins.incomplete(),
[`${KIBANA_ROOT}/src/plugins/plugin_c`]: Plugins.incompatible(),
+ [`${KIBANA_ROOT}/src/plugins/plugin_d`]: Plugins.incompatibleType('pluginD'),
[`${KIBANA_ROOT}/src/plugins/plugin_ad`]: Plugins.missingManifest(),
},
{ createCwd: false }
@@ -181,6 +207,9 @@ describe('plugins discovery system', () => {
`Error: Plugin "plugin" is only compatible with Kibana version "1", but used Kibana version is "1.2.3". (incompatible-version, ${manifestPath(
'plugin_c'
)})`,
+ `Error: The "type" in manifest for plugin "pluginD" is set to "evenEarlierThanPreboot", but it should either be "standard" or "preboot". (invalid-manifest, ${manifestPath(
+ 'plugin_d'
+ )})`,
])
);
});
@@ -271,7 +300,8 @@ describe('plugins discovery system', () => {
[`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'),
[`${KIBANA_ROOT}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'),
[`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'),
- [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(),
+ [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.validPreboot('pluginD'),
+ [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_e`]: Plugins.incomplete(),
},
{ createCwd: false }
);
@@ -279,8 +309,10 @@ describe('plugins discovery system', () => {
const plugins = await plugin$.pipe(toArray()).toPromise();
const pluginNames = plugins.map((plugin) => plugin.name);
- expect(pluginNames).toHaveLength(3);
- expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC']));
+ expect(pluginNames).toHaveLength(4);
+ expect(pluginNames).toEqual(
+ expect.arrayContaining(['pluginA', 'pluginB', 'pluginC', 'pluginD'])
+ );
const errors = await error$
.pipe(
@@ -294,7 +326,7 @@ describe('plugins discovery system', () => {
`Error: Plugin manifest must contain an "id" property. (invalid-manifest, ${manifestPath(
'sub1',
'sub2',
- 'plugin_d'
+ 'plugin_e'
)})`,
])
);
@@ -358,6 +390,7 @@ describe('plugins discovery system', () => {
[pluginFolder]: {
plugin_a: Plugins.valid('pluginA'),
plugin_b: Plugins.valid('pluginB'),
+ plugin_c: Plugins.validPreboot('pluginC'),
},
},
{ createCwd: false }
@@ -366,8 +399,8 @@ describe('plugins discovery system', () => {
const plugins = await plugin$.pipe(toArray()).toPromise();
const pluginNames = plugins.map((plugin) => plugin.name);
- expect(pluginNames).toHaveLength(2);
- expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB']));
+ expect(pluginNames).toHaveLength(3);
+ expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC']));
});
it('logs a warning about --plugin-path when used in development', async () => {
diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts
index a71df00b39c5c..1b655ccd8bd98 100644
--- a/src/core/server/plugins/index.ts
+++ b/src/core/server/plugins/index.ts
@@ -7,7 +7,12 @@
*/
export { PluginsService } from './plugins_service';
-export type { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from './plugins_service';
+export type {
+ PluginsServiceSetup,
+ PluginsServiceStart,
+ UiPlugins,
+ DiscoveredPlugins,
+} from './plugins_service';
export { config } from './plugins_config';
/** @internal */
export { isNewPlatformPlugin } from './discovery';
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 a29fb01fbc009..1b0caf7bf6255 100644
--- a/src/core/server/plugins/integration_tests/plugins_service.test.ts
+++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts
@@ -20,12 +20,12 @@ import { config } from '../plugins_config';
import { loggingSystemMock } from '../../logging/logging_system.mock';
import { environmentServiceMock } from '../../environment/environment_service.mock';
import { coreMock } from '../../mocks';
-import { AsyncPlugin } from '../types';
+import { AsyncPlugin, PluginType } from '../types';
import { PluginWrapper } from '../plugin';
describe('PluginsService', () => {
const logger = loggingSystemMock.create();
- const environmentSetup = environmentServiceMock.createSetupContract();
+ const environmentPreboot = environmentServiceMock.createPrebootContract();
let pluginsService: PluginsService;
const createPlugin = (
@@ -38,6 +38,7 @@ describe('PluginsService', () => {
requiredBundles = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
+ type = PluginType.standard,
configPath = [path],
server = true,
ui = true,
@@ -49,6 +50,7 @@ describe('PluginsService', () => {
requiredBundles?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
+ type?: PluginType;
configPath?: ConfigPath;
server?: boolean;
ui?: boolean;
@@ -61,6 +63,7 @@ describe('PluginsService', () => {
version,
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
+ type,
requiredPlugins,
requiredBundles,
optionalPlugins,
@@ -150,7 +153,10 @@ describe('PluginsService', () => {
}
);
- await pluginsService.discover({ environment: environmentSetup });
+ await pluginsService.discover({ environment: environmentPreboot });
+
+ const prebootDeps = coreMock.createInternalPreboot();
+ await pluginsService.preboot(prebootDeps);
const setupDeps = coreMock.createInternalSetup();
await pluginsService.setup(setupDeps);
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index c90d2e804225c..610bc1cac5a3b 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -15,10 +15,10 @@ import { Env } from '../config';
import { CoreContext } from '../core_context';
import { coreMock } from '../mocks';
import { loggingSystemMock } from '../logging/logging_system.mock';
-import { getEnvOptions, configServiceMock } from '../config/mocks';
+import { configServiceMock, getEnvOptions } from '../config/mocks';
import { PluginWrapper } from './plugin';
-import { PluginManifest } from './types';
+import { PluginManifest, PluginType } from './types';
import {
createPluginInitializerContext,
createPluginSetupContext,
@@ -45,6 +45,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
version: 'some-version',
configPath: 'path',
kibanaVersion: '7.0.0',
+ type: PluginType.standard,
requiredPlugins: ['some-required-dep'],
optionalPlugins: ['some-optional-dep'],
requiredBundles: [],
@@ -243,6 +244,31 @@ test('`start` fails if setup is not called first', () => {
);
});
+test('`start` fails invoked for the `preboot` plugin', async () => {
+ const manifest = createPluginManifest({ type: PluginType.preboot });
+ const opaqueId = Symbol();
+ const plugin = new PluginWrapper({
+ path: 'plugin-with-initializer-path',
+ manifest,
+ opaqueId,
+ initializerContext: createPluginInitializerContext(
+ coreContext,
+ opaqueId,
+ manifest,
+ instanceInfo
+ ),
+ });
+
+ const mockPluginInstance = { setup: jest.fn() };
+ mockPluginInitializer.mockReturnValue(mockPluginInstance);
+
+ await plugin.setup({} as any, {} as any);
+
+ expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot(
+ `"Plugin \\"some-plugin-id\\" is a preboot plugin and cannot be started."`
+ );
+});
+
test('`start` calls plugin.start with context and dependencies', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index ca7f11e28de75..76ed4f8f24b3d 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -15,15 +15,17 @@ import { isConfigSchema } from '@kbn/config-schema';
import { Logger } from '../logging';
import {
- Plugin,
AsyncPlugin,
+ Plugin,
+ PluginConfigDescriptor,
+ PluginInitializer,
PluginInitializerContext,
PluginManifest,
- PluginInitializer,
PluginOpaqueId,
- PluginConfigDescriptor,
+ PluginType,
+ PrebootPlugin,
} from './types';
-import { CoreSetup, CoreStart } from '..';
+import { CorePreboot, CoreSetup, CoreStart } from '..';
/**
* Lightweight wrapper around discovered plugin that is responsible for instantiating
@@ -53,6 +55,7 @@ export class PluginWrapper<
private instance?:
| Plugin
+ | PrebootPlugin
| AsyncPlugin;
private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>();
@@ -88,11 +91,16 @@ export class PluginWrapper<
* is the contract returned by the dependency's `setup` function.
*/
public setup(
- setupContext: CoreSetup,
+ setupContext: CoreSetup | CorePreboot,
plugins: TPluginsSetup
): TSetup | Promise {
this.instance = this.createPluginInstance();
- return this.instance.setup(setupContext, plugins);
+
+ if (this.isPrebootPluginInstance(this.instance)) {
+ return this.instance.setup(setupContext as CorePreboot, plugins);
+ }
+
+ return this.instance.setup(setupContext as CoreSetup, plugins);
}
/**
@@ -107,6 +115,10 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
+ if (this.isPrebootPluginInstance(this.instance)) {
+ throw new Error(`Plugin "${this.name}" is a preboot plugin and cannot be started.`);
+ }
+
const startContract = this.instance.start(startContext, plugins);
if (isPromise(startContract)) {
return startContract.then((resolvedContract) => {
@@ -185,4 +197,10 @@ export class PluginWrapper<
return instance;
}
+
+ private isPrebootPluginInstance(
+ instance: PluginWrapper['instance']
+ ): instance is PrebootPlugin {
+ return this.manifest.type === PluginType.preboot;
+ }
}
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 4ba34ccb2149f..7913bad3cad17 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -10,15 +10,21 @@ import { duration } from 'moment';
import { first } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/dev-utils';
import { fromRoot } from '@kbn/utils';
-import { createPluginInitializerContext, InstanceInfo } from './plugin_context';
+import {
+ createPluginInitializerContext,
+ createPluginPrebootSetupContext,
+ InstanceInfo,
+} from './plugin_context';
import { CoreContext } from '../core_context';
import { Env } from '../config';
import { loggingSystemMock } from '../logging/logging_system.mock';
-import { rawConfigServiceMock, getEnvOptions } from '../config/mocks';
-import { PluginManifest } from './types';
+import { rawConfigServiceMock, getEnvOptions, configServiceMock } from '../config/mocks';
+import { PluginManifest, PluginType } from './types';
import { Server } from '../server';
import { schema, ByteSizeValue } from '@kbn/config-schema';
import { ConfigService } from '@kbn/config';
+import { PluginWrapper } from './plugin';
+import { coreMock } from '../mocks';
function createPluginManifest(manifestProps: Partial = {}): PluginManifest {
return {
@@ -26,6 +32,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
version: 'some-version',
configPath: 'path',
kibanaVersion: '7.0.0',
+ type: PluginType.standard,
requiredPlugins: ['some-required-dep'],
requiredBundles: [],
optionalPlugins: ['some-optional-dep'],
@@ -141,5 +148,67 @@ describe('createPluginInitializerContext', () => {
);
expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid');
});
+
+ it('should expose paths to the config files', () => {
+ coreContext = {
+ ...coreContext,
+ env: Env.createDefault(
+ REPO_ROOT,
+ getEnvOptions({
+ configs: ['/home/kibana/config/kibana.yml', '/home/kibana/config/kibana.dev.yml'],
+ })
+ ),
+ };
+ const pluginInitializerContext = createPluginInitializerContext(
+ coreContext,
+ opaqueId,
+ createPluginManifest(),
+ instanceInfo
+ );
+ expect(pluginInitializerContext.env.configs).toEqual([
+ '/home/kibana/config/kibana.yml',
+ '/home/kibana/config/kibana.dev.yml',
+ ]);
+ });
+ });
+});
+
+describe('createPluginPrebootSetupContext', () => {
+ let coreContext: CoreContext;
+ let opaqueId: symbol;
+
+ beforeEach(async () => {
+ opaqueId = Symbol();
+ coreContext = {
+ coreId: Symbol('core'),
+ env: Env.createDefault(REPO_ROOT, getEnvOptions()),
+ logger: loggingSystemMock.create(),
+ configService: configServiceMock.create(),
+ };
+ });
+
+ it('`holdSetupUntilResolved` captures plugin.name', () => {
+ const manifest = createPluginManifest();
+ const plugin = new PluginWrapper({
+ path: 'some-path',
+ manifest,
+ opaqueId,
+ initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest, {
+ uuid: 'instance-uuid',
+ }),
+ });
+
+ const corePreboot = coreMock.createInternalPreboot();
+ const prebootSetupContext = createPluginPrebootSetupContext(coreContext, corePreboot, plugin);
+
+ const holdSetupPromise = Promise.resolve(undefined);
+ prebootSetupContext.preboot.holdSetupUntilResolved('some-reason', holdSetupPromise);
+
+ expect(corePreboot.preboot.holdSetupUntilResolved).toHaveBeenCalledTimes(1);
+ expect(corePreboot.preboot.holdSetupUntilResolved).toHaveBeenCalledWith(
+ 'some-plugin-id',
+ 'some-reason',
+ holdSetupPromise
+ );
});
});
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 70fd1c60efa61..29194b1e8fc62 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -10,11 +10,15 @@ import { shareReplay } from 'rxjs/operators';
import type { RequestHandlerContext } from 'src/core/server';
import { CoreContext } from '../core_context';
import { PluginWrapper } from './plugin';
-import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
+import {
+ PluginsServicePrebootSetupDeps,
+ PluginsServiceSetupDeps,
+ PluginsServiceStartDeps,
+} from './plugins_service';
import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types';
import { IRouter, RequestHandlerContextProvider } from '../http';
import { getGlobalConfig, getGlobalConfig$ } from './legacy_config';
-import { CoreSetup, CoreStart } from '..';
+import { CorePreboot, CoreSetup, CoreStart } from '..';
export interface InstanceInfo {
uuid: string;
@@ -49,6 +53,7 @@ export function createPluginInitializerContext(
mode: coreContext.env.mode,
packageInfo: coreContext.env.packageInfo,
instanceUuid: instanceInfo.uuid,
+ configs: coreContext.env.configs,
},
/**
@@ -83,6 +88,42 @@ export function createPluginInitializerContext(
};
}
+/**
+ * Provides `CorePreboot` contract that will be exposed to the `preboot` plugin `setup` method.
+ * This contract should be safe to use only within `setup` itself.
+ *
+ * This is called for each `preboot` plugin when it's set up, so each plugin gets its own
+ * version of these values.
+ *
+ * We should aim to be restrictive and specific in the APIs that we expose.
+ *
+ * @param coreContext Kibana core context
+ * @param deps Dependencies that Plugins services gets during setup.
+ * @param plugin The plugin we're building these values for.
+ * @internal
+ */
+export function createPluginPrebootSetupContext(
+ coreContext: CoreContext,
+ deps: PluginsServicePrebootSetupDeps,
+ plugin: PluginWrapper
+): CorePreboot {
+ return {
+ elasticsearch: {
+ config: deps.elasticsearch.config,
+ createClient: deps.elasticsearch.createClient,
+ },
+ http: {
+ registerRoutes: deps.http.registerRoutes,
+ basePath: deps.http.basePath,
+ },
+ preboot: {
+ isSetupOnHold: deps.preboot.isSetupOnHold,
+ holdSetupUntilResolved: (reason, promise) =>
+ deps.preboot.holdSetupUntilResolved(plugin.name, reason, promise),
+ },
+ };
+}
+
/**
* This returns a facade for `CoreContext` that will be exposed to the plugin `setup` method.
* This facade should be safe to use only within `setup` itself.
diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts
index f4f2263a1bdb0..ee7b35a412e80 100644
--- a/src/core/server/plugins/plugins_service.mock.ts
+++ b/src/core/server/plugins/plugins_service.mock.ts
@@ -20,6 +20,7 @@ const createStartContractMock = () => ({ contracts: new Map() });
const createServiceMock = (): PluginsServiceMock => ({
discover: jest.fn(),
getExposedPluginConfigsToUsage: jest.fn(),
+ preboot: jest.fn(),
setup: jest.fn().mockResolvedValue(createSetupContractMock()),
start: jest.fn().mockResolvedValue(createStartContractMock()),
stop: jest.fn(),
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 5c50df07dc697..3cd645cb74ac0 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -24,26 +24,31 @@ import { PluginsService } from './plugins_service';
import { PluginsSystem } from './plugins_system';
import { config } from './plugins_config';
import { take } from 'rxjs/operators';
-import { DiscoveredPlugin } from './types';
+import { DiscoveredPlugin, PluginType } from './types';
-const MockPluginsSystem: jest.Mock = PluginsSystem as any;
+const MockPluginsSystem: jest.Mock> = PluginsSystem as any;
let pluginsService: PluginsService;
let config$: BehaviorSubject>;
let configService: ConfigService;
let coreId: symbol;
let env: Env;
-let mockPluginSystem: jest.Mocked;
-let environmentSetup: ReturnType;
+let prebootMockPluginSystem: jest.Mocked>;
+let standardMockPluginSystem: jest.Mocked>;
+let environmentPreboot: ReturnType;
+const prebootDeps = coreMock.createInternalPreboot();
const setupDeps = coreMock.createInternalSetup();
+const startDeps = coreMock.createInternalStart();
const logger = loggingSystemMock.create();
expect.addSnapshotSerializer(createAbsolutePathSerializer());
['path-1', 'path-2', 'path-3', 'path-4', 'path-5', 'path-6', 'path-7', 'path-8'].forEach((path) => {
- jest.doMock(join(path, 'server'), () => ({}), {
- virtual: true,
+ [PluginType.preboot, PluginType.standard].forEach((type) => {
+ jest.doMock(join(`${path}-${type}`, 'server'), () => ({}), {
+ virtual: true,
+ });
});
});
@@ -53,6 +58,7 @@ const createPlugin = (
path = id,
disabled = false,
version = 'some-version',
+ type = PluginType.standard,
requiredPlugins = [],
requiredBundles = [],
optionalPlugins = [],
@@ -64,6 +70,7 @@ const createPlugin = (
path?: string;
disabled?: boolean;
version?: string;
+ type?: PluginType;
requiredPlugins?: string[];
requiredBundles?: string[];
optionalPlugins?: string[];
@@ -80,6 +87,7 @@ const createPlugin = (
version,
configPath: disabled ? configPath.concat('-disabled') : configPath,
kibanaVersion,
+ type,
requiredPlugins,
requiredBundles,
optionalPlugins,
@@ -111,11 +119,13 @@ async function testSetup() {
await configService.setSchema(config.path, config.schema);
pluginsService = new PluginsService({ coreId, env, logger, configService });
- [mockPluginSystem] = MockPluginsSystem.mock.instances as any;
- mockPluginSystem.uiPlugins.mockReturnValue(new Map());
- mockPluginSystem.getPlugins.mockReturnValue([]);
+ [prebootMockPluginSystem, standardMockPluginSystem] = MockPluginsSystem.mock.instances as any;
+ prebootMockPluginSystem.uiPlugins.mockReturnValue(new Map());
+ prebootMockPluginSystem.getPlugins.mockReturnValue([]);
+ standardMockPluginSystem.uiPlugins.mockReturnValue(new Map());
+ standardMockPluginSystem.getPlugins.mockReturnValue([]);
- environmentSetup = environmentServiceMock.createSetupContract();
+ environmentPreboot = environmentServiceMock.createPrebootContract();
}
afterEach(() => {
@@ -134,7 +144,7 @@ describe('PluginsService', () => {
plugin$: from([]),
});
- await expect(pluginsService.discover({ environment: environmentSetup })).rejects
+ await expect(pluginsService.discover({ environment: environmentPreboot })).rejects
.toMatchInlineSnapshot(`
[Error: Failed to initialize plugins:
Invalid JSON (invalid-manifest, path-1)]
@@ -156,7 +166,7 @@ describe('PluginsService', () => {
plugin$: from([]),
});
- await expect(pluginsService.discover({ environment: environmentSetup })).rejects
+ await expect(pluginsService.discover({ environment: environmentPreboot })).rejects
.toMatchInlineSnapshot(`
[Error: Failed to initialize plugins:
Incompatible version (incompatible-version, path-3)]
@@ -175,14 +185,14 @@ describe('PluginsService', () => {
error$: from([]),
plugin$: from([
createPlugin('conflicting-id', {
- path: 'path-4',
+ path: 'path-4-standard',
version: 'some-version',
configPath: 'path',
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
optionalPlugins: ['some-optional-plugin'],
}),
createPlugin('conflicting-id', {
- path: 'path-4',
+ path: 'path-4-standard',
version: 'some-version',
configPath: 'path',
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
@@ -192,13 +202,49 @@ describe('PluginsService', () => {
});
await expect(
- pluginsService.discover({ environment: environmentSetup })
+ pluginsService.discover({ environment: environmentPreboot })
).rejects.toMatchInlineSnapshot(
`[Error: Plugin with id "conflicting-id" is already registered!]`
);
- expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled();
- expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled();
+ expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ });
+
+ it('throws if discovered standard and preboot plugins with conflicting names', async () => {
+ mockDiscover.mockReturnValue({
+ error$: from([]),
+ plugin$: from([
+ createPlugin('conflicting-id', {
+ type: PluginType.preboot,
+ path: 'path-4-preboot',
+ version: 'some-version',
+ configPath: 'path',
+ requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
+ optionalPlugins: ['some-optional-plugin'],
+ }),
+ createPlugin('conflicting-id', {
+ path: 'path-4-standard',
+ version: 'some-version',
+ configPath: 'path',
+ requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
+ optionalPlugins: ['some-optional-plugin'],
+ }),
+ ]),
+ });
+
+ await expect(
+ pluginsService.discover({ environment: environmentPreboot })
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Plugin with id "conflicting-id" is already registered!]`
+ );
+
+ expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled();
+ expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
});
it('properly detects plugins that should be disabled.', async () => {
@@ -206,160 +252,356 @@ describe('PluginsService', () => {
.spyOn(configService, 'isEnabledAtPath')
.mockImplementation((path) => Promise.resolve(!path.includes('disabled')));
- mockPluginSystem.setupPlugins.mockResolvedValue(new Map());
+ prebootMockPluginSystem.setupPlugins.mockResolvedValue(new Map());
+ standardMockPluginSystem.setupPlugins.mockResolvedValue(new Map());
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
- createPlugin('explicitly-disabled-plugin', {
+ createPlugin('explicitly-disabled-plugin-preboot', {
+ type: PluginType.preboot,
+ disabled: true,
+ path: 'path-1-preboot',
+ configPath: 'path-1-preboot',
+ }),
+ createPlugin('explicitly-disabled-plugin-standard', {
disabled: true,
- path: 'path-1',
- configPath: 'path-1',
+ path: 'path-1-standard',
+ configPath: 'path-1-standard',
+ }),
+ createPlugin('plugin-with-missing-required-deps-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
+ configPath: 'path-2-preboot',
+ requiredPlugins: ['missing-plugin-preboot'],
}),
- createPlugin('plugin-with-missing-required-deps', {
- path: 'path-2',
- configPath: 'path-2',
- requiredPlugins: ['missing-plugin'],
+ createPlugin('plugin-with-missing-required-deps-standard', {
+ path: 'path-2-standard',
+ configPath: 'path-2-standard',
+ requiredPlugins: ['missing-plugin-standard'],
}),
- createPlugin('plugin-with-disabled-transitive-dep', {
- path: 'path-3',
- configPath: 'path-3',
- requiredPlugins: ['another-explicitly-disabled-plugin'],
+ createPlugin('plugin-with-disabled-transitive-dep-preboot', {
+ type: PluginType.preboot,
+ path: 'path-3-preboot',
+ configPath: 'path-3-preboot',
+ requiredPlugins: ['another-explicitly-disabled-plugin-preboot'],
}),
- createPlugin('another-explicitly-disabled-plugin', {
+ createPlugin('plugin-with-disabled-transitive-dep-standard', {
+ path: 'path-3-standard',
+ configPath: 'path-3-standard',
+ requiredPlugins: ['another-explicitly-disabled-plugin-standard'],
+ }),
+ createPlugin('another-explicitly-disabled-plugin-preboot', {
+ type: PluginType.preboot,
+ disabled: true,
+ path: 'path-4-preboot',
+ configPath: 'path-4-disabled-preboot',
+ }),
+ createPlugin('another-explicitly-disabled-plugin-standard', {
disabled: true,
- path: 'path-4',
- configPath: 'path-4-disabled',
+ path: 'path-4-standard',
+ configPath: 'path-4-disabled-standard',
+ }),
+ createPlugin('plugin-with-disabled-optional-dep-preboot', {
+ type: PluginType.preboot,
+ path: 'path-5-preboot',
+ configPath: 'path-5-preboot',
+ optionalPlugins: ['explicitly-disabled-plugin-preboot'],
}),
- createPlugin('plugin-with-disabled-optional-dep', {
- path: 'path-5',
- configPath: 'path-5',
- optionalPlugins: ['explicitly-disabled-plugin'],
+ createPlugin('plugin-with-disabled-optional-dep-standard', {
+ path: 'path-5-standard',
+ configPath: 'path-5-standard',
+ optionalPlugins: ['explicitly-disabled-plugin-standard'],
}),
- createPlugin('plugin-with-missing-optional-dep', {
- path: 'path-6',
- configPath: 'path-6',
- optionalPlugins: ['missing-plugin'],
+ createPlugin('plugin-with-missing-optional-dep-preboot', {
+ type: PluginType.preboot,
+ path: 'path-6-preboot',
+ configPath: 'path-6-preboot',
+ optionalPlugins: ['missing-plugin-preboot'],
}),
- createPlugin('plugin-with-disabled-nested-transitive-dep', {
- path: 'path-7',
- configPath: 'path-7',
- requiredPlugins: ['plugin-with-disabled-transitive-dep'],
+ createPlugin('plugin-with-missing-optional-dep-standard', {
+ path: 'path-6-standard',
+ configPath: 'path-6-standard',
+ optionalPlugins: ['missing-plugin-standard'],
}),
- createPlugin('plugin-with-missing-nested-dep', {
- path: 'path-8',
- configPath: 'path-8',
- requiredPlugins: ['plugin-with-missing-required-deps'],
+ createPlugin('plugin-with-disabled-nested-transitive-dep-preboot', {
+ type: PluginType.preboot,
+ path: 'path-7-preboot',
+ configPath: 'path-7-preboot',
+ requiredPlugins: ['plugin-with-disabled-transitive-dep-preboot'],
+ }),
+ createPlugin('plugin-with-disabled-nested-transitive-dep-standard', {
+ path: 'path-7-standard',
+ configPath: 'path-7-standard',
+ requiredPlugins: ['plugin-with-disabled-transitive-dep-standard'],
+ }),
+ createPlugin('plugin-with-missing-nested-dep-preboot', {
+ type: PluginType.preboot,
+ path: 'path-8-preboot',
+ configPath: 'path-8-preboot',
+ requiredPlugins: ['plugin-with-missing-required-deps-preboot'],
+ }),
+ createPlugin('plugin-with-missing-nested-dep-standard', {
+ path: 'path-8-standard',
+ configPath: 'path-8-standard',
+ requiredPlugins: ['plugin-with-missing-required-deps-standard'],
}),
]),
});
- await pluginsService.discover({ environment: environmentSetup });
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
const setup = await pluginsService.setup(setupDeps);
expect(setup.contracts).toBeInstanceOf(Map);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
- expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
- expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
+
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
+
+ expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
+
+ expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledWith(prebootDeps);
+ expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(`
Array [
Array [
- "Plugin \\"explicitly-disabled-plugin\\" is disabled.",
+ "Plugin \\"explicitly-disabled-plugin-preboot\\" is disabled.",
+ ],
+ Array [
+ "Plugin \\"explicitly-disabled-plugin-standard\\" is disabled.",
+ ],
+ Array [
+ "Plugin \\"plugin-with-missing-required-deps-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [missing-plugin-preboot]",
+ ],
+ Array [
+ "Plugin \\"plugin-with-missing-required-deps-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [missing-plugin-standard]",
+ ],
+ Array [
+ "Plugin \\"plugin-with-disabled-transitive-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [another-explicitly-disabled-plugin-preboot]",
+ ],
+ Array [
+ "Plugin \\"plugin-with-disabled-transitive-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [another-explicitly-disabled-plugin-standard]",
],
Array [
- "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [missing-plugin]",
+ "Plugin \\"another-explicitly-disabled-plugin-preboot\\" is disabled.",
],
Array [
- "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [another-explicitly-disabled-plugin]",
+ "Plugin \\"another-explicitly-disabled-plugin-standard\\" is disabled.",
],
Array [
- "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.",
+ "Plugin \\"plugin-with-disabled-nested-transitive-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-disabled-transitive-dep-preboot]",
],
Array [
- "Plugin \\"plugin-with-disabled-nested-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-disabled-transitive-dep]",
+ "Plugin \\"plugin-with-disabled-nested-transitive-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-disabled-transitive-dep-standard]",
],
Array [
- "Plugin \\"plugin-with-missing-nested-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-missing-required-deps]",
+ "Plugin \\"plugin-with-missing-nested-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-missing-required-deps-preboot]",
+ ],
+ Array [
+ "Plugin \\"plugin-with-missing-nested-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-missing-required-deps-standard]",
],
]
`);
});
it('does not throw in case of mutual plugin dependencies', async () => {
- const firstPlugin = createPlugin('first-plugin', {
- path: 'path-1',
- requiredPlugins: ['second-plugin'],
+ const prebootPlugins = [
+ createPlugin('first-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-1-preboot',
+ requiredPlugins: ['second-plugin-preboot'],
+ }),
+ createPlugin('second-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
+ requiredPlugins: ['first-plugin-preboot'],
+ }),
+ ];
+ const standardPlugins = [
+ createPlugin('first-plugin-standard', {
+ path: 'path-1-standard',
+ requiredPlugins: ['second-plugin-standard'],
+ }),
+ createPlugin('second-plugin-standard', {
+ path: 'path-2-standard',
+ requiredPlugins: ['first-plugin-standard'],
+ }),
+ ];
+
+ mockDiscover.mockReturnValue({
+ error$: from([]),
+ plugin$: from([...prebootPlugins, ...standardPlugins]),
});
- const secondPlugin = createPlugin('second-plugin', {
- path: 'path-2',
- requiredPlugins: ['first-plugin'],
+
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
});
+ expect(mockDiscover).toHaveBeenCalledTimes(1);
+
+ expect(preboot.pluginTree).toBeUndefined();
+ for (const plugin of prebootPlugins) {
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+ expect(standard.pluginTree).toBeUndefined();
+ for (const plugin of standardPlugins) {
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+ });
+
+ it('does not throw in case of mutual plugin dependencies between preboot and standard plugins', async () => {
mockDiscover.mockReturnValue({
error$: from([]),
- plugin$: from([firstPlugin, secondPlugin]),
+ plugin$: from([
+ createPlugin('first-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-1-preboot',
+ requiredPlugins: ['second-plugin-standard'],
+ }),
+ createPlugin('first-plugin-standard', {
+ path: 'path-1-standard',
+ requiredPlugins: ['second-plugin-preboot'],
+ }),
+ createPlugin('second-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
+ requiredPlugins: ['first-plugin-standard'],
+ }),
+ createPlugin('second-plugin-standard', {
+ path: 'path-2-standard',
+ requiredPlugins: ['first-plugin-preboot'],
+ }),
+ ]),
});
- const { pluginTree } = await pluginsService.discover({ environment: environmentSetup });
- expect(pluginTree).toBeUndefined();
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
+ });
+ expect(preboot.pluginTree).toBeUndefined();
+ expect(standard.pluginTree).toBeUndefined();
expect(mockDiscover).toHaveBeenCalledTimes(1);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin);
+ expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled();
});
it('does not throw in case of cyclic plugin dependencies', async () => {
- const firstPlugin = createPlugin('first-plugin', {
- path: 'path-1',
- requiredPlugins: ['second-plugin'],
- });
- const secondPlugin = createPlugin('second-plugin', {
- path: 'path-2',
- requiredPlugins: ['third-plugin', 'last-plugin'],
- });
- const thirdPlugin = createPlugin('third-plugin', {
- path: 'path-3',
- requiredPlugins: ['last-plugin', 'first-plugin'],
- });
- const lastPlugin = createPlugin('last-plugin', {
- path: 'path-4',
- requiredPlugins: ['first-plugin'],
- });
- const missingDepsPlugin = createPlugin('missing-deps-plugin', {
- path: 'path-5',
- requiredPlugins: ['not-a-plugin'],
- });
+ const prebootPlugins = [
+ createPlugin('first-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-1-preboot',
+ requiredPlugins: ['second-plugin-preboot'],
+ }),
+ createPlugin('second-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
+ requiredPlugins: ['third-plugin-preboot', 'last-plugin-preboot'],
+ }),
+ createPlugin('third-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-3-preboot',
+ requiredPlugins: ['last-plugin-preboot', 'first-plugin-preboot'],
+ }),
+ createPlugin('last-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-4-preboot',
+ requiredPlugins: ['first-plugin-preboot'],
+ }),
+ createPlugin('missing-deps-plugin-preboot', {
+ type: PluginType.preboot,
+ path: 'path-5-preboot',
+ requiredPlugins: ['not-a-plugin-preboot'],
+ }),
+ ];
+
+ const standardPlugins = [
+ createPlugin('first-plugin-standard', {
+ path: 'path-1-standard',
+ requiredPlugins: ['second-plugin-standard'],
+ }),
+ createPlugin('second-plugin-standard', {
+ path: 'path-2-standard',
+ requiredPlugins: ['third-plugin-standard', 'last-plugin-standard'],
+ }),
+ createPlugin('third-plugin-standard', {
+ path: 'path-3-standard',
+ requiredPlugins: ['last-plugin-standard', 'first-plugin-standard'],
+ }),
+ createPlugin('last-plugin-standard', {
+ path: 'path-4-standard',
+ requiredPlugins: ['first-plugin-standard'],
+ }),
+ createPlugin('missing-deps-plugin-standard', {
+ path: 'path-5-standard',
+ requiredPlugins: ['not-a-plugin-standard'],
+ }),
+ ];
mockDiscover.mockReturnValue({
error$: from([]),
- plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]),
+ plugin$: from([...prebootPlugins, ...standardPlugins]),
});
- const { pluginTree } = await pluginsService.discover({ environment: environmentSetup });
- expect(pluginTree).toBeUndefined();
-
+ const { standard, preboot } = await pluginsService.discover({
+ environment: environmentPreboot,
+ });
expect(mockDiscover).toHaveBeenCalledTimes(1);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin);
+
+ expect(preboot.pluginTree).toBeUndefined();
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(4);
+ for (const plugin of prebootPlugins) {
+ if (plugin.name.startsWith('missing-deps')) {
+ expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalledWith(plugin);
+ } else {
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+ }
+
+ expect(standard.pluginTree).toBeUndefined();
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(4);
+ for (const plugin of standardPlugins) {
+ if (plugin.name.startsWith('missing-deps')) {
+ expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalledWith(plugin);
+ } else {
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+ }
});
it('properly invokes plugin discovery and ignores non-critical errors.', async () => {
- const firstPlugin = createPlugin('some-id', {
- path: 'path-1',
- configPath: 'path',
- requiredPlugins: ['some-other-id'],
- optionalPlugins: ['missing-optional-dep'],
- });
- const secondPlugin = createPlugin('some-other-id', {
- path: 'path-2',
- version: 'some-other-version',
- configPath: ['plugin', 'path'],
- });
+ const prebootPlugins = [
+ createPlugin('some-id-preboot', {
+ type: PluginType.preboot,
+ path: 'path-1-preboot',
+ configPath: 'path-preboot',
+ requiredPlugins: ['some-other-id-preboot'],
+ optionalPlugins: ['missing-optional-dep'],
+ }),
+ createPlugin('some-other-id-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
+ version: 'some-other-version',
+ configPath: ['plugin-other-preboot', 'path'],
+ }),
+ ];
+
+ const standardPlugins = [
+ createPlugin('some-id-standard', {
+ type: PluginType.standard,
+ path: 'path-1-standard',
+ configPath: 'path-standard',
+ requiredPlugins: ['some-other-id-standard'],
+ optionalPlugins: ['missing-optional-dep'],
+ }),
+ createPlugin('some-other-id-standard', {
+ type: PluginType.standard,
+ path: 'path-2-standard',
+ version: 'some-other-version',
+ configPath: ['plugin-other-standard', 'path'],
+ }),
+ ];
mockDiscover.mockReturnValue({
error$: from([
@@ -367,13 +609,20 @@ describe('PluginsService', () => {
PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')),
PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')),
]),
- plugin$: from([firstPlugin, secondPlugin]),
+ plugin$: from([...prebootPlugins, ...standardPlugins]),
});
- await pluginsService.discover({ environment: environmentSetup });
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin);
- expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin);
+ await pluginsService.discover({ environment: environmentPreboot });
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
+ for (const plugin of prebootPlugins) {
+ expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
+ for (const plugin of standardPlugins) {
+ expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin);
+ }
+
expect(mockDiscover).toHaveBeenCalledTimes(1);
expect(mockDiscover).toHaveBeenCalledWith(
{
@@ -399,27 +648,33 @@ describe('PluginsService', () => {
const configSchema = schema.string();
jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve());
jest.doMock(
- join('path-with-schema', 'server'),
- () => ({
- config: {
- schema: configSchema,
- },
- }),
- {
- virtual: true,
- }
+ join('path-with-schema-preboot', 'server'),
+ () => ({ config: { schema: configSchema } }),
+ { virtual: true }
+ );
+ jest.doMock(
+ join('path-with-schema-standard', 'server'),
+ () => ({ config: { schema: configSchema } }),
+ { virtual: true }
);
+
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
- createPlugin('some-id', {
- path: 'path-with-schema',
- configPath: 'path',
+ createPlugin('some-id-preboot', {
+ type: PluginType.preboot,
+ path: 'path-with-schema-preboot',
+ configPath: 'path-preboot',
+ }),
+ createPlugin('some-id-standard', {
+ path: 'path-with-schema-standard',
+ configPath: 'path-standard',
}),
]),
});
- await pluginsService.discover({ environment: environmentSetup });
- expect(configService.setSchema).toBeCalledWith('path', configSchema);
+ await pluginsService.discover({ environment: environmentPreboot });
+ expect(configService.setSchema).toBeCalledWith('path-preboot', configSchema);
+ expect(configService.setSchema).toBeCalledWith('path-standard', configSchema);
});
it('registers plugin config deprecation provider in config service', async () => {
@@ -427,127 +682,183 @@ describe('PluginsService', () => {
jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve());
jest.spyOn(configService, 'addDeprecationProvider');
- const deprecationProvider = () => [];
+ const prebootDeprecationProvider = () => [];
jest.doMock(
- join('path-with-provider', 'server'),
- () => ({
- config: {
- schema: configSchema,
- deprecations: deprecationProvider,
- },
- }),
- {
- virtual: true,
- }
+ join('path-with-provider-preboot', 'server'),
+ () => ({ config: { schema: configSchema, deprecations: prebootDeprecationProvider } }),
+ { virtual: true }
+ );
+
+ const standardDeprecationProvider = () => [];
+ jest.doMock(
+ join('path-with-provider-standard', 'server'),
+ () => ({ config: { schema: configSchema, deprecations: standardDeprecationProvider } }),
+ { virtual: true }
);
+
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
- createPlugin('some-id', {
- path: 'path-with-provider',
- configPath: 'config-path',
+ createPlugin('some-id-preboot', {
+ type: PluginType.preboot,
+ path: 'path-with-provider-preboot',
+ configPath: 'config-path-preboot',
+ }),
+ createPlugin('some-id-standard', {
+ path: 'path-with-provider-standard',
+ configPath: 'config-path-standard',
}),
]),
});
- await pluginsService.discover({ environment: environmentSetup });
+ await pluginsService.discover({ environment: environmentPreboot });
+ expect(configService.addDeprecationProvider).toBeCalledWith(
+ 'config-path-preboot',
+ prebootDeprecationProvider
+ );
expect(configService.addDeprecationProvider).toBeCalledWith(
- 'config-path',
- deprecationProvider
+ 'config-path-standard',
+ standardDeprecationProvider
);
});
it('returns the paths of the plugins', async () => {
- const pluginA = createPlugin('A', { path: '/plugin-A-path', configPath: 'pathA' });
- const pluginB = createPlugin('B', { path: '/plugin-B-path', configPath: 'pathB' });
-
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([]),
});
- mockPluginSystem.getPlugins.mockReturnValue([pluginA, pluginB]);
-
- const { pluginPaths } = await pluginsService.discover({ environment: environmentSetup });
+ prebootMockPluginSystem.getPlugins.mockImplementation(() => [
+ createPlugin('A-preboot', {
+ type: PluginType.preboot,
+ path: '/plugin-A-path-preboot',
+ configPath: 'pathA-preboot',
+ }),
+ createPlugin('B-preboot', {
+ type: PluginType.preboot,
+ path: '/plugin-B-path-preboot',
+ configPath: 'pathB-preboot',
+ }),
+ ]);
- expect(pluginPaths).toEqual(['/plugin-A-path', '/plugin-B-path']);
- });
+ standardMockPluginSystem.getPlugins.mockImplementation(() => [
+ createPlugin('A-standard', {
+ path: '/plugin-A-path-standard',
+ configPath: 'pathA-standard',
+ }),
+ createPlugin('B-standard', {
+ path: '/plugin-B-path-standard',
+ configPath: 'pathB-standard',
+ }),
+ ]);
- it('ppopulates pluginConfigUsageDescriptors with plugins exposeToUsage property', async () => {
- const pluginA = createPlugin('plugin-with-expose-usage', {
- path: 'plugin-with-expose-usage',
- configPath: 'pathA',
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
});
- jest.doMock(
- join('plugin-with-expose-usage', 'server'),
- () => ({
- config: {
- exposeToUsage: {
- test: true,
- nested: {
- prop: true,
- },
+ expect(preboot.pluginPaths).toEqual(['/plugin-A-path-preboot', '/plugin-B-path-preboot']);
+ expect(standard.pluginPaths).toEqual(['/plugin-A-path-standard', '/plugin-B-path-standard']);
+ });
+
+ it('populates pluginConfigUsageDescriptors with plugins exposeToUsage property', async () => {
+ const pluginsWithExposeUsage = [
+ createPlugin('plugin-with-expose-usage-preboot', {
+ type: PluginType.preboot,
+ path: 'plugin-with-expose-usage-preboot',
+ configPath: 'pathA-preboot',
+ }),
+ createPlugin('plugin-with-expose-usage-standard', {
+ path: 'plugin-with-expose-usage-standard',
+ configPath: 'pathA-standard',
+ }),
+ ];
+ for (const plugin of pluginsWithExposeUsage) {
+ jest.doMock(
+ join(plugin.path, 'server'),
+ () => ({
+ config: {
+ exposeToUsage: { test: true, nested: { prop: true } },
+ schema: schema.maybe(schema.any()),
},
- schema: schema.maybe(schema.any()),
- },
+ }),
+ { virtual: true }
+ );
+ }
+
+ const pluginsWithArrayConfigPath = [
+ createPlugin('plugin-with-array-configPath-preboot', {
+ type: PluginType.preboot,
+ path: 'plugin-with-array-configPath-preboot',
+ version: 'some-other-version',
+ configPath: ['plugin-preboot', 'pathB'],
}),
- {
- virtual: true,
- }
- );
-
- const pluginB = createPlugin('plugin-with-array-configPath', {
- path: 'plugin-with-array-configPath',
- configPath: ['plugin', 'pathB'],
- });
-
- jest.doMock(
- join('plugin-with-array-configPath', 'server'),
- () => ({
- config: {
- exposeToUsage: {
- test: true,
+ createPlugin('plugin-with-array-configPath-standard', {
+ path: 'plugin-with-array-configPath-standard',
+ version: 'some-other-version',
+ configPath: ['plugin-standard', 'pathB'],
+ }),
+ ];
+ for (const plugin of pluginsWithArrayConfigPath) {
+ jest.doMock(
+ join(plugin.path, 'server'),
+ () => ({
+ config: {
+ exposeToUsage: { test: true },
+ schema: schema.maybe(schema.any()),
},
- schema: schema.maybe(schema.any()),
- },
+ }),
+ { virtual: true }
+ );
+ }
+
+ const pluginsWithoutExpose = [
+ createPlugin('plugin-without-expose-preboot', {
+ type: PluginType.preboot,
+ path: 'plugin-without-expose-preboot',
+ configPath: 'pathC-preboot',
}),
- {
- virtual: true,
- }
- );
-
- jest.doMock(
- join('plugin-without-expose', 'server'),
- () => ({
- config: {
- schema: schema.maybe(schema.any()),
- },
+ createPlugin('plugin-without-expose-standard', {
+ path: 'plugin-without-expose-standard',
+ configPath: 'pathC-standard',
}),
- {
- virtual: true,
- }
- );
-
- const pluginC = createPlugin('plugin-without-expose', {
- path: 'plugin-without-expose',
- configPath: 'pathC',
- });
+ ];
+ for (const plugin of pluginsWithoutExpose) {
+ jest.doMock(
+ join(plugin.path, 'server'),
+ () => ({
+ config: {
+ schema: schema.maybe(schema.any()),
+ },
+ }),
+ { virtual: true }
+ );
+ }
mockDiscover.mockReturnValue({
error$: from([]),
- plugin$: from([pluginA, pluginB, pluginC]),
+ plugin$: from([
+ ...pluginsWithExposeUsage,
+ ...pluginsWithArrayConfigPath,
+ ...pluginsWithoutExpose,
+ ]),
});
- await pluginsService.discover({ environment: environmentSetup });
+ await pluginsService.discover({ environment: environmentPreboot });
// eslint-disable-next-line dot-notation
expect(pluginsService['pluginConfigUsageDescriptors']).toMatchInlineSnapshot(`
Map {
- "pathA" => Object {
+ "pathA-preboot" => Object {
"nested.prop": true,
"test": true,
},
- "plugin.pathB" => Object {
+ "pathA-standard" => Object {
+ "nested.prop": true,
+ "test": true,
+ },
+ "plugin-preboot.pathB" => Object {
+ "test": true,
+ },
+ "plugin-standard.pathB" => Object {
"test": true,
},
}
@@ -560,6 +871,7 @@ describe('PluginsService', () => {
plugin.name,
{
id: plugin.name,
+ type: plugin.manifest.type,
configPath: plugin.manifest.configPath,
requiredPlugins: [],
requiredBundles: [],
@@ -568,140 +880,246 @@ describe('PluginsService', () => {
];
it('properly generates client configs for plugins according to `exposeToBrowser`', async () => {
- jest.doMock(
- join('plugin-with-expose', 'server'),
- () => ({
- config: {
- exposeToBrowser: {
- sharedProp: true,
- },
- schema: schema.object({
- serverProp: schema.string({ defaultValue: 'serverProp default value' }),
- sharedProp: schema.string({ defaultValue: 'sharedProp default value' }),
- }),
- },
- }),
- {
- virtual: true,
- }
- );
- const plugin = createPlugin('plugin-with-expose', {
- path: 'plugin-with-expose',
- configPath: 'path',
+ const prebootPlugin = createPlugin('plugin-with-expose-preboot', {
+ type: PluginType.preboot,
+ path: 'plugin-with-expose-preboot',
+ configPath: 'path-preboot',
});
+ const standardPlugin = createPlugin('plugin-with-expose-standard', {
+ path: 'plugin-with-expose-standard',
+ configPath: 'path-standard',
+ });
+ for (const plugin of [prebootPlugin, standardPlugin]) {
+ jest.doMock(
+ join(plugin.path, 'server'),
+ () => ({
+ config: {
+ exposeToBrowser: {
+ sharedProp: true,
+ },
+ schema: schema.object({
+ serverProp: schema.string({
+ defaultValue: `serverProp default value ${plugin.name}`,
+ }),
+ sharedProp: schema.string({
+ defaultValue: `sharedProp default value ${plugin.name}`,
+ }),
+ }),
+ },
+ }),
+ { virtual: true }
+ );
+ }
+
mockDiscover.mockReturnValue({
error$: from([]),
- plugin$: from([plugin]),
+ plugin$: from([prebootPlugin, standardPlugin]),
+ });
+ prebootMockPluginSystem.uiPlugins.mockReturnValue(
+ new Map([pluginToDiscoveredEntry(prebootPlugin)])
+ );
+ standardMockPluginSystem.uiPlugins.mockReturnValue(
+ new Map([pluginToDiscoveredEntry(standardPlugin)])
+ );
+
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
});
- mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)]));
- const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup });
- const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose');
- expect(uiConfig$).toBeDefined();
+ const prebootUIConfig$ = preboot.uiPlugins.browserConfigs.get('plugin-with-expose-preboot')!;
+ await expect(prebootUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({
+ sharedProp: 'sharedProp default value plugin-with-expose-preboot',
+ });
- const uiConfig = await uiConfig$!.pipe(take(1)).toPromise();
- expect(uiConfig).toMatchInlineSnapshot(`
- Object {
- "sharedProp": "sharedProp default value",
- }
- `);
+ const standardUIConfig$ = standard.uiPlugins.browserConfigs.get(
+ 'plugin-with-expose-standard'
+ )!;
+ await expect(standardUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({
+ sharedProp: 'sharedProp default value plugin-with-expose-standard',
+ });
});
it('does not generate config for plugins not exposing to client', async () => {
- jest.doMock(
- join('plugin-without-expose', 'server'),
- () => ({
- config: {
- schema: schema.object({
- serverProp: schema.string({ defaultValue: 'serverProp default value' }),
- }),
- },
- }),
- {
- virtual: true,
- }
- );
- const plugin = createPlugin('plugin-without-expose', {
- path: 'plugin-without-expose',
- configPath: 'path',
+ const prebootPlugin = createPlugin('plugin-without-expose-preboot', {
+ type: PluginType.preboot,
+ path: 'plugin-without-expose-preboot',
+ configPath: 'path-preboot',
+ });
+ const standardPlugin = createPlugin('plugin-without-expose-standard', {
+ path: 'plugin-without-expose-standard',
+ configPath: 'path-standard',
});
+ for (const plugin of [prebootPlugin, standardPlugin]) {
+ jest.doMock(
+ join(plugin.path, 'server'),
+ () => ({
+ config: {
+ schema: schema.object({
+ serverProp: schema.string({ defaultValue: 'serverProp default value' }),
+ }),
+ },
+ }),
+ { virtual: true }
+ );
+ }
+
mockDiscover.mockReturnValue({
error$: from([]),
- plugin$: from([plugin]),
+ plugin$: from([prebootPlugin, standardPlugin]),
});
- mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)]));
+ prebootMockPluginSystem.uiPlugins.mockReturnValue(
+ new Map([pluginToDiscoveredEntry(prebootPlugin)])
+ );
+ standardMockPluginSystem.uiPlugins.mockReturnValue(
+ new Map([pluginToDiscoveredEntry(standardPlugin)])
+ );
- const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup });
- expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0);
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
+ });
+ expect(preboot.uiPlugins.browserConfigs.size).toBe(0);
+ expect(standard.uiPlugins.browserConfigs.size).toBe(0);
});
});
- describe('#setup()', () => {
+ describe('plugin initialization', () => {
beforeEach(() => {
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
- createPlugin('plugin-1', {
- path: 'path-1',
+ createPlugin('plugin-1-preboot', {
+ type: PluginType.preboot,
+ path: 'path-1-preboot',
version: 'version-1',
- configPath: 'plugin1',
+ configPath: 'plugin1_preboot',
}),
- createPlugin('plugin-2', {
- path: 'path-2',
+ createPlugin('plugin-1-standard', {
+ path: 'path-1-standard',
+ version: 'version-1',
+ configPath: 'plugin1_standard',
+ }),
+ createPlugin('plugin-2-preboot', {
+ type: PluginType.preboot,
+ path: 'path-2-preboot',
version: 'version-2',
- configPath: 'plugin2',
+ configPath: 'plugin2_preboot',
+ }),
+ createPlugin('plugin-2-standard', {
+ path: 'path-2-standard',
+ version: 'version-2',
+ configPath: 'plugin2_standard',
}),
]),
});
- mockPluginSystem.uiPlugins.mockReturnValue(new Map());
+ prebootMockPluginSystem.uiPlugins.mockReturnValue(new Map());
+ standardMockPluginSystem.uiPlugins.mockReturnValue(new Map());
});
- describe('uiPlugins.internal', () => {
- it('contains internal properties for plugins', async () => {
- config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } });
- const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup });
- expect(uiPlugins.internal).toMatchInlineSnapshot(`
- Map {
- "plugin-1" => Object {
- "publicAssetsDir": /path-1/public/assets,
- "publicTargetDir": /path-1/target/public,
- "requiredBundles": Array [],
- "version": "version-1",
- },
- "plugin-2" => Object {
- "publicAssetsDir": /path-2/public/assets,
- "publicTargetDir": /path-2/target/public,
- "requiredBundles": Array [],
- "version": "version-2",
- },
- }
- `);
+ it('`uiPlugins.internal` contains internal properties for plugins', async () => {
+ config$.next({
+ plugins: { initialize: true },
+ plugin1_preboot: { enabled: false },
+ plugin1_standard: { enabled: false },
});
-
- it('includes disabled plugins', async () => {
- config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } });
- const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup });
- expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']);
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
});
+ expect(preboot.uiPlugins.internal).toMatchInlineSnapshot(`
+ Map {
+ "plugin-1-preboot" => Object {
+ "publicAssetsDir": /path-1-preboot/public/assets,
+ "publicTargetDir": /path-1-preboot/target/public,
+ "requiredBundles": Array [],
+ "version": "version-1",
+ },
+ "plugin-2-preboot" => Object {
+ "publicAssetsDir": /path-2-preboot/public/assets,
+ "publicTargetDir": /path-2-preboot/target/public,
+ "requiredBundles": Array [],
+ "version": "version-2",
+ },
+ }
+ `);
+ expect(standard.uiPlugins.internal).toMatchInlineSnapshot(`
+ Map {
+ "plugin-1-standard" => Object {
+ "publicAssetsDir": /path-1-standard/public/assets,
+ "publicTargetDir": /path-1-standard/target/public,
+ "requiredBundles": Array [],
+ "version": "version-1",
+ },
+ "plugin-2-standard" => Object {
+ "publicAssetsDir": /path-2-standard/public/assets,
+ "publicTargetDir": /path-2-standard/target/public,
+ "requiredBundles": Array [],
+ "version": "version-2",
+ },
+ }
+ `);
});
- describe('plugin initialization', () => {
- it('does initialize if plugins.initialize is true', async () => {
- config$.next({ plugins: { initialize: true } });
- await pluginsService.discover({ environment: environmentSetup });
- const { initialized } = await pluginsService.setup(setupDeps);
- expect(mockPluginSystem.setupPlugins).toHaveBeenCalled();
- expect(initialized).toBe(true);
+ it('`uiPlugins.internal` includes disabled plugins', async () => {
+ config$.next({
+ plugins: { initialize: true },
+ plugin1_preboot: { enabled: false },
+ plugin1_standard: { enabled: false },
});
-
- it('does not initialize if plugins.initialize is false', async () => {
- config$.next({ plugins: { initialize: false } });
- await pluginsService.discover({ environment: environmentSetup });
- const { initialized } = await pluginsService.setup(setupDeps);
- expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled();
- expect(initialized).toBe(false);
+ const { preboot, standard } = await pluginsService.discover({
+ environment: environmentPreboot,
});
+ expect([...preboot.uiPlugins.internal.keys()].sort()).toMatchInlineSnapshot(`
+ Array [
+ "plugin-1-preboot",
+ "plugin-2-preboot",
+ ]
+ `);
+ expect([...standard.uiPlugins.internal.keys()].sort()).toMatchInlineSnapshot(`
+ Array [
+ "plugin-1-standard",
+ "plugin-2-standard",
+ ]
+ `);
+ });
+
+ it('#preboot does initialize `preboot` plugins if plugins.initialize is true', async () => {
+ config$.next({ plugins: { initialize: true } });
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+
+ expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
+ expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledWith(prebootDeps);
+ expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ });
+
+ it('#preboot does not initialize `preboot` plugins if plugins.initialize is false', async () => {
+ config$.next({ plugins: { initialize: false } });
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+
+ expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ });
+
+ it('#setup does initialize `standard` plugins if plugins.initialize is true', async () => {
+ config$.next({ plugins: { initialize: true } });
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+
+ const { initialized } = await pluginsService.setup(setupDeps);
+ expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
+ expect(initialized).toBe(true);
+ });
+
+ it('#setup does not initialize `standard` plugins if plugins.initialize is false', async () => {
+ config$.next({ plugins: { initialize: false } });
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+ const { initialized } = await pluginsService.setup(setupDeps);
+ expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled();
+ expect(initialized).toBe(false);
});
});
@@ -719,10 +1137,74 @@ describe('PluginsService', () => {
});
});
+ describe('#start()', () => {
+ beforeEach(() => {
+ mockDiscover.mockReturnValue({
+ error$: from([]),
+ plugin$: from([
+ createPlugin('plugin-1-preboot', { type: PluginType.preboot, path: 'path-1-preboot' }),
+ createPlugin('plugin-1-standard', { path: 'path-1-standard' }),
+ ]),
+ });
+ });
+
+ it('does not try to stop `preboot` plugins and start `standard` ones if plugins.initialize is `false`', async () => {
+ config$.next({ plugins: { initialize: false } });
+
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+ await pluginsService.setup(setupDeps);
+
+ const { contracts } = await pluginsService.start(startDeps);
+ expect(contracts).toBeInstanceOf(Map);
+ expect(contracts.size).toBe(0);
+
+ expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.startPlugins).not.toHaveBeenCalled();
+ });
+
+ it('stops `preboot` plugins and starts `standard` ones', async () => {
+ await pluginsService.discover({ environment: environmentPreboot });
+ await pluginsService.preboot(prebootDeps);
+ await pluginsService.setup(setupDeps);
+
+ expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.startPlugins).not.toHaveBeenCalled();
+
+ await pluginsService.start(startDeps);
+
+ expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+
+ expect(standardMockPluginSystem.startPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.startPlugins).toHaveBeenCalledWith(startDeps);
+ expect(prebootMockPluginSystem.startPlugins).not.toHaveBeenCalled();
+ });
+ });
+
describe('#stop()', () => {
it('`stop` stops plugins system', async () => {
await pluginsService.stop();
- expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ });
+
+ it('`stop` does not try to stop preboot plugins system if it was stopped during `start`.', async () => {
+ await pluginsService.preboot(prebootDeps);
+ await pluginsService.setup(setupDeps);
+
+ expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+ expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+
+ await pluginsService.start(startDeps);
+
+ expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled();
+
+ await pluginsService.stop();
+
+ expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ expect(standardMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 99a9aaaddcb0b..05bb60fb22c6d 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -8,20 +8,36 @@
import Path from 'path';
import { Observable } from 'rxjs';
-import { filter, first, map, concatMap, tap, toArray } from 'rxjs/operators';
-import { pick, getFlattenedObject } from '@kbn/std';
+import { concatMap, filter, first, map, tap, toArray } from 'rxjs/operators';
+import { getFlattenedObject, pick } from '@kbn/std';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery';
import { PluginWrapper } from './plugin';
-import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types';
+import {
+ DiscoveredPlugin,
+ InternalPluginInfo,
+ PluginConfigDescriptor,
+ PluginDependencies,
+ PluginName,
+ PluginType,
+} from './types';
import { PluginsConfig, PluginsConfigType } from './plugins_config';
import { PluginsSystem } from './plugins_system';
-import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
+import { InternalCorePreboot, InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { IConfigService } from '../config';
-import { InternalEnvironmentServiceSetup } from '../environment';
+import { InternalEnvironmentServicePreboot } from '../environment';
+
+/** @internal */
+export type DiscoveredPlugins = {
+ [key in PluginType]: {
+ pluginTree: PluginDependencies;
+ pluginPaths: string[];
+ uiPlugins: UiPlugins;
+ };
+};
/** @internal */
export interface PluginsServiceSetup {
@@ -56,6 +72,9 @@ export interface PluginsServiceStart {
contracts: Map;
}
+/** @internal */
+export type PluginsServicePrebootSetupDeps = InternalCorePreboot;
+
/** @internal */
export type PluginsServiceSetupDeps = InternalCoreSetup;
@@ -64,29 +83,31 @@ export type PluginsServiceStartDeps = InternalCoreStart;
/** @internal */
export interface PluginsServiceDiscoverDeps {
- environment: InternalEnvironmentServiceSetup;
+ environment: InternalEnvironmentServicePreboot;
}
/** @internal */
export class PluginsService implements CoreService {
private readonly log: Logger;
- private readonly pluginsSystem: PluginsSystem;
+ private readonly prebootPluginsSystem = new PluginsSystem(this.coreContext, PluginType.preboot);
+ private arePrebootPluginsStopped = false;
+ private readonly prebootUiPluginInternalInfo = new Map();
+ private readonly standardPluginsSystem = new PluginsSystem(this.coreContext, PluginType.standard);
+ private readonly standardUiPluginInternalInfo = new Map();
private readonly configService: IConfigService;
private readonly config$: Observable;
private readonly pluginConfigDescriptors = new Map();
- private readonly uiPluginInternalInfo = new Map();
private readonly pluginConfigUsageDescriptors = new Map>();
constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('plugins-service');
- this.pluginsSystem = new PluginsSystem(coreContext);
this.configService = coreContext.configService;
this.config$ = coreContext.configService
.atPath('plugins')
.pipe(map((rawConfig) => new PluginsConfig(rawConfig, coreContext.env)));
}
- public async discover({ environment }: PluginsServiceDiscoverDeps) {
+ public async discover({ environment }: PluginsServiceDiscoverDeps): Promise {
const config = await this.config$.pipe(first()).toPromise();
const { error$, plugin$ } = discover(config, this.coreContext, {
@@ -96,16 +117,26 @@ export class PluginsService implements CoreService plugin.path),
- uiPlugins: {
- internal: this.uiPluginInternalInfo,
- public: uiPlugins,
- browserConfigs: this.generateUiPluginsConfigs(uiPlugins),
+ preboot: {
+ pluginPaths: this.prebootPluginsSystem.getPlugins().map((plugin) => plugin.path),
+ pluginTree: this.prebootPluginsSystem.getPluginDependencies(),
+ uiPlugins: {
+ internal: this.prebootUiPluginInternalInfo,
+ public: prebootUiPlugins,
+ browserConfigs: this.generateUiPluginsConfigs(prebootUiPlugins),
+ },
+ },
+ standard: {
+ pluginPaths: this.standardPluginsSystem.getPlugins().map((plugin) => plugin.path),
+ pluginTree: this.standardPluginsSystem.getPluginDependencies(),
+ uiPlugins: {
+ internal: this.standardUiPluginInternalInfo,
+ public: standardUiPlugins,
+ browserConfigs: this.generateUiPluginsConfigs(standardUiPlugins),
+ },
},
};
}
@@ -114,6 +145,20 @@ export class PluginsService implements CoreService();
if (config.initialize) {
- contracts = await this.pluginsSystem.setupPlugins(deps);
- this.registerPluginStaticDirs(deps);
+ contracts = await this.standardPluginsSystem.setupPlugins(deps);
+ this.registerPluginStaticDirs(deps, this.standardUiPluginInternalInfo);
} else {
- this.log.info('Plugin initialization disabled.');
+ this.log.info(
+ 'Skipping `setup` for `standard` plugins since plugin initialization is disabled.'
+ );
}
return {
@@ -135,13 +182,31 @@ export class PluginsService implements CoreService,
parents: PluginName[] = []
- ): { enabled: true } | { enabled: false; missingDependencies: string[] } {
+ ): { enabled: true } | { enabled: false; missingOrIncompatibleDependencies: string[] } {
const pluginInfo = pluginEnableStatuses.get(pluginName);
if (pluginInfo === undefined || !pluginInfo.isEnabled) {
return {
enabled: false,
- missingDependencies: [],
+ missingOrIncompatibleDependencies: [],
};
}
- const missingDependencies = pluginInfo.plugin.requiredPlugins
+ const missingOrIncompatibleDependencies = pluginInfo.plugin.requiredPlugins
.filter((dep) => !parents.includes(dep))
.filter(
(dependencyName) =>
+ pluginEnableStatuses.get(dependencyName)?.plugin.manifest.type !==
+ pluginInfo.plugin.manifest.type ||
!this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName])
.enabled
);
- if (missingDependencies.length === 0) {
+ if (missingOrIncompatibleDependencies.length === 0) {
return {
enabled: true,
};
@@ -308,12 +390,15 @@ export class PluginsService implements CoreService
+ ) {
+ for (const [pluginName, pluginInfo] of uiPluginInternalInfo) {
deps.http.registerStaticDir(
`/plugins/${pluginName}/assets/{path*}`,
pluginInfo.publicAssetsDir
diff --git a/src/core/server/plugins/plugins_system.test.mocks.ts b/src/core/server/plugins/plugins_system.test.mocks.ts
index 26175ab2b68a9..1e1264ba76a80 100644
--- a/src/core/server/plugins/plugins_system.test.mocks.ts
+++ b/src/core/server/plugins/plugins_system.test.mocks.ts
@@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
+export const mockCreatePluginPrebootSetupContext = jest.fn();
export const mockCreatePluginSetupContext = jest.fn();
export const mockCreatePluginStartContext = jest.fn();
jest.mock('./plugin_context', () => ({
+ createPluginPrebootSetupContext: mockCreatePluginPrebootSetupContext,
createPluginSetupContext: mockCreatePluginSetupContext,
createPluginStartContext: mockCreatePluginStartContext,
}));
diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts
index abcd00f4e2daf..e61c9c2002a12 100644
--- a/src/core/server/plugins/plugins_system.test.ts
+++ b/src/core/server/plugins/plugins_system.test.ts
@@ -7,6 +7,7 @@
*/
import {
+ mockCreatePluginPrebootSetupContext,
mockCreatePluginSetupContext,
mockCreatePluginStartContext,
} from './plugins_system.test.mocks';
@@ -20,7 +21,7 @@ import { CoreContext } from '../core_context';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginWrapper } from './plugin';
-import { PluginName } from './types';
+import { PluginName, PluginType } from './types';
import { PluginsSystem } from './plugins_system';
import { coreMock } from '../mocks';
import { Logger } from '../logging';
@@ -32,7 +33,14 @@ function createPlugin(
optional = [],
server = true,
ui = true,
- }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {}
+ type = PluginType.standard,
+ }: {
+ required?: string[];
+ optional?: string[];
+ server?: boolean;
+ ui?: boolean;
+ type?: PluginType;
+ } = {}
): PluginWrapper {
return new PluginWrapper({
path: 'some-path',
@@ -41,6 +49,7 @@ function createPlugin(
version: 'some-version',
configPath: 'path',
kibanaVersion: '7.0.0',
+ type,
requiredPlugins: required,
optionalPlugins: optional,
requiredBundles: [],
@@ -52,10 +61,11 @@ function createPlugin(
});
}
+const prebootDeps = coreMock.createInternalPreboot();
const setupDeps = coreMock.createInternalSetup();
const startDeps = coreMock.createInternalStart();
-let pluginsSystem: PluginsSystem;
+let pluginsSystem: PluginsSystem;
let configService: ReturnType;
let logger: ReturnType;
let env: Env;
@@ -70,7 +80,7 @@ beforeEach(() => {
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
- pluginsSystem = new PluginsSystem(coreContext);
+ pluginsSystem = new PluginsSystem(coreContext, PluginType.standard);
});
test('can be setup even without plugins', async () => {
@@ -80,6 +90,26 @@ test('can be setup even without plugins', async () => {
expect(pluginsSetup.size).toBe(0);
});
+test('throws if adding plugin with incompatible type', () => {
+ const prebootPlugin = createPlugin('plugin-preboot', { type: PluginType.preboot });
+ const standardPlugin = createPlugin('plugin-standard');
+
+ const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot);
+ const standardPluginSystem = new PluginsSystem(coreContext, PluginType.standard);
+
+ prebootPluginSystem.addPlugin(prebootPlugin);
+ expect(() => prebootPluginSystem.addPlugin(standardPlugin)).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot add plugin with type \\"standard\\" to plugin system with type \\"preboot\\"."`
+ );
+ expect(prebootPluginSystem.getPlugins()).toEqual([prebootPlugin]);
+
+ standardPluginSystem.addPlugin(standardPlugin);
+ expect(() => standardPluginSystem.addPlugin(prebootPlugin)).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot add plugin with type \\"preboot\\" to plugin system with type \\"standard\\"."`
+ );
+ expect(standardPluginSystem.getPlugins()).toEqual([standardPlugin]);
+});
+
test('getPlugins returns the list of plugins', () => {
const pluginA = createPlugin('plugin-a');
const pluginB = createPlugin('plugin-b');
@@ -293,6 +323,83 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
}
});
+test('correctly orders preboot plugins and returns exposed values for "setup"', async () => {
+ const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot);
+ const plugins = new Map([
+ [
+ createPlugin('order-4', { type: PluginType.preboot, required: ['order-2'] }),
+ { 'order-2': 'added-as-2' },
+ ],
+ [createPlugin('order-0', { type: PluginType.preboot }), {}],
+ [
+ createPlugin('order-2', {
+ type: PluginType.preboot,
+ required: ['order-1'],
+ optional: ['order-0'],
+ }),
+ { 'order-1': 'added-as-3', 'order-0': 'added-as-1' },
+ ],
+ [
+ createPlugin('order-1', { type: PluginType.preboot, required: ['order-0'] }),
+ { 'order-0': 'added-as-1' },
+ ],
+ [
+ createPlugin('order-3', {
+ type: PluginType.preboot,
+ required: ['order-2'],
+ optional: ['missing-dep'],
+ }),
+ { 'order-2': 'added-as-2' },
+ ],
+ ] as Array<[PluginWrapper, Record]>);
+
+ const setupContextMap = new Map();
+ [...plugins.keys()].forEach((plugin, index) => {
+ jest.spyOn(plugin, 'setup').mockResolvedValue(`added-as-${index}`);
+ setupContextMap.set(plugin.name, `setup-for-${plugin.name}`);
+ prebootPluginSystem.addPlugin(plugin);
+ });
+
+ mockCreatePluginPrebootSetupContext.mockImplementation((context, deps, plugin) =>
+ setupContextMap.get(plugin.name)
+ );
+
+ expect([...(await prebootPluginSystem.setupPlugins(prebootDeps))]).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "order-0",
+ "added-as-1",
+ ],
+ Array [
+ "order-1",
+ "added-as-3",
+ ],
+ Array [
+ "order-2",
+ "added-as-2",
+ ],
+ Array [
+ "order-3",
+ "added-as-4",
+ ],
+ Array [
+ "order-4",
+ "added-as-0",
+ ],
+ ]
+ `);
+
+ for (const [plugin, deps] of plugins) {
+ expect(mockCreatePluginPrebootSetupContext).toHaveBeenCalledWith(
+ coreContext,
+ prebootDeps,
+ plugin
+ );
+ expect(plugin.setup).toHaveBeenCalledTimes(1);
+ expect(plugin.setup).toHaveBeenCalledWith(setupContextMap.get(plugin.name), deps);
+ }
+});
+
test('`setupPlugins` only setups plugins that have server side', async () => {
const firstPluginToRun = createPlugin('order-0');
const secondPluginNotToRun = createPlugin('order-not-run', { server: false });
@@ -399,6 +506,21 @@ test('can start without plugins', async () => {
expect(pluginsStart.size).toBe(0);
});
+test('cannot start preboot plugins', async () => {
+ const prebootPlugin = createPlugin('order-0', { type: PluginType.preboot });
+ jest.spyOn(prebootPlugin, 'setup').mockResolvedValue({});
+ jest.spyOn(prebootPlugin, 'start').mockResolvedValue({});
+
+ const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot);
+ prebootPluginSystem.addPlugin(prebootPlugin);
+ await prebootPluginSystem.setupPlugins(prebootDeps);
+
+ await expect(
+ prebootPluginSystem.startPlugins(startDeps)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"Preboot plugins cannot be started."`);
+ expect(prebootPlugin.start).not.toHaveBeenCalled();
+});
+
test('`startPlugins` only starts plugins that were setup', async () => {
const firstPluginToRun = createPlugin('order-0');
const secondPluginNotToRun = createPlugin('order-not-run', { server: false });
@@ -525,7 +647,7 @@ describe('asynchronous plugins', () => {
})
);
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
- pluginsSystem = new PluginsSystem(coreContext);
+ pluginsSystem = new PluginsSystem(coreContext, PluginType.standard);
const syncPlugin = createPlugin('sync-plugin');
jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync');
diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts
index f6327d4eabf43..4a156c5fbb976 100644
--- a/src/core/server/plugins/plugins_system.ts
+++ b/src/core/server/plugins/plugins_system.ts
@@ -10,25 +10,38 @@ import { withTimeout, isPromise } from '@kbn/std';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { PluginWrapper } from './plugin';
-import { DiscoveredPlugin, PluginName } from './types';
-import { createPluginSetupContext, createPluginStartContext } from './plugin_context';
-import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
-import { PluginDependencies } from '.';
+import { DiscoveredPlugin, PluginDependencies, PluginName, PluginType } from './types';
+import {
+ createPluginPrebootSetupContext,
+ createPluginSetupContext,
+ createPluginStartContext,
+} from './plugin_context';
+import {
+ PluginsServicePrebootSetupDeps,
+ PluginsServiceSetupDeps,
+ PluginsServiceStartDeps,
+} from './plugins_service';
const Sec = 1000;
/** @internal */
-export class PluginsSystem {
+export class PluginsSystem {
private readonly plugins = new Map();
private readonly log: Logger;
// `satup`, the past-tense version of the noun `setup`.
private readonly satupPlugins: PluginName[] = [];
- constructor(private readonly coreContext: CoreContext) {
- this.log = coreContext.logger.get('plugins-system');
+ constructor(private readonly coreContext: CoreContext, public readonly type: T) {
+ this.log = coreContext.logger.get('plugins-system', this.type);
}
public addPlugin(plugin: PluginWrapper) {
+ if (plugin.manifest.type !== this.type) {
+ throw new Error(
+ `Cannot add plugin with type "${plugin.manifest.type}" to plugin system with type "${this.type}".`
+ );
+ }
+
this.plugins.set(plugin.name, plugin);
}
@@ -67,7 +80,9 @@ export class PluginsSystem {
return { asNames, asOpaqueIds };
}
- public async setupPlugins(deps: PluginsServiceSetupDeps) {
+ public async setupPlugins(
+ deps: T extends PluginType.preboot ? PluginsServicePrebootSetupDeps : PluginsServiceSetupDeps
+ ): Promise