Skip to content

Commit

Permalink
add config.get to plugin context and add tsdoc
Browse files Browse the repository at this point in the history
  • Loading branch information
pgayvallet committed Jan 26, 2021
1 parent e46be42 commit 8fbb27b
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 57 deletions.
1 change: 1 addition & 0 deletions packages/kbn-config/src/config_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export class ConfigService {
);
}

// TODO: only one consumer, remove method + remove `markAsHandled`
private getValidatedConfigAtPath$(path: ConfigPath) {
this.markAsHandled(path);
return this.config$.pipe(
Expand Down
140 changes: 92 additions & 48 deletions src/core/server/plugins/plugin_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -43,8 +36,18 @@ 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',
};
Expand All @@ -55,49 +58,90 @@ describe('createPluginInitializerContext', () => {
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');
});
});
});
13 changes: 4 additions & 9 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,6 @@ 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),
Expand All @@ -102,14 +97,14 @@ export function createPluginInitializerContext(

/**
* 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));
},
get<T>() {
return coreContext.configService.atPathSync<T>(pluginManifest.configPath);
},
},
};
}
Expand Down
84 changes: 84 additions & 0 deletions src/core/server/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,94 @@ 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: {
/**
* 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> };
/**
* 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>;
/**
* 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;
};
}

Expand Down

0 comments on commit 8fbb27b

Please sign in to comment.