Make core
responsible for reading and merging of config files. Simplify legacy config adapter. (#21956)
This commit is contained in:
parent
22d3bcf334
commit
6034cc7a63
34 changed files with 711 additions and 786 deletions
|
@ -1 +0,0 @@
|
|||
foo: "${KBN_NON_EXISTENT_ENV_VAR}"
|
|
@ -1,2 +0,0 @@
|
|||
foo: 1
|
||||
bar: true
|
|
@ -1,2 +0,0 @@
|
|||
foo: 2
|
||||
baz: bonkers
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
7
src/core/server/config/__tests__/__fixtures__/one.yml
Normal file
7
src/core/server/config/__tests__/__fixtures__/one.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
foo: 1
|
||||
bar: true
|
||||
xyz: ['1', '2']
|
||||
abc:
|
||||
def: test
|
||||
qwe: 1
|
||||
pom.bom: 3
|
7
src/core/server/config/__tests__/__fixtures__/two.yml
Normal file
7
src/core/server/config/__tests__/__fixtures__/two.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
foo: 2
|
||||
baz: bonkers
|
||||
xyz: ['3', '4']
|
||||
abc:
|
||||
ghi: test2
|
||||
qwe: 2
|
||||
pom.mob: 4
|
|
@ -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"`;
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {}
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
`;
|
|
@ -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();
|
||||
});
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue