add synchronous config access API (#88981) (#89542)

* add synchronous config accessor

* add `config.get` to plugin context and add tsdoc

* remove useless markAsHandled calls

* fix mocks

* update generated docs

* fix unit tests

* add sync accessor for legacy config
This commit is contained in:
Pierre Gayvallet 2021-01-28 13:51:46 +01:00 committed by GitHub
parent 758fdeb78b
commit c7b900bc1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 478 additions and 190 deletions

View file

@ -4,14 +4,17 @@
## PluginInitializerContext.config property
Accessors for the plugin's configuration
<b>Signature:</b>
```typescript
config: {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
get: <T = ConfigSchema>() => T;
};
```

View file

@ -4,8 +4,29 @@
## PluginInitializerContext.logger property
instance already bound to the plugin's logging context
<b>Signature:</b>
```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`
}
}
```

View file

@ -16,8 +16,8 @@ export interface PluginInitializerContext<ConfigSchema = unknown>
| Property | Type | Description |
| --- | --- | --- |
| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | <code>{</code><br/><code> legacy: {</code><br/><code> globalConfig$: Observable&lt;SharedGlobalConfig&gt;;</code><br/><code> };</code><br/><code> create: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T&gt;;</code><br/><code> createIfExists: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T &#124; undefined&gt;;</code><br/><code> }</code> | |
| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | <code>{</code><br/><code> legacy: {</code><br/><code> globalConfig$: Observable&lt;SharedGlobalConfig&gt;;</code><br/><code> get: () =&gt; SharedGlobalConfig;</code><br/><code> };</code><br/><code> create: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T&gt;;</code><br/><code> get: &lt;T = ConfigSchema&gt;() =&gt; T;</code><br/><code> }</code> | Accessors for the plugin's configuration |
| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | <code>{</code><br/><code> mode: EnvironmentMode;</code><br/><code> packageInfo: Readonly&lt;PackageInfo&gt;;</code><br/><code> instanceUuid: string;</code><br/><code> }</code> | |
| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | <code>LoggerFactory</code> | |
| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | <code>LoggerFactory</code> | instance already bound to the plugin's logging context |
| [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | <code>PluginOpaqueId</code> | |

View file

@ -17,8 +17,8 @@ const createConfigServiceMock = ({
}: { atPath?: Record<string, any>; getConfig$?: Record<string, any> } = {}) => {
const mocked: jest.Mocked<IConfigService> = {
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([]);

View file

@ -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<Record<string, any>>({ 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<Record<string, any>>({ 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');
});
});

View file

@ -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<Config>;
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<ConfigPath> = new Set();
private readonly schemas = new Map<string, Type<unknown>>();
private readonly deprecations = new BehaviorSubject<ConfigDeprecationWithContext[]>([]);
@ -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<unknown>) {
public setSchema(path: ConfigPath, schema: Type<unknown>) {
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<TSchema>(path: ConfigPath) {
return this.validateConfigAtPath(path) as Observable<TSchema>;
return this.getValidatedConfigAtPath$(path) as Observable<TSchema>;
}
/**
* 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<TSchema>(path: ConfigPath) {
return this.getDistinctConfig(path).pipe(
map((config) => {
if (config === undefined) return undefined;
return this.validateAtPath(path, config) as TSchema;
})
);
public atPathSync<TSchema>(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);
}
}

View file

@ -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 {

View file

@ -69,9 +69,12 @@ export function pluginInitializerContextConfigMock<T>(config: T) {
};
const mock: jest.Mocked<PluginInitializerContext<T>['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;

View file

@ -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<typeof loggingSystemMock.create>;
beforeEach(() => {
env = Env.createDefault(REPO_ROOT, getEnvOptions());
logger = loggingSystemMock.create();
});
const createConfigService = (rawConfig: Record<string, any> = {}): 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) },
});
});
});
});

View file

@ -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<KibanaConfigType>(kibanaConfig.path),
elasticsearch: configService.atPathSync<ElasticsearchConfigType>(elasticsearchConfig.path),
path: configService.atPathSync<PathConfigType>(pathConfig.path),
savedObjects: configService.atPathSync<SavedObjectsConfigType>(savedObjectsConfig.path),
});
};
export const getGlobalConfig$ = (configService: IConfigService): Observable<SharedGlobalConfig> => {
return combineLatest([
configService.atPath<KibanaConfigType>(kibanaConfig.path),
configService.atPath<ElasticsearchConfigType>(elasticsearchConfig.path),
configService.atPath<PathConfigType>(pathConfig.path),
configService.atPath<SavedObjectsConfigType>(savedObjectsConfig.path),
]).pipe(
map(
([kibana, elasticsearch, path, savedObjects]) =>
createGlobalConfig({
kibana,
elasticsearch,
path,
savedObjects,
}),
shareReplay(1)
)
);
};

View file

@ -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> = {}): PluginManifest {
return {
@ -43,61 +36,112 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
}
describe('createPluginInitializerContext', () => {
let logger: ReturnType<typeof loggingSystemMock.create>;
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,
},
},
});
expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined();
const configService = new ConfigService(config$, env, logger);
configService.setSchema(
'plugin',
schema.object({
foo: schema.string(),
answer: schema.number(),
})
);
await configService.validate();
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) },
coreContext = { coreId, env, logger, configService };
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');
});
});
});

View file

@ -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<KibanaConfigType>(kibanaConfig.path),
coreContext.configService.atPath<ElasticsearchConfigType>(elasticsearchConfig.path),
coreContext.configService.atPath<PathConfigType>(pathConfig.path),
coreContext.configService.atPath<SavedObjectsConfigType>(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<T>() {
return coreContext.configService.atPath<T>(pluginManifest.configPath).pipe(shareReplay(1));
},
createIfExists() {
return coreContext.configService.optionalAtPath(pluginManifest.configPath);
get<T>() {
return coreContext.configService.atPathSync<T>(pluginManifest.configPath);
},
},
};

View file

@ -219,10 +219,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
configDescriptor.deprecations
);
}
await this.coreContext.configService.setSchema(
plugin.configPath,
configDescriptor.schema
);
this.coreContext.configService.setSchema(plugin.configPath, configDescriptor.schema);
}
const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath);

View file

@ -278,11 +278,97 @@ export interface PluginInitializerContext<ConfigSchema = unknown> {
packageInfo: Readonly<PackageInfo>;
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<SharedGlobalConfig> };
/**
* 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<SharedGlobalConfig>;
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<MyPluginConfigType>().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<MyPluginConfigType>().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: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
/**
* 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<MyPluginConfigType>();
* // 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 = ConfigSchema>() => T;
};
}

View file

@ -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();

View file

@ -1840,13 +1840,13 @@ export type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = obj
// @public
export interface PluginInitializerContext<ConfigSchema = unknown> {
// (undocumented)
config: {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
get: <T = ConfigSchema>() => T;
};
// (undocumented)
env: {
@ -1854,7 +1854,7 @@ export interface PluginInitializerContext<ConfigSchema = unknown> {
packageInfo: Readonly<PackageInfo>;
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"
```

View file

@ -300,7 +300,7 @@ export class Server {
);
}
public async setupCoreConfig() {
public setupCoreConfig() {
const configDescriptors: Array<ServiceConfigDescriptor<unknown>> = [
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);
}
}
}