Make core responsible for reading and merging of config files. Simplify legacy config adapter. (#21956)

This commit is contained in:
Aleh Zasypkin 2018-08-28 14:17:55 +02:00 committed by GitHub
parent 22d3bcf334
commit 6034cc7a63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 711 additions and 786 deletions

View file

@ -1 +0,0 @@
foo: "${KBN_NON_EXISTENT_ENV_VAR}"

View file

@ -1,2 +0,0 @@
foo: 1
bar: true

View file

@ -1,2 +0,0 @@
foo: 2
baz: bonkers

View file

@ -23,7 +23,7 @@ import { relative, resolve } from 'path';
import { safeDump } from 'js-yaml';
import es from 'event-stream';
import stripAnsi from 'strip-ansi';
import { readYamlConfig } from '../read_yaml_config';
import { getConfigFromFiles } from '../../../core/server/config';
const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml');
const kibanaPath = follow('../../../../scripts/kibana.js');
@ -33,7 +33,7 @@ function follow(file) {
}
function setLoggingJson(enabled) {
const conf = readYamlConfig(testConfigFile);
const conf = getConfigFromFiles([testConfigFile]);
conf.logging = conf.logging || {};
conf.logging.json = enabled;

View file

@ -1,63 +0,0 @@
/*
* 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 { isArray, isPlainObject, forOwn, set, transform, isString } from 'lodash';
import { readFileSync as read } from 'fs';
import { safeLoad } from 'js-yaml';
function replaceEnvVarRefs(val) {
return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => {
if (process.env[envVarName] !== undefined) {
return process.env[envVarName];
} else {
throw new Error(`Unknown environment variable referenced in config : ${envVarName}`);
}
});
}
export function merge(sources) {
return transform(sources, (merged, source) => {
forOwn(source, function apply(val, key) {
if (isPlainObject(val)) {
forOwn(val, function (subVal, subKey) {
apply(subVal, key + '.' + subKey);
});
return;
}
if (isArray(val)) {
set(merged, key, []);
val.forEach((subVal, i) => apply(subVal, key + '.' + i));
return;
}
if (isString(val)) {
val = replaceEnvVarRefs(val);
}
set(merged, key, val);
});
}, {});
}
export function readYamlConfig(paths) {
const files = [].concat(paths || []);
const yamls = files.map(path => safeLoad(read(path, 'utf8')));
return merge(yamls);
}

View file

@ -1,98 +0,0 @@
/*
* 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 { relative, resolve } from 'path';
import { readYamlConfig } from './read_yaml_config';
function fixture(name) {
return resolve(__dirname, '__fixtures__', name);
}
describe('cli/serve/read_yaml_config', function () {
it('reads a single config file', function () {
const config = readYamlConfig(fixture('one.yml'));
expect(config).toEqual({
foo: 1,
bar: true,
});
});
it('reads and merged multiple config file', function () {
const config = readYamlConfig([
fixture('one.yml'),
fixture('two.yml')
]);
expect(config).toEqual({
foo: 2,
bar: true,
baz: 'bonkers'
});
});
it('should inject an environment variable value when setting a value with ${ENV_VAR}', function () {
process.env.KBN_ENV_VAR1 = 'val1';
process.env.KBN_ENV_VAR2 = 'val2';
const config = readYamlConfig([ fixture('en_var_ref_config.yml') ]);
expect(config).toEqual({
foo: 1,
bar: 'pre-val1-mid-val2-post',
elasticsearch: {
requestHeadersWhitelist: ['val1', 'val2']
}
});
});
it('should thow an exception when referenced environment variable in a config value does not exist', function () {
expect(function () {
readYamlConfig([ fixture('invalid_en_var_ref_config.yml') ]);
}).toThrow();
});
describe('different cwd()', function () {
const originalCwd = process.cwd();
const tempCwd = resolve(__dirname);
beforeAll(() => process.chdir(tempCwd));
afterAll(() => process.chdir(originalCwd));
it('resolves relative files based on the cwd', function () {
const relativePath = relative(tempCwd, fixture('one.yml'));
const config = readYamlConfig(relativePath);
expect(config).toEqual({
foo: 1,
bar: true,
});
});
it('fails to load relative paths, not found because of the cwd', function () {
expect(function () {
const relativePath = relative(
resolve(__dirname, '../../'),
fixture('one.yml')
);
readYamlConfig(relativePath);
}).toThrowError(/ENOENT/);
});
});
});

View file

@ -25,7 +25,7 @@ import { resolve } from 'path';
import { fromRoot } from '../../utils';
import { getConfig } from '../../server/path';
import { Config } from '../../server/config/config';
import { readYamlConfig } from './read_yaml_config';
import { getConfigFromFiles } from '../../core/server/config';
import { readKeystore } from './read_keystore';
import { transformDeprecations } from '../../server/config/transform_deprecations';
@ -80,7 +80,7 @@ const pluginDirCollector = pathCollector();
const pluginPathCollector = pathCollector();
function readServerSettings(opts, extraCliOptions) {
const settings = readYamlConfig(opts.config);
const settings = getConfigFromFiles([].concat(opts.config || []));
const set = _.partial(_.set, settings);
const get = _.partial(_.get, settings);
const has = _.partial(_.has, settings);
@ -256,7 +256,7 @@ export default function (program) {
// If new platform config subscription is active, let's notify it with the updated config.
if (kbnServer.newPlatform) {
kbnServer.newPlatform.updateConfig(config);
kbnServer.newPlatform.updateConfig(config.get());
}
});

View file

@ -0,0 +1,7 @@
foo: 1
bar: true
xyz: ['1', '2']
abc:
def: test
qwe: 1
pom.bom: 3

View file

@ -0,0 +1,7 @@
foo: 2
baz: bonkers
xyz: ['3', '4']
abc:
ghi: test2
qwe: 2
pom.mob: 4

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`different cwd() resolves relative files based on the cwd 1`] = `
Object {
"abc": Object {
"def": "test",
"qwe": 1,
},
"bar": true,
"foo": 1,
"pom": Object {
"bom": 3,
},
"xyz": Array [
"1",
"2",
],
}
`;
exports[`reads and merges multiple yaml files from file system and parses to json 1`] = `
Object {
"abc": Object {
"def": "test",
"ghi": "test2",
"qwe": 2,
},
"bar": true,
"baz": "bonkers",
"foo": 2,
"pom": Object {
"bom": 3,
"mob": 4,
},
"xyz": Array [
"3",
"4",
],
}
`;
exports[`reads single yaml from file system and parses to json 1`] = `
Object {
"pid": Object {
"enabled": true,
"file": "/var/run/kibana.pid",
},
}
`;
exports[`returns a deep object 1`] = `
Object {
"pid": Object {
"enabled": true,
"file": "/var/run/kibana.pid",
},
}
`;
exports[`should inject an environment variable value when setting a value with \${ENV_VAR} 1`] = `
Object {
"bar": "pre-val1-mid-val2-post",
"elasticsearch": Object {
"requestHeadersWhitelist": Array [
"val1",
"val2",
],
},
"foo": 1,
}
`;
exports[`should throw an exception when referenced environment variable in a config value does not exist 1`] = `"Unknown environment variable referenced in config : KBN_ENV_VAR1"`;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { ObjectToRawConfigAdapter, RawConfig } from '..';
import { Config, ObjectToConfigAdapter } from '..';
/**
* Overrides some config values with ones from argv.
@ -25,7 +25,7 @@ import { ObjectToRawConfigAdapter, RawConfig } from '..';
* @param config `RawConfig` instance to update config values for.
* @param argv Argv object with key/value pairs.
*/
export function overrideConfigWithArgv(config: RawConfig, argv: { [key: string]: any }) {
export function overrideConfigWithArgv(config: Config, argv: { [key: string]: any }) {
if (argv.port != null) {
config.set(['server', 'port'], argv.port);
}
@ -42,7 +42,7 @@ test('port', () => {
port: 123,
};
const config = new ObjectToRawConfigAdapter({
const config = new ObjectToConfigAdapter({
server: { port: 456 },
});
@ -56,7 +56,7 @@ test('host', () => {
host: 'example.org',
};
const config = new ObjectToRawConfigAdapter({
const config = new ObjectToConfigAdapter({
server: { host: 'org.example' },
});
@ -70,7 +70,7 @@ test('ignores unknown', () => {
unknown: 'some value',
};
const config = new ObjectToRawConfigAdapter({});
const config = new ObjectToConfigAdapter({});
jest.spyOn(config, 'set');
overrideConfigWithArgv(config, argv);

View file

@ -27,7 +27,7 @@ jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage }));
import { schema, Type, TypeOf } from '../schema';
import { ConfigService, ObjectToRawConfigAdapter } from '..';
import { ConfigService, ObjectToConfigAdapter } from '..';
import { logger } from '../../logging/__mocks__';
import { Env } from '../env';
import { getEnvOptions } from './__mocks__/env';
@ -36,7 +36,7 @@ const emptyArgv = getEnvOptions();
const defaultEnv = new Env('/kibana', emptyArgv);
test('returns config at path as observable', async () => {
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'foo' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.atPath('key', ExampleClassWithStringSchema);
@ -48,7 +48,7 @@ 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 ObjectToRawConfigAdapter({ key: 123 }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 123 }));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.atPath('key', ExampleClassWithStringSchema);
@ -61,7 +61,7 @@ test('throws if config at path does not match schema', async () => {
});
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: 'bar' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: 'bar' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.optionalAtPath('unique-name', ExampleClassWithStringSchema);
@ -71,7 +71,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 ObjectToRawConfigAdapter({ value: 'bar' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ value: 'bar' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.optionalAtPath('value', ExampleClassWithStringSchema);
@ -82,7 +82,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 ObjectToRawConfigAdapter({ key: 'value' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const valuesReceived: any[] = [];
@ -90,13 +90,13 @@ test("does not push new configs when reloading if config at path hasn't changed"
valuesReceived.push(config.value);
});
config$.next(new ObjectToRawConfigAdapter({ key: 'value' }));
config$.next(new ObjectToConfigAdapter({ key: 'value' }));
expect(valuesReceived).toEqual(['value']);
});
test('pushes new config when reloading and config at path has changed', async () => {
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'value' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const valuesReceived: any[] = [];
@ -104,7 +104,7 @@ test('pushes new config when reloading and config at path has changed', async ()
valuesReceived.push(config.value);
});
config$.next(new ObjectToRawConfigAdapter({ key: 'new value' }));
config$.next(new ObjectToConfigAdapter({ key: 'new value' }));
expect(valuesReceived).toEqual(['value', 'new value']);
});
@ -114,7 +114,7 @@ test("throws error if config class does not implement 'schema'", async () => {
class ExampleClass {}
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'value' }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const configs = configService.atPath('key', ExampleClass as any);
@ -147,7 +147,7 @@ test('tracks unhandled paths', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
configService.atPath('foo', createClassWithSchema(schema.string()));
@ -178,7 +178,7 @@ test('correctly passes context', async () => {
const env = new Env('/kibana', getEnvOptions());
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} }));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: {} }));
const configService = new ConfigService(config$, env, logger);
const configs = configService.atPath(
'foo',
@ -213,7 +213,7 @@ test('handles enabled path, but only marks the enabled path as used', async () =
},
};
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');
@ -231,7 +231,7 @@ test('handles enabled path when path is array', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath(['pid']);
@ -249,7 +249,7 @@ test('handles disabled path and marks config as used', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');
@ -262,7 +262,7 @@ test('handles disabled path and marks config as used', async () => {
test('treats config as enabled if config path is not present in config', async () => {
const initialConfig = {};
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');

View file

@ -17,49 +17,73 @@
* under the License.
*/
const mockGetConfigFromFile = jest.fn();
const mockGetConfigFromFiles = jest.fn();
jest.mock('../read_config', () => ({
getConfigFromFile: mockGetConfigFromFile,
getConfigFromFiles: mockGetConfigFromFiles,
}));
import { first } from 'rxjs/operators';
import { RawConfigService } from '../raw_config_service';
const configFile = '/config/kibana.yml';
const anotherConfigFile = '/config/kibana.dev.yml';
beforeEach(() => {
mockGetConfigFromFile.mockReset();
mockGetConfigFromFile.mockImplementation(() => ({}));
mockGetConfigFromFiles.mockReset();
mockGetConfigFromFiles.mockImplementation(() => ({}));
});
test('loads raw config when started', () => {
const configService = new RawConfigService(configFile);
test('loads single raw config when started', () => {
const configService = new RawConfigService([configFile]);
configService.loadConfig();
expect(mockGetConfigFromFile).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFile).toHaveBeenLastCalledWith(configFile);
expect(mockGetConfigFromFiles).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFiles).toHaveBeenLastCalledWith([configFile]);
});
test('re-reads the config when reloading', () => {
const configService = new RawConfigService(configFile);
test('loads multiple raw configs when started', () => {
const configService = new RawConfigService([configFile, anotherConfigFile]);
configService.loadConfig();
mockGetConfigFromFile.mockClear();
mockGetConfigFromFile.mockImplementation(() => ({ foo: 'bar' }));
expect(mockGetConfigFromFiles).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFiles).toHaveBeenLastCalledWith([configFile, anotherConfigFile]);
});
test('re-reads single config when reloading', () => {
const configService = new RawConfigService([configFile]);
configService.loadConfig();
mockGetConfigFromFiles.mockClear();
mockGetConfigFromFiles.mockImplementation(() => ({ foo: 'bar' }));
configService.reloadConfig();
expect(mockGetConfigFromFile).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFile).toHaveBeenLastCalledWith(configFile);
expect(mockGetConfigFromFiles).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFiles).toHaveBeenLastCalledWith([configFile]);
});
test('re-reads multiple configs when reloading', () => {
const configService = new RawConfigService([configFile, anotherConfigFile]);
configService.loadConfig();
mockGetConfigFromFiles.mockClear();
mockGetConfigFromFiles.mockImplementation(() => ({ foo: 'bar' }));
configService.reloadConfig();
expect(mockGetConfigFromFiles).toHaveBeenCalledTimes(1);
expect(mockGetConfigFromFiles).toHaveBeenLastCalledWith([configFile, anotherConfigFile]);
});
test('returns config at path as observable', async () => {
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'value' }));
const configService = new RawConfigService(configFile);
const configService = new RawConfigService([configFile]);
configService.loadConfig();
@ -73,9 +97,9 @@ test('returns config at path as observable', async () => {
});
test("does not push new configs when reloading if config at path hasn't changed", async () => {
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'value' }));
const configService = new RawConfigService(configFile);
const configService = new RawConfigService([configFile]);
configService.loadConfig();
@ -84,8 +108,8 @@ test("does not push new configs when reloading if config at path hasn't changed"
valuesReceived.push(config);
});
mockGetConfigFromFile.mockClear();
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
mockGetConfigFromFiles.mockClear();
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'value' }));
configService.reloadConfig();
@ -95,9 +119,9 @@ 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 () => {
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'value' }));
const configService = new RawConfigService(configFile);
const configService = new RawConfigService([configFile]);
configService.loadConfig();
@ -106,8 +130,8 @@ test('pushes new config when reloading and config at path has changed', async ()
valuesReceived.push(config);
});
mockGetConfigFromFile.mockClear();
mockGetConfigFromFile.mockImplementation(() => ({ key: 'new value' }));
mockGetConfigFromFiles.mockClear();
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'new value' }));
configService.reloadConfig();
@ -121,9 +145,9 @@ test('pushes new config when reloading and config at path has changed', async ()
test('completes config observables when stopped', done => {
expect.assertions(0);
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
mockGetConfigFromFiles.mockImplementation(() => ({ key: 'value' }));
const configService = new RawConfigService(configFile);
const configService = new RawConfigService([configFile]);
configService.loadConfig();

View file

@ -17,28 +17,63 @@
* under the License.
*/
import { getConfigFromFile } from '../read_config';
import { relative, resolve } from 'path';
import { getConfigFromFiles } from '../read_config';
const fixtureFile = (name: string) => `${__dirname}/__fixtures__/${name}`;
test('reads yaml from file system and parses to json', () => {
const config = getConfigFromFile(fixtureFile('config.yml'));
test('reads single yaml from file system and parses to json', () => {
const config = getConfigFromFiles([fixtureFile('config.yml')]);
expect(config).toEqual({
pid: {
enabled: true,
file: '/var/run/kibana.pid',
},
});
expect(config).toMatchSnapshot();
});
test('returns a deep object', () => {
const config = getConfigFromFile(fixtureFile('/config_flat.yml'));
const config = getConfigFromFiles([fixtureFile('/config_flat.yml')]);
expect(config).toEqual({
pid: {
enabled: true,
file: '/var/run/kibana.pid',
},
expect(config).toMatchSnapshot();
});
test('reads and merges multiple yaml files from file system and parses to json', () => {
const config = getConfigFromFiles([fixtureFile('/one.yml'), fixtureFile('/two.yml')]);
expect(config).toMatchSnapshot();
});
test('should inject an environment variable value when setting a value with ${ENV_VAR}', () => {
process.env.KBN_ENV_VAR1 = 'val1';
process.env.KBN_ENV_VAR2 = 'val2';
const config = getConfigFromFiles([fixtureFile('/en_var_ref_config.yml')]);
delete process.env.KBN_ENV_VAR1;
delete process.env.KBN_ENV_VAR2;
expect(config).toMatchSnapshot();
});
test('should throw an exception when referenced environment variable in a config value does not exist', () => {
expect(() =>
getConfigFromFiles([fixtureFile('/en_var_ref_config.yml')])
).toThrowErrorMatchingSnapshot();
});
describe('different cwd()', () => {
const originalCwd = process.cwd();
const tempCwd = resolve(__dirname);
beforeAll(() => process.chdir(tempCwd));
afterAll(() => process.chdir(originalCwd));
test('resolves relative files based on the cwd', () => {
const relativePath = relative(tempCwd, fixtureFile('/one.yml'));
const config = getConfigFromFiles([relativePath]);
expect(config).toMatchSnapshot();
});
test('fails to load relative paths, not found because of the cwd', () => {
const relativePath = relative(resolve(__dirname, '../../'), fixtureFile('/one.yml'));
expect(() => getConfigFromFiles([relativePath])).toThrowError(/ENOENT/);
});
});

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { ConfigPath } from './config_service';
export type ConfigPath = string | string[];
/**
* Represents raw config store.
* Represents config store.
*/
export interface RawConfig {
export interface Config {
/**
* Returns whether or not there is a config value located at the specified path.
* @param configPath Path to locate value at.
@ -49,4 +49,11 @@ export interface RawConfig {
* @returns List of the string config paths.
*/
getFlattenedPaths(): string[];
/**
* Returns a full copy of the underlying raw config object. Should be used ONLY
* in extreme cases when there is no other better way, e.g. bridging with the
* "legacy" systems that consume and process config in a different way.
*/
toRaw(): Record<string, any>;
}

View file

@ -21,14 +21,10 @@ import { isEqual } from 'lodash';
import { Observable } from 'rxjs';
import { distinctUntilChanged, first, map } from 'rxjs/operators';
import { Config, ConfigPath, ConfigWithSchema, Env } from '.';
import { Logger, LoggerFactory } from '../logging';
import { ConfigWithSchema } from './config_with_schema';
import { Env } from './env';
import { RawConfig } from './raw_config';
import { Type } from './schema';
export type ConfigPath = string | string[];
export class ConfigService {
private readonly log: Logger;
@ -39,7 +35,7 @@ export class ConfigService {
private readonly handledPaths: ConfigPath[] = [];
constructor(
private readonly config$: Observable<RawConfig>,
private readonly config$: Observable<Config>,
readonly env: Env,
logger: LoggerFactory
) {
@ -62,12 +58,12 @@ 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<Schema extends Type<any>, Config>(
public atPath<TSchema extends Type<any>, TConfig>(
path: ConfigPath,
ConfigClass: ConfigWithSchema<Schema, Config>
ConfigClass: ConfigWithSchema<TSchema, TConfig>
) {
return this.getDistinctRawConfig(path).pipe(
map(rawConfig => this.createConfig(path, rawConfig, ConfigClass))
return this.getDistinctConfig(path).pipe(
map(config => this.createConfig(path, config, ConfigClass))
);
}
@ -77,14 +73,13 @@ export class ConfigService {
*
* @see atPath
*/
public optionalAtPath<Schema extends Type<any>, Config>(
public optionalAtPath<TSchema extends Type<any>, TConfig>(
path: ConfigPath,
ConfigClass: ConfigWithSchema<Schema, Config>
ConfigClass: ConfigWithSchema<TSchema, TConfig>
) {
return this.getDistinctRawConfig(path).pipe(
return this.getDistinctConfig(path).pipe(
map(
rawConfig =>
rawConfig === undefined ? undefined : this.createConfig(path, rawConfig, ConfigClass)
config => (config === undefined ? undefined : this.createConfig(path, config, ConfigClass))
)
);
}
@ -93,13 +88,11 @@ export class ConfigService {
const enabledPath = createPluginEnabledPath(path);
const config = await this.config$.pipe(first()).toPromise();
if (!config.has(enabledPath)) {
return true;
}
const isEnabled = config.get(enabledPath);
if (isEnabled === false) {
// If the plugin is _not_ enabled, we mark the entire plugin path as
// handled, as it's expected that it won't be used.
@ -121,10 +114,10 @@ export class ConfigService {
return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths));
}
private createConfig<Schema extends Type<any>, Config>(
private createConfig<TSchema extends Type<any>, TConfig>(
path: ConfigPath,
rawConfig: {},
ConfigClass: ConfigWithSchema<Schema, Config>
config: Record<string, any>,
ConfigClass: ConfigWithSchema<TSchema, TConfig>
) {
const namespace = Array.isArray(path) ? path.join('.') : path;
@ -138,8 +131,8 @@ export class ConfigService {
);
}
const config = ConfigClass.schema.validate(
rawConfig,
const validatedConfig = ConfigClass.schema.validate(
config,
{
dev: this.env.mode.dev,
prod: this.env.mode.prod,
@ -147,10 +140,10 @@ export class ConfigService {
},
namespace
);
return new ConfigClass(config, this.env);
return new ConfigClass(validatedConfig, this.env);
}
private getDistinctRawConfig(path: ConfigPath) {
private getDistinctConfig(path: ConfigPath) {
this.markAsHandled(path);
return this.config$.pipe(map(config => config.get(path)), distinctUntilChanged(isEqual));

View file

@ -17,18 +17,11 @@
* under the License.
*/
/**
* This is a name of configuration node that is specifically dedicated to
* the configuration values used by the new platform only. Eventually all
* its nested values will be migrated to the stable config and this node
* will be deprecated.
*/
export const NEW_PLATFORM_CONFIG_ROOT = '__newPlatform';
export { ConfigService } from './config_service';
export { RawConfigService } from './raw_config_service';
export { RawConfig } from './raw_config';
export { Config, ConfigPath } from './config';
/** @internal */
export { ObjectToRawConfigAdapter } from './object_to_raw_config_adapter';
export { ObjectToConfigAdapter } from './object_to_config_adapter';
export { Env } from './env';
export { ConfigWithSchema } from './config_with_schema';
export { getConfigFromFiles } from './read_config';

View file

@ -17,32 +17,35 @@
* under the License.
*/
import { get, has, set } from 'lodash';
import { cloneDeep, get, has, set } from 'lodash';
import { ConfigPath } from './config_service';
import { RawConfig } from './raw_config';
import { Config, ConfigPath } from './';
/**
* Allows plain javascript object to behave like `RawConfig` instance.
* @internal
*/
export class ObjectToRawConfigAdapter implements RawConfig {
constructor(private readonly rawValue: { [key: string]: any }) {}
export class ObjectToConfigAdapter implements Config {
constructor(private readonly rawConfig: Record<string, any>) {}
public has(configPath: ConfigPath) {
return has(this.rawValue, configPath);
return has(this.rawConfig, configPath);
}
public get(configPath: ConfigPath) {
return get(this.rawValue, configPath);
return get(this.rawConfig, configPath);
}
public set(configPath: ConfigPath, value: any) {
set(this.rawValue, configPath, value);
set(this.rawConfig, configPath, value);
}
public getFlattenedPaths() {
return [...flattenObjectKeys(this.rawValue)];
return [...flattenObjectKeys(this.rawConfig)];
}
public toRaw() {
return cloneDeep(this.rawConfig);
}
}

View file

@ -17,52 +17,41 @@
* under the License.
*/
import { isEqual, isPlainObject } from 'lodash';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { cloneDeep, isEqual, isPlainObject } from 'lodash';
import { Observable, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import typeDetect from 'type-detect';
import { ObjectToRawConfigAdapter } from './object_to_raw_config_adapter';
import { RawConfig } from './raw_config';
import { getConfigFromFile } from './read_config';
// Used to indicate that no config has been received yet
const notRead = Symbol('config not yet read');
import { Config } from './config';
import { ObjectToConfigAdapter } from './object_to_config_adapter';
import { getConfigFromFiles } from './read_config';
export class RawConfigService {
/**
* The stream of configs read from the config file. Will be the symbol
* `notRead` before the config is initially read, and after that it can
* potentially be `null` for an empty yaml file.
* The stream of configs read from the config file.
*
* This is the _raw_ config before any overrides are applied.
*
* As we have a notion of a _current_ config we rely on a BehaviorSubject so
* every new subscription will immediately receive the current config.
*/
private readonly rawConfigFromFile$ = new BehaviorSubject<any>(notRead);
private readonly rawConfigFromFile$: ReplaySubject<Record<string, any>> = new ReplaySubject(1);
private readonly config$: Observable<RawConfig>;
private readonly config$: Observable<Config>;
constructor(readonly configFile: string) {
constructor(
readonly configFiles: ReadonlyArray<string>,
configAdapter: (rawConfig: Record<string, any>) => Config = rawConfig =>
new ObjectToConfigAdapter(rawConfig)
) {
this.config$ = this.rawConfigFromFile$.pipe(
filter(rawConfig => rawConfig !== notRead),
// We only want to update the config if there are changes to it.
distinctUntilChanged(isEqual),
map(rawConfig => {
// If the raw config is null, e.g. if empty config file, we default to
// an empty config
if (rawConfig == null) {
return new ObjectToRawConfigAdapter({});
}
if (isPlainObject(rawConfig)) {
// TODO Make config consistent, e.g. handle dots in keys
return new ObjectToRawConfigAdapter(rawConfig);
return configAdapter(cloneDeep(rawConfig));
}
throw new Error(`the raw config must be an object, got [${typeDetect(rawConfig)}]`);
}),
// We only want to update the config if there are changes to it
distinctUntilChanged(isEqual)
})
);
}
@ -70,8 +59,7 @@ export class RawConfigService {
* Read the initial Kibana config.
*/
public loadConfig() {
const config = getConfigFromFile(this.configFile);
this.rawConfigFromFile$.next(config);
this.rawConfigFromFile$.next(getConfigFromFiles(this.configFiles));
}
public stop() {

View file

@ -20,11 +20,43 @@
import { readFileSync } from 'fs';
import { safeLoad } from 'js-yaml';
import { isPlainObject, set } from 'lodash';
import { ensureDeepObject } from './ensure_deep_object';
const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8'));
export const getConfigFromFile = (configFile: string) => {
const yaml = readYaml(configFile);
return yaml == null ? yaml : ensureDeepObject(yaml);
function replaceEnvVarRefs(val: string) {
return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => {
const envVarValue = process.env[envVarName];
if (envVarValue !== undefined) {
return envVarValue;
}
throw new Error(`Unknown environment variable referenced in config : ${envVarName}`);
});
}
function merge(target: Record<string, any>, value: any, key?: string) {
if (isPlainObject(value) || Array.isArray(value)) {
for (const [subKey, subVal] of Object.entries(value)) {
merge(target, subVal, key ? `${key}.${subKey}` : subKey);
}
} else if (key !== undefined) {
set(target, key, typeof value === 'string' ? replaceEnvVarRefs(value) : value);
}
return target;
}
export const getConfigFromFiles = (configFiles: ReadonlyArray<string>) => {
let mergedYaml = {};
for (const configFile of configFiles) {
const yaml = readYaml(configFile);
if (yaml !== null) {
mergedYaml = merge(mergedYaml, yaml);
}
}
return ensureDeepObject(mergedYaml);
};

View file

@ -30,6 +30,8 @@ export class HttpsRedirectServer {
constructor(private readonly log: Logger) {}
public async start(config: HttpConfig) {
this.log.debug('starting http --> https redirect server');
if (!config.ssl.enabled || config.ssl.redirectHttpFromPort === undefined) {
throw new Error(
'Redirect server cannot be started when [ssl.enabled] is set to `false`' +
@ -37,10 +39,6 @@ export class HttpsRedirectServer {
);
}
this.log.info(
`starting HTTP --> HTTPS redirect server [${config.host}:${config.ssl.redirectHttpFromPort}]`
);
// Redirect server is configured in the same way as any other HTTP server
// within the platform with the only exception that it should always be a
// plain HTTP server, so we just ignore `tls` part of options.
@ -65,6 +63,7 @@ export class HttpsRedirectServer {
try {
await this.server.start();
this.log.debug(`http --> https redirect server running at ${this.server.info.uri}`);
} catch (err) {
if (err.code === 'EADDRINUSE') {
throw new Error(
@ -79,11 +78,12 @@ export class HttpsRedirectServer {
}
public async stop() {
this.log.info('stopping HTTPS redirect server');
if (this.server !== undefined) {
await this.server.stop();
this.server = undefined;
if (this.server === undefined) {
return;
}
this.log.debug('stopping http --> https redirect server');
await this.server.stop();
this.server = undefined;
}
}

View file

@ -43,7 +43,11 @@ export class Server {
const unhandledConfigPaths = await this.configService.getUnusedPaths();
if (unhandledConfigPaths.length > 0) {
throw new Error(`some config paths are not handled: ${JSON.stringify(unhandledConfigPaths)}`);
// We don't throw here since unhandled paths are verified by the "legacy"
// Kibana right now, but this will eventually change.
this.log.trace(
`some config paths are not handled by the core: ${JSON.stringify(unhandledConfigPaths)}`
);
}
}

View file

@ -1,45 +0,0 @@
/*
* 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.
*/
/**
* This is a partial mock of src/server/config/config.js.
*/
export class LegacyConfigMock {
public readonly set = jest.fn((key, value) => {
// Real legacy config throws error if key is not presented in the schema.
if (!this.rawData.has(key)) {
throw new TypeError(`Unknown schema key: ${key}`);
}
this.rawData.set(key, value);
});
public readonly get = jest.fn(key => {
// Real legacy config throws error if key is not presented in the schema.
if (!this.rawData.has(key)) {
throw new TypeError(`Unknown schema key: ${key}`);
}
return this.rawData.get(key);
});
public readonly has = jest.fn(key => this.rawData.has(key));
constructor(public rawData: Map<string, any> = new Map()) {}
}

View file

@ -1,74 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#get correctly handles paths that do not exist in legacy config. 1`] = `"Unknown schema key: one"`;
exports[`#get correctly handles paths that do not exist in legacy config. 2`] = `"Unknown schema key: one.two"`;
exports[`#get correctly handles paths that do not exist in legacy config. 3`] = `"Unknown schema key: one.three"`;
exports[`#get correctly handles silent logging config. 1`] = `
Object {
"appenders": Object {
"default": Object {
"kind": "legacy-appender",
"legacyLoggingConfig": Object {
"silent": true,
},
},
},
"root": Object {
"level": "off",
},
}
`;
exports[`#get correctly handles verbose file logging config with json format. 1`] = `
Object {
"appenders": Object {
"default": Object {
"kind": "legacy-appender",
"legacyLoggingConfig": Object {
"dest": "/some/path.log",
"json": true,
"verbose": true,
},
},
},
"root": Object {
"level": "all",
},
}
`;
exports[`#set correctly sets values for new platform config. 1`] = `
Object {
"plugins": Object {
"scanDirs": Array [
"bar",
],
},
}
`;
exports[`#set correctly sets values for new platform config. 2`] = `
Object {
"plugins": Object {
"scanDirs": Array [
"baz",
],
},
}
`;
exports[`#set tries to set values for paths that do not exist in legacy config. 1`] = `"Unknown schema key: unknown"`;
exports[`#set tries to set values for paths that do not exist in legacy config. 2`] = `"Unknown schema key: unknown.sub1"`;
exports[`#set tries to set values for paths that do not exist in legacy config. 3`] = `"Unknown schema key: unknown.sub2"`;
exports[`\`getFlattenedPaths\` returns paths from new platform config only. 1`] = `
Array [
"__newPlatform.known",
"__newPlatform.known2.sub",
]
`;

View file

@ -1,170 +0,0 @@
/*
* 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 { LegacyConfigToRawConfigAdapter } from '..';
import { LegacyConfigMock } from '../__mocks__/legacy_config_mock';
let legacyConfigMock: LegacyConfigMock;
let configAdapter: LegacyConfigToRawConfigAdapter;
beforeEach(() => {
legacyConfigMock = new LegacyConfigMock(new Map<string, any>([['__newPlatform', null]]));
configAdapter = new LegacyConfigToRawConfigAdapter(legacyConfigMock);
});
describe('#get', () => {
test('correctly handles paths that do not exist in legacy config.', () => {
expect(() => configAdapter.get('one')).toThrowErrorMatchingSnapshot();
expect(() => configAdapter.get(['one', 'two'])).toThrowErrorMatchingSnapshot();
expect(() => configAdapter.get(['one.three'])).toThrowErrorMatchingSnapshot();
});
test('returns undefined for new platform config values, even if they do not exist', () => {
expect(configAdapter.get(['__newPlatform', 'plugins'])).toBe(undefined);
});
test('returns new platform config values if they exist', () => {
configAdapter = new LegacyConfigToRawConfigAdapter(
new LegacyConfigMock(
new Map<string, any>([['__newPlatform', { plugins: { scanDirs: ['foo'] } }]])
)
);
expect(configAdapter.get(['__newPlatform', 'plugins'])).toEqual({
scanDirs: ['foo'],
});
expect(configAdapter.get('__newPlatform.plugins')).toEqual({
scanDirs: ['foo'],
});
});
test('correctly handles paths that do not need to be transformed.', () => {
legacyConfigMock.rawData = new Map<string, any>([
['one', 'value-one'],
['one.sub', 'value-one-sub'],
['container', { value: 'some' }],
]);
expect(configAdapter.get('one')).toEqual('value-one');
expect(configAdapter.get(['one', 'sub'])).toEqual('value-one-sub');
expect(configAdapter.get('one.sub')).toEqual('value-one-sub');
expect(configAdapter.get('container')).toEqual({ value: 'some' });
});
test('correctly handles silent logging config.', () => {
legacyConfigMock.rawData = new Map([['logging', { silent: true }]]);
expect(configAdapter.get('logging')).toMatchSnapshot();
});
test('correctly handles verbose file logging config with json format.', () => {
legacyConfigMock.rawData = new Map([
['logging', { verbose: true, json: true, dest: '/some/path.log' }],
]);
expect(configAdapter.get('logging')).toMatchSnapshot();
});
});
describe('#set', () => {
test('tries to set values for paths that do not exist in legacy config.', () => {
expect(() => configAdapter.set('unknown', 'value')).toThrowErrorMatchingSnapshot();
expect(() =>
configAdapter.set(['unknown', 'sub1'], 'sub-value-1')
).toThrowErrorMatchingSnapshot();
expect(() => configAdapter.set('unknown.sub2', 'sub-value-2')).toThrowErrorMatchingSnapshot();
});
test('correctly sets values for existing paths.', () => {
legacyConfigMock.rawData = new Map([['known', ''], ['known.sub1', ''], ['known.sub2', '']]);
configAdapter.set('known', 'value');
configAdapter.set(['known', 'sub1'], 'sub-value-1');
configAdapter.set('known.sub2', 'sub-value-2');
expect(legacyConfigMock.rawData.get('known')).toEqual('value');
expect(legacyConfigMock.rawData.get('known.sub1')).toEqual('sub-value-1');
expect(legacyConfigMock.rawData.get('known.sub2')).toEqual('sub-value-2');
});
test('correctly sets values for new platform config.', () => {
legacyConfigMock.rawData = new Map<string, any>([
['__newPlatform', { plugins: { scanDirs: ['foo'] } }],
]);
configAdapter = new LegacyConfigToRawConfigAdapter(legacyConfigMock);
configAdapter.set(['__newPlatform', 'plugins', 'scanDirs'], ['bar']);
expect(legacyConfigMock.rawData.get('__newPlatform')).toMatchSnapshot();
configAdapter.set('__newPlatform.plugins.scanDirs', ['baz']);
expect(legacyConfigMock.rawData.get('__newPlatform')).toMatchSnapshot();
});
});
describe('#has', () => {
test('returns false if config is not set', () => {
expect(configAdapter.has('unknown')).toBe(false);
expect(configAdapter.has(['unknown', 'sub1'])).toBe(false);
expect(configAdapter.has('unknown.sub2')).toBe(false);
});
test('returns false if new platform config is not set', () => {
expect(configAdapter.has('__newPlatform.unknown')).toBe(false);
expect(configAdapter.has(['__newPlatform', 'unknown'])).toBe(false);
});
test('returns true if config is set.', () => {
legacyConfigMock.rawData = new Map([
['known', 'foo'],
['known.sub1', 'bar'],
['known.sub2', 'baz'],
]);
expect(configAdapter.has('known')).toBe(true);
expect(configAdapter.has(['known', 'sub1'])).toBe(true);
expect(configAdapter.has('known.sub2')).toBe(true);
});
test('returns true if new platform config is set.', () => {
legacyConfigMock.rawData = new Map<string, any>([
['__newPlatform', { known: 'foo', known2: { sub: 'bar' } }],
]);
configAdapter = new LegacyConfigToRawConfigAdapter(legacyConfigMock);
expect(configAdapter.has('__newPlatform.known')).toBe(true);
expect(configAdapter.has('__newPlatform.known2')).toBe(true);
expect(configAdapter.has('__newPlatform.known2.sub')).toBe(true);
expect(configAdapter.has(['__newPlatform', 'known'])).toBe(true);
expect(configAdapter.has(['__newPlatform', 'known2'])).toBe(true);
expect(configAdapter.has(['__newPlatform', 'known2', 'sub'])).toBe(true);
});
});
test('`getFlattenedPaths` returns paths from new platform config only.', () => {
legacyConfigMock.rawData = new Map<string, any>([
['__newPlatform', { known: 'foo', known2: { sub: 'bar' } }],
['legacy', { known: 'baz' }],
]);
configAdapter = new LegacyConfigToRawConfigAdapter(legacyConfigMock);
expect(configAdapter.getFlattenedPaths()).toMatchSnapshot();
});

View file

@ -0,0 +1,102 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#get correctly handles server config. 1`] = `
Object {
"basePath": "/abc",
"cors": false,
"host": "host",
"maxPayload": 1000,
"port": 1234,
"rewriteBasePath": false,
"ssl": Object {
"enabled": true,
"keyPassphrase": "some-phrase",
"someNewValue": "new",
},
}
`;
exports[`#get correctly handles silent logging config. 1`] = `
Object {
"appenders": Object {
"default": Object {
"kind": "legacy-appender",
"legacyLoggingConfig": Object {
"silent": true,
},
},
},
"root": Object {
"level": "off",
},
}
`;
exports[`#get correctly handles verbose file logging config with json format. 1`] = `
Object {
"appenders": Object {
"default": Object {
"kind": "legacy-appender",
"legacyLoggingConfig": Object {
"dest": "/some/path.log",
"json": true,
"verbose": true,
},
},
},
"root": Object {
"level": "all",
},
}
`;
exports[`#getFlattenedPaths returns all paths of the underlying object. 1`] = `
Array [
"known",
"knownContainer.sub1",
"knownContainer.sub2",
"legacy.known",
]
`;
exports[`#set correctly sets values for existing paths. 1`] = `
Object {
"known": "value",
"knownContainer": Object {
"sub1": "sub-value-1",
"sub2": "sub-value-2",
},
}
`;
exports[`#set correctly sets values for paths that do not exist. 1`] = `
Object {
"unknown": "value",
}
`;
exports[`#toRaw returns a deep copy of the underlying raw config object. 1`] = `
Object {
"known": "foo",
"knownContainer": Object {
"sub1": "bar",
"sub2": "baz",
},
"legacy": Object {
"known": "baz",
},
}
`;
exports[`#toRaw returns a deep copy of the underlying raw config object. 2`] = `
Object {
"known": "bar",
"knownContainer": Object {
"sub1": "baz",
"sub2": "baz",
},
"legacy": Object {
"known": "baz",
},
}
`;

View file

@ -0,0 +1,178 @@
/*
* 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 { LegacyObjectToConfigAdapter } from '../legacy_object_to_config_adapter';
describe('#get', () => {
test('correctly handles paths that do not exist.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({});
expect(configAdapter.get('one')).not.toBeDefined();
expect(configAdapter.get(['one', 'two'])).not.toBeDefined();
expect(configAdapter.get(['one.three'])).not.toBeDefined();
});
test('correctly handles paths that do not need to be transformed.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
one: 'value-one',
two: {
sub: 'value-two-sub',
},
container: {
value: 'some',
},
});
expect(configAdapter.get('one')).toEqual('value-one');
expect(configAdapter.get(['two', 'sub'])).toEqual('value-two-sub');
expect(configAdapter.get('two.sub')).toEqual('value-two-sub');
expect(configAdapter.get('container')).toEqual({ value: 'some' });
});
test('correctly handles silent logging config.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
logging: { silent: true },
});
expect(configAdapter.get('logging')).toMatchSnapshot();
});
test('correctly handles verbose file logging config with json format.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
logging: { verbose: true, json: true, dest: '/some/path.log' },
});
expect(configAdapter.get('logging')).toMatchSnapshot();
});
test('correctly handles server config.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
server: {
autoListen: true,
basePath: '/abc',
cors: false,
host: 'host',
maxPayloadBytes: 1000,
port: 1234,
rewriteBasePath: false,
ssl: {
enabled: true,
keyPassphrase: 'some-phrase',
someNewValue: 'new',
},
someNotSupportedValue: 'val',
},
});
expect(configAdapter.get('server')).toMatchSnapshot();
});
});
describe('#set', () => {
test('correctly sets values for paths that do not exist.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({});
configAdapter.set('unknown', 'value');
configAdapter.set(['unknown', 'sub1'], 'sub-value-1');
configAdapter.set('unknown.sub2', 'sub-value-2');
expect(configAdapter.toRaw()).toMatchSnapshot();
});
test('correctly sets values for existing paths.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
known: '',
knownContainer: {
sub1: 'sub-1',
sub2: 'sub-2',
},
});
configAdapter.set('known', 'value');
configAdapter.set(['knownContainer', 'sub1'], 'sub-value-1');
configAdapter.set('knownContainer.sub2', 'sub-value-2');
expect(configAdapter.toRaw()).toMatchSnapshot();
});
});
describe('#has', () => {
test('returns false if config is not set', () => {
const configAdapter = new LegacyObjectToConfigAdapter({});
expect(configAdapter.has('unknown')).toBe(false);
expect(configAdapter.has(['unknown', 'sub1'])).toBe(false);
expect(configAdapter.has('unknown.sub2')).toBe(false);
});
test('returns true if config is set.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
known: 'foo',
knownContainer: {
sub1: 'bar',
sub2: 'baz',
},
});
expect(configAdapter.has('known')).toBe(true);
expect(configAdapter.has(['knownContainer', 'sub1'])).toBe(true);
expect(configAdapter.has('knownContainer.sub2')).toBe(true);
});
});
describe('#toRaw', () => {
test('returns a deep copy of the underlying raw config object.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
known: 'foo',
knownContainer: {
sub1: 'bar',
sub2: 'baz',
},
legacy: { known: 'baz' },
});
const firstRawCopy = configAdapter.toRaw();
configAdapter.set('known', 'bar');
configAdapter.set(['knownContainer', 'sub1'], 'baz');
const secondRawCopy = configAdapter.toRaw();
expect(firstRawCopy).not.toBe(secondRawCopy);
expect(firstRawCopy.knownContainer).not.toBe(secondRawCopy.knownContainer);
expect(firstRawCopy).toMatchSnapshot();
expect(secondRawCopy).toMatchSnapshot();
});
});
describe('#getFlattenedPaths', () => {
test('returns all paths of the underlying object.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
known: 'foo',
knownContainer: {
sub1: 'bar',
sub2: 'baz',
},
legacy: { known: 'baz' },
});
expect(configAdapter.getFlattenedPaths()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { ConfigPath, ObjectToConfigAdapter } from '../../config';
/**
* Represents logging config supported by the legacy platform.
*/
interface LegacyLoggingConfig {
silent?: boolean;
verbose?: boolean;
quiet?: boolean;
dest?: string;
json?: boolean;
events?: Record<string, string>;
}
/**
* Represents adapter between config provided by legacy platform and `RawConfig`
* supported by the current platform.
*/
export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
private static transformLogging(configValue: LegacyLoggingConfig = {}) {
const loggingConfig = {
appenders: {
default: { kind: 'legacy-appender', legacyLoggingConfig: configValue },
},
root: { level: 'info' },
};
if (configValue.silent) {
loggingConfig.root.level = 'off';
} else if (configValue.quiet) {
loggingConfig.root.level = 'error';
} else if (configValue.verbose) {
loggingConfig.root.level = 'all';
}
return loggingConfig;
}
private static transformServer(configValue: any = {}) {
// TODO: New platform uses just a subset of `server` config from the legacy platform,
// new values will be exposed once we need them (eg. customResponseHeaders or xsrf).
return {
basePath: configValue.basePath,
cors: configValue.cors,
host: configValue.host,
maxPayload: configValue.maxPayloadBytes,
port: configValue.port,
rewriteBasePath: configValue.rewriteBasePath,
ssl: configValue.ssl,
};
}
public get(configPath: ConfigPath) {
const configValue = super.get(configPath);
switch (configPath) {
case 'logging':
return LegacyObjectToConfigAdapter.transformLogging(configValue);
case 'server':
return LegacyObjectToConfigAdapter.transformServer(configValue);
default:
return configValue;
}
}
}

View file

@ -23,35 +23,29 @@ import { map } from 'rxjs/operators';
/** @internal */
export { LegacyPlatformProxifier } from './legacy_platform_proxifier';
/** @internal */
export { LegacyConfigToRawConfigAdapter, LegacyConfig } from './legacy_platform_config';
export { LegacyObjectToConfigAdapter } from './config/legacy_object_to_config_adapter';
import { LegacyConfig, LegacyConfigToRawConfigAdapter, LegacyPlatformProxifier } from '.';
import { LegacyObjectToConfigAdapter, LegacyPlatformProxifier } from '.';
import { Env } from '../config';
import { Root } from '../root';
import { BasePathProxyRoot } from '../root/base_path_proxy_root';
function initEnvironment(rawKbnServer: any, isDevClusterMaster = false) {
const config: LegacyConfig = rawKbnServer.config;
const legacyConfig$ = new BehaviorSubject(config);
const config$ = legacyConfig$.pipe(
map(legacyConfig => new LegacyConfigToRawConfigAdapter(legacyConfig))
);
const env = Env.createDefault({
// The core doesn't work with configs yet, everything is provided by the
// "legacy" Kibana, so we can have empty array here.
configs: [],
// `dev` is the only CLI argument we currently use.
cliArgs: { dev: config.get('env.dev') },
cliArgs: { dev: rawKbnServer.config.get('env.dev') },
isDevClusterMaster,
});
const legacyConfig$ = new BehaviorSubject<Record<string, any>>(rawKbnServer.config.get());
return {
config$,
config$: legacyConfig$.pipe(map(legacyConfig => new LegacyObjectToConfigAdapter(legacyConfig))),
env,
// Propagates legacy config updates to the new platform.
updateConfig(legacyConfig: LegacyConfig) {
updateConfig(legacyConfig: Record<string, any>) {
legacyConfig$.next(legacyConfig);
},
};

View file

@ -1,147 +0,0 @@
/*
* 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 { NEW_PLATFORM_CONFIG_ROOT, ObjectToRawConfigAdapter, RawConfig } from '../config';
import { ConfigPath } from '../config/config_service';
/**
* Represents legacy Kibana config class.
* @internal
*/
export interface LegacyConfig {
get: (configPath: string) => any;
set: (configPath: string, configValue: any) => void;
has: (configPath: string) => boolean;
}
/**
* Represents logging config supported by the legacy platform.
*/
interface LegacyLoggingConfig {
silent?: boolean;
verbose?: boolean;
quiet?: boolean;
dest?: string;
json?: boolean;
}
/**
* Represents adapter between config provided by legacy platform and `RawConfig`
* supported by the current platform.
*/
export class LegacyConfigToRawConfigAdapter implements RawConfig {
private static flattenConfigPath(configPath: ConfigPath) {
if (!Array.isArray(configPath)) {
return configPath;
}
return configPath.join('.');
}
private static transformLogging(configValue: LegacyLoggingConfig) {
const loggingConfig = {
appenders: {
default: { kind: 'legacy-appender', legacyLoggingConfig: configValue },
},
root: { level: 'info' },
};
if (configValue.silent) {
loggingConfig.root.level = 'off';
} else if (configValue.quiet) {
loggingConfig.root.level = 'error';
} else if (configValue.verbose) {
loggingConfig.root.level = 'all';
}
return loggingConfig;
}
private static transformServer(configValue: any) {
// TODO: New platform uses just a subset of `server` config from the legacy platform,
// new values will be exposed once we need them (eg. customResponseHeaders, cors or xsrf).
return {
basePath: configValue.basePath,
cors: configValue.cors,
host: configValue.host,
maxPayload: configValue.maxPayloadBytes,
port: configValue.port,
rewriteBasePath: configValue.rewriteBasePath,
ssl: configValue.ssl,
};
}
private static isNewPlatformConfig(configPath: ConfigPath) {
if (Array.isArray(configPath)) {
return configPath[0] === NEW_PLATFORM_CONFIG_ROOT;
}
return configPath.startsWith(NEW_PLATFORM_CONFIG_ROOT);
}
private newPlatformConfig: ObjectToRawConfigAdapter;
constructor(private readonly legacyConfig: LegacyConfig) {
this.newPlatformConfig = new ObjectToRawConfigAdapter({
[NEW_PLATFORM_CONFIG_ROOT]: legacyConfig.get(NEW_PLATFORM_CONFIG_ROOT) || {},
});
}
public has(configPath: ConfigPath) {
if (LegacyConfigToRawConfigAdapter.isNewPlatformConfig(configPath)) {
return this.newPlatformConfig.has(configPath);
}
return this.legacyConfig.has(LegacyConfigToRawConfigAdapter.flattenConfigPath(configPath));
}
public get(configPath: ConfigPath) {
if (LegacyConfigToRawConfigAdapter.isNewPlatformConfig(configPath)) {
return this.newPlatformConfig.get(configPath);
}
configPath = LegacyConfigToRawConfigAdapter.flattenConfigPath(configPath);
const configValue = this.legacyConfig.get(configPath);
switch (configPath) {
case 'logging':
return LegacyConfigToRawConfigAdapter.transformLogging(configValue);
case 'server':
return LegacyConfigToRawConfigAdapter.transformServer(configValue);
default:
return configValue;
}
}
public set(configPath: ConfigPath, value: any) {
if (LegacyConfigToRawConfigAdapter.isNewPlatformConfig(configPath)) {
return this.newPlatformConfig.set(configPath, value);
}
this.legacyConfig.set(LegacyConfigToRawConfigAdapter.flattenConfigPath(configPath), value);
}
public getFlattenedPaths() {
// This method is only used to detect unused config paths, but when we run
// new platform within the legacy one then the new platform is in charge of
// only `__newPlatform` config node and the legacy platform will check the rest.
return this.newPlatformConfig.getFlattenedPaths();
}
}

View file

@ -33,12 +33,12 @@ jest.mock('../../', () => ({ Server: jest.fn(() => mockServer) }));
import { BehaviorSubject } from 'rxjs';
import { filter, first } from 'rxjs/operators';
import { Root } from '../';
import { Env, RawConfig } from '../../config';
import { Config, Env } from '../../config';
import { getEnvOptions } from '../../config/__tests__/__mocks__/env';
import { logger } from '../../logging/__mocks__';
const env = new Env('.', getEnvOptions());
const config$ = new BehaviorSubject({} as RawConfig);
const config$ = new BehaviorSubject({} as Config);
const mockProcessExit = jest.spyOn(global.process, 'exit').mockImplementation(() => {
// noop

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { Observable, Subscription } from 'rxjs';
import { catchError, first, map, shareReplay } from 'rxjs/operators';
import { ConnectableObservable, Observable, Subscription } from 'rxjs';
import { catchError, first, map, publishReplay } from 'rxjs/operators';
import { Server } from '..';
import { ConfigService, Env, RawConfig } from '../config';
import { Config, ConfigService, Env } from '../config';
import { Logger, LoggerFactory, LoggingConfig, LoggingService } from '../logging';
@ -39,7 +39,7 @@ export class Root {
private loggingConfigSubscription?: Subscription;
constructor(
rawConfig$: Observable<RawConfig>,
config$: Observable<Config>,
private readonly env: Env,
private readonly onShutdown: OnShutdown = () => {
// noop
@ -49,7 +49,7 @@ export class Root {
this.logger = this.loggingService.asLoggerFactory();
this.log = this.logger.get('root');
this.configService = new ConfigService(rawConfig$, env, this.logger);
this.configService = new ConfigService(config$, env, this.logger);
}
public async start() {
@ -104,15 +104,23 @@ export class Root {
throw err;
}),
shareReplay(1)
);
publishReplay(1)
) as ConnectableObservable<void>;
// Wait for the first update to complete and throw if it fails.
// Subscription and wait for the first update to complete and throw if it fails.
const connectSubscription = update$.connect();
await update$.pipe(first()).toPromise();
// Send subsequent update failures to this.shutdown(), stopped via loggingConfigSubscription.
this.loggingConfigSubscription = update$.subscribe({
error: error => this.shutdown(error),
error: err => this.shutdown(err),
});
// Add subscription we got from `connect` so that we can dispose both of them
// at once. We can't inverse this and add consequent updates subscription to
// the one we got from `connect` because in the error case the latter will be
// automatically disposed before the error is forwarded to the former one so
// the shutdown logic won't be called.
this.loggingConfigSubscription.add(connectSubscription);
}
}

View file

@ -255,8 +255,4 @@ export default () => Joi.object({
locale: Joi.string().default('en'),
}).default(),
// This is a configuration node that is specifically handled by the config system
// in the new platform, and that the current platform doesn't need to handle at all.
__newPlatform: Joi.any(),
}).default();