diff --git a/docs/development/core/server/kibana-plugin-server.configservice.setschema.md b/docs/development/core/server/kibana-plugin-server.configservice.setschema.md
new file mode 100644
index 0000000000000..3db6b071dbdad
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.configservice.setschema.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [setSchema](./kibana-plugin-server.configservice.setschema.md)
+
+## ConfigService.setSchema() method
+
+Set config schema for a path and performs its validation
+
+Signature:
+
+```typescript
+setSchema(path: ConfigPath, schema: Type): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| path | ConfigPath
| |
+| schema | Type<unknown>
| |
+
+Returns:
+
+`Promise`
+
diff --git a/src/core/server/__snapshots__/server.test.ts.snap b/src/core/server/__snapshots__/server.test.ts.snap
deleted file mode 100644
index 19a3d188e1b48..0000000000000
--- a/src/core/server/__snapshots__/server.test.ts.snap
+++ /dev/null
@@ -1,17 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`does not fail on "setup" if there are unused paths detected: unused paths logs 1`] = `
-Object {
- "debug": Array [
- Array [
- "setting up server",
- ],
- ],
- "error": Array [],
- "fatal": Array [],
- "info": Array [],
- "log": Array [],
- "trace": Array [],
- "warn": Array [],
-}
-`;
diff --git a/src/core/server/config/__snapshots__/config_service.test.ts.snap b/src/core/server/config/__snapshots__/config_service.test.ts.snap
index f24b6d1168fd9..9327b80dc79a0 100644
--- a/src/core/server/config/__snapshots__/config_service.test.ts.snap
+++ b/src/core/server/config/__snapshots__/config_service.test.ts.snap
@@ -12,7 +12,3 @@ ExampleClassWithSchema {
},
}
`;
-
-exports[`throws error if config class does not implement 'schema' 1`] = `[Error: The config class [ExampleClass] did not contain a static 'schema' field, which is required when creating a config instance]`;
-
-exports[`throws if config at path does not match schema 1`] = `"[key]: expected value of type [string] but got [number]"`;
diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts
index 92b0f117b1a02..b9c4fa91ae702 100644
--- a/src/core/server/config/config_service.mock.ts
+++ b/src/core/server/config/config_service.mock.ts
@@ -22,15 +22,16 @@ import { ObjectToConfigAdapter } from './object_to_config_adapter';
import { ConfigService } from './config_service';
-type ConfigSericeContract = PublicMethodsOf;
+type ConfigServiceContract = PublicMethodsOf;
const createConfigServiceMock = () => {
- const mocked: jest.Mocked = {
+ const mocked: jest.Mocked = {
atPath: jest.fn(),
getConfig$: jest.fn(),
optionalAtPath: jest.fn(),
getUsedPaths: jest.fn(),
getUnusedPaths: jest.fn(),
isEnabledAtPath: jest.fn(),
+ setSchema: jest.fn(),
};
mocked.atPath.mockReturnValue(new BehaviorSubject({}));
mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter({})));
diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts
index fd03554e9209d..27c9473c16324 100644
--- a/src/core/server/config/config_service.test.ts
+++ b/src/core/server/config/config_service.test.ts
@@ -34,9 +34,16 @@ const emptyArgv = getEnvOptions();
const defaultEnv = new Env('/kibana', emptyArgv);
const logger = loggingServiceMock.create();
+class ExampleClassWithStringSchema {
+ public static schema = schema.string();
+
+ constructor(readonly value: string) {}
+}
+
test('returns config at path as observable', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' }));
const configService = new ConfigService(config$, defaultEnv, logger);
+ await configService.setSchema('key', schema.string());
const configs = configService.atPath('key', ExampleClassWithStringSchema);
const exampleConfig = await configs.pipe(first()).toPromise();
@@ -45,22 +52,45 @@ test('returns config at path as observable', async () => {
});
test('throws if config at path does not match schema', async () => {
- expect.assertions(1);
-
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 123 }));
const configService = new ConfigService(config$, defaultEnv, logger);
- const configs = configService.atPath('key', ExampleClassWithStringSchema);
- try {
- await configs.pipe(first()).toPromise();
- } catch (e) {
- expect(e.message).toMatchSnapshot();
- }
+ await expect(
+ configService.setSchema('key', schema.string())
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"[key]: expected value of type [string] but got [number]"`
+ );
+});
+
+test('re-validate config when updated', async () => {
+ const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
+
+ const configService = new ConfigService(config$, defaultEnv, logger);
+ configService.setSchema('key', schema.string());
+
+ const valuesReceived: any[] = [];
+ await configService.atPath('key', ExampleClassWithStringSchema).subscribe(
+ config => {
+ valuesReceived.push(config.value);
+ },
+ error => {
+ valuesReceived.push(error);
+ }
+ );
+
+ config$.next(new ObjectToConfigAdapter({ key: 123 }));
+
+ await expect(valuesReceived).toMatchInlineSnapshot(`
+Array [
+ "value",
+ [Error: [key]: expected value of type [string] but got [number]],
+]
+`);
});
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
- const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: 'bar' }));
+ const config$ = new BehaviorSubject(new ObjectToConfigAdapter({}));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.optionalAtPath('unique-name', ExampleClassWithStringSchema);
@@ -72,6 +102,7 @@ test("returns undefined if fetching optional config at a path that doesn't exist
test('returns observable config at optional path if it exists', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ value: 'bar' }));
const configService = new ConfigService(config$, defaultEnv, logger);
+ await configService.setSchema('value', schema.string());
const configs = configService.optionalAtPath('value', ExampleClassWithStringSchema);
const exampleConfig: any = await configs.pipe(first()).toPromise();
@@ -83,6 +114,7 @@ test('returns observable config at optional path if it exists', async () => {
test("does not push new configs when reloading if config at path hasn't changed", async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
+ await configService.setSchema('key', schema.string());
const valuesReceived: any[] = [];
configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => {
@@ -97,6 +129,7 @@ test("does not push new configs when reloading if config at path hasn't changed"
test('pushes new config when reloading and config at path has changed', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
+ await configService.setSchema('key', schema.string());
const valuesReceived: any[] = [];
configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => {
@@ -108,9 +141,7 @@ test('pushes new config when reloading and config at path has changed', async ()
expect(valuesReceived).toEqual(['value', 'new value']);
});
-test("throws error if config class does not implement 'schema'", async () => {
- expect.assertions(1);
-
+test("throws error if 'schema' is not defined for a key", async () => {
class ExampleClass {}
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
@@ -118,11 +149,19 @@ test("throws error if config class does not implement 'schema'", async () => {
const configs = configService.atPath('key', ExampleClass as any);
- try {
- await configs.pipe(first()).toPromise();
- } catch (e) {
- expect(e).toMatchSnapshot();
- }
+ await expect(configs.pipe(first()).toPromise()).rejects.toMatchInlineSnapshot(
+ `[Error: No validation schema has been defined for key]`
+ );
+});
+
+test("throws error if 'setSchema' called several times for the same key", async () => {
+ const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
+ const configService = new ConfigService(config$, defaultEnv, logger);
+ const addSchema = async () => await configService.setSchema('key', schema.string());
+ await addSchema();
+ await expect(addSchema()).rejects.toMatchInlineSnapshot(
+ `[Error: Validation schema for key was already registered.]`
+ );
});
test('tracks unhandled paths', async () => {
@@ -178,28 +217,25 @@ test('correctly passes context', async () => {
const env = new Env('/kibana', getEnvOptions());
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: {} }));
+ const schemaDefinition = schema.object({
+ branchRef: schema.string({
+ defaultValue: schema.contextRef('branch'),
+ }),
+ buildNumRef: schema.number({
+ defaultValue: schema.contextRef('buildNum'),
+ }),
+ buildShaRef: schema.string({
+ defaultValue: schema.contextRef('buildSha'),
+ }),
+ devRef: schema.boolean({ defaultValue: schema.contextRef('dev') }),
+ prodRef: schema.boolean({ defaultValue: schema.contextRef('prod') }),
+ versionRef: schema.string({
+ defaultValue: schema.contextRef('version'),
+ }),
+ });
const configService = new ConfigService(config$, env, logger);
- const configs = configService.atPath(
- 'foo',
- createClassWithSchema(
- schema.object({
- branchRef: schema.string({
- defaultValue: schema.contextRef('branch'),
- }),
- buildNumRef: schema.number({
- defaultValue: schema.contextRef('buildNum'),
- }),
- buildShaRef: schema.string({
- defaultValue: schema.contextRef('buildSha'),
- }),
- devRef: schema.boolean({ defaultValue: schema.contextRef('dev') }),
- prodRef: schema.boolean({ defaultValue: schema.contextRef('prod') }),
- versionRef: schema.string({
- defaultValue: schema.contextRef('version'),
- }),
- })
- )
- );
+ await configService.setSchema('foo', schemaDefinition);
+ const configs = configService.atPath('foo', createClassWithSchema(schemaDefinition));
expect(await configs.pipe(first()).toPromise()).toMatchSnapshot();
});
@@ -278,9 +314,3 @@ function createClassWithSchema(s: Type) {
constructor(readonly value: TypeOf) {}
};
}
-
-class ExampleClassWithStringSchema {
- public static schema = schema.string();
-
- constructor(readonly value: string) {}
-}
diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts
index b92b7323bba58..dd3be624ebf51 100644
--- a/src/core/server/config/config_service.ts
+++ b/src/core/server/config/config_service.ts
@@ -34,6 +34,7 @@ export class ConfigService {
* then list all unhandled config paths when the startup process is completed.
*/
private readonly handledPaths: ConfigPath[] = [];
+ private readonly schemas = new Map>();
constructor(
private readonly config$: Observable,
@@ -43,6 +44,22 @@ export class ConfigService {
this.log = logger.get('config');
}
+ /**
+ * Set config schema for a path and performs its validation
+ */
+ public async setSchema(path: ConfigPath, schema: Type) {
+ const namespace = pathToString(path);
+ if (this.schemas.has(namespace)) {
+ throw new Error(`Validation schema for ${path} was already registered.`);
+ }
+
+ this.schemas.set(namespace, schema);
+
+ await this.validateConfig(path)
+ .pipe(first())
+ .toPromise();
+ }
+
/**
* Returns the full config object observable. This is not intended for
* "normal use", but for features that _need_ access to the full object.
@@ -59,13 +76,11 @@ export class ConfigService {
* @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.
*/
- public atPath, TConfig>(
+ public atPath, TConfig>(
path: ConfigPath,
ConfigClass: ConfigWithSchema
) {
- return this.getDistinctConfig(path).pipe(
- map(config => this.createConfig(path, config, ConfigClass))
- );
+ return this.validateConfig(path).pipe(map(config => this.createConfig(config, ConfigClass)));
}
/**
@@ -79,9 +94,11 @@ export class ConfigService {
ConfigClass: ConfigWithSchema
) {
return this.getDistinctConfig(path).pipe(
- map(config =>
- config === undefined ? undefined : this.createConfig(path, config, ConfigClass)
- )
+ map(config => {
+ if (config === undefined) return undefined;
+ const validatedConfig = this.validate(path, config);
+ return this.createConfig(validatedConfig, ConfigClass);
+ })
);
}
@@ -122,24 +139,13 @@ export class ConfigService {
return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths));
}
- private createConfig, TConfig>(
- path: ConfigPath,
- config: Record,
- ConfigClass: ConfigWithSchema
- ) {
- const namespace = Array.isArray(path) ? path.join('.') : path;
-
- const configSchema = ConfigClass.schema;
-
- if (configSchema === undefined || typeof configSchema.validate !== 'function') {
- throw new Error(
- `The config class [${
- ConfigClass.name
- }] did not contain a static 'schema' field, which is required when creating a config instance`
- );
+ private validate(path: ConfigPath, config: Record) {
+ const namespace = pathToString(path);
+ const schema = this.schemas.get(namespace);
+ if (!schema) {
+ throw new Error(`No validation schema has been defined for ${namespace}`);
}
-
- const validatedConfig = ConfigClass.schema.validate(
+ return schema.validate(
config,
{
dev: this.env.mode.dev,
@@ -148,9 +154,19 @@ export class ConfigService {
},
namespace
);
+ }
+
+ private createConfig, TConfig>(
+ validatedConfig: unknown,
+ ConfigClass: ConfigWithSchema
+ ) {
return new ConfigClass(validatedConfig, this.env);
}
+ private validateConfig(path: ConfigPath) {
+ return this.getDistinctConfig(path).pipe(map(config => this.validate(path, config)));
+ }
+
private getDistinctConfig(path: ConfigPath) {
this.markAsHandled(path);
diff --git a/src/core/server/dev/dev_config.ts b/src/core/server/dev/dev_config.ts
index 174b108883792..6c99025da3fc0 100644
--- a/src/core/server/dev/dev_config.ts
+++ b/src/core/server/dev/dev_config.ts
@@ -25,8 +25,12 @@ const createDevSchema = schema.object({
}),
});
-type DevConfigType = TypeOf;
+export const config = {
+ path: 'dev',
+ schema: createDevSchema,
+};
+export type DevConfigType = TypeOf;
export class DevConfig {
/**
* @internal
@@ -38,7 +42,7 @@ export class DevConfig {
/**
* @internal
*/
- constructor(config: DevConfigType) {
- this.basePathProxyTargetPort = config.basePathProxyTarget;
+ constructor(rawConfig: DevConfigType) {
+ this.basePathProxyTargetPort = rawConfig.basePathProxyTarget;
}
}
diff --git a/src/core/server/dev/index.ts b/src/core/server/dev/index.ts
index b3fa85892330e..cccd6b1c5d07f 100644
--- a/src/core/server/dev/index.ts
+++ b/src/core/server/dev/index.ts
@@ -17,4 +17,4 @@
* under the License.
*/
-export { DevConfig } from './dev_config';
+export { DevConfig, config } from './dev_config';
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index e456d7a74e10b..a2b3e03e2cbec 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -60,7 +60,13 @@ const configSchema = schema.object({
healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }),
});
-type SslConfigSchema = TypeOf['ssl'];
+export type ElasticsearchConfigType = TypeOf;
+type SslConfigSchema = ElasticsearchConfigType['ssl'];
+
+export const config = {
+ path: 'elasticsearch',
+ schema: configSchema,
+};
export class ElasticsearchConfig {
public static schema = configSchema;
@@ -154,34 +160,34 @@ export class ElasticsearchConfig {
* headers cannot be overwritten by client-side headers and aren't affected by
* `requestHeadersWhitelist` configuration.
*/
- public readonly customHeaders: TypeOf['customHeaders'];
-
- constructor(config: TypeOf) {
- this.apiVersion = config.apiVersion;
- this.logQueries = config.logQueries;
- this.hosts = Array.isArray(config.hosts) ? config.hosts : [config.hosts];
- this.requestHeadersWhitelist = Array.isArray(config.requestHeadersWhitelist)
- ? config.requestHeadersWhitelist
- : [config.requestHeadersWhitelist];
- this.pingTimeout = config.pingTimeout;
- this.requestTimeout = config.requestTimeout;
- this.shardTimeout = config.shardTimeout;
- this.sniffOnStart = config.sniffOnStart;
- this.sniffOnConnectionFault = config.sniffOnConnectionFault;
- this.sniffInterval = config.sniffInterval;
- this.healthCheckDelay = config.healthCheck.delay;
- this.username = config.username;
- this.password = config.password;
- this.customHeaders = config.customHeaders;
-
- const certificateAuthorities = Array.isArray(config.ssl.certificateAuthorities)
- ? config.ssl.certificateAuthorities
- : typeof config.ssl.certificateAuthorities === 'string'
- ? [config.ssl.certificateAuthorities]
+ public readonly customHeaders: ElasticsearchConfigType['customHeaders'];
+
+ constructor(rawConfig: ElasticsearchConfigType) {
+ this.apiVersion = rawConfig.apiVersion;
+ this.logQueries = rawConfig.logQueries;
+ this.hosts = Array.isArray(rawConfig.hosts) ? rawConfig.hosts : [rawConfig.hosts];
+ this.requestHeadersWhitelist = Array.isArray(rawConfig.requestHeadersWhitelist)
+ ? rawConfig.requestHeadersWhitelist
+ : [rawConfig.requestHeadersWhitelist];
+ this.pingTimeout = rawConfig.pingTimeout;
+ this.requestTimeout = rawConfig.requestTimeout;
+ this.shardTimeout = rawConfig.shardTimeout;
+ this.sniffOnStart = rawConfig.sniffOnStart;
+ this.sniffOnConnectionFault = rawConfig.sniffOnConnectionFault;
+ this.sniffInterval = rawConfig.sniffInterval;
+ this.healthCheckDelay = rawConfig.healthCheck.delay;
+ this.username = rawConfig.username;
+ this.password = rawConfig.password;
+ this.customHeaders = rawConfig.customHeaders;
+
+ const certificateAuthorities = Array.isArray(rawConfig.ssl.certificateAuthorities)
+ ? rawConfig.ssl.certificateAuthorities
+ : typeof rawConfig.ssl.certificateAuthorities === 'string'
+ ? [rawConfig.ssl.certificateAuthorities]
: undefined;
this.ssl = {
- ...config.ssl,
+ ...rawConfig.ssl,
certificateAuthorities,
};
}
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 0697d9152f0a9..41e72a11a7ee1 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -22,32 +22,33 @@ import { first } from 'rxjs/operators';
import { MockClusterClient } from './elasticsearch_service.test.mocks';
import { BehaviorSubject, combineLatest } from 'rxjs';
-import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
+import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
+import { configServiceMock } from '../config/config_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
let elasticsearchService: ElasticsearchService;
-let configService: ConfigService;
+const configService = configServiceMock.create();
+configService.atPath.mockReturnValue(
+ new BehaviorSubject(
+ new ElasticsearchConfig({
+ hosts: ['http://1.2.3.4'],
+ healthCheck: {},
+ ssl: {},
+ } as any)
+ )
+);
+
let env: Env;
let coreContext: CoreContext;
const logger = loggingServiceMock.create();
beforeEach(() => {
env = Env.createDefault(getEnvOptions());
- configService = new ConfigService(
- new BehaviorSubject(
- new ObjectToConfigAdapter({
- elasticsearch: { hosts: ['http://1.2.3.4'], username: 'jest' },
- })
- ),
- env,
- logger
- );
-
- coreContext = { env, logger, configService };
+ coreContext = { env, logger, configService: configService as any };
elasticsearchService = new ElasticsearchService(coreContext);
});
diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts
index 0f60864dcc5d8..b9925e42d736a 100644
--- a/src/core/server/elasticsearch/index.ts
+++ b/src/core/server/elasticsearch/index.ts
@@ -21,3 +21,4 @@ export { ElasticsearchServiceSetup, ElasticsearchService } from './elasticsearch
export { CallAPIOptions, ClusterClient } from './cluster_client';
export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client';
export { ElasticsearchClientConfig } from './elasticsearch_client_config';
+export { config } from './elasticsearch_config';
diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts
index ebf3bd8023f1a..1848070b2a56f 100644
--- a/src/core/server/http/http_config.ts
+++ b/src/core/server/http/http_config.ts
@@ -85,6 +85,11 @@ const createHttpSchema = schema.object(
export type HttpConfigType = TypeOf;
+export const config = {
+ path: 'server',
+ schema: createHttpSchema,
+};
+
export class HttpConfig {
/**
* @internal
@@ -104,15 +109,15 @@ export class HttpConfig {
/**
* @internal
*/
- constructor(config: HttpConfigType, env: Env) {
- this.autoListen = config.autoListen;
- this.host = config.host;
- this.port = config.port;
- this.cors = config.cors;
- this.maxPayload = config.maxPayload;
- this.basePath = config.basePath;
- this.rewriteBasePath = config.rewriteBasePath;
+ constructor(rawConfig: HttpConfigType, env: Env) {
+ this.autoListen = rawConfig.autoListen;
+ this.host = rawConfig.host;
+ this.port = rawConfig.port;
+ this.cors = rawConfig.cors;
+ this.maxPayload = rawConfig.maxPayload;
+ this.basePath = rawConfig.basePath;
+ this.rewriteBasePath = rawConfig.rewriteBasePath;
this.publicDir = env.staticFilesDir;
- this.ssl = new SslConfig(config.ssl);
+ this.ssl = new SslConfig(rawConfig.ssl);
}
}
diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts
index dd66a050e3302..7b3fd024b477c 100644
--- a/src/core/server/http/http_service.test.ts
+++ b/src/core/server/http/http_service.test.ts
@@ -22,7 +22,7 @@ import { mockHttpServer } from './http_service.test.mocks';
import { noop } from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { HttpService, Router } from '.';
-import { HttpConfigType } from './http_config';
+import { HttpConfigType, config } from './http_config';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
@@ -30,16 +30,19 @@ import { getEnvOptions } from '../config/__mocks__/env';
const logger = loggingServiceMock.create();
const env = Env.createDefault(getEnvOptions());
-const createConfigService = (value: Partial = {}) =>
- new ConfigService(
+const createConfigService = (value: Partial = {}) => {
+ const configService = new ConfigService(
new BehaviorSubject(
new ObjectToConfigAdapter({
- http: value,
+ server: value,
})
),
env,
logger
);
+ configService.setSchema(config.path, config.schema);
+ return configService;
+};
afterEach(() => {
jest.clearAllMocks();
@@ -115,7 +118,7 @@ test('stops http server', async () => {
});
test('register route handler', async () => {
- const configService = createConfigService({});
+ const configService = createConfigService();
const registerRouterMock = jest.fn();
const httpServer = {
@@ -155,7 +158,7 @@ test('returns http server contract on setup', async () => {
});
test('does not start http server if process is dev cluster master', async () => {
- const configService = createConfigService({});
+ const configService = createConfigService();
const httpServer = {
isListening: () => false,
setup: noop,
@@ -190,7 +193,7 @@ test('does not start http server if configured with `autoListen:false`', async (
const service = new HttpService({
configService,
- env: new Env('.', getEnvOptions({ isDevClusterMaster: true })),
+ env: new Env('.', getEnvOptions()),
logger,
});
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index 633fcd9885212..465c5cb6a859b 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-export { HttpConfig } from './http_config';
+export { config, HttpConfig, HttpConfigType } from './http_config';
export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service';
export { Router, KibanaRequest } from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
diff --git a/src/core/server/index.test.mocks.ts b/src/core/server/index.test.mocks.ts
index a3bcaa56da975..4e61316fcff94 100644
--- a/src/core/server/index.test.mocks.ts
+++ b/src/core/server/index.test.mocks.ts
@@ -23,7 +23,8 @@ jest.doMock('./http/http_service', () => ({
HttpService: jest.fn(() => httpService),
}));
-export const mockPluginsService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() };
+import { pluginServiceMock } from './plugins/plugins_service.mock';
+export const mockPluginsService = pluginServiceMock.create();
jest.doMock('./plugins/plugins_service', () => ({
PluginsService: jest.fn(() => mockPluginsService),
}));
@@ -38,3 +39,9 @@ export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jes
jest.mock('./legacy/legacy_service', () => ({
LegacyService: jest.fn(() => mockLegacyService),
}));
+
+import { configServiceMock } from './config/config_service.mock';
+export const configService = configServiceMock.create();
+jest.doMock('./config/config_service', () => ({
+ ConfigService: jest.fn(() => configService),
+}));
diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts
index a99ee9ec12e8b..aa23a11e39b44 100644
--- a/src/core/server/logging/index.ts
+++ b/src/core/server/logging/index.ts
@@ -22,6 +22,6 @@ export { LoggerFactory } from './logger_factory';
export { LogRecord } from './log_record';
export { LogLevel } from './log_level';
/** @internal */
-export { LoggingConfig } from './logging_config';
+export { LoggingConfig, config } from './logging_config';
/** @internal */
export { LoggingService } from './logging_service';
diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts
index 64de8ed1de216..de85bde3959df 100644
--- a/src/core/server/logging/logging_config.ts
+++ b/src/core/server/logging/logging_config.ts
@@ -79,6 +79,10 @@ const loggingSchema = schema.object({
/** @internal */
export type LoggerConfigType = TypeOf;
+export const config = {
+ path: 'logging',
+ schema: loggingSchema,
+};
type LoggingConfigType = TypeOf;
diff --git a/src/core/server/plugins/discovery/plugin_discovery.test.mocks.ts b/src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts
similarity index 100%
rename from src/core/server/plugins/discovery/plugin_discovery.test.mocks.ts
rename to src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts
diff --git a/src/core/server/plugins/discovery/plugin_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts
similarity index 96%
rename from src/core/server/plugins/discovery/plugin_discovery.test.ts
rename to src/core/server/plugins/discovery/plugins_discovery.test.ts
index f897fe1527aa2..16fa4a18804d3 100644
--- a/src/core/server/plugins/discovery/plugin_discovery.test.ts
+++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugin_discovery.test.mocks';
+import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
@@ -26,7 +26,7 @@ import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config'
import { getEnvOptions } from '../../config/__mocks__/env';
import { loggingServiceMock } from '../../logging/logging_service.mock';
import { PluginWrapper } from '../plugin';
-import { PluginsConfig } from '../plugins_config';
+import { PluginsConfig, config } from '../plugins_config';
import { discover } from './plugins_discovery';
const TEST_PLUGIN_SEARCH_PATHS = {
@@ -58,10 +58,6 @@ beforeEach(() => {
}
});
- mockStat.mockImplementation((path, cb) =>
- cb(null, { isDirectory: () => !path.includes('non-dir') })
- );
-
mockStat.mockImplementation((path, cb) => {
if (path.includes('9-inaccessible-dir')) {
cb(new Error(`ENOENT (disappeared between "readdir" and "stat").`));
@@ -125,6 +121,7 @@ test('properly iterates through plugin search locations', async () => {
env,
logger
);
+ await configService.setSchema(config.path, config.schema);
const pluginsConfig = await configService
.atPath('plugins', PluginsConfig)
diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts
index 6faef56a43ce7..9a1107b979fc5 100644
--- a/src/core/server/plugins/index.ts
+++ b/src/core/server/plugins/index.ts
@@ -18,7 +18,7 @@
*/
export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plugins_service';
-
+export { config } from './plugins_config';
/** @internal */
export { isNewPlatformPlugin } from './discovery';
/** @internal */
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index cb4203244e86d..0ce4ba2666198 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -19,9 +19,12 @@
import { join } from 'path';
import { BehaviorSubject } from 'rxjs';
-import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
+import { schema } from '@kbn/config-schema';
+
+import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
+import { configServiceMock } from '../config/config_service.mock';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
@@ -57,7 +60,9 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
};
}
-let configService: ConfigService;
+const configService = configServiceMock.create();
+configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true }));
+
let env: Env;
let coreContext: CoreContext;
const setupDeps = {
@@ -67,13 +72,7 @@ const setupDeps = {
beforeEach(() => {
env = Env.createDefault(getEnvOptions());
- configService = new ConfigService(
- new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })),
- env,
- logger
- );
-
- coreContext = { env, logger, configService };
+ coreContext = { env, logger, configService: configService as any };
});
afterEach(() => {
@@ -263,3 +262,70 @@ test('`stop` calls `stop` defined by the plugin instance', async () => {
await expect(plugin.stop()).resolves.toBeUndefined();
expect(mockPluginInstance.stop).toHaveBeenCalledTimes(1);
});
+
+describe('#getConfigSchema()', () => {
+ it('reads config schema from plugin', () => {
+ const pluginSchema = schema.any();
+ jest.doMock(
+ 'plugin-with-schema/server',
+ () => ({
+ config: {
+ schema: pluginSchema,
+ },
+ }),
+ { virtual: true }
+ );
+ const manifest = createPluginManifest();
+ const plugin = new PluginWrapper(
+ 'plugin-with-schema',
+ manifest,
+ createPluginInitializerContext(coreContext, manifest)
+ );
+
+ expect(plugin.getConfigSchema()).toBe(pluginSchema);
+ });
+
+ it('returns null if config definition not specified', () => {
+ jest.doMock('plugin-with-no-definition/server', () => ({}), { virtual: true });
+ const manifest = createPluginManifest();
+ const plugin = new PluginWrapper(
+ 'plugin-with-no-definition',
+ manifest,
+ createPluginInitializerContext(coreContext, manifest)
+ );
+ expect(plugin.getConfigSchema()).toBe(null);
+ });
+
+ it('returns null for plugins without a server part', () => {
+ const manifest = createPluginManifest({ server: false });
+ const plugin = new PluginWrapper(
+ 'plugin-with-no-definition',
+ manifest,
+ createPluginInitializerContext(coreContext, manifest)
+ );
+ expect(plugin.getConfigSchema()).toBe(null);
+ });
+
+ it('throws if plugin contains invalid schema', () => {
+ jest.doMock(
+ 'plugin-invalid-schema/server',
+ () => ({
+ config: {
+ schema: {
+ validate: () => null,
+ },
+ },
+ }),
+ { virtual: true }
+ );
+ const manifest = createPluginManifest();
+ const plugin = new PluginWrapper(
+ 'plugin-invalid-schema',
+ manifest,
+ createPluginInitializerContext(coreContext, manifest)
+ );
+ expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot(
+ `"Configuration schema expected to be an instance of Type"`
+ );
+ });
+});
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index bedb374536970..d47ed2a742fd7 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -19,10 +19,15 @@
import { join } from 'path';
import typeDetect from 'type-detect';
+
+import { Type } from '@kbn/config-schema';
+
import { ConfigPath } from '../config';
import { Logger } from '../logging';
import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context';
+export type PluginConfigSchema = Type | null;
+
/**
* Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays
* that use it as a key or value more obvious.
@@ -237,6 +242,25 @@ export class PluginWrapper<
this.instance = undefined;
}
+ public getConfigSchema(): PluginConfigSchema {
+ if (!this.manifest.server) {
+ return null;
+ }
+ const pluginPathServer = join(this.path, 'server');
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const pluginDefinition = require(pluginPathServer);
+
+ if (!('config' in pluginDefinition)) {
+ this.log.debug(`"${pluginPathServer}" does not export "config".`);
+ return null;
+ }
+
+ if (!(pluginDefinition.config.schema instanceof Type)) {
+ throw new Error('Configuration schema expected to be an instance of Type');
+ }
+ return pluginDefinition.config.schema;
+ }
+
private createPluginInstance() {
this.log.debug('Initializing plugin');
diff --git a/src/core/server/plugins/plugins_config.ts b/src/core/server/plugins/plugins_config.ts
index 9e440748dff4a..d2c258faab308 100644
--- a/src/core/server/plugins/plugins_config.ts
+++ b/src/core/server/plugins/plugins_config.ts
@@ -30,7 +30,11 @@ const pluginsSchema = schema.object({
paths: schema.arrayOf(schema.string(), { defaultValue: [] }),
});
-type PluginsConfigType = TypeOf;
+export type PluginsConfigType = TypeOf;
+export const config = {
+ path: 'plugins',
+ schema: pluginsSchema,
+};
/** @internal */
export class PluginsConfig {
@@ -51,10 +55,10 @@ export class PluginsConfig {
*/
public readonly additionalPluginPaths: ReadonlyArray;
- constructor(config: PluginsConfigType, env: Env) {
- this.initialize = config.initialize;
+ constructor(rawConfig: PluginsConfigType, env: Env) {
+ this.initialize = rawConfig.initialize;
this.pluginSearchPaths = env.pluginSearchPaths;
// Only allow custom plugin paths in dev.
- this.additionalPluginPaths = env.mode.dev ? config.paths : [];
+ this.additionalPluginPaths = env.mode.dev ? rawConfig.paths : [];
}
}
diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts
new file mode 100644
index 0000000000000..59b6f7fbd1026
--- /dev/null
+++ b/src/core/server/plugins/plugins_service.mock.ts
@@ -0,0 +1,34 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginsService } from './plugins_service';
+
+type ServiceContract = PublicMethodsOf;
+const createServiceMock = () => {
+ const mocked: jest.Mocked = {
+ setup: jest.fn(),
+ start: jest.fn(),
+ stop: jest.fn(),
+ };
+ return mocked;
+};
+
+export const pluginServiceMock = {
+ create: createServiceMock,
+};
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 774639758738c..0721000318133 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -19,8 +19,9 @@
import { mockDiscover, mockPackage } from './plugins_service.test.mocks';
-import { resolve } from 'path';
+import { resolve, join } from 'path';
import { BehaviorSubject, from } from 'rxjs';
+import { schema } from '@kbn/config-schema';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
@@ -31,6 +32,7 @@ import { PluginDiscoveryError } from './discovery';
import { PluginWrapper } from './plugin';
import { PluginsService } from './plugins_service';
import { PluginsSystem } from './plugins_system';
+import { config } from './plugins_config';
const MockPluginsSystem: jest.Mock = PluginsSystem as any;
@@ -43,7 +45,14 @@ const setupDeps = {
http: httpServiceMock.createSetupContract(),
};
const logger = loggingServiceMock.create();
-beforeEach(() => {
+
+['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach(path => {
+ jest.doMock(join(path, 'server'), () => ({}), {
+ virtual: true,
+ });
+});
+
+beforeEach(async () => {
mockPackage.raw = {
branch: 'feature-v1',
version: 'v1',
@@ -61,6 +70,7 @@ beforeEach(() => {
env,
logger
);
+ await configService.setSchema(config.path, config.schema);
pluginsService = new PluginsService({ env, logger, configService });
[mockPluginSystem] = MockPluginsSystem.mock.instances as any;
@@ -326,3 +336,40 @@ test('`stop` stops plugins system', async () => {
await pluginsService.stop();
expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
});
+
+test('`setup` registers plugin config schema in config service', async () => {
+ const configSchema = schema.string();
+ jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve());
+ jest.doMock(
+ join('path-with-schema', 'server'),
+ () => ({
+ config: {
+ schema: configSchema,
+ },
+ }),
+ {
+ virtual: true,
+ }
+ );
+ mockDiscover.mockReturnValue({
+ error$: from([]),
+ plugin$: from([
+ new PluginWrapper(
+ 'path-with-schema',
+ {
+ id: 'some-id',
+ version: 'some-version',
+ configPath: 'path',
+ kibanaVersion: '7.0.0',
+ requiredPlugins: [],
+ optionalPlugins: [],
+ server: true,
+ ui: true,
+ },
+ { logger } as any
+ ),
+ ]),
+ });
+ await pluginsService.setup(setupDeps);
+ expect(configService.setSchema).toBeCalledWith('path', configSchema);
+});
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 98fa08931e23d..e26f0f5d22c10 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -130,6 +130,10 @@ export class PluginsService implements CoreService {
+ const schema = plugin.getConfigSchema();
+ if (schema) {
+ await this.coreContext.configService.setSchema(plugin.configPath, schema);
+ }
const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath);
if (pluginEnableStatuses.has(plugin.name)) {
diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts
index 2ae05556663f3..9754c1b03d030 100644
--- a/src/core/server/plugins/plugins_system.test.ts
+++ b/src/core/server/plugins/plugins_system.test.ts
@@ -23,9 +23,11 @@ import {
} from './plugins_system.test.mocks';
import { BehaviorSubject } from 'rxjs';
-import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
+
+import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
+import { configServiceMock } from '../config/config_service.mock';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
@@ -58,7 +60,8 @@ function createPlugin(
}
let pluginsSystem: PluginsSystem;
-let configService: ConfigService;
+const configService = configServiceMock.create();
+configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true }));
let env: Env;
let coreContext: CoreContext;
const setupDeps = {
@@ -68,13 +71,7 @@ const setupDeps = {
beforeEach(() => {
env = Env.createDefault(getEnvOptions());
- configService = new ConfigService(
- new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })),
- env,
- logger
- );
-
- coreContext = { env, logger, configService };
+ coreContext = { env, logger, configService: configService as any };
pluginsSystem = new PluginsSystem(coreContext);
});
diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts
index 602fa2368a766..5754e5a5b9321 100644
--- a/src/core/server/root/index.test.mocks.ts
+++ b/src/core/server/root/index.test.mocks.ts
@@ -19,7 +19,7 @@
import { loggingServiceMock } from '../logging/logging_service.mock';
export const logger = loggingServiceMock.create();
-jest.doMock('../logging', () => ({
+jest.doMock('../logging/logging_service', () => ({
LoggingService: jest.fn(() => logger),
}));
@@ -29,5 +29,10 @@ jest.doMock('../config/config_service', () => ({
ConfigService: jest.fn(() => configService),
}));
-export const mockServer = { setup: jest.fn(), stop: jest.fn() };
+export const mockServer = {
+ setupConfigSchemas: jest.fn(),
+ setup: jest.fn(),
+ stop: jest.fn(),
+ configService,
+};
jest.mock('../server', () => ({ Server: jest.fn(() => mockServer) }));
diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts
index d3125543f4189..ff4c9da4bcc9a 100644
--- a/src/core/server/root/index.ts
+++ b/src/core/server/root/index.ts
@@ -20,7 +20,7 @@
import { ConnectableObservable, Observable, Subscription } from 'rxjs';
import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators';
-import { Config, ConfigService, Env } from '../config';
+import { Config, Env } from '../config';
import { Logger, LoggerFactory, LoggingConfig, LoggingService } from '../logging';
import { Server } from '../server';
@@ -29,30 +29,27 @@ import { Server } from '../server';
*/
export class Root {
public readonly logger: LoggerFactory;
- private readonly configService: ConfigService;
private readonly log: Logger;
- private readonly server: Server;
private readonly loggingService: LoggingService;
+ private readonly server: Server;
private loggingConfigSubscription?: Subscription;
constructor(
config$: Observable,
- private readonly env: Env,
+ env: Env,
private readonly onShutdown?: (reason?: Error | string) => void
) {
this.loggingService = new LoggingService();
this.logger = this.loggingService.asLoggerFactory();
this.log = this.logger.get('root');
-
- this.configService = new ConfigService(config$, env, this.logger);
- this.server = new Server(this.configService, this.logger, this.env);
+ this.server = new Server(config$, env, this.logger);
}
public async setup() {
- this.log.debug('setting up root');
-
try {
+ await this.server.setupConfigSchemas();
await this.setupLogging();
+ this.log.debug('setting up root');
return await this.server.setup();
} catch (e) {
await this.shutdown(e);
@@ -62,7 +59,6 @@ export class Root {
public async start() {
this.log.debug('starting root');
-
try {
return await this.server.start();
} catch (e) {
@@ -98,10 +94,11 @@ export class Root {
}
private async setupLogging() {
+ const { configService } = this.server;
// Stream that maps config updates to logger updates, including update failures.
- const update$ = this.configService.getConfig$().pipe(
+ const update$ = configService.getConfig$().pipe(
// always read the logging config when the underlying config object is re-read
- switchMap(() => this.configService.atPath('logging', LoggingConfig)),
+ switchMap(() => configService.atPath('logging', LoggingConfig)),
map(config => this.loggingService.upgrade(config)),
// This specifically console.logs because we were not able to configure the logger.
// eslint-disable-next-line no-console
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index d4ba9e3bb8208..ddee8e3eb07e3 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -61,9 +61,8 @@ export class ConfigService {
// Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts
constructor(config$: Observable, env: Env, logger: LoggerFactory);
- // Warning: (ae-forgotten-export) The symbol "ConfigPath" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ConfigWithSchema" needs to be exported by the entry point index.d.ts
- atPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable;
+ atPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable;
getConfig$(): Observable;
// (undocumented)
getUnusedPaths(): Promise;
@@ -72,7 +71,9 @@ export class ConfigService {
// (undocumented)
isEnabledAtPath(path: ConfigPath): Promise;
optionalAtPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable;
-}
+ // Warning: (ae-forgotten-export) The symbol "ConfigPath" needs to be exported by the entry point index.d.ts
+ setSchema(path: ConfigPath, schema: Type): Promise;
+ }
// @public (undocumented)
export interface CoreSetup {
diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts
index 6b673b7a68089..257b9e72fd081 100644
--- a/src/core/server/server.test.ts
+++ b/src/core/server/server.test.ts
@@ -22,17 +22,16 @@ import {
httpService,
mockLegacyService,
mockPluginsService,
+ configService,
} from './index.test.mocks';
import { BehaviorSubject } from 'rxjs';
-import { Env } from './config';
+import { Env, Config, ObjectToConfigAdapter } from './config';
import { Server } from './server';
import { getEnvOptions } from './config/__mocks__/env';
-import { configServiceMock } from './config/config_service.mock';
import { loggingServiceMock } from './logging/logging_service.mock';
-const configService = configServiceMock.create();
const env = new Env('.', getEnvOptions());
const logger = loggingServiceMock.create();
@@ -42,48 +41,35 @@ beforeEach(() => {
afterEach(() => {
jest.clearAllMocks();
-
- configService.atPath.mockReset();
- httpService.setup.mockClear();
- httpService.start.mockClear();
- httpService.stop.mockReset();
- elasticsearchService.setup.mockReset();
- elasticsearchService.stop.mockReset();
- mockPluginsService.setup.mockReset();
- mockPluginsService.stop.mockReset();
- mockLegacyService.setup.mockReset();
- mockLegacyService.start.mockReset();
- mockLegacyService.stop.mockReset();
});
+const config$ = new BehaviorSubject(new ObjectToConfigAdapter({}));
test('sets up services on "setup"', async () => {
- const mockPluginsServiceSetup = new Map([['some-plugin', 'some-value']]);
- mockPluginsService.setup.mockReturnValue(Promise.resolve(mockPluginsServiceSetup));
-
- const server = new Server(configService as any, logger, env);
+ const server = new Server(config$, env, logger);
expect(httpService.setup).not.toHaveBeenCalled();
expect(elasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
- expect(mockLegacyService.start).not.toHaveBeenCalled();
+ expect(mockLegacyService.setup).not.toHaveBeenCalled();
await server.setup();
expect(httpService.setup).toHaveBeenCalledTimes(1);
expect(elasticsearchService.setup).toHaveBeenCalledTimes(1);
expect(mockPluginsService.setup).toHaveBeenCalledTimes(1);
+ expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
});
test('runs services on "start"', async () => {
- const mockPluginsServiceSetup = new Map([['some-plugin', 'some-value']]);
- mockPluginsService.setup.mockReturnValue(Promise.resolve(mockPluginsServiceSetup));
-
- const server = new Server(configService as any, logger, env);
+ const server = new Server(config$, env, logger);
expect(httpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.setup();
+
+ expect(httpService.start).not.toHaveBeenCalled();
+ expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.start();
expect(httpService.start).toHaveBeenCalledTimes(1);
@@ -93,13 +79,13 @@ test('runs services on "start"', async () => {
test('does not fail on "setup" if there are unused paths detected', async () => {
configService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']);
- const server = new Server(configService as any, logger, env);
+ const server = new Server(config$, env, logger);
+
await expect(server.setup()).resolves.toBeDefined();
- expect(loggingServiceMock.collect(logger)).toMatchSnapshot('unused paths logs');
});
test('stops services on "stop"', async () => {
- const server = new Server(configService as any, logger, env);
+ const server = new Server(config$, env, logger);
await server.setup();
@@ -115,3 +101,17 @@ test('stops services on "stop"', async () => {
expect(mockPluginsService.stop).toHaveBeenCalledTimes(1);
expect(mockLegacyService.stop).toHaveBeenCalledTimes(1);
});
+
+test(`doesn't setup core services if config validation fails`, async () => {
+ configService.setSchema.mockImplementation(() => {
+ throw new Error('invalid config');
+ });
+ const server = new Server(config$, env, logger);
+ await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"invalid config"`
+ );
+ expect(httpService.setup).not.toHaveBeenCalled();
+ expect(elasticsearchService.setup).not.toHaveBeenCalled();
+ expect(mockPluginsService.setup).not.toHaveBeenCalled();
+ expect(mockLegacyService.setup).not.toHaveBeenCalled();
+});
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 4d85c79b13e06..9a416546d02e5 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -16,24 +16,38 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { Observable } from 'rxjs';
+import { Type } from '@kbn/config-schema';
-import { ConfigService, Env } from './config';
+import { ConfigService, Env, Config, ConfigPath } from './config';
import { ElasticsearchService } from './elasticsearch';
import { HttpService, HttpServiceSetup, Router } from './http';
import { LegacyService } from './legacy';
import { Logger, LoggerFactory } from './logging';
-import { PluginsService } from './plugins';
+import { PluginsService, config as pluginsConfig } from './plugins';
+
+import { config as elasticsearchConfig } from './elasticsearch';
+import { config as httpConfig } from './http';
+import { config as loggingConfig } from './logging';
+import { config as devConfig } from './dev';
export class Server {
+ public readonly configService: ConfigService;
private readonly elasticsearch: ElasticsearchService;
private readonly http: HttpService;
private readonly plugins: PluginsService;
private readonly legacy: LegacyService;
private readonly log: Logger;
- constructor(configService: ConfigService, logger: LoggerFactory, env: Env) {
- const core = { env, configService, logger };
- this.log = logger.get('server');
+ constructor(
+ readonly config$: Observable,
+ readonly env: Env,
+ private readonly logger: LoggerFactory
+ ) {
+ this.log = this.logger.get('server');
+ this.configService = new ConfigService(config$, env, logger);
+
+ const core = { configService: this.configService, env, logger };
this.http = new HttpService(core);
this.plugins = new PluginsService(core);
this.legacy = new LegacyService(core);
@@ -47,6 +61,7 @@ export class Server {
this.registerDefaultRoute(httpSetup);
const elasticsearchServiceSetup = await this.elasticsearch.setup();
+
const pluginsSetup = await this.plugins.setup({
elasticsearch: elasticsearchServiceSetup,
http: httpSetup,
@@ -91,4 +106,18 @@ export class Server {
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' }));
httpSetup.registerRouter(router);
}
+
+ public async setupConfigSchemas() {
+ const schemas: Array<[ConfigPath, Type]> = [
+ [elasticsearchConfig.path, elasticsearchConfig.schema],
+ [loggingConfig.path, loggingConfig.schema],
+ [httpConfig.path, httpConfig.schema],
+ [pluginsConfig.path, pluginsConfig.schema],
+ [devConfig.path, devConfig.schema],
+ ];
+
+ for (const [path, schema] of schemas) {
+ await this.configService.setSchema(path, schema);
+ }
+ }
}
diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts
index e430488dd268b..538d3a3d07c9b 100644
--- a/src/plugins/testbed/server/index.ts
+++ b/src/plugins/testbed/server/index.ts
@@ -76,3 +76,7 @@ class Plugin {
export const plugin = (initializerContext: PluginInitializerContext) =>
new Plugin(initializerContext);
+
+export const config = {
+ schema: TestBedConfig.schema,
+};