diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md
index 4ab0cb74f809f..3b5754eb4fa39 100644
--- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md
+++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md
@@ -4,14 +4,17 @@
## PluginInitializerContext.config property
+Accessors for the plugin's configuration
+
Signature:
```typescript
config: {
legacy: {
globalConfig$: Observable;
+ get: () => SharedGlobalConfig;
};
create: () => Observable;
- createIfExists: () => Observable;
+ get: () => T;
};
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md
index 106fdaad9bc22..e5de046eccf1d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md
+++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md
@@ -4,8 +4,29 @@
## PluginInitializerContext.logger property
+ instance already bound to the plugin's logging context
+
Signature:
```typescript
logger: LoggerFactory;
```
+
+## Example
+
+
+```typescript
+// plugins/my-plugin/server/plugin.ts
+// "id: myPlugin" in `plugins/my-plugin/kibana.yaml`
+
+export class MyPlugin implements Plugin {
+ constructor(private readonly initContext: PluginInitializerContext) {
+ this.logger = initContext.logger.get();
+ // `logger` context: `plugins.myPlugin`
+ this.mySubLogger = initContext.logger.get('sub'); // or this.logger.get('sub');
+ // `mySubLogger` context: `plugins.myPlugin.sub`
+ }
+}
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md
index 18760170afa1f..90a19d53bd5e1 100644
--- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md
+++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md
@@ -16,8 +16,8 @@ export interface PluginInitializerContext
| Property | Type | Description |
| --- | --- | --- |
-| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
}
| |
+| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
get: <T = ConfigSchema>() => T;
}
| Accessors for the plugin's configuration |
| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
}
| |
-| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory
| |
+| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory
| instance already bound to the plugin's logging context |
| [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId
| |
diff --git a/packages/kbn-config/src/config_service.mock.ts b/packages/kbn-config/src/config_service.mock.ts
index cd6f399ddcce2..59f788767004c 100644
--- a/packages/kbn-config/src/config_service.mock.ts
+++ b/packages/kbn-config/src/config_service.mock.ts
@@ -17,8 +17,8 @@ const createConfigServiceMock = ({
}: { atPath?: Record; getConfig$?: Record } = {}) => {
const mocked: jest.Mocked = {
atPath: jest.fn(),
+ atPathSync: jest.fn(),
getConfig$: jest.fn(),
- optionalAtPath: jest.fn(),
getUsedPaths: jest.fn(),
getUnusedPaths: jest.fn(),
isEnabledAtPath: jest.fn(),
@@ -27,6 +27,7 @@ const createConfigServiceMock = ({
validate: jest.fn(),
};
mocked.atPath.mockReturnValue(new BehaviorSubject(atPath));
+ mocked.atPathSync.mockReturnValue(atPath);
mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$)));
mocked.getUsedPaths.mockResolvedValue([]);
mocked.getUnusedPaths.mockResolvedValue([]);
diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts
index 96d1f794a691c..e55916d7d348c 100644
--- a/packages/kbn-config/src/config_service.test.ts
+++ b/packages/kbn-config/src/config_service.test.ts
@@ -105,27 +105,6 @@ test('re-validate config when updated', async () => {
`);
});
-test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
- const rawConfig = getRawConfigProvider({});
- const configService = new ConfigService(rawConfig, defaultEnv, logger);
-
- const value$ = configService.optionalAtPath('unique-name');
- const value = await value$.pipe(first()).toPromise();
-
- expect(value).toBeUndefined();
-});
-
-test('returns observable config at optional path if it exists', async () => {
- const rawConfig = getRawConfigProvider({ value: 'bar' });
- const configService = new ConfigService(rawConfig, defaultEnv, logger);
- await configService.setSchema('value', schema.string());
-
- const value$ = configService.optionalAtPath('value');
- const value: any = await value$.pipe(first()).toPromise();
-
- expect(value).toBe('bar');
-});
-
test("does not push new configs when reloading if config at path hasn't changed", async () => {
const rawConfig$ = new BehaviorSubject>({ key: 'value' });
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ });
@@ -209,34 +188,38 @@ test('flags schema paths as handled when registering a schema', async () => {
test('tracks unhandled paths', async () => {
const initialConfig = {
- bar: {
- deep1: {
- key: '123',
- },
- deep2: {
- key: '321',
- },
+ service: {
+ string: 'str',
+ number: 42,
},
- foo: 'value',
- quux: {
- deep1: {
- key: 'hello',
- },
- deep2: {
- key: 'world',
- },
+ plugin: {
+ foo: 'bar',
+ },
+ unknown: {
+ hello: 'dolly',
+ number: 9000,
},
};
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
-
- configService.atPath('foo');
- configService.atPath(['bar', 'deep2']);
+ await configService.setSchema(
+ 'service',
+ schema.object({
+ string: schema.string(),
+ number: schema.number(),
+ })
+ );
+ await configService.setSchema(
+ 'plugin',
+ schema.object({
+ foo: schema.string(),
+ })
+ );
const unused = await configService.getUnusedPaths();
- expect(unused).toEqual(['bar.deep1.key', 'quux.deep1.key', 'quux.deep2.key']);
+ expect(unused).toEqual(['unknown.hello', 'unknown.number']);
});
test('correctly passes context', async () => {
@@ -339,22 +322,18 @@ test('does not throw if schema does not define "enabled" schema', async () => {
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
- await expect(
+ expect(
configService.setSchema(
'pid',
schema.object({
file: schema.string(),
})
)
- ).resolves.toBeUndefined();
+ ).toBeUndefined();
const value$ = configService.atPath('pid');
const value: any = await value$.pipe(first()).toPromise();
expect(value.enabled).toBe(undefined);
-
- const valueOptional$ = configService.optionalAtPath('pid');
- const valueOptional: any = await valueOptional$.pipe(first()).toPromise();
- expect(valueOptional.enabled).toBe(undefined);
});
test('treats config as enabled if config path is not present in config', async () => {
@@ -457,3 +436,44 @@ test('logs deprecation warning during validation', async () => {
]
`);
});
+
+describe('atPathSync', () => {
+ test('returns the value at path', async () => {
+ const rawConfig = getRawConfigProvider({ key: 'foo' });
+ const configService = new ConfigService(rawConfig, defaultEnv, logger);
+ const stringSchema = schema.string();
+ await configService.setSchema('key', stringSchema);
+
+ await configService.validate();
+
+ const value = configService.atPathSync('key');
+ expect(value).toBe('foo');
+ });
+
+ test('throws if called before `validate`', async () => {
+ const rawConfig = getRawConfigProvider({ key: 'foo' });
+ const configService = new ConfigService(rawConfig, defaultEnv, logger);
+ const stringSchema = schema.string();
+ await configService.setSchema('key', stringSchema);
+
+ expect(() => configService.atPathSync('key')).toThrowErrorMatchingInlineSnapshot(
+ `"\`atPathSync\` called before config was validated"`
+ );
+ });
+
+ test('returns the last config value', async () => {
+ const rawConfig$ = new BehaviorSubject>({ key: 'value' });
+ const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ });
+
+ const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
+ await configService.setSchema('key', schema.string());
+
+ await configService.validate();
+
+ expect(configService.atPathSync('key')).toEqual('value');
+
+ rawConfig$.next({ key: 'new-value' });
+
+ expect(configService.atPathSync('key')).toEqual('new-value');
+ });
+});
diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts
index 9518279f35766..929735ffc15f2 100644
--- a/packages/kbn-config/src/config_service.ts
+++ b/packages/kbn-config/src/config_service.ts
@@ -10,7 +10,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
import { Type } from '@kbn/config-schema';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
-import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators';
+import { distinctUntilChanged, first, map, shareReplay, take, tap } from 'rxjs/operators';
import { Logger, LoggerFactory } from '@kbn/logging';
import { Config, ConfigPath, Env } from '.';
@@ -32,13 +32,15 @@ export class ConfigService {
private readonly log: Logger;
private readonly deprecationLog: Logger;
+ private validated = false;
private readonly config$: Observable;
+ private lastConfig?: Config;
/**
* Whenever a config if read at a path, we mark that path as 'handled'. We can
* then list all unhandled config paths when the startup process is completed.
*/
- private readonly handledPaths: ConfigPath[] = [];
+ private readonly handledPaths: Set = new Set();
private readonly schemas = new Map>();
private readonly deprecations = new BehaviorSubject([]);
@@ -55,6 +57,9 @@ export class ConfigService {
const migrated = applyDeprecations(rawConfig, deprecations);
return new LegacyObjectToConfigAdapter(migrated);
}),
+ tap((config) => {
+ this.lastConfig = config;
+ }),
shareReplay(1)
);
}
@@ -62,7 +67,7 @@ export class ConfigService {
/**
* Set config schema for a path and performs its validation
*/
- public async setSchema(path: ConfigPath, schema: Type) {
+ public setSchema(path: ConfigPath, schema: Type) {
const namespace = pathToString(path);
if (this.schemas.has(namespace)) {
throw new Error(`Validation schema for [${path}] was already registered.`);
@@ -94,15 +99,16 @@ export class ConfigService {
public async validate() {
const namespaces = [...this.schemas.keys()];
for (let i = 0; i < namespaces.length; i++) {
- await this.validateConfigAtPath(namespaces[i]).pipe(first()).toPromise();
+ await this.getValidatedConfigAtPath$(namespaces[i]).pipe(first()).toPromise();
}
await this.logDeprecation();
+ this.validated = true;
}
/**
* Returns the full config object observable. This is not intended for
- * "normal use", but for features that _need_ access to the full object.
+ * "normal use", but for internal features that _need_ access to the full object.
*/
public getConfig$() {
return this.config$;
@@ -110,27 +116,26 @@ export class ConfigService {
/**
* Reads the subset of the config at the specified `path` and validates it
- * against the static `schema` on the given `ConfigClass`.
+ * against its registered schema.
*
* @param path - The path to the desired subset of the config.
*/
public atPath(path: ConfigPath) {
- return this.validateConfigAtPath(path) as Observable;
+ return this.getValidatedConfigAtPath$(path) as Observable;
}
/**
- * Same as `atPath`, but returns `undefined` if there is no config at the
- * specified path.
+ * Similar to {@link atPath}, but return the last emitted value synchronously instead of an
+ * observable.
*
- * {@link ConfigService.atPath}
+ * @param path - The path to the desired subset of the config.
*/
- public optionalAtPath(path: ConfigPath) {
- return this.getDistinctConfig(path).pipe(
- map((config) => {
- if (config === undefined) return undefined;
- return this.validateAtPath(path, config) as TSchema;
- })
- );
+ public atPathSync(path: ConfigPath) {
+ if (!this.validated) {
+ throw new Error('`atPathSync` called before config was validated');
+ }
+ const configAtPath = this.lastConfig!.get(path);
+ return this.validateAtPath(path, configAtPath) as TSchema;
}
public async isEnabledAtPath(path: ConfigPath) {
@@ -144,10 +149,7 @@ export class ConfigService {
const config = await this.config$.pipe(first()).toPromise();
// if plugin hasn't got a config schema, we try to read "enabled" directly
- const isEnabled =
- validatedConfig && validatedConfig.enabled !== undefined
- ? validatedConfig.enabled
- : config.get(enabledPath);
+ const isEnabled = validatedConfig?.enabled ?? config.get(enabledPath);
// not declared. consider that plugin is enabled by default
if (isEnabled === undefined) {
@@ -170,15 +172,13 @@ export class ConfigService {
public async getUnusedPaths() {
const config = await this.config$.pipe(first()).toPromise();
- const handledPaths = this.handledPaths.map(pathToString);
-
+ const handledPaths = [...this.handledPaths.values()].map(pathToString);
return config.getFlattenedPaths().filter((path) => !isPathHandled(path, handledPaths));
}
public async getUsedPaths() {
const config = await this.config$.pipe(first()).toPromise();
- const handledPaths = this.handledPaths.map(pathToString);
-
+ const handledPaths = [...this.handledPaths.values()].map(pathToString);
return config.getFlattenedPaths().filter((path) => isPathHandled(path, handledPaths));
}
@@ -210,22 +210,17 @@ export class ConfigService {
);
}
- private validateConfigAtPath(path: ConfigPath) {
- return this.getDistinctConfig(path).pipe(map((config) => this.validateAtPath(path, config)));
- }
-
- private getDistinctConfig(path: ConfigPath) {
- this.markAsHandled(path);
-
+ private getValidatedConfigAtPath$(path: ConfigPath) {
return this.config$.pipe(
map((config) => config.get(path)),
- distinctUntilChanged(isEqual)
+ distinctUntilChanged(isEqual),
+ map((config) => this.validateAtPath(path, config))
);
}
private markAsHandled(path: ConfigPath) {
this.log.debug(`Marking config path as handled: ${path}`);
- this.handledPaths.push(path);
+ this.handledPaths.add(path);
}
}
diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
index c037c5f0308c8..c12a147fddddc 100644
--- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
+++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
@@ -84,7 +84,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
};
}
- private static transformPlugins(configValue: LegacyVars) {
+ private static transformPlugins(configValue: LegacyVars = {}) {
// These properties are the only ones we use from the existing `plugins` config node
// since `scanDirs` isn't respected by new platform plugin discovery.
return {
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 2d053300273fb..b86e2e4c6dedb 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -69,9 +69,12 @@ export function pluginInitializerContextConfigMock(config: T) {
};
const mock: jest.Mocked['config']> = {
- legacy: { globalConfig$: of(globalConfig) },
+ legacy: {
+ globalConfig$: of(globalConfig),
+ get: () => globalConfig,
+ },
create: jest.fn().mockReturnValue(of(config)),
- createIfExists: jest.fn().mockReturnValue(of(config)),
+ get: jest.fn().mockReturnValue(config),
};
return mock;
diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts
new file mode 100644
index 0000000000000..fd8234d72bd17
--- /dev/null
+++ b/src/core/server/plugins/legacy_config.test.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { take } from 'rxjs/operators';
+import { ConfigService, Env } from '@kbn/config';
+import { getEnvOptions, rawConfigServiceMock } from '../config/mocks';
+import { getGlobalConfig, getGlobalConfig$ } from './legacy_config';
+import { REPO_ROOT } from '@kbn/utils';
+import { loggingSystemMock } from '../logging/logging_system.mock';
+import { duration } from 'moment';
+import { fromRoot } from '../utils';
+import { ByteSizeValue } from '@kbn/config-schema';
+import { Server } from '../server';
+
+describe('Legacy config', () => {
+ let env: Env;
+ let logger: ReturnType;
+
+ beforeEach(() => {
+ env = Env.createDefault(REPO_ROOT, getEnvOptions());
+ logger = loggingSystemMock.create();
+ });
+
+ const createConfigService = (rawConfig: Record = {}): ConfigService => {
+ const rawConfigService = rawConfigServiceMock.create({ rawConfig });
+ const server = new Server(rawConfigService, env, logger);
+ server.setupCoreConfig();
+ return server.configService;
+ };
+
+ describe('getGlobalConfig', () => {
+ it('should return the global config', async () => {
+ const configService = createConfigService();
+ await configService.validate();
+
+ const legacyConfig = getGlobalConfig(configService);
+
+ expect(legacyConfig).toStrictEqual({
+ kibana: {
+ index: '.kibana',
+ autocompleteTerminateAfter: duration(100000),
+ autocompleteTimeout: duration(1000),
+ },
+ elasticsearch: {
+ shardTimeout: duration(30, 's'),
+ requestTimeout: duration(30, 's'),
+ pingTimeout: duration(30, 's'),
+ },
+ path: { data: fromRoot('data') },
+ savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) },
+ });
+ });
+ });
+
+ describe('getGlobalConfig$', () => {
+ it('should return an observable for the global config', async () => {
+ const configService = createConfigService();
+
+ const legacyConfig = await getGlobalConfig$(configService).pipe(take(1)).toPromise();
+
+ expect(legacyConfig).toStrictEqual({
+ kibana: {
+ index: '.kibana',
+ autocompleteTerminateAfter: duration(100000),
+ autocompleteTimeout: duration(1000),
+ },
+ elasticsearch: {
+ shardTimeout: duration(30, 's'),
+ requestTimeout: duration(30, 's'),
+ pingTimeout: duration(30, 's'),
+ },
+ path: { data: fromRoot('data') },
+ savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) },
+ });
+ });
+ });
+});
diff --git a/src/core/server/plugins/legacy_config.ts b/src/core/server/plugins/legacy_config.ts
new file mode 100644
index 0000000000000..748a1e3190640
--- /dev/null
+++ b/src/core/server/plugins/legacy_config.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { map, shareReplay } from 'rxjs/operators';
+import { combineLatest, Observable } from 'rxjs';
+import { PathConfigType, config as pathConfig } from '@kbn/utils';
+import { pick, deepFreeze } from '@kbn/std';
+import { IConfigService } from '@kbn/config';
+
+import { SharedGlobalConfig, SharedGlobalConfigKeys } from './types';
+import { KibanaConfigType, config as kibanaConfig } from '../kibana_config';
+import {
+ ElasticsearchConfigType,
+ config as elasticsearchConfig,
+} from '../elasticsearch/elasticsearch_config';
+import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config';
+
+const createGlobalConfig = ({
+ kibana,
+ elasticsearch,
+ path,
+ savedObjects,
+}: {
+ kibana: KibanaConfigType;
+ elasticsearch: ElasticsearchConfigType;
+ path: PathConfigType;
+ savedObjects: SavedObjectsConfigType;
+}): SharedGlobalConfig => {
+ return deepFreeze({
+ kibana: pick(kibana, SharedGlobalConfigKeys.kibana),
+ elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch),
+ path: pick(path, SharedGlobalConfigKeys.path),
+ savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects),
+ });
+};
+
+export const getGlobalConfig = (configService: IConfigService): SharedGlobalConfig => {
+ return createGlobalConfig({
+ kibana: configService.atPathSync(kibanaConfig.path),
+ elasticsearch: configService.atPathSync(elasticsearchConfig.path),
+ path: configService.atPathSync(pathConfig.path),
+ savedObjects: configService.atPathSync(savedObjectsConfig.path),
+ });
+};
+
+export const getGlobalConfig$ = (configService: IConfigService): Observable => {
+ return combineLatest([
+ configService.atPath(kibanaConfig.path),
+ configService.atPath(elasticsearchConfig.path),
+ configService.atPath(pathConfig.path),
+ configService.atPath(savedObjectsConfig.path),
+ ]).pipe(
+ map(
+ ([kibana, elasticsearch, path, savedObjects]) =>
+ createGlobalConfig({
+ kibana,
+ elasticsearch,
+ path,
+ savedObjects,
+ }),
+ shareReplay(1)
+ )
+ );
+};
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 3d212bc555828..c71102df9929b 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -17,15 +17,8 @@ import { rawConfigServiceMock, getEnvOptions } from '../config/mocks';
import { PluginManifest } from './types';
import { Server } from '../server';
import { fromRoot } from '../utils';
-import { ByteSizeValue } from '@kbn/config-schema';
-
-const logger = loggingSystemMock.create();
-
-let coreId: symbol;
-let env: Env;
-let coreContext: CoreContext;
-let server: Server;
-let instanceInfo: InstanceInfo;
+import { schema, ByteSizeValue } from '@kbn/config-schema';
+import { ConfigService } from '@kbn/config';
function createPluginManifest(manifestProps: Partial = {}): PluginManifest {
return {
@@ -43,61 +36,112 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
}
describe('createPluginInitializerContext', () => {
+ let logger: ReturnType;
+ let coreId: symbol;
+ let opaqueId: symbol;
+ let env: Env;
+ let coreContext: CoreContext;
+ let server: Server;
+ let instanceInfo: InstanceInfo;
+
beforeEach(async () => {
+ logger = loggingSystemMock.create();
coreId = Symbol('core');
+ opaqueId = Symbol();
instanceInfo = {
uuid: 'instance-uuid',
};
env = Env.createDefault(REPO_ROOT, getEnvOptions());
const config$ = rawConfigServiceMock.create({ rawConfig: {} });
server = new Server(config$, env, logger);
- await server.setupCoreConfig();
+ server.setupCoreConfig();
coreContext = { coreId, env, logger, configService: server.configService };
});
- it('should return a globalConfig handler in the context', async () => {
- const manifest = createPluginManifest();
- const opaqueId = Symbol();
- const pluginInitializerContext = createPluginInitializerContext(
- coreContext,
- opaqueId,
- manifest,
- instanceInfo
- );
+ describe('context.config', () => {
+ it('config.get() should return the plugin config synchronously', async () => {
+ const config$ = rawConfigServiceMock.create({
+ rawConfig: {
+ plugin: {
+ foo: 'bar',
+ answer: 42,
+ },
+ },
+ });
+
+ const configService = new ConfigService(config$, env, logger);
+ configService.setSchema(
+ 'plugin',
+ schema.object({
+ foo: schema.string(),
+ answer: schema.number(),
+ })
+ );
+ await configService.validate();
- expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined();
+ coreContext = { coreId, env, logger, configService };
- const configObject = await pluginInitializerContext.config.legacy.globalConfig$
- .pipe(first())
- .toPromise();
- expect(configObject).toStrictEqual({
- kibana: {
- index: '.kibana',
- autocompleteTerminateAfter: duration(100000),
- autocompleteTimeout: duration(1000),
- },
- elasticsearch: {
- shardTimeout: duration(30, 's'),
- requestTimeout: duration(30, 's'),
- pingTimeout: duration(30, 's'),
- },
- path: { data: fromRoot('data') },
- savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) },
+ const manifest = createPluginManifest({
+ configPath: 'plugin',
+ });
+
+ const pluginInitializerContext = createPluginInitializerContext(
+ coreContext,
+ opaqueId,
+ manifest,
+ instanceInfo
+ );
+
+ expect(pluginInitializerContext.config.get()).toEqual({
+ foo: 'bar',
+ answer: 42,
+ });
+ });
+
+ it('config.globalConfig$ should be an observable for the global config', async () => {
+ const manifest = createPluginManifest();
+ const pluginInitializerContext = createPluginInitializerContext(
+ coreContext,
+ opaqueId,
+ manifest,
+ instanceInfo
+ );
+
+ expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined();
+
+ const configObject = await pluginInitializerContext.config.legacy.globalConfig$
+ .pipe(first())
+ .toPromise();
+ expect(configObject).toStrictEqual({
+ kibana: {
+ index: '.kibana',
+ autocompleteTerminateAfter: duration(100000),
+ autocompleteTimeout: duration(1000),
+ },
+ elasticsearch: {
+ shardTimeout: duration(30, 's'),
+ requestTimeout: duration(30, 's'),
+ pingTimeout: duration(30, 's'),
+ },
+ path: { data: fromRoot('data') },
+ savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) },
+ });
});
});
- it('allow to access the provided instance uuid', () => {
- const manifest = createPluginManifest();
- const opaqueId = Symbol();
- instanceInfo = {
- uuid: 'kibana-uuid',
- };
- const pluginInitializerContext = createPluginInitializerContext(
- coreContext,
- opaqueId,
- manifest,
- instanceInfo
- );
- expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid');
+ describe('context.env', () => {
+ it('should expose the correct instance uuid', () => {
+ const manifest = createPluginManifest();
+ instanceInfo = {
+ uuid: 'kibana-uuid',
+ };
+ const pluginInitializerContext = createPluginInitializerContext(
+ coreContext,
+ opaqueId,
+ manifest,
+ instanceInfo
+ );
+ expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid');
+ });
});
});
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 5b0e2ee21a887..3b7dc70b9c054 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -6,27 +6,14 @@
* Public License, v 1.
*/
-import { map, shareReplay } from 'rxjs/operators';
-import { combineLatest } from 'rxjs';
-import { PathConfigType, config as pathConfig } from '@kbn/utils';
-import { pick, deepFreeze } from '@kbn/std';
+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 {
- PluginInitializerContext,
- PluginManifest,
- PluginOpaqueId,
- SharedGlobalConfigKeys,
-} from './types';
-import { KibanaConfigType, config as kibanaConfig } from '../kibana_config';
-import {
- ElasticsearchConfigType,
- config as elasticsearchConfig,
-} from '../elasticsearch/elasticsearch_config';
+import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types';
import { IRouter, RequestHandlerContextProvider } from '../http';
-import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config';
+import { getGlobalConfig, getGlobalConfig$ } from './legacy_config';
import { CoreSetup, CoreStart } from '..';
export interface InstanceInfo {
@@ -78,40 +65,19 @@ export function createPluginInitializerContext(
*/
config: {
legacy: {
- /**
- * Global configuration
- * Note: naming not final here, it will be renamed in a near future (https://github.com/elastic/kibana/issues/46240)
- * @deprecated
- */
- globalConfig$: combineLatest([
- coreContext.configService.atPath(kibanaConfig.path),
- coreContext.configService.atPath(elasticsearchConfig.path),
- coreContext.configService.atPath(pathConfig.path),
- coreContext.configService.atPath(savedObjectsConfig.path),
- ]).pipe(
- map(([kibana, elasticsearch, path, savedObjects]) =>
- deepFreeze({
- kibana: pick(kibana, SharedGlobalConfigKeys.kibana),
- elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch),
- path: pick(path, SharedGlobalConfigKeys.path),
- savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects),
- })
- )
- ),
+ globalConfig$: getGlobalConfig$(coreContext.configService),
+ get: () => getGlobalConfig(coreContext.configService),
},
/**
* Reads the subset of the config at the `configPath` defined in the plugin
- * manifest and validates it against the schema in the static `schema` on
- * the given `ConfigClass`.
- * @param ConfigClass A class (not an instance of a class) that contains a
- * static `schema` that we validate the config at the given `path` against.
+ * manifest.
*/
create() {
return coreContext.configService.atPath(pluginManifest.configPath).pipe(shareReplay(1));
},
- createIfExists() {
- return coreContext.configService.optionalAtPath(pluginManifest.configPath);
+ get() {
+ return coreContext.configService.atPathSync(pluginManifest.configPath);
},
},
};
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 9a1403bda3bca..dd2831f77f537 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -219,10 +219,7 @@ export class PluginsService implements CoreService {
packageInfo: Readonly;
instanceUuid: string;
};
+ /**
+ * {@link LoggerFactory | logger factory} instance already bound to the plugin's logging context
+ *
+ * @example
+ * ```typescript
+ * // plugins/my-plugin/server/plugin.ts
+ * // "id: myPlugin" in `plugins/my-plugin/kibana.yaml`
+ *
+ * export class MyPlugin implements Plugin {
+ * constructor(private readonly initContext: PluginInitializerContext) {
+ * this.logger = initContext.logger.get();
+ * // `logger` context: `plugins.myPlugin`
+ * this.mySubLogger = initContext.logger.get('sub'); // or this.logger.get('sub');
+ * // `mySubLogger` context: `plugins.myPlugin.sub`
+ * }
+ * }
+ * ```
+ */
logger: LoggerFactory;
+ /**
+ * Accessors for the plugin's configuration
+ */
config: {
- legacy: { globalConfig$: Observable };
+ /**
+ * Provide access to Kibana legacy configuration values.
+ *
+ * @remarks Naming not final here, it may be renamed in a near future
+ * @deprecated Accessing configuration values outside of the plugin's config scope is highly discouraged
+ */
+ legacy: {
+ globalConfig$: Observable;
+ get: () => SharedGlobalConfig;
+ };
+ /**
+ * Return an observable of the plugin's configuration
+ *
+ * @example
+ * ```typescript
+ * // plugins/my-plugin/server/plugin.ts
+ *
+ * export class MyPlugin implements Plugin {
+ * constructor(private readonly initContext: PluginInitializerContext) {}
+ * setup(core) {
+ * this.configSub = this.initContext.config.create().subscribe((config) => {
+ * this.myService.reconfigure(config);
+ * });
+ * }
+ * stop() {
+ * this.configSub.unsubscribe();
+ * }
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // plugins/my-plugin/server/plugin.ts
+ *
+ * export class MyPlugin implements Plugin {
+ * constructor(private readonly initContext: PluginInitializerContext) {}
+ * async setup(core) {
+ * this.config = await this.initContext.config.create().pipe(take(1)).toPromise();
+ * }
+ * stop() {
+ * this.configSub.unsubscribe();
+ * }
+ * ```
+ *
+ * @remarks The underlying observable has a replay effect, meaning that awaiting for the first emission
+ * will be resolved at next tick, without risks to delay any asynchronous code's workflow.
+ */
create: () => Observable;
- createIfExists: () => Observable;
+ /**
+ * Return the current value of the plugin's configuration synchronously.
+ *
+ * @example
+ * ```typescript
+ * // plugins/my-plugin/server/plugin.ts
+ *
+ * export class MyPlugin implements Plugin {
+ * constructor(private readonly initContext: PluginInitializerContext) {}
+ * setup(core) {
+ * const config = this.initContext.config.get();
+ * // do something with the config
+ * }
+ * }
+ * ```
+ *
+ * @remarks This should only be used when synchronous access is an absolute necessity, such
+ * as during the plugin's setup or start lifecycle. For all other usages,
+ * {@link create} should be used instead.
+ */
+ get: () => T;
};
}
diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts
index 17cb209897c25..a918580392caa 100644
--- a/src/core/server/root/index.ts
+++ b/src/core/server/root/index.ts
@@ -36,7 +36,7 @@ export class Root {
public async setup() {
try {
- await this.server.setupCoreConfig();
+ this.server.setupCoreConfig();
await this.setupLogging();
this.log.debug('setting up root');
return await this.server.setup();
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 0b058011267eb..0a138917717fc 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1840,13 +1840,13 @@ export type PluginInitializer {
- // (undocumented)
config: {
legacy: {
globalConfig$: Observable;
+ get: () => SharedGlobalConfig;
};
create: () => Observable;
- createIfExists: () => Observable;
+ get: () => T;
};
// (undocumented)
env: {
@@ -1854,7 +1854,7 @@ export interface PluginInitializerContext {
packageInfo: Readonly;
instanceUuid: string;
};
- // (undocumented)
+ // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported
logger: LoggerFactory;
// (undocumented)
opaqueId: PluginOpaqueId;
@@ -3137,5 +3137,6 @@ export const validBodyOutput: readonly ["data", "stream"];
// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
```
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 60f3f90428d40..cc1087a422e39 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -300,7 +300,7 @@ export class Server {
);
}
- public async setupCoreConfig() {
+ public setupCoreConfig() {
const configDescriptors: Array> = [
pathConfig,
cspConfig,
@@ -325,7 +325,7 @@ export class Server {
if (descriptor.deprecations) {
this.configService.addDeprecationProvider(descriptor.path, descriptor.deprecations);
}
- await this.configService.setSchema(descriptor.path, descriptor.schema);
+ this.configService.setSchema(descriptor.path, descriptor.schema);
}
}
}