mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* 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:
parent
758fdeb78b
commit
c7b900bc1a
17 changed files with 478 additions and 190 deletions
|
@ -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;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -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<SharedGlobalConfig>;</code><br/><code> };</code><br/><code> create: <T = ConfigSchema>() => Observable<T>;</code><br/><code> createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;</code><br/><code> }</code> | |
|
||||
| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | <code>{</code><br/><code> legacy: {</code><br/><code> globalConfig$: Observable<SharedGlobalConfig>;</code><br/><code> get: () => SharedGlobalConfig;</code><br/><code> };</code><br/><code> create: <T = ConfigSchema>() => Observable<T>;</code><br/><code> get: <T = ConfigSchema>() => 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<PackageInfo>;</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> | |
|
||||
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
82
src/core/server/plugins/legacy_config.test.ts
Normal file
82
src/core/server/plugins/legacy_config.test.ts
Normal 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) },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
69
src/core/server/plugins/legacy_config.ts
Normal file
69
src/core/server/plugins/legacy_config.ts
Normal 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)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue