From 545403143ef78c29d1051309ada3b91c3f5adb9a Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 14 Aug 2018 12:11:26 +0200 Subject: [PATCH] Make `core` responsible for reading and merging of config files. Simplify legacy config adapter. --- .../invalid_en_var_ref_config.yml | 1 - src/cli/serve/__fixtures__/one.yml | 2 - src/cli/serve/__fixtures__/two.yml | 2 - .../reload_logging_config.test.js | 4 +- src/cli/serve/read_yaml_config.js | 63 ------- src/cli/serve/read_yaml_config.test.js | 98 ---------- src/cli/serve/serve.js | 6 +- .../__fixtures__/en_var_ref_config.yml | 0 .../config/__tests__/__fixtures__/one.yml | 7 + .../config/__tests__/__fixtures__/two.yml | 7 + .../__snapshots__/read_config.test.ts.snap | 73 ++++++++ .../__tests__/raw_config_service.test.ts | 76 +++++--- .../config/__tests__/read_config.test.ts | 65 +++++-- src/core/server/config/config_service.ts | 6 +- src/core/server/config/index.ts | 11 +- .../config/object_to_raw_config_adapter.ts | 8 +- src/core/server/config/raw_config.ts | 9 +- src/core/server/config/raw_config_service.ts | 27 ++- src/core/server/config/read_config.ts | 38 +++- src/core/server/http/https_redirect_server.ts | 18 +- src/core/server/index.ts | 6 +- .../__mocks__/legacy_config_mock.ts | 45 ----- .../legacy_platform_config.test.ts.snap | 74 -------- .../__tests__/legacy_platform_config.test.ts | 170 ------------------ ..._object_to_raw_config_adapter.test.ts.snap | 76 ++++++++ ...egacy_object_to_raw_config_adapter.test.ts | 150 ++++++++++++++++ .../legacy_object_to_raw_config_adapter.ts | 83 +++++++++ src/core/server/legacy_compat/index.ts | 20 +-- .../legacy_compat/legacy_platform_config.ts | 147 --------------- src/server/config/schema.js | 4 - 30 files changed, 587 insertions(+), 709 deletions(-) delete mode 100644 src/cli/serve/__fixtures__/invalid_en_var_ref_config.yml delete mode 100644 src/cli/serve/__fixtures__/one.yml delete mode 100644 src/cli/serve/__fixtures__/two.yml delete mode 100644 src/cli/serve/read_yaml_config.js delete mode 100644 src/cli/serve/read_yaml_config.test.js rename src/{cli/serve => core/server/config/__tests__}/__fixtures__/en_var_ref_config.yml (100%) create mode 100644 src/core/server/config/__tests__/__fixtures__/one.yml create mode 100644 src/core/server/config/__tests__/__fixtures__/two.yml create mode 100644 src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap delete mode 100644 src/core/server/legacy_compat/__mocks__/legacy_config_mock.ts delete mode 100644 src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_config.test.ts.snap delete mode 100644 src/core/server/legacy_compat/__tests__/legacy_platform_config.test.ts create mode 100644 src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_raw_config_adapter.test.ts.snap create mode 100644 src/core/server/legacy_compat/config/__tests__/legacy_object_to_raw_config_adapter.test.ts create mode 100644 src/core/server/legacy_compat/config/legacy_object_to_raw_config_adapter.ts delete mode 100644 src/core/server/legacy_compat/legacy_platform_config.ts diff --git a/src/cli/serve/__fixtures__/invalid_en_var_ref_config.yml b/src/cli/serve/__fixtures__/invalid_en_var_ref_config.yml deleted file mode 100644 index 23458124e5f0e35..000000000000000 --- a/src/cli/serve/__fixtures__/invalid_en_var_ref_config.yml +++ /dev/null @@ -1 +0,0 @@ -foo: "${KBN_NON_EXISTENT_ENV_VAR}" diff --git a/src/cli/serve/__fixtures__/one.yml b/src/cli/serve/__fixtures__/one.yml deleted file mode 100644 index e577d50638d5f99..000000000000000 --- a/src/cli/serve/__fixtures__/one.yml +++ /dev/null @@ -1,2 +0,0 @@ -foo: 1 -bar: true diff --git a/src/cli/serve/__fixtures__/two.yml b/src/cli/serve/__fixtures__/two.yml deleted file mode 100644 index aef807fcaebe975..000000000000000 --- a/src/cli/serve/__fixtures__/two.yml +++ /dev/null @@ -1,2 +0,0 @@ -foo: 2 -baz: bonkers diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js index 100fccceadbac75..61e590f2b51d513 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ b/src/cli/serve/integration_tests/reload_logging_config.test.js @@ -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; diff --git a/src/cli/serve/read_yaml_config.js b/src/cli/serve/read_yaml_config.js deleted file mode 100644 index f2ad397abf7c395..000000000000000 --- a/src/cli/serve/read_yaml_config.js +++ /dev/null @@ -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); -} diff --git a/src/cli/serve/read_yaml_config.test.js b/src/cli/serve/read_yaml_config.test.js deleted file mode 100644 index 09898a25c45b002..000000000000000 --- a/src/cli/serve/read_yaml_config.test.js +++ /dev/null @@ -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/); - }); - }); - -}); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index ecb0ae3a53dc553..08495566d845ed9 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -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()); } }); diff --git a/src/cli/serve/__fixtures__/en_var_ref_config.yml b/src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml similarity index 100% rename from src/cli/serve/__fixtures__/en_var_ref_config.yml rename to src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml diff --git a/src/core/server/config/__tests__/__fixtures__/one.yml b/src/core/server/config/__tests__/__fixtures__/one.yml new file mode 100644 index 000000000000000..1dbe11095b19aec --- /dev/null +++ b/src/core/server/config/__tests__/__fixtures__/one.yml @@ -0,0 +1,7 @@ +foo: 1 +bar: true +xyz: ['1', '2'] +abc: + def: test + qwe: 1 +pom.bom: 3 diff --git a/src/core/server/config/__tests__/__fixtures__/two.yml b/src/core/server/config/__tests__/__fixtures__/two.yml new file mode 100644 index 000000000000000..2fd2963e5668cb9 --- /dev/null +++ b/src/core/server/config/__tests__/__fixtures__/two.yml @@ -0,0 +1,7 @@ +foo: 2 +baz: bonkers +xyz: ['3', '4'] +abc: + ghi: test2 + qwe: 2 +pom.mob: 4 diff --git a/src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap b/src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap new file mode 100644 index 000000000000000..c83e902d8d098f3 --- /dev/null +++ b/src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap @@ -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"`; diff --git a/src/core/server/config/__tests__/raw_config_service.test.ts b/src/core/server/config/__tests__/raw_config_service.test.ts index baa4d2f8fe92e28..66cc31bc77774ff 100644 --- a/src/core/server/config/__tests__/raw_config_service.test.ts +++ b/src/core/server/config/__tests__/raw_config_service.test.ts @@ -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(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(mockGetConfigFromFile).toHaveBeenCalledTimes(1); - expect(mockGetConfigFromFile).toHaveBeenLastCalledWith(configFile); + 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(); diff --git a/src/core/server/config/__tests__/read_config.test.ts b/src/core/server/config/__tests__/read_config.test.ts index 74683f4227929f2..b9aa3871b17947d 100644 --- a/src/core/server/config/__tests__/read_config.test.ts +++ b/src/core/server/config/__tests__/read_config.test.ts @@ -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).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(); + }); - expect(config).toEqual({ - pid: { - enabled: true, - file: '/var/run/kibana.pid', - }, + 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/); }); }); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index bd839e51cbcf563..80c7e8ef1c85f85 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -21,14 +21,10 @@ import { isEqual } from 'lodash'; import { Observable } from 'rxjs'; import { distinctUntilChanged, first, map } from 'rxjs/operators'; +import { ConfigPath, ConfigWithSchema, Env, RawConfig } 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; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 030fdca252d33de..387c96693d94521 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -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 { RawConfig, ConfigPath } from './raw_config'; /** @internal */ export { ObjectToRawConfigAdapter } from './object_to_raw_config_adapter'; export { Env } from './env'; export { ConfigWithSchema } from './config_with_schema'; +export { getConfigFromFiles } from './read_config'; diff --git a/src/core/server/config/object_to_raw_config_adapter.ts b/src/core/server/config/object_to_raw_config_adapter.ts index 3abd73f01fcb9a8..d3a86d4e18b9318 100644 --- a/src/core/server/config/object_to_raw_config_adapter.ts +++ b/src/core/server/config/object_to_raw_config_adapter.ts @@ -19,7 +19,7 @@ import { get, has, set } from 'lodash'; -import { ConfigPath } from './config_service'; +import { ConfigPath } from './'; import { RawConfig } from './raw_config'; /** @@ -27,7 +27,7 @@ import { RawConfig } from './raw_config'; * @internal */ export class ObjectToRawConfigAdapter implements RawConfig { - constructor(private readonly rawValue: { [key: string]: any }) {} + constructor(protected readonly rawValue: Record) {} public has(configPath: ConfigPath) { return has(this.rawValue, configPath); @@ -44,6 +44,10 @@ export class ObjectToRawConfigAdapter implements RawConfig { public getFlattenedPaths() { return [...flattenObjectKeys(this.rawValue)]; } + + public getRaw() { + return Object.freeze(this.rawValue); + } } function* flattenObjectKeys( diff --git a/src/core/server/config/raw_config.ts b/src/core/server/config/raw_config.ts index c87a6fe5768a5a8..ba8978703d6f7f3 100644 --- a/src/core/server/config/raw_config.ts +++ b/src/core/server/config/raw_config.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ConfigPath } from './config_service'; +export type ConfigPath = string | string[]; /** * Represents raw config store. @@ -49,4 +49,11 @@ export interface RawConfig { * @returns List of the string config paths. */ getFlattenedPaths(): string[]; + + /** + * Returns frozen raw underlying 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 drastically different way. + */ + getRaw(): Readonly>; } diff --git a/src/core/server/config/raw_config_service.ts b/src/core/server/config/raw_config_service.ts index ae1b99fd42650b7..0cafe1b68cdd561 100644 --- a/src/core/server/config/raw_config_service.ts +++ b/src/core/server/config/raw_config_service.ts @@ -17,14 +17,14 @@ * under the License. */ -import { isEqual, isPlainObject } from 'lodash'; +import { cloneDeep, isEqual, isPlainObject } from 'lodash'; import { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged, filter, 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'; +import { getConfigFromFiles } from './read_config'; // Used to indicate that no config has been received yet const notRead = Symbol('config not yet read'); @@ -44,25 +44,23 @@ export class RawConfigService { private readonly config$: Observable; - constructor(readonly configFile: string) { + constructor( + readonly configFiles: ReadonlyArray, + configAdapter: (rawValue: Record) => RawConfig = rawValue => + new ObjectToRawConfigAdapter(rawValue) + ) { 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 +68,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() { diff --git a/src/core/server/config/read_config.ts b/src/core/server/config/read_config.ts index c1b8ce930af2e77..9440c59509e6c89 100644 --- a/src/core/server/config/read_config.ts +++ b/src/core/server/config/read_config.ts @@ -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: 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 { + set(target, key, typeof value === 'string' ? replaceEnvVarRefs(value) : value); + } + + return target; +} + +export const getConfigFromFiles = (configFiles: ReadonlyArray) => { + let mergedYaml = {}; + + for (const configFile of configFiles) { + const yaml = readYaml(configFile); + if (yaml !== null) { + mergedYaml = merge(mergedYaml, yaml); + } + } + + return ensureDeepObject(mergedYaml); }; diff --git a/src/core/server/http/https_redirect_server.ts b/src/core/server/http/https_redirect_server.ts index 9a77c63f1b85b15..b2664c5e24b550c 100644 --- a/src/core/server/http/https_redirect_server.ts +++ b/src/core/server/http/https_redirect_server.ts @@ -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; } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 27d9a12081897b4..7d55670239f5e95 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -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)}` + ); } } diff --git a/src/core/server/legacy_compat/__mocks__/legacy_config_mock.ts b/src/core/server/legacy_compat/__mocks__/legacy_config_mock.ts deleted file mode 100644 index ed1b7c046257565..000000000000000 --- a/src/core/server/legacy_compat/__mocks__/legacy_config_mock.ts +++ /dev/null @@ -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 = new Map()) {} -} diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_config.test.ts.snap b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_config.test.ts.snap deleted file mode 100644 index 527f2154eb250b1..000000000000000 --- a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_config.test.ts.snap +++ /dev/null @@ -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", -] -`; diff --git a/src/core/server/legacy_compat/__tests__/legacy_platform_config.test.ts b/src/core/server/legacy_compat/__tests__/legacy_platform_config.test.ts deleted file mode 100644 index f15baebcb2434a8..000000000000000 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_config.test.ts +++ /dev/null @@ -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([['__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([['__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([ - ['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([ - ['__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([ - ['__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([ - ['__newPlatform', { known: 'foo', known2: { sub: 'bar' } }], - ['legacy', { known: 'baz' }], - ]); - - configAdapter = new LegacyConfigToRawConfigAdapter(legacyConfigMock); - - expect(configAdapter.getFlattenedPaths()).toMatchSnapshot(); -}); diff --git a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_raw_config_adapter.test.ts.snap b/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_raw_config_adapter.test.ts.snap new file mode 100644 index 000000000000000..33b1c674fec1142 --- /dev/null +++ b/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_raw_config_adapter.test.ts.snap @@ -0,0 +1,76 @@ +// 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[`#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[`\`getFlattenedPaths\` returns all paths. 1`] = ` +Array [ + "known", + "knownContainer.sub1", + "knownContainer.sub2", + "legacy.known", +] +`; diff --git a/src/core/server/legacy_compat/config/__tests__/legacy_object_to_raw_config_adapter.test.ts b/src/core/server/legacy_compat/config/__tests__/legacy_object_to_raw_config_adapter.test.ts new file mode 100644 index 000000000000000..17e421a802229c6 --- /dev/null +++ b/src/core/server/legacy_compat/config/__tests__/legacy_object_to_raw_config_adapter.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { LegacyObjectToRawConfigAdapter } from '../legacy_object_to_raw_config_adapter'; + +describe('#get', () => { + test('correctly handles paths that do not exist.', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({}); + + 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 LegacyObjectToRawConfigAdapter({ + 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 LegacyObjectToRawConfigAdapter({ + logging: { silent: true }, + }); + + expect(configAdapter.get('logging')).toMatchSnapshot(); + }); + + test('correctly handles verbose file logging config with json format.', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({ + logging: { verbose: true, json: true, dest: '/some/path.log' }, + }); + + expect(configAdapter.get('logging')).toMatchSnapshot(); + }); + + test('correctly handles server config.', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({ + 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 LegacyObjectToRawConfigAdapter({}); + + configAdapter.set('unknown', 'value'); + configAdapter.set(['unknown', 'sub1'], 'sub-value-1'); + configAdapter.set('unknown.sub2', 'sub-value-2'); + + expect(configAdapter.getRaw()).toMatchSnapshot(); + }); + + test('correctly sets values for existing paths.', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({ + 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.getRaw()).toMatchSnapshot(); + }); +}); + +describe('#has', () => { + test('returns false if config is not set', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({}); + + 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 LegacyObjectToRawConfigAdapter({ + 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); + }); +}); + +test('`getFlattenedPaths` returns all paths.', () => { + const configAdapter = new LegacyObjectToRawConfigAdapter({ + known: 'foo', + knownContainer: { + sub1: 'bar', + sub2: 'baz', + }, + legacy: { known: 'baz' }, + }); + + expect(configAdapter.getFlattenedPaths()).toMatchSnapshot(); +}); diff --git a/src/core/server/legacy_compat/config/legacy_object_to_raw_config_adapter.ts b/src/core/server/legacy_compat/config/legacy_object_to_raw_config_adapter.ts new file mode 100644 index 000000000000000..d751239099622d8 --- /dev/null +++ b/src/core/server/legacy_compat/config/legacy_object_to_raw_config_adapter.ts @@ -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, ObjectToRawConfigAdapter } from '../../config'; + +/** + * Represents logging config supported by the legacy platform. + */ +interface LegacyLoggingConfig { + silent?: boolean; + verbose?: boolean; + quiet?: boolean; + dest?: string; + json?: boolean; + events?: Record; +} + +/** + * Represents adapter between config provided by legacy platform and `RawConfig` + * supported by the current platform. + */ +export class LegacyObjectToRawConfigAdapter extends ObjectToRawConfigAdapter { + 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 LegacyObjectToRawConfigAdapter.transformLogging(configValue); + case 'server': + return LegacyObjectToRawConfigAdapter.transformServer(configValue); + default: + return configValue; + } + } +} diff --git a/src/core/server/legacy_compat/index.ts b/src/core/server/legacy_compat/index.ts index dcc5c31fbb87d96..d3b4dd9fdf3b246 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -23,35 +23,31 @@ import { map } from 'rxjs/operators'; /** @internal */ export { LegacyPlatformProxifier } from './legacy_platform_proxifier'; /** @internal */ -export { LegacyConfigToRawConfigAdapter, LegacyConfig } from './legacy_platform_config'; +export { LegacyObjectToRawConfigAdapter } from './config/legacy_object_to_raw_config_adapter'; -import { LegacyConfig, LegacyConfigToRawConfigAdapter, LegacyPlatformProxifier } from '.'; +import { LegacyObjectToRawConfigAdapter, 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>(rawKbnServer.config.get()); return { - config$, + config$: legacyConfig$.pipe( + map(legacyConfig => new LegacyObjectToRawConfigAdapter(legacyConfig)) + ), env, // Propagates legacy config updates to the new platform. - updateConfig(legacyConfig: LegacyConfig) { + updateConfig(legacyConfig: Record) { legacyConfig$.next(legacyConfig); }, }; diff --git a/src/core/server/legacy_compat/legacy_platform_config.ts b/src/core/server/legacy_compat/legacy_platform_config.ts deleted file mode 100644 index 47dc2082e2ccd34..000000000000000 --- a/src/core/server/legacy_compat/legacy_platform_config.ts +++ /dev/null @@ -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(); - } -} diff --git a/src/server/config/schema.js b/src/server/config/schema.js index bbeb41eae0020ff..6fa09a400cc09b3 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -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();